From fca0930b016b3b35fdf3f7f6b01dfe7660dc7ea3 Mon Sep 17 00:00:00 2001 From: Dimitris C Date: Sun, 27 Feb 2022 00:05:15 +0200 Subject: [PATCH] Fixes remote audio source network issues (#35) --- .../AudioExample/AppCoordinator.swift | 5 ++-- .../Controllers/EqualizerViewController.swift | 8 +++---- .../Controllers/EqualizerViewModel.swift | 3 +-- .../Controllers/PlayerViewController.swift | 16 ++++++------- .../Controllers/PlayerViewModel.swift | 5 ++-- .../Services/EqualizerService.swift | 4 ++-- .../Services/NowPlayingCenter.swift | 3 +-- .../Services/PlaylistItemsService.swift | 4 ++-- .../AVAudioFormat+Convenience.swift | 4 ++-- AudioStreaming/Core/Helpers/Retrier.swift | 1 + .../Core/Network/NetStatusService.swift | 24 +++++++++---------- .../Audio Source/RemoteAudioSource.swift | 23 +++++++++--------- .../Streaming/AudioPlayer/AudioPlayer.swift | 2 +- .../Processors/AudioFileStreamProcessor.swift | 6 ++--- .../Processors/FrameFilterProcessor.swift | 2 -- .../Processors/IcycastHeadersProcessor.swift | 6 ++--- .../Parsers/IcycastHeaderParser.swift | 2 -- .../Streaming/Parsers/MetadataParser.swift | 14 +++++++---- .../Streaming/Parsers/MetadataParser.swift | 20 ++++++++++++++++ Package.swift | 2 +- 20 files changed, 87 insertions(+), 67 deletions(-) diff --git a/AudioExample/AudioExample/AppCoordinator.swift b/AudioExample/AudioExample/AppCoordinator.swift index 3280b94..58f4094 100644 --- a/AudioExample/AudioExample/AppCoordinator.swift +++ b/AudioExample/AudioExample/AppCoordinator.swift @@ -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() } } diff --git a/AudioExample/AudioExample/Controllers/EqualizerViewController.swift b/AudioExample/AudioExample/Controllers/EqualizerViewController.swift index 7146fd7..c3d8a94 100644 --- a/AudioExample/AudioExample/Controllers/EqualizerViewController.swift +++ b/AudioExample/AudioExample/Controllers/EqualizerViewController.swift @@ -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..= 1_000 { + if item.frequency >= 1000 { measurement = item.frequency / 1000 frequency = "\(String(Int(measurement)))K" } diff --git a/AudioExample/AudioExample/Controllers/PlayerViewController.swift b/AudioExample/AudioExample/Controllers/PlayerViewController.swift index d184a7e..ff41733 100644 --- a/AudioExample/AudioExample/Controllers/PlayerViewController.swift +++ b/AudioExample/AudioExample/Controllers/PlayerViewController.swift @@ -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") } } diff --git a/AudioExample/AudioExample/Controllers/PlayerViewModel.swift b/AudioExample/AudioExample/Controllers/PlayerViewModel.swift index 4b2280d..4f6a80b 100644 --- a/AudioExample/AudioExample/Controllers/PlayerViewModel.swift +++ b/AudioExample/AudioExample/Controllers/PlayerViewModel.swift @@ -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 diff --git a/AudioExample/AudioExample/Services/EqualizerService.swift b/AudioExample/AudioExample/Services/EqualizerService.swift index 8a0e0bd..90599b4 100644 --- a/AudioExample/AudioExample/Services/EqualizerService.swift +++ b/AudioExample/AudioExample/Services/EqualizerService.swift @@ -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]) diff --git a/AudioExample/AudioExample/Services/NowPlayingCenter.swift b/AudioExample/AudioExample/Services/NowPlayingCenter.swift index dbb20cd..4ea2a34 100644 --- a/AudioExample/AudioExample/Services/NowPlayingCenter.swift +++ b/AudioExample/AudioExample/Services/NowPlayingCenter.swift @@ -9,10 +9,9 @@ import MediaPlayer final class NowPlayingCenter { - private let infoCenter: MPNowPlayingInfoCenter - init(infoCenter: MPNowPlayingInfoCenter = .default()){ + init(infoCenter: MPNowPlayingInfoCenter = .default()) { self.infoCenter = infoCenter } diff --git a/AudioExample/AudioExample/Services/PlaylistItemsService.swift b/AudioExample/AudioExample/Services/PlaylistItemsService.swift index e2f07a9..30e0c35 100644 --- a/AudioExample/AudioExample/Services/PlaylistItemsService.swift +++ b/AudioExample/AudioExample/Services/PlaylistItemsService.swift @@ -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 } diff --git a/AudioStreaming/Core/Extensions/AVAudioFormat+Convenience.swift b/AudioStreaming/Core/Extensions/AVAudioFormat+Convenience.swift index b69cb0c..ba73c63 100644 --- a/AudioStreaming/Core/Extensions/AVAudioFormat+Convenience.swift +++ b/AudioStreaming/Core/Extensions/AVAudioFormat+Convenience.swift @@ -5,11 +5,11 @@ import AVFoundation -extension AVAudioFormat { +public extension AVAudioFormat { /// The underlying audio stream description. /// /// This exposes the `pointee` value of the `UsafePointer` - public var basicStreamDescription: AudioStreamBasicDescription { + var basicStreamDescription: AudioStreamBasicDescription { return streamDescription.pointee } } diff --git a/AudioStreaming/Core/Helpers/Retrier.swift b/AudioStreaming/Core/Helpers/Retrier.swift index 5c48b69..8748c87 100644 --- a/AudioStreaming/Core/Helpers/Retrier.swift +++ b/AudioStreaming/Core/Helpers/Retrier.swift @@ -38,6 +38,7 @@ final class Retrier { /// Cancels retrying func cancel() { + interval = .seconds(1) timeoutTimer.removeHandler() timeoutTimer.suspend() } diff --git a/AudioStreaming/Core/Network/NetStatusService.swift b/AudioStreaming/Core/Network/NetStatusService.swift index 2e8ed94..617aca6 100644 --- a/AudioStreaming/Core/Network/NetStatusService.swift +++ b/AudioStreaming/Core/Network/NetStatusService.swift @@ -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 diff --git a/AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift b/AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift index 710e625..c78e452 100644 --- a/AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift +++ b/AudioStreaming/Streaming/Audio Source/RemoteAudioSource.swift @@ -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) } } diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift index 043fa32..a541f1e 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift @@ -470,7 +470,7 @@ open class AudioPlayer { if let first = customAttachedNodes.first { audioEngine.connect(rateNode, to: first, format: nil) } - for index in 0..= icyPrefix.count { // in case the first 4 chars are not "ICY " nor "HTTP" then we stop the flow - if icecastHeaders[.. HTTPHeaderParserOutput? { - guard let icecastValue = String(data: input, encoding: .utf8) else { return nil } diff --git a/AudioStreaming/Streaming/Parsers/MetadataParser.swift b/AudioStreaming/Streaming/Parsers/MetadataParser.swift index 38a66b1..dfe6539 100644 --- a/AudioStreaming/Streaming/Parsers/MetadataParser.swift +++ b/AudioStreaming/Streaming/Parsers/MetadataParser.swift @@ -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 } diff --git a/AudioStreamingTests/Streaming/Parsers/MetadataParser.swift b/AudioStreamingTests/Streaming/Parsers/MetadataParser.swift index 7d87604..7d2673b 100644 --- a/AudioStreamingTests/Streaming/Parsers/MetadataParser.swift +++ b/AudioStreamingTests/Streaming/Parsers/MetadataParser.swift @@ -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() diff --git a/Package.swift b/Package.swift index e7a15fb..674445b 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( .target( name: "AudioStreaming", path: "AudioStreaming" - ) + ), ], swiftLanguageVersions: [.v5] )