diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index a3e1f22a..7ad0aadb 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -20,10 +20,10 @@ public enum PlaybackEndedReason: String { case failed } -class AVPlayerWrapper: AVPlayerWrapperProtocol { +public class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties - fileprivate var avPlayer = AVPlayer() + var avPlayer = AVQueuePlayer() private let playerObserver = AVPlayerObserver() internal let playerTimeObserver: AVPlayerTimeObserver private let playerItemNotificationObserver = AVPlayerItemNotificationObserver() @@ -37,7 +37,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { label: "AVPlayerWrapper.stateQueue", attributes: .concurrent ) - + + private var nextAsset: AVAsset? + private var nextPreloadUrl: URL? + private var wasPlayingBeforeStall = false + public init() { playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime()) @@ -56,20 +60,17 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { var _state: AVPlayerWrapperState = AVPlayerWrapperState.idle var state: AVPlayerWrapperState { get { - var state: AVPlayerWrapperState! - stateQueue.sync { - state = _state - } - - return state + return stateQueue.sync { _state } } set { stateQueue.async(flags: .barrier) { [weak self] in guard let self = self else { return } let currentState = self._state - if (currentState != newValue) { + if currentState != newValue { self._state = newValue - self.delegate?.AVWrapper(didChangeState: newValue) + DispatchQueue.main.async { + self.delegate?.AVWrapper(didChangeState: newValue) + } } } } @@ -174,7 +175,15 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { func pause() { playWhenReady = false } + + func recreatePlayer() { + recreateAVPlayer() + } + func clearAvPlayerQueue() { + avPlayer.removeAllItems() + } + func togglePlaying() { switch avPlayer.timeControlStatus { case .playing, .waitingToPlayAtSpecifiedRate: @@ -228,73 +237,146 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { } func load() { - if (state == .failed) { + if state == .failed { recreateAVPlayer() + } + if let asset { + asset.cancelLoading() + self.asset = nil + } + self.stopObservingAVPlayerItem() + nextAsset?.cancelLoading() + guard let url = url else { return } + state = .loading + print("---- requesting new item \(url.absoluteString.suffix(10)) ") + print("---- \(avPlayer.items().compactMap { ($0.asset as? AVURLAsset)?.url.absoluteString.suffix(10) })") + if let item = avPlayer.items().first { ($0.asset as? AVURLAsset)?.url == url }, avPlayer.items().count == 2 { + print("---- load existing item") + self.item = item + self.avPlayer.advanceToNextItem() + self.applyAVPlayerRate() + self.asset = item.asset + self.startObservingAVPlayer(item: item) + self.applyAVPlayerRate() + self.prefetchNextTracks() + return; + } + let pendingAsset = AVURLAsset(url: url, options: urlOptions) + asset = pendingAsset + let keys: [String] = [ + "playable", + "availableChapterLocales", + "availableMetadataFormats", + "commonMetadata", + "duration" + ] + + pendingAsset.loadValuesAsynchronously(forKeys: keys) { [weak self] in + guard let self = self else { return } + DispatchQueue.main.async { + var error: NSError? + let playableStatus = pendingAsset.statusOfValue(forKey: "playable", error: &error) + switch playableStatus { + case .cancelled, .loading, .unknown: + return + case .failed: + self.playbackFailed(error: AudioPlayerError.PlaybackError.failedToLoadKeyValue) + return + default: break + } + self.handleLoadedAsset(pendingAsset) + } + } + } + + private func handleLoadedAsset(_ pendingAsset: AVURLAsset) { + guard pendingAsset == asset else { return } + + if !pendingAsset.isPlayable { + playbackFailed(error: AudioPlayerError.PlaybackError.itemWasUnplayable) + return + } + + let keysToLoad: [String] = [ + "playable", + "availableChapterLocales", + "availableMetadataFormats", + "commonMetadata", + "duration" + ] + + let item = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: keysToLoad) + self.item = item + item.preferredForwardBufferDuration = self.bufferDuration + print("---- avplayer removeallitems ----") + self.avPlayer.removeAllItems() + self.avPlayer.insert(item, after: nil) + self.prefetchNextTracks() + self.startObservingAVPlayer(item: item) + self.applyAVPlayerRate() + if !pendingAsset.availableChapterLocales.isEmpty { + for locale in pendingAsset.availableChapterLocales { + let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil) + self.delegate?.AVWrapper(didReceiveMetadata: chapters) + } } else { - clearCurrentItem() + for format in pendingAsset.availableMetadataFormats { + let timeRange = CMTimeRange(start: .zero, end: pendingAsset.duration) + let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange) + self.delegate?.AVWrapper(didReceiveMetadata: [group]) + } } - if let url = url { - let keys = ["playable"] - let pendingAsset = AVURLAsset(url: url, options: urlOptions) - asset = pendingAsset - state = .loading - pendingAsset.loadValuesAsynchronously(forKeys: keys, completionHandler: { [weak self] in + + if let initialTime = self.timeToSeekToAfterLoading { + self.timeToSeekToAfterLoading = nil + self.seek(to: initialTime) + } + } + + func prefetchNextTracks() { + QueuedAudioPlayer.nextAudioItem.first?.getSourceUrl({ [weak self] urlString in + guard let self = self else { return } + guard let url = URL(string: urlString) else { return } + nextAsset = AVURLAsset(url: url) + guard let asset = nextAsset else { return } + let keys = [ + "playable", + "availableChapterLocales", + "availableMetadataFormats", + "commonMetadata", + "duration" + ] + asset.loadValuesAsynchronously(forKeys: keys) { [weak self] in guard let self = self else { return } + var error: NSError? + let status = asset.statusOfValue(forKey: "playable", error: &error) + guard status == .loaded else { return } + + let item = AVPlayerItem(asset: asset) + item.preferredForwardBufferDuration = 30 + item.canUseNetworkResourcesForLiveStreamingWhilePaused = true DispatchQueue.main.async { - if (pendingAsset != self.asset) { return; } - - for key in keys { - var error: NSError? - let keyStatus = pendingAsset.statusOfValue(forKey: key, error: &error) - switch keyStatus { - case .failed: - self.playbackFailed(error: AudioPlayerError.PlaybackError.failedToLoadKeyValue) - return - case .cancelled, .loading, .unknown: - return - case .loaded: - break - default: break - } - } - - if (!pendingAsset.isPlayable) { - self.playbackFailed(error: AudioPlayerError.PlaybackError.itemWasUnplayable) - return; - } - - let item = AVPlayerItem( - asset: pendingAsset, - automaticallyLoadedAssetKeys: keys - ) - self.item = item; - item.preferredForwardBufferDuration = self.bufferDuration - self.avPlayer.replaceCurrentItem(with: item) - self.startObservingAVPlayer(item: item) - self.applyAVPlayerRate() - if pendingAsset.availableChapterLocales.count > 0 { - for locale in pendingAsset.availableChapterLocales { - let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil) - self.delegate?.AVWrapper(didReceiveMetadata: chapters) - } - } else { - for format in pendingAsset.availableMetadataFormats { - let timeRange = CMTimeRange(start: CMTime(seconds: 0, preferredTimescale: 1000), end: pendingAsset.duration) - let group = AVTimedMetadataGroup(items: pendingAsset.metadata(forFormat: format), timeRange: timeRange) - self.delegate?.AVWrapper(didReceiveMetadata: [group]) - } - } - - if let initialTime = self.timeToSeekToAfterLoading { - self.timeToSeekToAfterLoading = nil - self.seek(to: initialTime) + if self.avPlayer.items().count > 1 { + let nextItems = self.avPlayer.items().dropFirst() + nextItems.forEach { self.avPlayer.remove($0) } } + self.avPlayer.insert(item, after: nil) + print("---- preload next item \((self.avPlayer.items().compactMap { ($0.asset as? AVURLAsset)?.url.absoluteString.suffix(10) }).joined(separator: " || ") ) ") } - }) - } + } + }) } + func preloadNextTracks(_ url: URL) { + guard !avPlayer.items().contains(where: { ($0.asset as? AVURLAsset)?.url == url} ) else { return } + self.nextPreloadUrl = url + } + + func clearPreloadedTracks() { + avPlayer.removeAllItems() + } + func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) { self.playWhenReady = playWhenReady self.url = url @@ -377,7 +459,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerItemNotificationObserver.stopObservingCurrentItem() } - private func recreateAVPlayer() { + func recreateAVPlayer() { playbackError = nil playerTimeObserver.unregisterForBoundaryTimeEvents() playerTimeObserver.unregisterForPeriodicEvents() @@ -385,7 +467,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { stopObservingAVPlayerItem() clearCurrentItem() - avPlayer = AVPlayer(); + avPlayer = AVQueuePlayer(); setupAVPlayer() delegate?.AVWrapperDidRecreateAVPlayer() @@ -394,7 +476,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { private func setupAVPlayer() { // disabled since we're not making use of video playback avPlayer.allowsExternalPlayback = false; - + AudioPlayer.avPlayerRef = avPlayer playerObserver.player = avPlayer playerObserver.startObserving() @@ -403,6 +485,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerTimeObserver.registerForPeriodicTimeEvents() applyAVPlayerRate() + avPlayer.actionAtItemEnd = .none } private func applyAVPlayerRate() { @@ -421,11 +504,13 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate { if self.asset == nil && state != .stopped { self.state = .idle } else if (state != .failed && state != .stopped) { - // Playback may have become paused externally for example due to a bluetooth device disconnecting: + // Playback may have become paused externally for example due to a bluetooth device disconnecting, + // or a buffer stall when automaticallyWaitsToMinimizeStalling = false: if (self.playWhenReady) { // Only if we are not on the boundaries of the track, otherwise itemDidPlayToEndTime will handle it instead. if (self.currentTime > 0 && self.currentTime < self.duration) { - self.playWhenReady = false; + wasPlayingBeforeStall = true + self.playWhenReady = false } } else { self.state = .paused @@ -436,6 +521,7 @@ extension AVPlayerWrapper: AVPlayerObserverDelegate { self.state = .buffering } case .playing: + wasPlayingBeforeStall = false self.state = .playing @unknown default: break @@ -489,8 +575,12 @@ extension AVPlayerWrapper: AVPlayerItemObserverDelegate { // MARK: - AVPlayerItemObserverDelegate func item(didUpdatePlaybackLikelyToKeepUp playbackLikelyToKeepUp: Bool) { - if (playbackLikelyToKeepUp && state != .playing) { + if playbackLikelyToKeepUp && state != .playing { state = .ready + if wasPlayingBeforeStall { + wasPlayingBeforeStall = false + playWhenReady = true + } } } diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift index 09033395..abc5d885 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift @@ -46,11 +46,15 @@ protocol AVPlayerWrapperProtocol: AnyObject { func play() func pause() + + func recreatePlayer() + func clearAvPlayerQueue() + func togglePlaying() func stop() - + func seek(to seconds: TimeInterval) func seek(by offset: TimeInterval) @@ -64,4 +68,7 @@ protocol AVPlayerWrapperProtocol: AnyObject { func unload() func reload(startFromCurrentTime: Bool) + + func preloadNextTracks(_ url: URL) + } diff --git a/SwiftAudioEx/Classes/AudioItem.swift b/SwiftAudioEx/Classes/AudioItem.swift index 4b70dbda..1bfb0103 100755 --- a/SwiftAudioEx/Classes/AudioItem.swift +++ b/SwiftAudioEx/Classes/AudioItem.swift @@ -12,11 +12,13 @@ import UIKit public enum SourceType { case stream case file + case offline } public protocol AudioItem { - - func getSourceUrl() -> String + + var id: String? { get set } + func getSourceUrl(_ handler: @escaping (String) -> Void) func getArtist() -> String? func getTitle() -> String? func getAlbumTitle() -> String? @@ -43,7 +45,9 @@ public protocol AssetOptionsProviding { } public class DefaultAudioItem: AudioItem { - + + public var id: String? + public var audioUrl: String public var artist: String? @@ -65,8 +69,8 @@ public class DefaultAudioItem: AudioItem { self.artwork = artwork } - public func getSourceUrl() -> String { - audioUrl + public func getSourceUrl(_ handler: @escaping (String) -> Void) { + handler(audioUrl) } public func getArtist() -> String? { @@ -91,6 +95,8 @@ public class DefaultAudioItem: AudioItem { } +//extension AudioItem { } + /// An AudioItem that also conforms to the `TimePitching`-protocol public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching { diff --git a/SwiftAudioEx/Classes/AudioPlayer.swift b/SwiftAudioEx/Classes/AudioPlayer.swift index 08bbe826..84408d69 100755 --- a/SwiftAudioEx/Classes/AudioPlayer.swift +++ b/SwiftAudioEx/Classes/AudioPlayer.swift @@ -13,6 +13,8 @@ public typealias AudioPlayerState = AVPlayerWrapperState public class AudioPlayer: AVPlayerWrapperDelegate { /// The wrapper around the underlying AVPlayer let wrapper: AVPlayerWrapperProtocol = AVPlayerWrapper() + + public static var avPlayerRef: AVPlayer? = nil public let nowPlayingInfoController: NowPlayingInfoControllerProtocol public let remoteCommandController: RemoteCommandController @@ -162,7 +164,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate { - parameter item: The AudioItem to load. The info given in this item is the one used for the InfoCenter. - parameter playWhenReady: Optional, whether to start playback when the item is ready. */ - public func load(item: AudioItem, playWhenReady: Bool? = nil) { + public func load(item: AudioItem, playWhenReady: Bool? = nil, url: String? = nil) { currentItem = item if let playWhenReady = playWhenReady { @@ -181,9 +183,8 @@ public class AudioPlayer: AVPlayerWrapperDelegate { } enableRemoteCommands(forItem: item) - wrapper.load( - from: item.getSourceUrl(), + from: url ?? "", type: item.getSourceType(), playWhenReady: self.playWhenReady, initialTime: (item as? InitialTiming)?.getInitialTime(), @@ -198,6 +199,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate { wrapper.togglePlaying() } + public func recreatePlayer() { + wrapper.recreatePlayer() + } + /** Start playback */ @@ -298,7 +303,7 @@ public class AudioPlayer: AVPlayerWrapperDelegate { - Duration - Playback rate */ - func updateNowPlayingPlaybackValues() { + public func updateNowPlayingPlaybackValues() { nowPlayingInfoController.set(keyValues: [ MediaItemProperty.duration(wrapper.duration), NowPlayingInfoProperty.playbackRate(Double(wrapper.rate)), @@ -315,6 +320,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate { event.playbackEnd.emit(data: .cleared) } } + + public func clearAvPlayerQueue() { + wrapper.clearAvPlayerQueue() + } // MARK: - Private diff --git a/SwiftAudioEx/Classes/Event.swift b/SwiftAudioEx/Classes/Event.swift index 4be34f30..f64d1946 100644 --- a/SwiftAudioEx/Classes/Event.swift +++ b/SwiftAudioEx/Classes/Event.swift @@ -91,6 +91,9 @@ extension AudioPlayer { - Note: It is only fired for instances of a QueuedAudioPlayer. */ public let currentItem: AudioPlayer.Event = AudioPlayer.Event() + + // Queue item move event + public let onItemMoveEvent: AudioPlayer.Event<()> = AudioPlayer.Event() } public typealias EventClosure = (EventData) -> Void @@ -134,7 +137,9 @@ extension AudioPlayer { func emit(data: EventData) { queue.async { - self.invokers = self.invokers.filter { $0.invoke(data) } + DispatchQueue.main.async { + self.invokers = self.invokers.filter { $0.invoke(data) } + } } } } diff --git a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift index 4ba5415f..87dce17e 100644 --- a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift +++ b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift @@ -69,6 +69,9 @@ class AVPlayerItemObserver: NSObject { item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.duration, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.loadedTimeRanges, options: [.new], context: &AVPlayerItemObserver.context) item.addObserver(self, forKeyPath: AVPlayerItemKeyPath.playbackLikelyToKeepUp, options: [.new], context: &AVPlayerItemObserver.context) + if item.outputs.contains(metadataOutput) { + item.remove(metadataOutput) + } item.add(metadataOutput) } @@ -96,10 +99,10 @@ class AVPlayerItemObserver: NSObject { delegate?.item(didUpdateDuration: duration.seconds) } - case AVPlayerItemKeyPath.loadedTimeRanges: - if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration { - delegate?.item(didUpdateDuration: duration.seconds) - } + case AVPlayerItemKeyPath.loadedTimeRanges: break +// if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration { +// delegate?.item(didUpdateDuration: duration.seconds) +// } case AVPlayerItemKeyPath.playbackLikelyToKeepUp: if let playbackLikelyToKeepUp = change?[.newKey] as? Bool { diff --git a/SwiftAudioEx/Classes/Observer/AVPlayerObserver.swift b/SwiftAudioEx/Classes/Observer/AVPlayerObserver.swift index 1e0256e7..352bea77 100644 --- a/SwiftAudioEx/Classes/Observer/AVPlayerObserver.swift +++ b/SwiftAudioEx/Classes/Observer/AVPlayerObserver.swift @@ -33,10 +33,13 @@ class AVPlayerObserver: NSObject { private struct AVPlayerKeyPath { static let status = #keyPath(AVPlayer.status) static let timeControlStatus = #keyPath(AVPlayer.timeControlStatus) + static let currentItem = #keyPath(AVPlayer.currentItem) } private let statusChangeOptions: NSKeyValueObservingOptions = [.new, .initial] private let timeControlStatusChangeOptions: NSKeyValueObservingOptions = [.new] + private let currentItemChangeOptions: NSKeyValueObservingOptions = [.new, .initial] + private(set) var isObserving: Bool = false weak var delegate: AVPlayerObserverDelegate? @@ -50,51 +53,68 @@ class AVPlayerObserver: NSObject { stopObserving() } - /** - Start receiving events from this observer. - */ func startObserving() { - if (isObserving) { return }; - guard let player = player else { - return - } + if (isObserving) { return } + guard let player = player else { return } + isObserving = true + player.addObserver( self, forKeyPath: AVPlayerKeyPath.status, options: statusChangeOptions, context: &AVPlayerObserver.context ) + player.addObserver( self, forKeyPath: AVPlayerKeyPath.timeControlStatus, options: timeControlStatusChangeOptions, context: &AVPlayerObserver.context ) + + player.addObserver( + self, + forKeyPath: AVPlayerKeyPath.currentItem, + options: currentItemChangeOptions, + context: &AVPlayerObserver.context + ) } func stopObserving() { - guard let player = player, isObserving else { - return - } + guard let player = player, isObserving else { return } + player.removeObserver(self, forKeyPath: AVPlayerKeyPath.status, context: &AVPlayerObserver.context) player.removeObserver(self, forKeyPath: AVPlayerKeyPath.timeControlStatus, context: &AVPlayerObserver.context) + player.removeObserver(self, forKeyPath: AVPlayerKeyPath.currentItem, context: &AVPlayerObserver.context) + isObserving = false } - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { - guard context == &AVPlayerObserver.context, let observedKeyPath = keyPath else { + override func observeValue(forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey: Any]?, + context: UnsafeMutableRawPointer?) { + + guard context == &AVPlayerObserver.context, + let observedKeyPath = keyPath else { super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) return } switch observedKeyPath { + case AVPlayerKeyPath.status: handleStatusChange(change) case AVPlayerKeyPath.timeControlStatus: handleTimeControlStatusChange(change) + case AVPlayerKeyPath.currentItem: + if let player = player { + delegate?.player(didChangeTimeControlStatus: player.timeControlStatus) + } + default: break } @@ -111,9 +131,7 @@ class AVPlayerObserver: NSObject { } private func handleTimeControlStatusChange(_ change: [NSKeyValueChangeKey: Any]?) { - let status: AVPlayer.TimeControlStatus - if let statusNumber = change?[.newKey] as? NSNumber { - status = AVPlayer.TimeControlStatus(rawValue: statusNumber.intValue)! + if let statusNumber = change?[.newKey] as? NSNumber, let status = AVPlayer.TimeControlStatus(rawValue: statusNumber.intValue) { delegate?.player(didChangeTimeControlStatus: status) } } diff --git a/SwiftAudioEx/Classes/QueueManager.swift b/SwiftAudioEx/Classes/QueueManager.swift index 8d968a7b..ac4b22f3 100755 --- a/SwiftAudioEx/Classes/QueueManager.swift +++ b/SwiftAudioEx/Classes/QueueManager.swift @@ -11,6 +11,7 @@ protocol QueueManagerDelegate: AnyObject { func onReceivedFirstItem() func onCurrentItemChanged() func onSkippedToSameCurrentItem() + func onItemMoveEvent() } class QueueManager { @@ -258,6 +259,7 @@ class QueueManager { if (fromIndex == currentIndex) { currentIndex = toIndex; } + delegate?.onItemMoveEvent() } } @@ -286,6 +288,7 @@ class QueueManager { if (currentItemChanged) { delegate?.onCurrentItemChanged() } + delegate?.onItemMoveEvent() return result } @@ -322,6 +325,7 @@ class QueueManager { guard currentIndex > 0 else { return } items.removeSubrange(0.. { let nextIndex = currentIndex + 1 guard nextIndex < items.count else { return } items.removeSubrange(nextIndex..() fileprivate var lastIndex: Int = -1 fileprivate var lastItem: AudioItem? = nil + private var spuriousEndRetryCount = 0 public override init(nowPlayingInfoController: NowPlayingInfoControllerProtocol = NowPlayingInfoController(), remoteCommandController: RemoteCommandController = RemoteCommandController()) { super.init(nowPlayingInfoController: nowPlayingInfoController, remoteCommandController: remoteCommandController) @@ -23,7 +24,13 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { /// The repeat mode for the queue player. public var repeatMode: RepeatMode = .off - + + public var preloadingQueue = false + + public var isOfflineMode = false + + static var nextAudioItem = [AudioItem]() + public override var currentItem: AudioItem? { queue.current } @@ -37,6 +44,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { override public func clear() { queue.clearQueue() + clearAvPlayetQueue() super.clear() } @@ -67,7 +75,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { - parameter item: The AudioItem to replace the current item. - parameter playWhenReady: Optional, whether to start playback when the item is ready. */ - public override func load(item: AudioItem, playWhenReady: Bool? = nil) { + + public override func load(item: AudioItem, playWhenReady: Bool? = nil, url: String? = nil) { if let playWhenReady = playWhenReady { self.playWhenReady = playWhenReady } @@ -102,6 +111,17 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public func add(items: [AudioItem], at index: Int) throws { try queue.add(items, at: index) + if index == currentIndex + 1 { + updatePrefetchedSongs() + } + } + + public func add(items: [AudioItem], playingIndex: Int, seekTo rate: Double) throws { + playWhenReady = false + queue.add(items) + try queue.jump(to: playingIndex) + wrapper.seek(to: rate) + wrapper.pause() } /** @@ -110,7 +130,17 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public func next() { let lastIndex = currentIndex let playbackWasActive = wrapper.playbackActive; - _ = queue.next(wrap: repeatMode == .queue) + if isOfflineMode, let nextOffline = queue.nextItems.first( + where: { + $0.getSourceType() == .offline + }), let index = queue.items.firstIndex( + where: { + $0.id == nextOffline.id + }) { + _ = try? queue.jump(to: index) + } else { + _ = queue.next(wrap: repeatMode == .queue) + } if (playbackWasActive && lastIndex != currentIndex || repeatMode == .queue) { event.playbackEnd.emit(data: .skippedToNext) } @@ -167,6 +197,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { */ public func moveItem(fromIndex: Int, toIndex: Int) throws { try queue.moveItem(fromIndex: fromIndex, toIndex: toIndex) + if toIndex == currentIndex + 1 { + updatePrefetchedSongs() + } } /** @@ -184,32 +217,88 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { } func replay() { - seek(to: 0); - play() + guard let currentItem else { return } + clearAvPlayetQueue() + load(item: currentItem) + } + + public func setOfflineMode(_ isOn: Bool) { + self.isOfflineMode = isOn } // MARK: - AVPlayerWrapperDelegate + + override func AVWrapper(didChangeState state: AVPlayerWrapperState) { + super.AVWrapper(didChangeState: state) + if state == .playing { + spuriousEndRetryCount = 0 + } + if state == .loading { + if repeatMode == .queue && queue.nextItems.isEmpty { + self.queue.items.first?.getSourceUrl { url in + guard let preloadUrl = URL(string: url) else { return } + super.wrapper.preloadNextTracks(preloadUrl) + } + } else { + self.queue.nextItems.first?.getSourceUrl { url in + guard let preloadUrl = URL(string: url) else { return } + super.wrapper.preloadNextTracks(preloadUrl) + } + } + } + + } override func AVWrapperItemDidPlayToEndTime() { + let duration = wrapper.duration + let currentTime = wrapper.currentTime + + if duration > 0, currentTime < duration - 2.0 { + if spuriousEndRetryCount < 2 { + spuriousEndRetryCount += 1 + wrapper.reload(startFromCurrentTime: true) + } else { + // Reloading didn't recover — treat as failure and skip + spuriousEndRetryCount = 0 + event.playbackEnd.emit(data: .failed) + _ = queue.next(wrap: false) + } + return + } + + spuriousEndRetryCount = 0 event.playbackEnd.emit(data: .playedUntilEnd) if (repeatMode == .track) { - // quick workaround for race condition - place call bottom of call stack DispatchQueue.main.async { [weak self] in self?.replay() } } else if (repeatMode == .queue) { _ = queue.next(wrap: true) } else if (currentIndex != items.count - 1) { - _ = queue.next(wrap: false) + if isOfflineMode, let nextOffline = queue.nextItems.first( + where: { $0.getSourceType() == .offline }), + let index = queue.items.firstIndex(where: { $0.id == nextOffline.id }) { + _ = try? queue.jump(to: index) + } else { + _ = queue.next(wrap: false) + } } else { wrapper.state = .ended } } // MARK: - QueueManagerDelegate + + + func onItemMoveEvent() { + event.onItemMoveEvent.emit(data: ()) + } func onCurrentItemChanged() { + Self.nextAudioItem = nextItems let lastPosition = currentTime; - if let currentItem = currentItem { - super.load(item: currentItem) + if let currentItem = currentItem as? AudioItem { + currentItem.getSourceUrl { url in + super.load(item: currentItem, playWhenReady: !self.preloadingQueue, url: url) + } } else { super.clear() } @@ -235,4 +324,13 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { func onReceivedFirstItem() { try! queue.jump(to: 0) } + + func clearAvPlayetQueue() { + wrapper.clearAvPlayerQueue() + } + + public func updatePrefetchedSongs() { + Self.nextAudioItem = nextItems + (wrapper as? AVPlayerWrapper)?.prefetchNextTracks() + } }