diff --git a/AudioExample/AudioExample/Services/AudioContent.swift b/AudioExample/AudioExample/Services/AudioContent.swift index bce5eb3..5daa0b5 100644 --- a/AudioExample/AudioExample/Services/AudioContent.swift +++ b/AudioExample/AudioExample/Services/AudioContent.swift @@ -12,6 +12,7 @@ enum AudioContent: Int, CaseIterable { case offradio case enlefko case pepper966 + case kosmos case radiox case khruangbin case piano @@ -26,6 +27,8 @@ enum AudioContent: Int, CaseIterable { return "Enlefko (stream)" case .pepper966: return "Pepper 96.6 (stream)" + case .kosmos: + return "Kosmos 93.6 (stream)" case .radiox: return "Radio X (stream)" case .khruangbin: @@ -47,6 +50,8 @@ enum AudioContent: Int, CaseIterable { return URL(string: "https://s3.yesstreaming.net:17062/stream")! case .pepper966: return URL(string: "https://ample-09.radiojar.com/pepper.m4a?1593699983=&rj-tok=AAABcw_1KyMAIViq2XpI098ZSQ&rj-ttl=5")! + case .kosmos: + return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")! case .radiox: return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")! case .khruangbin: diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift index 5a29f55..816c523 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift @@ -131,7 +131,7 @@ public final class AudioPlayer { entriesQueue = PlayerQueueEntries() serializationQueue = DispatchQueue(label: "streaming.core.queue", qos: .userInitiated) - sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated, target: serializationQueue) + sourceQueue = DispatchQueue(label: "source.queue", qos: .userInitiated) audioReadSource = DispatchTimerSource(interval: .milliseconds(200), queue: sourceQueue) entryProvider = AudioEntryProvider(networkingClient: NetworkingClient(), @@ -152,7 +152,6 @@ public final class AudioPlayer { } deinit { - // todo more stuff to release... playerContext.audioPlayingEntry?.close() clearQueue() stopReadProccessFromSource() @@ -175,18 +174,21 @@ public final class AudioPlayer { public func play(url: URL, headers: [String: String]) { let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers) audioEntry.delegate = self - clearQueue() - entriesQueue.enqueue(item: audioEntry, type: .upcoming) - playerContext.setInternalState(to: .pendingNext) checkRenderWaitingAndNotifyIfNeeded() - sourceQueue.async { [weak self] in - guard let self = self else { return } + serializationQueue.sync { + clearQueue() + entriesQueue.enqueue(item: audioEntry, type: .upcoming) + playerContext.setInternalState(to: .pendingNext) do { try self.startEngineIfNeeded() } catch { self.raiseUnxpected(error: .audioSystemError(.engineFailure)) } + } + + sourceQueue.async { [weak self] in + guard let self = self else { return } self.processSource() self.startReadProcessFromSourceIfNeeded() } @@ -194,19 +196,46 @@ public final class AudioPlayer { /// Queues the specified URL /// - /// - Parameter url: A `URL` specifying the audio context to be played. + /// - Parameter url: A `URL` specifying the audio content to be played. public func queue(url: URL) { queue(url: url, headers: [:]) } + /// Queues the specified URLs + /// + /// - Parameter url: A `URL` specifying the audio content to be played. + public func queue(urls: [URL]) { + queue(urls: urls, headers: [:]) + } + /// Queues the specified URL /// - /// - Parameter url: A `URL` specifying the audio context to be played. + /// - Parameter url: A `URL` specifying the audio content to be played. /// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request. public func queue(url: URL, headers: [String: String]) { - let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers) - audioEntry.delegate = self - entriesQueue.enqueue(item: audioEntry, type: .upcoming) + serializationQueue.sync { + let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers) + audioEntry.delegate = self + entriesQueue.enqueue(item: audioEntry, type: .upcoming) + } + checkRenderWaitingAndNotifyIfNeeded() + sourceQueue.async { [weak self] in + self?.processSource() + } + } + + /// Queues the specified URLs + /// + /// - Parameter url: A array of `URL`s specifying the audio content to be played. + /// - parameter headers: A `Dictionary` specifying any additional headers to be pass to the network request. + public func queue(urls: [URL], headers: [String: String]) { + serializationQueue.sync { + for url in urls { + let audioEntry = entryProvider.provideAudioEntry(url: url, headers: headers) + audioEntry.delegate = self + entriesQueue.enqueue(item: audioEntry, type: .upcoming) + } + } checkRenderWaitingAndNotifyIfNeeded() sourceQueue.async { [weak self] in self?.processSource() @@ -218,7 +247,10 @@ public final class AudioPlayer { guard playerContext.internalState != .stopped else { return } stopReadProccessFromSource() - stopEngine(reason: .userAction) + serializationQueue.sync { + stopEngine(reason: .userAction) + } + checkRenderWaitingAndNotifyIfNeeded() sourceQueue.async { [weak self] in guard let self = self else { return } self.playerContext.audioReadingEntry?.delegate = nil @@ -235,7 +267,6 @@ public final class AudioPlayer { self.processSource() } - checkRenderWaitingAndNotifyIfNeeded() } /// Pauses the audio playback @@ -243,8 +274,9 @@ public final class AudioPlayer { if playerContext.internalState != .paused, playerContext.internalState.contains(.running) { stateBeforePaused = playerContext.internalState playerContext.setInternalState(to: .paused) - - pauseEngine() + serializationQueue.sync { + pauseEngine() + } stopReadProccessFromSource() playerContext.audioPlayingEntry?.suspend() sourceQueue.async { [weak self] in @@ -257,20 +289,20 @@ public final class AudioPlayer { public func resume() { guard playerContext.internalState == .paused else { return } playerContext.setInternalState(to: stateBeforePaused) - // check if seek time requested and reset buffers - do { - try startEngine() - } catch { - Logger.debug("resuming audio engine failed: %@", category: .generic, args: error.localizedDescription) - } - - if let playingEntry = playerContext.audioReadingEntry { - if playingEntry.seekRequest.requested { - rendererContext.resetBuffers() + serializationQueue.sync { + do { + try startEngine() + } catch { + Logger.debug("resuming audio engine failed: %@", category: .generic, args: error.localizedDescription) } - playingEntry.resume() + if let playingEntry = playerContext.audioReadingEntry { + if playingEntry.seekRequest.requested { + rendererContext.resetBuffers() + } + playingEntry.resume() + } + startPlayer(resetBuffers: false) } - startPlayer(resetBuffers: false) startReadProcessFromSourceIfNeeded() } @@ -546,7 +578,7 @@ public final class AudioPlayer { let entry = entriesQueue.dequeue(type: .upcoming) let shouldStartPlaying = playerContext.audioPlayingEntry == nil playerContext.setInternalState(to: .waitingForData) - setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: true) + setCurrentReading(entry: entry, startPlaying: shouldStartPlaying, shouldClearQueue: false) } else if playerContext.audioPlayingEntry == nil { if playerContext.internalState != .stopped { stopReadProccessFromSource() @@ -578,7 +610,8 @@ public final class AudioPlayer { } private func proccessSeekTime() { - assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry, "reading and playing entry must be the same") + assert(playerContext.audioReadingEntry === playerContext.audioPlayingEntry, + "reading and playing entry must be the same") fileStreamProcessor.processSeek() } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift index 23bd3b5..5ee50c0 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift @@ -450,7 +450,7 @@ final class AudioFileStreamProcessor { fillUsedFrames(framesCount: framesAdded) return } else if status != 0 { - /// raise undexpected error... codec error + fileStreamCallback?(.raiseError(.codecError)) return } @@ -479,7 +479,7 @@ final class AudioFileStreamProcessor { fillUsedFrames(framesCount: framesAdded) continue packetProccess } else if status != 0 { - /// raise undexpected error... codec error + fileStreamCallback?(.raiseError(.codecError)) return } } else { @@ -506,7 +506,7 @@ final class AudioFileStreamProcessor { fillUsedFrames(framesCount: framesAdded) continue packetProccess } else if status != 0 { - /// raise undexpected error... codec error + fileStreamCallback?(.raiseError(.codecError)) return } } diff --git a/README.md b/README.md index 7ce03e1..851b265 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,14 @@ player.play(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!) ### Queueing audio files ``` let player = AudioPlayer() +// when you want to queue a single url player.queue(url: URL(string: "https://your-remote-url/to/audio-file.mp3")!) -player.queue(url: URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!) + +// or if you want to queue a list of urls use +player.queue(urls: [ + URL(fileURLWithPath: "your-local-path/to/audio-file.mp3")!, + URL(fileURLWithPath: "your-local-path/to/audio-file-2.mp3")! +]) ``` ### Adjusting playback properties