From 8e0b643bc47c2e8b4f0fba009b4f16bae0e9ae5a Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 3 Aug 2023 19:24:46 +0530 Subject: [PATCH 01/28] - update audioItem protocol --- SwiftAudioEx/Classes/AudioItem.swift | 8 +++++--- SwiftAudioEx/Classes/AudioPlayer.swift | 5 ++--- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 9 ++++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/SwiftAudioEx/Classes/AudioItem.swift b/SwiftAudioEx/Classes/AudioItem.swift index 4b70dbd..055a466 100755 --- a/SwiftAudioEx/Classes/AudioItem.swift +++ b/SwiftAudioEx/Classes/AudioItem.swift @@ -16,7 +16,7 @@ public enum SourceType { public protocol AudioItem { - func getSourceUrl() -> String + func getSourceUrl(_ handler: @escaping (String) -> Void) func getArtist() -> String? func getTitle() -> String? func getAlbumTitle() -> String? @@ -65,8 +65,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 +91,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 08bbe82..801e58f 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(), diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 18142b2..604d1ea 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -67,7 +67,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 } @@ -208,8 +209,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { func onCurrentItemChanged() { 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, url: url) + } } else { super.clear() } From a6fb1d436c127857c65f5972d443cd073b67d800 Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 8 Aug 2023 11:40:50 +0530 Subject: [PATCH 02/28] - remove private access of updateNowPlayingPlaybackValues --- SwiftAudioEx/Classes/AudioPlayer.swift | 2 +- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SwiftAudioEx/Classes/AudioPlayer.swift b/SwiftAudioEx/Classes/AudioPlayer.swift index 801e58f..daff22e 100755 --- a/SwiftAudioEx/Classes/AudioPlayer.swift +++ b/SwiftAudioEx/Classes/AudioPlayer.swift @@ -297,7 +297,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)), diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 604d1ea..6fcca8b 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -211,7 +211,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in - super.load(item: currentItem, url: url) + super.load(item: currentItem, playWhenReady: true, url: url) } } else { super.clear() From c8574790871759703b4f09c714dcdbe9e30a13f7 Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 8 Aug 2023 18:26:16 +0530 Subject: [PATCH 03/28] - update audioItem protocol --- SwiftAudioEx/Classes/AudioItem.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/SwiftAudioEx/Classes/AudioItem.swift b/SwiftAudioEx/Classes/AudioItem.swift index 055a466..cd31bf0 100755 --- a/SwiftAudioEx/Classes/AudioItem.swift +++ b/SwiftAudioEx/Classes/AudioItem.swift @@ -15,7 +15,8 @@ public enum SourceType { } public protocol AudioItem { - + + var id: String? { get set } func getSourceUrl(_ handler: @escaping (String) -> Void) func getArtist() -> String? func getTitle() -> String? @@ -43,7 +44,9 @@ public protocol AssetOptionsProviding { } public class DefaultAudioItem: AudioItem { - + + public var id: String? + public var audioUrl: String public var artist: String? From b06991045599db632e67374af1246cc809a0c907 Mon Sep 17 00:00:00 2001 From: Parth Date: Fri, 25 Apr 2025 10:09:46 +0530 Subject: [PATCH 04/28] - add func recreatePlayer --- .../Classes/AVPlayerWrapper/AVPlayerWrapper.swift | 10 +++++++--- .../AVPlayerWrapper/AVPlayerWrapperProtocol.swift | 6 ++++-- SwiftAudioEx/Classes/AudioPlayer.swift | 4 ++++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index a3e1f22..7f1eb0f 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -20,7 +20,7 @@ public enum PlaybackEndedReason: String { case failed } -class AVPlayerWrapper: AVPlayerWrapperProtocol { +public class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties fileprivate var avPlayer = AVPlayer() @@ -174,7 +174,11 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { func pause() { playWhenReady = false } - + + func recreatePlayer() { + recreateAVPlayer() + } + func togglePlaying() { switch avPlayer.timeControlStatus { case .playing, .waitingToPlayAtSpecifiedRate: @@ -377,7 +381,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol { playerItemNotificationObserver.stopObservingCurrentItem() } - private func recreateAVPlayer() { + func recreateAVPlayer() { playbackError = nil playerTimeObserver.unregisterForBoundaryTimeEvents() playerTimeObserver.unregisterForPeriodicEvents() diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift index 0903339..b927e62 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift @@ -46,11 +46,13 @@ protocol AVPlayerWrapperProtocol: AnyObject { func play() func pause() - + + func recreatePlayer() + func togglePlaying() func stop() - + func seek(to seconds: TimeInterval) func seek(by offset: TimeInterval) diff --git a/SwiftAudioEx/Classes/AudioPlayer.swift b/SwiftAudioEx/Classes/AudioPlayer.swift index daff22e..7e4f63a 100755 --- a/SwiftAudioEx/Classes/AudioPlayer.swift +++ b/SwiftAudioEx/Classes/AudioPlayer.swift @@ -197,6 +197,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate { wrapper.togglePlaying() } + public func recreatePlayer() { + wrapper.recreatePlayer() + } + /** Start playback */ From 66ab91b3b9824e14198b0ef768e108b2ba18673f Mon Sep 17 00:00:00 2001 From: Parth Date: Fri, 25 Apr 2025 18:20:14 +0530 Subject: [PATCH 05/28] - add item move event --- SwiftAudioEx/Classes/Event.swift | 3 +++ SwiftAudioEx/Classes/QueueManager.swift | 2 ++ SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/SwiftAudioEx/Classes/Event.swift b/SwiftAudioEx/Classes/Event.swift index 4be34f3..d09809d 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 diff --git a/SwiftAudioEx/Classes/QueueManager.swift b/SwiftAudioEx/Classes/QueueManager.swift index 8d968a7..1db6330 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() } } diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 6fcca8b..7df9a5a 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -207,6 +207,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { // MARK: - QueueManagerDelegate + func onItemMoveEvent() { + event.onItemMoveEvent.emit(data: ()) + } + func onCurrentItemChanged() { let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { From d459d93281ad950186fc7bdcb1bfba589bf972aa Mon Sep 17 00:00:00 2001 From: Parth Date: Mon, 28 Apr 2025 14:11:39 +0530 Subject: [PATCH 06/28] - add event for queue update --- SwiftAudioEx/Classes/QueueManager.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SwiftAudioEx/Classes/QueueManager.swift b/SwiftAudioEx/Classes/QueueManager.swift index 1db6330..28a22e9 100755 --- a/SwiftAudioEx/Classes/QueueManager.swift +++ b/SwiftAudioEx/Classes/QueueManager.swift @@ -288,6 +288,7 @@ class QueueManager { if (currentItemChanged) { delegate?.onCurrentItemChanged() } + delegate?.onItemMoveEvent() return result } @@ -324,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.. { if (currentItemChanged) { delegate?.onCurrentItemChanged() } + delegate?.onItemMoveEvent() } } From 3cddd6302acc7bd23ccd7c58a5adb6cb16830d63 Mon Sep 17 00:00:00 2001 From: Parth Kshatriya Date: Mon, 28 Apr 2025 14:40:10 +0530 Subject: [PATCH 07/28] Update QueueManager.swift --- SwiftAudioEx/Classes/QueueManager.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/SwiftAudioEx/Classes/QueueManager.swift b/SwiftAudioEx/Classes/QueueManager.swift index 28a22e9..ac4b22f 100755 --- a/SwiftAudioEx/Classes/QueueManager.swift +++ b/SwiftAudioEx/Classes/QueueManager.swift @@ -357,7 +357,6 @@ class QueueManager { if (currentItemChanged) { delegate?.onCurrentItemChanged() } - delegate?.onItemMoveEvent() } } From bbecf46047271120d2794535245a88e4feb860e5 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 14 May 2025 13:51:50 +0530 Subject: [PATCH 08/28] - fix thread sync issue --- .../Classes/AVPlayerWrapper/AVPlayerWrapper.swift | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 7f1eb0f..6620871 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -56,20 +56,17 @@ public 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) + } } } } From 2d16af5398cd466fe1d02c98fa05959e7089f4ce Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 22 May 2025 11:17:55 +0530 Subject: [PATCH 09/28] - add item function with seek position --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 7df9a5a..093415d 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -104,6 +104,14 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public func add(items: [AudioItem], at index: Int) throws { try queue.add(items, at: index) } + + 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() + } /** Step to the next item in the queue. @@ -215,7 +223,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in - super.load(item: currentItem, playWhenReady: true, url: url) + super.load(item: currentItem, playWhenReady: self.playWhenReady, url: url) } } else { super.clear() From 249ec1986d75b9591af864e6b73bca277a1e7cd2 Mon Sep 17 00:00:00 2001 From: Parth Date: Sun, 25 May 2025 17:28:16 +0530 Subject: [PATCH 10/28] - update logs --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 093415d..32fd49a 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -223,7 +223,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in - super.load(item: currentItem, playWhenReady: self.playWhenReady, url: url) + super.load(item: currentItem, playWhenReady: true, url: url) } } else { super.clear() From 8bcdc8a12e59e8c15aca310762dc04aa26eb1f94 Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 27 May 2025 11:05:43 +0530 Subject: [PATCH 11/28] - fix asset loading on main thread issue --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 133 ++++++++++-------- 1 file changed, 75 insertions(+), 58 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 6620871..0d2700d 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -229,70 +229,87 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { } func load() { - if (state == .failed) { + if state == .failed { recreateAVPlayer() } else { clearCurrentItem() } - 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 - guard let self = self else { return } - - 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) + + guard let url = url else { return } + + let pendingAsset = AVURLAsset(url: url, options: urlOptions) + asset = pendingAsset + state = .loading + + let keys: [String] = [ + "playable", + "availableChapterLocales", + "availableMetadataFormats", + "commonMetadata", + "duration" + ] + + pendingAsset.loadValuesAsynchronously(forKeys: keys) { [weak self] in + guard let self = self else { return } + DispatchQueue.main.async { + 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 } } - }) + 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 + self.avPlayer.replaceCurrentItem(with: item) + 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 { + 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 initialTime = self.timeToSeekToAfterLoading { + self.timeToSeekToAfterLoading = nil + self.seek(to: initialTime) } } From fa491036868d335be5f11b3b201c29246e977d85 Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 27 May 2025 16:39:25 +0530 Subject: [PATCH 12/28] - add preload bool --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 32fd49a..ad2c05d 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -23,6 +23,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { /// The repeat mode for the queue player. public var repeatMode: RepeatMode = .off + + public var preloadingQueue = false public override var currentItem: AudioItem? { queue.current @@ -223,7 +225,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in - super.load(item: currentItem, playWhenReady: true, url: url) + super.load(item: currentItem, playWhenReady: !self.preloadingQueue, url: url) } } else { super.clear() From e58f79abe4364171faa5284cb8c8e68877a859c6 Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 29 May 2025 11:08:07 +0530 Subject: [PATCH 13/28] - remove unnecessary preload data for streaming --- SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 0d2700d..9e8ef30 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -242,11 +242,7 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { state = .loading let keys: [String] = [ - "playable", - "availableChapterLocales", - "availableMetadataFormats", - "commonMetadata", - "duration" + "playable" ] pendingAsset.loadValuesAsynchronously(forKeys: keys) { [weak self] in From 3f7968b511a960e40cf72b65d99ac18c241c053f Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 29 May 2025 17:28:36 +0530 Subject: [PATCH 14/28] - handle fail for only playable --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 9e8ef30..d823bed 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -240,27 +240,27 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { let pendingAsset = AVURLAsset(url: url, options: urlOptions) asset = pendingAsset state = .loading - + let keys: [String] = [ - "playable" + "playable", + "availableChapterLocales", + "availableMetadataFormats", + "commonMetadata", + "duration" ] pendingAsset.loadValuesAsynchronously(forKeys: keys) { [weak self] in guard let self = self else { return } DispatchQueue.main.async { - 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 - } + 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) } From f990bd3da09bd6d3d44e9e3ab2b8ede2c3999b04 Mon Sep 17 00:00:00 2001 From: Parth Date: Mon, 14 Jul 2025 16:02:19 +0530 Subject: [PATCH 15/28] - remove time range --- SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift index 4ba5415..0900734 100644 --- a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift +++ b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift @@ -97,9 +97,9 @@ class AVPlayerItemObserver: NSObject { } case AVPlayerItemKeyPath.loadedTimeRanges: - if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration { - delegate?.item(didUpdateDuration: duration.seconds) - } +// 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 { From aff88a96b50bb0c4d3d42ff0f2d3480a859fa3d6 Mon Sep 17 00:00:00 2001 From: Parth Date: Mon, 14 Jul 2025 16:03:43 +0530 Subject: [PATCH 16/28] - fix compile issue --- SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift index 0900734..7f315fa 100644 --- a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift +++ b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift @@ -96,7 +96,7 @@ class AVPlayerItemObserver: NSObject { delegate?.item(didUpdateDuration: duration.seconds) } - case AVPlayerItemKeyPath.loadedTimeRanges: + case AVPlayerItemKeyPath.loadedTimeRanges: break // if let ranges = change?[.newKey] as? [NSValue], let duration = ranges.first?.timeRangeValue.duration { // delegate?.item(didUpdateDuration: duration.seconds) // } From fa9944b836e1ebc578a98b4f12ceae2b41744fa1 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 23 Jul 2025 10:16:15 +0530 Subject: [PATCH 17/28] - fix crash in startObserving --- SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift b/SwiftAudioEx/Classes/Observer/AVPlayerItemObserver.swift index 7f315fa..87dce17 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) } From bbf695307bc03d8d124e8faa510c3a2c5860ceb6 Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 7 Aug 2025 15:46:48 +0530 Subject: [PATCH 18/28] - add offline mode in queue --- SwiftAudioEx/Classes/AudioItem.swift | 1 + SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 32 ++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/SwiftAudioEx/Classes/AudioItem.swift b/SwiftAudioEx/Classes/AudioItem.swift index cd31bf0..1bfb010 100755 --- a/SwiftAudioEx/Classes/AudioItem.swift +++ b/SwiftAudioEx/Classes/AudioItem.swift @@ -12,6 +12,7 @@ import UIKit public enum SourceType { case stream case file + case offline } public protocol AudioItem { diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index ad2c05d..f682c37 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -25,7 +25,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public var repeatMode: RepeatMode = .off public var preloadingQueue = false - + + public var isOfflineMode = false + public override var currentItem: AudioItem? { queue.current } @@ -121,7 +123,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) } @@ -198,6 +210,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { seek(to: 0); play() } + + public func setOfflineMode(_ isOn: Bool) { + self.isOfflineMode = isOn + } // MARK: - AVPlayerWrapperDelegate @@ -209,7 +225,17 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { } 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 } From 0002e5c38f6c7d4caa011aa2a1d72589a78cea5b Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 9 Oct 2025 13:16:18 +0530 Subject: [PATCH 19/28] - preload next song --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 66 ++++++++++++++++--- .../AVPlayerWrapperProtocol.swift | 3 + SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 4 ++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index d823bed..9d8d5af 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -23,7 +23,7 @@ public enum PlaybackEndedReason: String { public class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties - fileprivate var avPlayer = AVPlayer() + fileprivate var avPlayer = AVQueuePlayer() private let playerObserver = AVPlayerObserver() internal let playerTimeObserver: AVPlayerTimeObserver private let playerItemNotificationObserver = AVPlayerItemNotificationObserver() @@ -37,7 +37,9 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { label: "AVPlayerWrapper.stateQueue", attributes: .concurrent ) - + + private var nextAsset: AVAsset? + public init() { playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime()) @@ -231,16 +233,27 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { func load() { if state == .failed { recreateAVPlayer() - } else { - clearCurrentItem() } - + if let asset { + asset.cancelLoading() + self.asset = nil + } + self.stopObservingAVPlayerItem() + nextAsset?.cancelLoading() guard let url = url else { return } - + state = .loading + if let item = avPlayer.items().first { ($0.asset as? AVURLAsset)?.url == url } { + self.item = item + self.avPlayer.advanceToNextItem() + self.applyAVPlayerRate() + self.asset = item.asset + self.startObservingAVPlayer(item: item) + self.applyAVPlayerRate() + return; + } let pendingAsset = AVURLAsset(url: url, options: urlOptions) asset = pendingAsset - state = .loading - + self.clearPreloadedTracks() let keys: [String] = [ "playable", "availableChapterLocales", @@ -286,7 +299,8 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { let item = AVPlayerItem(asset: pendingAsset, automaticallyLoadedAssetKeys: keysToLoad) self.item = item item.preferredForwardBufferDuration = self.bufferDuration - self.avPlayer.replaceCurrentItem(with: item) + self.avPlayer.removeAllItems() + self.avPlayer.insert(item, after: nil) self.startObservingAVPlayer(item: item) self.applyAVPlayerRate() @@ -309,6 +323,38 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { } } + func preloadNextTracks(_ url: URL) { + guard !avPlayer.items().contains(where: { ($0.asset as? AVURLAsset)?.url == url} ) 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 { + let lastItem = self.avPlayer.items().last + self.avPlayer.insert(item, after: lastItem) + } + } + } + + func clearPreloadedTracks() { + avPlayer.removeAllItems() + } + func load(from url: URL, playWhenReady: Bool, options: [String: Any]? = nil) { self.playWhenReady = playWhenReady self.url = url @@ -399,7 +445,7 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { stopObservingAVPlayerItem() clearCurrentItem() - avPlayer = AVPlayer(); + avPlayer = AVQueuePlayer(); setupAVPlayer() delegate?.AVWrapperDidRecreateAVPlayer() diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift index b927e62..0cac80a 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift @@ -66,4 +66,7 @@ protocol AVPlayerWrapperProtocol: AnyObject { func unload() func reload(startFromCurrentTime: Bool) + + func preloadNextTracks(_ url: URL) + } diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index f682c37..e851a62 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -252,6 +252,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in super.load(item: currentItem, playWhenReady: !self.preloadingQueue, url: url) + self.queue.nextItems.first?.getSourceUrl { url in + guard let preloadUrl = URL(string: url) else { return } + super.wrapper.preloadNextTracks(preloadUrl) + } } } else { super.clear() From a0400b2592d623f1b92628f8f450399406b9eb2b Mon Sep 17 00:00:00 2001 From: Parth Date: Thu, 16 Oct 2025 14:16:53 +0530 Subject: [PATCH 20/28] - add clear avplayer queue function --- SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift | 7 +++++-- .../Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift | 2 ++ SwiftAudioEx/Classes/AudioPlayer.swift | 4 ++++ SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 4 ++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 9d8d5af..340435a 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -23,7 +23,7 @@ public enum PlaybackEndedReason: String { public class AVPlayerWrapper: AVPlayerWrapperProtocol { // MARK: - Properties - fileprivate var avPlayer = AVQueuePlayer() + var avPlayer = AVQueuePlayer() private let playerObserver = AVPlayerObserver() internal let playerTimeObserver: AVPlayerTimeObserver private let playerItemNotificationObserver = AVPlayerItemNotificationObserver() @@ -177,6 +177,10 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { func recreatePlayer() { recreateAVPlayer() } + + func clearAvPlayerQueue() { + avPlayer.removeAllItems() + } func togglePlaying() { switch avPlayer.timeControlStatus { @@ -303,7 +307,6 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { self.avPlayer.insert(item, after: nil) self.startObservingAVPlayer(item: item) self.applyAVPlayerRate() - if !pendingAsset.availableChapterLocales.isEmpty { for locale in pendingAsset.availableChapterLocales { let chapters = pendingAsset.chapterMetadataGroups(withTitleLocale: locale, containingItemsWithCommonKeys: nil) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift index 0cac80a..abc5d88 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapperProtocol.swift @@ -48,6 +48,8 @@ protocol AVPlayerWrapperProtocol: AnyObject { func pause() func recreatePlayer() + + func clearAvPlayerQueue() func togglePlaying() diff --git a/SwiftAudioEx/Classes/AudioPlayer.swift b/SwiftAudioEx/Classes/AudioPlayer.swift index 7e4f63a..3759855 100755 --- a/SwiftAudioEx/Classes/AudioPlayer.swift +++ b/SwiftAudioEx/Classes/AudioPlayer.swift @@ -318,6 +318,10 @@ public class AudioPlayer: AVPlayerWrapperDelegate { event.playbackEnd.emit(data: .cleared) } } + + public func clearAvPlayerQueue() { + wrapper.clearAvPlayerQueue() + } // MARK: - Private diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index e851a62..41507dc 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -282,4 +282,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { func onReceivedFirstItem() { try! queue.jump(to: 0) } + + func clearAvPlayetQueue() { + wrapper.clearAvPlayerQueue() + } } From 93f52f24ca2401d4ea325dfe8fe66dc7c0595e7a Mon Sep 17 00:00:00 2001 From: Parth Date: Tue, 28 Oct 2025 13:31:09 +0530 Subject: [PATCH 21/28] - fix wrong song play issue --- .../AVPlayerWrapper/AVPlayerWrapper.swift | 70 ++++++++++++------- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 20 ++++-- 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 340435a..0165972 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -39,6 +39,7 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { ) private var nextAsset: AVAsset? + private var nextPreloadUrl: URL? public init() { playerTimeObserver = AVPlayerTimeObserver(periodicObserverTimeInterval: timeEventFrequency.getTime()) @@ -246,18 +247,21 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { nextAsset?.cancelLoading() guard let url = url else { return } state = .loading - if let item = avPlayer.items().first { ($0.asset as? AVURLAsset)?.url == url } { + 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 - self.clearPreloadedTracks() let keys: [String] = [ "playable", "availableChapterLocales", @@ -303,8 +307,10 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { 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 { @@ -326,32 +332,44 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { } } - func preloadNextTracks(_ url: URL) { - guard !avPlayer.items().contains(where: { ($0.asset as? AVURLAsset)?.url == url} ) 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 + func prefetchNextTracks() { + QueuedAudioPlayer.nextAudioItem.first?.getSourceUrl({ [weak self] urlString 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 { - let lastItem = self.avPlayer.items().last - self.avPlayer.insert(item, after: lastItem) + 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 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() { diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 41507dc..e4b8c2a 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -28,6 +28,8 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public var isOfflineMode = false + static var nextAudioItem = [AudioItem]() + public override var currentItem: AudioItem? { queue.current } @@ -41,6 +43,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { override public func clear() { queue.clearQueue() + clearAvPlayetQueue() super.clear() } @@ -216,6 +219,17 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { } // MARK: - AVPlayerWrapperDelegate + + override func AVWrapper(didChangeState state: AVPlayerWrapperState) { + super.AVWrapper(didChangeState: state) + if state == .loading { + self.queue.nextItems.first?.getSourceUrl { url in + guard let preloadUrl = URL(string: url) else { return } + super.wrapper.preloadNextTracks(preloadUrl) + } + } + + } override func AVWrapperItemDidPlayToEndTime() { event.playbackEnd.emit(data: .playedUntilEnd) @@ -242,20 +256,18 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { } // MARK: - QueueManagerDelegate + func onItemMoveEvent() { event.onItemMoveEvent.emit(data: ()) } func onCurrentItemChanged() { + Self.nextAudioItem = nextItems let lastPosition = currentTime; if let currentItem = currentItem as? AudioItem { currentItem.getSourceUrl { url in super.load(item: currentItem, playWhenReady: !self.preloadingQueue, url: url) - self.queue.nextItems.first?.getSourceUrl { url in - guard let preloadUrl = URL(string: url) else { return } - super.wrapper.preloadNextTracks(preloadUrl) - } } } else { super.clear() From 3ed92ad1d287ba82274d0f48fb19cf996790da81 Mon Sep 17 00:00:00 2001 From: Parth Date: Wed, 29 Oct 2025 11:01:17 +0530 Subject: [PATCH 22/28] - fix thread issue --- SwiftAudioEx/Classes/Event.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/Event.swift b/SwiftAudioEx/Classes/Event.swift index d09809d..f64d194 100644 --- a/SwiftAudioEx/Classes/Event.swift +++ b/SwiftAudioEx/Classes/Event.swift @@ -137,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) } + } } } } From e165e6c078ad4d9b3d7e6cb39018233f5c0f20b3 Mon Sep 17 00:00:00 2001 From: bansi-coda <154321082+bansi-coda@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:00:48 +0530 Subject: [PATCH 23/28] - update next preloaded song --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index e4b8c2a..8b7b89c 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -110,6 +110,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { public func add(items: [AudioItem], at index: Int) throws { try queue.add(items, at: index) + if index == currentIndex + 1 { + Self.nextAudioItem = nextItems + (wrapper as? AVPlayerWrapper)?.prefetchNextTracks() + } } public func add(items: [AudioItem], playingIndex: Int, seekTo rate: Double) throws { From 5db07548fe18f27a59f87f339fc95e8d7716048d Mon Sep 17 00:00:00 2001 From: bansi-coda <154321082+bansi-coda@users.noreply.github.com> Date: Mon, 8 Dec 2025 14:00:50 +0530 Subject: [PATCH 24/28] - update next preloaded song on queue reorder --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 8b7b89c..40162b7 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -1,3 +1,4 @@ + // // QueuedAudioPlayer.swift // SwiftAudio @@ -197,6 +198,10 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { */ public func moveItem(fromIndex: Int, toIndex: Int) throws { try queue.moveItem(fromIndex: fromIndex, toIndex: toIndex) + if toIndex == currentIndex + 1 { + Self.nextAudioItem = nextItems + (wrapper as? AVPlayerWrapper)?.prefetchNextTracks() + } } /** From 86f36c4250eb30d2a5d5967cb632b43213fca3f8 Mon Sep 17 00:00:00 2001 From: bansi-coda <154321082+bansi-coda@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:00:48 +0530 Subject: [PATCH 25/28] - update next preloaded song --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index e4b8c2a..f8a22bd 100755 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -110,6 +110,9 @@ 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 { @@ -193,6 +196,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() + } } /** @@ -298,4 +304,9 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { func clearAvPlayetQueue() { wrapper.clearAvPlayerQueue() } + + func updatePrefetchedSongs() { + Self.nextAudioItem = nextItems + (wrapper as? AVPlayerWrapper)?.prefetchNextTracks() + } } From e1b055247648bd859093ba563d9369495147dd3c Mon Sep 17 00:00:00 2001 From: mbansi Date: Tue, 9 Dec 2025 14:14:41 +0530 Subject: [PATCH 26/28] - allow public access --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index f8a22bd..6bcbe3c 100644 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -305,7 +305,7 @@ public class QueuedAudioPlayer: AudioPlayer, QueueManagerDelegate { wrapper.clearAvPlayerQueue() } - func updatePrefetchedSongs() { + public func updatePrefetchedSongs() { Self.nextAudioItem = nextItems (wrapper as? AVPlayerWrapper)?.prefetchNextTracks() } From 23857228cfd80adedf6f8ef834af196bde8245c0 Mon Sep 17 00:00:00 2001 From: Parth Date: Fri, 2 Jan 2026 11:52:27 +0530 Subject: [PATCH 27/28] - fix reply issue --- SwiftAudioEx/Classes/QueuedAudioPlayer.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift index 6bcbe3c..825fd7b 100644 --- a/SwiftAudioEx/Classes/QueuedAudioPlayer.swift +++ b/SwiftAudioEx/Classes/QueuedAudioPlayer.swift @@ -216,8 +216,9 @@ 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) { From a08652fedeb859cf0c08002f282ba02aa7146dba Mon Sep 17 00:00:00 2001 From: Parth Date: Mon, 9 Feb 2026 18:11:48 +0530 Subject: [PATCH 28/28] - fix replay auto issue --- SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift index 0165972..ac6b309 100755 --- a/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift +++ b/SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift @@ -484,6 +484,7 @@ public class AVPlayerWrapper: AVPlayerWrapperProtocol { playerTimeObserver.registerForPeriodicTimeEvents() applyAVPlayerRate() + avPlayer.actionAtItemEnd = .none } private func applyAVPlayerRate() {