diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index a3e1f22a..ac6b3091 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,10 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { label: "AVPlayerWrapper.stateQueue", attributes: .concurrent ) - + + private var nextAsset: AVAsset? + private var nextPreloadUrl: URL? + public init() { playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime()) @@ -56,20 +59,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 +174,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 +236,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 +458,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerItemNotificationObserver.stopObservingCurrentItem() } - private func recreateAVPlayer() { + func recreateAVPlayer() { playbackError = nil playerTimeObserver.unregisterForBoundaryTimeEvents() playerTimeObserver.unregisterForPeriodicEvents() @@ -385,7 +466,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { stopObservingAVPlayerItem() clearCurrentItem() - avPlayer = AVPlayer(); + avPlayer = AVQueuePlayer(); setupAVPlayer() delegate?.AVWrapperDidRecreateAVPlayer() @@ -403,6 +484,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerTimeObserver.registerForPeriodicTimeEvents() applyAVPlayerRate() + avPlayer.actionAtItemEnd = .none } private func applyAVPlayerRate() { 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..37598556 100755 --- a/SwiftAudioEx/Classes/AudioPlayer.swift +++ b/SwiftAudioEx/Classes/AudioPlayer.swift @@ -162,7 +162,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 +181,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 +197,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate { wrapper.togglePlaying() } + public func recreatePlayer() { + wrapper.recreatePlayer() + } + /** Start playback */ @@ -298,7 +301,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 +318,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/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..