Fixes remote audio source network issues (#35)

This commit is contained in:
Dimitris C
2022-02-27 00:05:15 +02:00
committed by GitHub
parent 2f08ea4131
commit fca0930b01
20 changed files with 87 additions and 67 deletions
@@ -9,7 +9,6 @@
import UIKit
final class AppCoordinator {
enum Route {
case equalizer
}
@@ -44,8 +43,8 @@ final class AppCoordinator {
private func routeTo(_ route: AppCoordinator.Route) {
switch route {
case .equalizer:
showEqualizerControls()
case .equalizer:
showEqualizerControls()
}
}
@@ -9,7 +9,6 @@
import UIKit
class EqualizerViewController: UIViewController {
private lazy var enableTextLabel = UILabel()
private lazy var enableButton = UISwitch()
@@ -22,7 +21,8 @@ class EqualizerViewController: UIViewController {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -69,7 +69,7 @@ class EqualizerViewController: UIViewController {
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8)
stackView.heightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.heightAnchor, multiplier: 0.8),
]
)
}
@@ -86,7 +86,7 @@ class EqualizerViewController: UIViewController {
private func buildSliders() -> [UIView] {
var sliders = [UIView]()
for index in 0..<viewModel.numberOfBands() {
for index in 0 ..< viewModel.numberOfBands() {
guard let item = viewModel.band(at: index) else { continue }
let slider = buildSlider(item: item, index: index)
sliders.append(slider)
@@ -16,7 +16,6 @@ struct EQBand {
}
final class EqualzerViewModel {
private var bands: [EQBand] = []
private let equalizerService: EqualizerService
@@ -31,7 +30,7 @@ final class EqualzerViewModel {
bands = equalizerService.bands.map { item in
var measurement = item.frequency
var frequency = String(Int(measurement))
if item.frequency >= 1_000 {
if item.frequency >= 1000 {
measurement = item.frequency / 1000
frequency = "\(String(Int(measurement)))K"
}
@@ -51,7 +51,7 @@ class PlayerViewController: UIViewController {
style: .plain,
target: self,
action: #selector(showEqualizer))
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.dataSource = self
@@ -92,12 +92,13 @@ class PlayerViewController: UIViewController {
@objc private func addNowPlaylistItem() {
let controller = UIAlertController(title: "Add new item", message: "", preferredStyle: .alert)
controller.addTextField { (textField) in
controller.addTextField { textField in
textField.placeholder = "Insert url here"
}
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] action in
let saveAction = UIAlertAction(title: "Save", style: .default) { [viewModel] _ in
if let textfield = controller.textFields?.first,
let text = textfield.text {
let text = textfield.text
{
viewModel.add(urlString: text)
}
}
@@ -105,7 +106,7 @@ class PlayerViewController: UIViewController {
controller.addAction(saveAction)
controller.addAction(cancelAction)
self.present(controller, animated: true, completion: nil)
present(controller, animated: true, completion: nil)
}
}
@@ -149,14 +150,13 @@ extension PlayerViewController: UITableViewDelegate {
}
}
final class PlaylistTableViewCell: UITableViewCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
override init(style _: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -18,14 +18,15 @@ final class PlayerViewModel {
private let playerService: AudioPlayerService
private let playlistItemsService: PlaylistItemsService
private let routeTo: ((AppCoordinator.Route) -> Void)
private let routeTo: (AppCoordinator.Route) -> Void
private var currentPlayingItemIndex: Int?
var reloadContent: ((ReloadAction) -> Void)?
init(playlistItemsService: PlaylistItemsService,
playerService: AudioPlayerService,
routeTo: @escaping (AppCoordinator.Route) -> Void) {
routeTo: @escaping (AppCoordinator.Route) -> Void)
{
self.playlistItemsService = playlistItemsService
self.playerService = playerService
self.routeTo = routeTo
@@ -10,7 +10,7 @@ import AVFoundation
final class EqualizerService {
private let playerService: AudioPlayerService
private let _freqs = [32, 64, 128, 250, 500, 1_000, 2_000, 4_000, 8_000, 16_000]
private let _freqs = [32, 64, 128, 250, 500, 1000, 2000, 4000, 8000, 16000]
private let eqUnit: AVAudioUnitEQ
var bands: [AVAudioUnitEQFilterParameters] {
@@ -23,7 +23,7 @@ final class EqualizerService {
self.playerService = playerService
eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count)
for i in 0..<_freqs.count {
for i in 0 ..< _freqs.count {
eqUnit.bands[i].bypass = false
eqUnit.bands[i].filterType = .parametric
eqUnit.bands[i].frequency = Float(_freqs[i])
@@ -9,10 +9,9 @@
import MediaPlayer
final class NowPlayingCenter {
private let infoCenter: MPNowPlayingInfoCenter
init(infoCenter: MPNowPlayingInfoCenter = .default()){
init(infoCenter: MPNowPlayingInfoCenter = .default()) {
self.infoCenter = infoCenter
}
@@ -80,7 +80,7 @@ final class PlaylistItemsService {
func provideInitialPlaylistItems() -> [PlaylistItem] {
let allCases = AudioContent.allCases
let casesForQueueing: [AudioContent] = [.piano, .local, .khruangbin]
let allItems = allCases.map { PlaylistItem.init(content: $0 , queues: false) }
let casesForQueuingItems = casesForQueueing.map { PlaylistItem.init(content: $0 , queues: true) }
let allItems = allCases.map { PlaylistItem(content: $0, queues: false) }
let casesForQueuingItems = casesForQueueing.map { PlaylistItem(content: $0, queues: true) }
return allItems + casesForQueuingItems
}
@@ -5,11 +5,11 @@
import AVFoundation
extension AVAudioFormat {
public extension AVAudioFormat {
/// The underlying audio stream description.
///
/// This exposes the `pointee` value of the `UsafePointer<AudioStreamBasicDescription>`
public var basicStreamDescription: AudioStreamBasicDescription {
var basicStreamDescription: AudioStreamBasicDescription {
return streamDescription.pointee
}
}
@@ -38,6 +38,7 @@ final class Retrier {
/// Cancels retrying
func cancel() {
interval = .seconds(1)
timeoutTimer.removeHandler()
timeoutTimer.suspend()
}
@@ -9,12 +9,14 @@ import Network
enum NetConnectionType: Equatable {
case cellular(connected: Bool)
case wifi(connected: Bool)
case other(connected: Bool)
case undetermined
var isConnected: Bool {
switch self {
case let .cellular(connected),
let .wifi(connected):
let .wifi(connected),
let .other(connected):
return connected
default:
return false
@@ -39,15 +41,13 @@ final class NetStatusService: NetStatusProvider {
network.currentPath.toNetConnectionType()
}
private var currentConnectionType: NetConnectionType = .undetermined
private let network: NWPathMonitor
private let monitorQueue: DispatchQueue
init(network: NWPathMonitor) {
self.network = network
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .background)
monitorQueue = DispatchQueue(label: "net.path.queue", qos: .utility)
}
deinit {
@@ -59,20 +59,15 @@ final class NetStatusService: NetStatusProvider {
/// - parameter connectionChange: A callback block to listen to changes of the network type, this skips duplicates.
/// - Note: The callback will be executed on the main thread.
func start(connectionChange: @escaping (NetConnectionType) -> Void) {
network.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let connecionType = path.toNetConnectionType()
if self.currentConnectionType != connecionType {
connectionChange(self.connectionType)
self.currentConnectionType = self.connectionType
}
network.pathUpdateHandler = { path in
let connectionType = path.toNetConnectionType()
connectionChange(connectionType)
}
startIfNeeded()
}
func stop() {
network.cancel()
network.pathUpdateHandler = nil
}
func startIfNeeded() {
@@ -85,12 +80,17 @@ extension NWPath {
func toNetConnectionType() -> NetConnectionType {
let isCellular = usesInterfaceType(.cellular)
let isWifi = usesInterfaceType(.wifi)
let isOther = usesInterfaceType(.loopback)
|| usesInterfaceType(.other)
|| usesInterfaceType(.wiredEthernet)
let isConnected = status == .satisfied
if isCellular {
return .cellular(connected: isConnected)
} else if isWifi {
return .wifi(connected: isConnected)
} else if isOther {
return .other(connected: isConnected)
}
return .undetermined
@@ -109,7 +109,6 @@ public class RemoteAudioSource: AudioStreamSource {
func close() {
retrierTimeout.cancel()
netStatusService.stop()
streamOperationQueue.isSuspended = false
streamOperationQueue.cancelAllOperations()
if let streamTask = streamRequest {
@@ -152,8 +151,8 @@ public class RemoteAudioSource: AudioStreamSource {
guard let self = self else { return }
guard connection.isConnected else { return }
if self.waitingForNetwork {
self.seek(at: self.supportsSeek ? self.position : 0 )
self.waitingForNetwork = false
self.seek(at: self.position)
}
}
}
@@ -161,14 +160,13 @@ public class RemoteAudioSource: AudioStreamSource {
private func performOpen(seek seekOffset: Int) {
let urlRequest = buildUrlRequest(with: url, seekIfNeeded: seekOffset)
let request = networkingClient.stream(request: urlRequest)
streamRequest = networkingClient.stream(request: urlRequest)
.responseStream { [weak self] event in
guard let self = self else { return }
self.handleResponse(event: event)
}
.resume()
streamRequest = request
metadataStreamProcessor.delegate = self
}
@@ -231,12 +229,12 @@ public class RemoteAudioSource: AudioStreamSource {
/// - Parameter data: The audio to be processed
/// - Returns: An `Int` value representing the amount of audio data bytes.
private func processAudio(data: Data) -> Int {
if self.metadataStreamProcessor.canProcessMetadata {
let extractedAudioData = self.metadataStreamProcessor.processMetadata(data: data)
self.delegate?.dataAvailable(source: self, data: extractedAudioData)
if metadataStreamProcessor.canProcessMetadata {
let extractedAudioData = metadataStreamProcessor.processMetadata(data: data)
delegate?.dataAvailable(source: self, data: extractedAudioData)
return extractedAudioData.count
} else {
self.delegate?.dataAvailable(source: self, data: data)
delegate?.dataAvailable(source: self, data: data)
return data.count
}
}
@@ -270,7 +268,10 @@ public class RemoteAudioSource: AudioStreamSource {
if length >= 0 { seekOffset = length }
delegate?.endOfFileOccurred(source: self)
} else if statusCode >= 300 {
delegate?.errorOccurred(source: self, error: NetworkError.serverError)
delegate?.errorOccurred(
source: self,
error: NetworkError.serverError
)
}
}
@@ -287,7 +288,7 @@ public class RemoteAudioSource: AudioStreamSource {
urlRequest.addValue("1", forHTTPHeaderField: "Icy-MetaData")
urlRequest.addValue("identity", forHTTPHeaderField: "Accept-Encoding")
if supportsSeek && seekOffset > 0 {
if supportsSeek, seekOffset > 0 {
urlRequest.addValue("bytes=\(seekOffset)-", forHTTPHeaderField: "Range")
}
return urlRequest
@@ -296,7 +297,7 @@ public class RemoteAudioSource: AudioStreamSource {
private func retryOnError() {
retrierTimeout.retry { [weak self] in
guard let self = self else { return }
self.seek(at: self.position)
self.seek(at: self.supportsSeek ? self.position : 0)
}
}
@@ -470,7 +470,7 @@ open class AudioPlayer {
if let first = customAttachedNodes.first {
audioEngine.connect(rateNode, to: first, format: nil)
}
for index in 0..<customAttachedNodes.count - 1 {
for index in 0 ..< customAttachedNodes.count - 1 {
let current = customAttachedNodes[index]
let next = customAttachedNodes[index + 1]
let format = current.inputFormat(forBus: 0)
@@ -195,7 +195,7 @@ final class AudioFileStreamProcessor {
}
guard let converter = audioConverter else {
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
return
return
}
guard AudioConverterSetProperty(converter, kAudioConverterDecompressionMagicCookie, cookieSize, cookie) == noErr else {
fileStreamCallback?(.raiseError(.audioSystemError(.fileStreamError(.unknownError))))
@@ -265,7 +265,7 @@ final class AudioFileStreamProcessor {
var size = UInt32(4)
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_FileFormat, &size, &fileFormat)
if let stringFileFormat = String(data: Data(fileFormat), encoding: .utf8) {
self.currentFileFormat = stringFileFormat
currentFileFormat = stringFileFormat
}
}
@@ -378,7 +378,7 @@ final class AudioFileStreamProcessor {
}
updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
inNumberPackets: inNumberPackets)
var status: OSStatus = noErr
packetProcess: while status == noErr {
@@ -33,7 +33,6 @@ public struct FilterEntry: Equatable {
}
public protocol FrameFiltering {
/// A Boolean value indicating whether there are filter entries
var hasEntries: Bool { get }
@@ -73,7 +72,6 @@ public protocol FrameFiltering {
}
final class FrameFilterProcessor: NSObject, FrameFiltering {
public var hasEntries: Bool {
lock.lock(); defer { lock.unlock() }
return !entries.isEmpty
@@ -26,7 +26,6 @@ import Foundation
/// ```
final class IcycastHeadersProcessor {
private var icecastHeaders = Data(capacity: 1024)
private var searchComplete = false
private var iceHeaderAvailable = false
@@ -72,8 +71,9 @@ final class IcycastHeadersProcessor {
if icecastHeaders.count >= icyPrefix.count {
// in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false &&
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false {
if icecastHeaders[..<icyPrefix.count].elementsEqual(icyPrefix) == false,
icecastHeaders[..<httpPrefix.count].elementsEqual(httpPrefix) == false
{
iceHeaderAvailable = false
searchComplete = true
}
@@ -9,9 +9,7 @@
import Foundation
struct IcycastHeaderParser: Parser {
func parse(input: Data) -> HTTPHeaderParserOutput? {
guard let icecastValue = String(data: input, encoding: .utf8) else {
return nil
}
@@ -20,11 +20,15 @@ struct MetadataParser: Parser {
guard let string = String(data: input, encoding: .utf8) else { return .failure(.unableToParse) }
// remove added bytes (zeros) and separate the string on every ';' char
let pairs = string.trimmingCharacters(in: CharacterSet(charactersIn: "\0")).components(separatedBy: ";")
let temp: [String: String] = [:]
let metadata = pairs.reduce(into: temp) { result, next in
let paired = next.components(separatedBy: "=")
if let key = paired.first,
let value = paired.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
let metadata = pairs.reduce(into: [String: String]()) { result, next in
let split = next.split(
separator: "=",
maxSplits: 1,
omittingEmptySubsequences: true
)
.map(String.init)
if let key = split.first,
let value = split.last?.replacingOccurrences(of: "'", with: ""), !key.isEmpty
{
result[key] = value
}
@@ -50,6 +50,26 @@ class MetadataParserTests: XCTestCase {
}
}
func testParserOutputsCorrectResultWhenEntryContainsEqualSign() throws {
let string = "StreamTitle=\'Gramatik - In This Whole World (Original Mix)\';StreamUrl=\'\';track_info=\'k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==\';UTC=\'20220226T214447.206\';\0\0\0\0\0\0\0\0\0"
let data = string.data(using: .utf8)!
let parser = MetadataParser()
let output = parser.parse(input: data)
switch output {
case let .success(values):
XCTAssertFalse(values.isEmpty)
XCTAssertEqual(values["StreamTitle"], "Gramatik - In This Whole World (Original Mix)")
XCTAssertEqual(values["StreamUrl"], "")
XCTAssertEqual(values["track_info"], "k4Smc3RhdHVzoUihQNJiGp6BpHR5cGWhVKJpZKhNWDUxMTYzNISmc3RhdHVzoUOhQNJiGp9cpHR5cGWhVKJpZKhNWDUxMDM3MoSmc3RhdHVzoUOhQNJiGqAqpHR5cGWhVKJpZKhNWDUxMjA5Ng==")
XCTAssertEqual(values["UTC"], "20220226T214447.206")
case .failure:
XCTFail()
}
}
func testParserOutputsFailureOnEmptyStringData() throws {
let data = "".data(using: .utf8)!
let parser = MetadataParser()
+1 -1
View File
@@ -17,7 +17,7 @@ let package = Package(
.target(
name: "AudioStreaming",
path: "AudioStreaming"
)
),
],
swiftLanguageVersions: [.v5]
)