Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8e0b643
- update audioItem protocol
parth-codamusic Aug 3, 2023
a6fb1d4
- remove private access of updateNowPlayingPlaybackValues
parth-codamusic Aug 8, 2023
c857479
- update audioItem protocol
parth-codamusic Aug 8, 2023
b069910
- add func recreatePlayer
parth-codamusic Apr 25, 2025
66ab91b
- add item move event
parth-codamusic Apr 25, 2025
d459d93
- add event for queue update
parth-codamusic Apr 28, 2025
3cddd63
Update QueueManager.swift
parth0072 Apr 28, 2025
bbecf46
- fix thread sync issue
parth-codamusic May 14, 2025
2d16af5
- add item function with seek position
parth-codamusic May 22, 2025
249ec19
- update logs
parth-codamusic May 25, 2025
8bcdc8a
- fix asset loading on main thread issue
parth-codamusic May 27, 2025
fa49103
- add preload bool
parth-codamusic May 27, 2025
e58f79a
- remove unnecessary preload data for streaming
parth-codamusic May 29, 2025
3f7968b
- handle fail for only playable
parth-codamusic May 29, 2025
f990bd3
- remove time range
parth-codamusic Jul 14, 2025
aff88a9
- fix compile issue
parth-codamusic Jul 14, 2025
fa9944b
- fix crash in startObserving
parth-codamusic Jul 23, 2025
bbf6953
- add offline mode in queue
parth-codamusic Aug 7, 2025
0002e5c
- preload next song
parth-codamusic Oct 9, 2025
a0400b2
- add clear avplayer queue function
parth-codamusic Oct 16, 2025
93f52f2
- fix wrong song play issue
parth-codamusic Oct 28, 2025
3ed92ad
- fix thread issue
parth-codamusic Oct 29, 2025
e165e6c
- update next preloaded song
bansi-coda Nov 18, 2025
5db0754
- update next preloaded song on queue reorder
bansi-coda Dec 8, 2025
86f36c4
- update next preloaded song
bansi-coda Nov 18, 2025
bf5c01c
Merge remote-tracking branch 'refs/remotes/origin/main'
bansi-coda Dec 9, 2025
e1b0552
- allow public access
bansi-coda Dec 9, 2025
2385722
- fix reply issue
parth-codamusic Jan 2, 2026
a08652f
- fix replay auto issue
parth-codamusic Feb 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 152 additions & 70 deletions SwiftAudioEx/Classes/AVPlayerWrapper/AVPlayerWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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())

Expand All @@ -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)
}
}
}
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -377,15 +458,15 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerItemNotificationObserver.stopObservingCurrentItem()
}

private func recreateAVPlayer() {
func recreateAVPlayer() {
playbackError = nil
playerTimeObserver.unregisterForBoundaryTimeEvents()
playerTimeObserver.unregisterForPeriodicEvents()
playerObserver.stopObserving()
stopObservingAVPlayerItem()
clearCurrentItem()

avPlayer = AVPlayer();
avPlayer = AVQueuePlayer();
setupAVPlayer()

delegate?.AVWrapperDidRecreateAVPlayer()
Expand All @@ -403,6 +484,7 @@ class AVPlayerWrapper: AVPlayerWrapperProtocol {
playerTimeObserver.registerForPeriodicTimeEvents()

applyAVPlayerRate()
avPlayer.actionAtItemEnd = .none
}

private func applyAVPlayerRate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -64,4 +68,7 @@ protocol AVPlayerWrapperProtocol: AnyObject {
func unload()

func reload(startFromCurrentTime: Bool)

func preloadNextTracks(_ url: URL)

}
16 changes: 11 additions & 5 deletions SwiftAudioEx/Classes/AudioItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -43,7 +45,9 @@ public protocol AssetOptionsProviding {
}

public class DefaultAudioItem: AudioItem {


public var id: String?

public var audioUrl: String

public var artist: String?
Expand All @@ -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? {
Expand All @@ -91,6 +95,8 @@ public class DefaultAudioItem: AudioItem {

}

//extension AudioItem { }

/// An AudioItem that also conforms to the `TimePitching`-protocol
public class DefaultAudioItemTimePitching: DefaultAudioItem, TimePitching {

Expand Down
Loading