react-native-nitro-player 0.3.0-alpha.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +444 -4
- package/android/build.gradle +4 -1
- package/android/src/main/AndroidManifest.xml +16 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +8 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +225 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +105 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +6 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +37 -12
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +970 -213
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +475 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadFileManager.kt +159 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadManagerCore.kt +489 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadWorker.kt +209 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +486 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaBrowserService.kt +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +14 -6
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +27 -0
- package/ios/HybridDownloadManager.swift +226 -0
- package/ios/HybridEqualizer.swift +111 -0
- package/ios/HybridTrackPlayer.swift +36 -8
- package/ios/core/TrackPlayerCore.swift +998 -276
- package/ios/download/DownloadDatabase.swift +493 -0
- package/ios/download/DownloadFileManager.swift +241 -0
- package/ios/download/DownloadManagerCore.swift +923 -0
- package/ios/equalizer/EqualizerCore.swift +685 -0
- package/ios/media/MediaSessionManager.swift +40 -28
- package/ios/playlist/PlaylistManager.swift +40 -9
- package/ios/queue/HybridPlayerQueue.swift +33 -13
- package/lib/hooks/downloadCallbackManager.d.ts +36 -0
- package/lib/hooks/downloadCallbackManager.js +108 -0
- package/lib/hooks/equalizerCallbackManager.d.ts +37 -0
- package/lib/hooks/equalizerCallbackManager.js +109 -0
- package/lib/hooks/index.d.ts +16 -0
- package/lib/hooks/index.js +10 -0
- package/lib/hooks/useActualQueue.d.ts +48 -0
- package/lib/hooks/useActualQueue.js +98 -0
- package/lib/hooks/useDownloadActions.d.ts +26 -0
- package/lib/hooks/useDownloadActions.js +117 -0
- package/lib/hooks/useDownloadProgress.d.ts +25 -0
- package/lib/hooks/useDownloadProgress.js +79 -0
- package/lib/hooks/useDownloadStorage.d.ts +19 -0
- package/lib/hooks/useDownloadStorage.js +60 -0
- package/lib/hooks/useDownloadedTracks.d.ts +25 -0
- package/lib/hooks/useDownloadedTracks.js +69 -0
- package/lib/hooks/useEqualizer.d.ts +25 -0
- package/lib/hooks/useEqualizer.js +124 -0
- package/lib/hooks/useEqualizerPresets.d.ts +22 -0
- package/lib/hooks/useEqualizerPresets.js +96 -0
- package/lib/hooks/useNowPlaying.js +3 -2
- package/lib/hooks/useOnChangeTrack.js +15 -12
- package/lib/hooks/useOnPlaybackStateChange.js +16 -13
- package/lib/hooks/usePlaylist.d.ts +48 -0
- package/lib/hooks/usePlaylist.js +136 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/specs/DownloadManager.nitro.d.ts +152 -0
- package/lib/specs/DownloadManager.nitro.js +1 -0
- package/lib/specs/Equalizer.nitro.d.ts +43 -0
- package/lib/specs/Equalizer.nitro.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +6 -2
- package/lib/types/DownloadTypes.d.ts +110 -0
- package/lib/types/DownloadTypes.js +1 -0
- package/lib/types/EqualizerTypes.d.ts +52 -0
- package/lib/types/EqualizerTypes.js +1 -0
- package/lib/types/PlayerQueue.d.ts +4 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/NitroPlayer+autolinking.cmake +10 -1
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +32 -2
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
- package/nitrogen/generated/android/c++/JDownloadConfig.hpp +92 -0
- package/nitrogen/generated/android/c++/JDownloadError.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +74 -0
- package/nitrogen/generated/android/c++/JDownloadProgress.hpp +79 -0
- package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +81 -0
- package/nitrogen/generated/android/c++/JDownloadState.hpp +71 -0
- package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +73 -0
- package/nitrogen/generated/android/c++/JDownloadTask.hpp +108 -0
- package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +111 -0
- package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +92 -0
- package/nitrogen/generated/android/c++/JEqualizerBand.hpp +69 -0
- package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +78 -0
- package/nitrogen/generated/android/c++/JEqualizerState.hpp +91 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +80 -0
- package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +89 -0
- package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +81 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +83 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +97 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -0
- package/nitrogen/generated/android/c++/JGainRange.hpp +61 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +470 -0
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +99 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +204 -0
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +82 -0
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +2 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +117 -15
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +6 -2
- package/nitrogen/generated/android/c++/JPlaybackSource.hpp +62 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +11 -3
- package/nitrogen/generated/android/c++/JPlaylist.hpp +2 -0
- package/nitrogen/generated/android/c++/JPresetType.hpp +59 -0
- package/nitrogen/generated/android/c++/JStorageLocation.hpp +59 -0
- package/nitrogen/generated/android/c++/JTrackItem.hpp +9 -3
- package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +3 -3
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +69 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +74 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +84 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +85 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +80 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +2 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +2 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadConfig.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadError.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadErrorReason.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadProgress.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadQueueStatus.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadState.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadStorageInfo.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadTask.kt +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedPlaylist.kt +53 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/DownloadedTrack.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerBand.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerPreset.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/EqualizerState.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadProgress.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_DownloadedTrack.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__optional_std__variant_nitro__NullType__std__string__.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_EqualizerBand_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/GainRange.kt +41 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +210 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +141 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +19 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlaybackSource.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PresetType.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/StorageLocation.kt +21 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackItem.kt +7 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/TrackPlayerState.kt +2 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +59 -0
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +138 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +1046 -121
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +66 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.mm +16 -0
- package/nitrogen/generated/ios/NitroPlayerAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +386 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +223 -0
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +1 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +46 -6
- package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
- package/nitrogen/generated/ios/swift/DownloadConfig.swift +270 -0
- package/nitrogen/generated/ios/swift/DownloadError.swift +69 -0
- package/nitrogen/generated/ios/swift/DownloadErrorReason.swift +60 -0
- package/nitrogen/generated/ios/swift/DownloadProgress.swift +91 -0
- package/nitrogen/generated/ios/swift/DownloadQueueStatus.swift +102 -0
- package/nitrogen/generated/ios/swift/DownloadState.swift +56 -0
- package/nitrogen/generated/ios/swift/DownloadStorageInfo.swift +80 -0
- package/nitrogen/generated/ios/swift/DownloadTask.swift +315 -0
- package/nitrogen/generated/ios/swift/DownloadedPlaylist.swift +103 -0
- package/nitrogen/generated/ios/swift/DownloadedTrack.swift +147 -0
- package/nitrogen/generated/ios/swift/EqualizerBand.swift +69 -0
- package/nitrogen/generated/ios/swift/EqualizerPreset.swift +70 -0
- package/nitrogen/generated/ios/swift/EqualizerState.swift +115 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadProgress.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadStorageInfo.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_DownloadedTrack.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_PlayerState.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__optional_std__variant_nitro__NullType__std__string__.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__string_DownloadState_std__optional_DownloadError_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem_.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +47 -0
- package/nitrogen/generated/ios/swift/GainRange.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +90 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +705 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +73 -0
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +396 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +6 -2
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +105 -8
- package/nitrogen/generated/ios/swift/PlaybackSource.swift +44 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
- package/nitrogen/generated/ios/swift/PresetType.swift +40 -0
- package/nitrogen/generated/ios/swift/StorageLocation.swift +40 -0
- package/nitrogen/generated/ios/swift/TrackItem.swift +31 -1
- package/nitrogen/generated/ios/swift/TrackPlayerState.swift +4 -4
- package/nitrogen/generated/ios/swift/Variant_NullType_Double.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadError.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadTask.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedPlaylist.swift +18 -0
- package/nitrogen/generated/ios/swift/Variant_NullType_DownloadedTrack.swift +18 -0
- package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
- package/nitrogen/generated/shared/c++/DownloadConfig.hpp +108 -0
- package/nitrogen/generated/shared/c++/DownloadError.hpp +89 -0
- package/nitrogen/generated/shared/c++/DownloadErrorReason.hpp +96 -0
- package/nitrogen/generated/shared/c++/DownloadProgress.hpp +97 -0
- package/nitrogen/generated/shared/c++/DownloadQueueStatus.hpp +99 -0
- package/nitrogen/generated/shared/c++/DownloadState.hpp +92 -0
- package/nitrogen/generated/shared/c++/DownloadStorageInfo.hpp +91 -0
- package/nitrogen/generated/shared/c++/DownloadTask.hpp +122 -0
- package/nitrogen/generated/shared/c++/DownloadedPlaylist.hpp +101 -0
- package/nitrogen/generated/shared/c++/DownloadedTrack.hpp +107 -0
- package/nitrogen/generated/shared/c++/EqualizerBand.hpp +87 -0
- package/nitrogen/generated/shared/c++/EqualizerPreset.hpp +86 -0
- package/nitrogen/generated/shared/c++/EqualizerState.hpp +89 -0
- package/nitrogen/generated/shared/c++/GainRange.hpp +79 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.cpp +55 -0
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +134 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.cpp +38 -0
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +95 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +11 -5
- package/nitrogen/generated/shared/c++/PlaybackSource.hpp +80 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
- package/nitrogen/generated/shared/c++/PresetType.hpp +76 -0
- package/nitrogen/generated/shared/c++/StorageLocation.hpp +76 -0
- package/nitrogen/generated/shared/c++/TrackItem.hpp +7 -2
- package/nitrogen/generated/shared/c++/TrackPlayerState.hpp +5 -5
- package/package.json +1 -1
- package/src/hooks/downloadCallbackManager.ts +149 -0
- package/src/hooks/equalizerCallbackManager.ts +138 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/useActualQueue.ts +116 -0
- package/src/hooks/useDownloadActions.ts +179 -0
- package/src/hooks/useDownloadProgress.ts +126 -0
- package/src/hooks/useDownloadStorage.ts +84 -0
- package/src/hooks/useDownloadedTracks.ts +138 -0
- package/src/hooks/useEqualizer.ts +173 -0
- package/src/hooks/useEqualizerPresets.ts +140 -0
- package/src/hooks/useNowPlaying.ts +3 -2
- package/src/hooks/useOnChangeTrack.ts +15 -11
- package/src/hooks/useOnPlaybackStateChange.ts +19 -15
- package/src/hooks/usePlaylist.ts +161 -0
- package/src/index.ts +12 -0
- package/src/specs/DownloadManager.nitro.ts +203 -0
- package/src/specs/Equalizer.nitro.ts +69 -0
- package/src/specs/TrackPlayer.nitro.ts +6 -2
- package/src/types/DownloadTypes.ts +135 -0
- package/src/types/EqualizerTypes.ts +72 -0
- package/src/types/PlayerQueue.ts +9 -0
|
@@ -57,10 +57,44 @@ class TrackPlayerCore: NSObject {
|
|
|
57
57
|
private var preloadedAssets: [String: AVURLAsset] = [:]
|
|
58
58
|
private let preloadQueue = DispatchQueue(label: "com.nitroplayer.preload", qos: .utility)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
var
|
|
62
|
-
var
|
|
63
|
-
var
|
|
60
|
+
// Temporary tracks for addToUpNext and playNext
|
|
61
|
+
private var playNextStack: [TrackItem] = [] // LIFO - last added plays first
|
|
62
|
+
private var upNextQueue: [TrackItem] = [] // FIFO - first added plays first
|
|
63
|
+
private var currentTemporaryType: TemporaryType = .none
|
|
64
|
+
|
|
65
|
+
// Enum to track what type of track is currently playing
|
|
66
|
+
private enum TemporaryType {
|
|
67
|
+
case none // Playing from original playlist
|
|
68
|
+
case playNext // Currently in playNextStack
|
|
69
|
+
case upNext // Currently in upNextQueue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// MARK: - Weak Callback Wrapper
|
|
73
|
+
|
|
74
|
+
/// Wrapper to hold callbacks with weak reference for auto-cleanup
|
|
75
|
+
private class WeakCallbackBox<T> {
|
|
76
|
+
private(set) weak var owner: AnyObject?
|
|
77
|
+
let callback: T
|
|
78
|
+
|
|
79
|
+
init(owner: AnyObject, callback: T) {
|
|
80
|
+
self.owner = owner
|
|
81
|
+
self.callback = callback
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var isAlive: Bool { owner != nil }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Event callbacks - support multiple listeners with auto-cleanup
|
|
88
|
+
private var onChangeTrackListeners: [WeakCallbackBox<(TrackItem, Reason?) -> Void>] = []
|
|
89
|
+
private var onPlaybackStateChangeListeners:
|
|
90
|
+
[WeakCallbackBox<(TrackPlayerState, Reason?) -> Void>] = []
|
|
91
|
+
private var onSeekListeners: [WeakCallbackBox<(Double, Double) -> Void>] = []
|
|
92
|
+
private var onPlaybackProgressChangeListeners:
|
|
93
|
+
[WeakCallbackBox<(Double, Double, Bool?) -> Void>] = []
|
|
94
|
+
|
|
95
|
+
// Thread-safe queue for listener access
|
|
96
|
+
private let listenersQueue = DispatchQueue(
|
|
97
|
+
label: "com.trackplayer.listeners", attributes: .concurrent)
|
|
64
98
|
|
|
65
99
|
static let shared = TrackPlayerCore()
|
|
66
100
|
|
|
@@ -229,10 +263,10 @@ class TrackPlayerCore: NSObject {
|
|
|
229
263
|
guard duration > 0 && !duration.isNaN && !duration.isInfinite else { return }
|
|
230
264
|
|
|
231
265
|
print(
|
|
232
|
-
"⏱️ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(
|
|
266
|
+
"⏱️ TrackPlayerCore: Boundary crossed - position: \(Int(position))s / \(Int(duration))s, callback exists: \(!onPlaybackProgressChangeListeners.isEmpty)"
|
|
233
267
|
)
|
|
234
268
|
|
|
235
|
-
|
|
269
|
+
notifyPlaybackProgress(
|
|
236
270
|
position,
|
|
237
271
|
duration,
|
|
238
272
|
isManuallySeeked ? true : nil
|
|
@@ -251,10 +285,22 @@ class TrackPlayerCore: NSObject {
|
|
|
251
285
|
return
|
|
252
286
|
}
|
|
253
287
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
288
|
+
// Determine what type of track just finished and remove it from temporary lists
|
|
289
|
+
if let trackId = finishedItem.trackId {
|
|
290
|
+
// Check if it was a playNext track
|
|
291
|
+
if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
|
|
292
|
+
let track = playNextStack.remove(at: index)
|
|
293
|
+
print("🏁 Finished playNext track: \(track.title) - removed from stack")
|
|
294
|
+
}
|
|
295
|
+
// Check if it was an upNext track
|
|
296
|
+
else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
|
|
297
|
+
let track = upNextQueue.remove(at: index)
|
|
298
|
+
print("🏁 Finished upNext track: \(track.title) - removed from queue")
|
|
299
|
+
}
|
|
300
|
+
// Otherwise it was from original playlist
|
|
301
|
+
else if let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
302
|
+
print("🏁 Finished original track: \(track.title)")
|
|
303
|
+
}
|
|
258
304
|
}
|
|
259
305
|
|
|
260
306
|
// Check remaining queue
|
|
@@ -269,23 +315,35 @@ class TrackPlayerCore: NSObject {
|
|
|
269
315
|
print("🔁 TrackPlayerCore: Repeat mode is TRACK - replaying current track")
|
|
270
316
|
DispatchQueue.main.async { [weak self] in
|
|
271
317
|
guard let self = self, let player = self.player else { return }
|
|
272
|
-
//
|
|
273
|
-
self.
|
|
318
|
+
// For temporary tracks, just seek to beginning
|
|
319
|
+
if self.currentTemporaryType != .none {
|
|
320
|
+
player.seek(to: .zero)
|
|
321
|
+
player.play()
|
|
322
|
+
} else {
|
|
323
|
+
// For original tracks, recreate via playFromIndex
|
|
324
|
+
self.playFromIndex(index: self.currentTrackIndex)
|
|
325
|
+
}
|
|
274
326
|
}
|
|
275
327
|
return
|
|
276
328
|
|
|
277
329
|
case .playlist:
|
|
278
|
-
// Check if we're at the end of the playlist
|
|
279
|
-
if currentTrackIndex >= currentTracks.count - 1 {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
330
|
+
// Check if we're at the end of the ORIGINAL playlist (ignore temps)
|
|
331
|
+
if currentTemporaryType == .none && currentTrackIndex >= currentTracks.count - 1 {
|
|
332
|
+
// Check if there are still temporary tracks
|
|
333
|
+
if !playNextStack.isEmpty || !upNextQueue.isEmpty {
|
|
334
|
+
print("🔁 TrackPlayerCore: Temporary tracks remaining, continuing...")
|
|
335
|
+
} else {
|
|
336
|
+
print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
|
|
337
|
+
// Clear temps and restart
|
|
338
|
+
playNextStack.removeAll()
|
|
339
|
+
upNextQueue.removeAll()
|
|
340
|
+
DispatchQueue.main.async { [weak self] in
|
|
341
|
+
guard let self = self else { return }
|
|
342
|
+
self.playFromIndex(index: 0)
|
|
343
|
+
}
|
|
344
|
+
return
|
|
285
345
|
}
|
|
286
|
-
return
|
|
287
346
|
} else {
|
|
288
|
-
// Not at end, just continue to next track
|
|
289
347
|
print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - continuing to next track")
|
|
290
348
|
}
|
|
291
349
|
|
|
@@ -295,7 +353,7 @@ class TrackPlayerCore: NSObject {
|
|
|
295
353
|
}
|
|
296
354
|
|
|
297
355
|
// Track ended naturally
|
|
298
|
-
|
|
356
|
+
notifyTrackChange(
|
|
299
357
|
getCurrentTrack()
|
|
300
358
|
?? TrackItem(
|
|
301
359
|
id: "",
|
|
@@ -304,7 +362,8 @@ class TrackPlayerCore: NSObject {
|
|
|
304
362
|
album: "",
|
|
305
363
|
duration: 0,
|
|
306
364
|
url: "",
|
|
307
|
-
artwork: nil
|
|
365
|
+
artwork: nil,
|
|
366
|
+
extraPayload: nil
|
|
308
367
|
), .end)
|
|
309
368
|
|
|
310
369
|
// Try to play next track
|
|
@@ -314,7 +373,7 @@ class TrackPlayerCore: NSObject {
|
|
|
314
373
|
@objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
|
|
315
374
|
if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
|
|
316
375
|
print("❌ TrackPlayerCore: Playback failed - \(error)")
|
|
317
|
-
|
|
376
|
+
notifyPlaybackStateChange(.stopped, .error)
|
|
318
377
|
}
|
|
319
378
|
}
|
|
320
379
|
|
|
@@ -346,7 +405,7 @@ class TrackPlayerCore: NSObject {
|
|
|
346
405
|
print("🎯 TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
|
|
347
406
|
|
|
348
407
|
// Call onSeek callback immediately
|
|
349
|
-
|
|
408
|
+
notifySeek(position, duration)
|
|
350
409
|
|
|
351
410
|
// Mark that this was a manual seek
|
|
352
411
|
isManuallySeeked = true
|
|
@@ -371,7 +430,7 @@ class TrackPlayerCore: NSObject {
|
|
|
371
430
|
emitStateChange()
|
|
372
431
|
} else if player.status == .failed {
|
|
373
432
|
print("❌ TrackPlayerCore: Player failed")
|
|
374
|
-
|
|
433
|
+
notifyPlaybackStateChange(.stopped, .error)
|
|
375
434
|
}
|
|
376
435
|
} else if keyPath == "rate" {
|
|
377
436
|
print("👀 TrackPlayerCore: Rate changed to: \(player.rate)")
|
|
@@ -437,12 +496,34 @@ class TrackPlayerCore: NSObject {
|
|
|
437
496
|
// Setup KVO observers for current item
|
|
438
497
|
setupCurrentItemObservers(item: currentItem)
|
|
439
498
|
|
|
440
|
-
// Update track index
|
|
499
|
+
// Update track index and determine temporary type
|
|
441
500
|
if let trackId = currentItem.trackId {
|
|
442
501
|
print("🔍 TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
|
|
443
502
|
print(" Current index BEFORE lookup: \(currentTrackIndex)")
|
|
444
503
|
|
|
445
|
-
|
|
504
|
+
// Update temporary type
|
|
505
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
506
|
+
print(" 🎯 Track type: \(currentTemporaryType)")
|
|
507
|
+
|
|
508
|
+
// If it's a temporary track, don't update currentTrackIndex
|
|
509
|
+
if currentTemporaryType != .none {
|
|
510
|
+
// Find and emit the temporary track
|
|
511
|
+
var tempTrack: TrackItem? = nil
|
|
512
|
+
if currentTemporaryType == .playNext {
|
|
513
|
+
tempTrack = playNextStack.first(where: { $0.id == trackId })
|
|
514
|
+
} else if currentTemporaryType == .upNext {
|
|
515
|
+
tempTrack = upNextQueue.first(where: { $0.id == trackId })
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if let track = tempTrack {
|
|
519
|
+
print(" 🎵 Temporary track: \(track.title) - \(track.artist)")
|
|
520
|
+
print(" 📢 Emitting onChangeTrack for temporary track")
|
|
521
|
+
notifyTrackChange(track, .skip)
|
|
522
|
+
mediaSessionManager?.onTrackChanged()
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// It's an original playlist track
|
|
526
|
+
else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
|
|
446
527
|
print(" ✅ Found track at index: \(index)")
|
|
447
528
|
print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
|
|
448
529
|
|
|
@@ -456,7 +537,7 @@ class TrackPlayerCore: NSObject {
|
|
|
456
537
|
// This prevents duplicate emissions
|
|
457
538
|
if oldIndex != index {
|
|
458
539
|
print(" 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
|
|
459
|
-
|
|
540
|
+
notifyTrackChange(track, .skip)
|
|
460
541
|
mediaSessionManager?.onTrackChanged()
|
|
461
542
|
} else {
|
|
462
543
|
print(" ⏭️ Skipping onChangeTrack emission (index unchanged)")
|
|
@@ -485,14 +566,16 @@ class TrackPlayerCore: NSObject {
|
|
|
485
566
|
private func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
486
567
|
print("📱 TrackPlayerCore: Setting up item observers")
|
|
487
568
|
|
|
488
|
-
// Observe status - recreate boundaries when ready
|
|
569
|
+
// Observe status - recreate boundaries when ready and update now playing info
|
|
489
570
|
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
490
571
|
if item.status == .readyToPlay {
|
|
491
572
|
print("✅ TrackPlayerCore: Item ready, setting up boundaries")
|
|
492
573
|
self?.setupBoundaryTimeObserver()
|
|
574
|
+
// Update now playing info now that duration is available
|
|
575
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
493
576
|
} else if item.status == .failed {
|
|
494
577
|
print("❌ TrackPlayerCore: Item failed")
|
|
495
|
-
self?.
|
|
578
|
+
self?.notifyPlaybackStateChange(.stopped, .error)
|
|
496
579
|
}
|
|
497
580
|
}
|
|
498
581
|
currentItemObservers.append(statusObserver)
|
|
@@ -517,32 +600,42 @@ class TrackPlayerCore: NSObject {
|
|
|
517
600
|
// MARK: - Playlist Management
|
|
518
601
|
|
|
519
602
|
func loadPlaylist(playlistId: String) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
if let playlist = playlist {
|
|
529
|
-
print(" ✅ Found playlist: \(playlist.name)")
|
|
530
|
-
print(" 📋 Contains \(playlist.tracks.count) tracks:")
|
|
531
|
-
for (index, track) in playlist.tracks.enumerated() {
|
|
532
|
-
print(" [\(index + 1)] \(track.title) - \(track.artist)")
|
|
533
|
-
}
|
|
534
|
-
print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
603
|
+
if Thread.isMainThread {
|
|
604
|
+
loadPlaylistInternal(playlistId: playlistId)
|
|
605
|
+
} else {
|
|
606
|
+
DispatchQueue.main.sync { [weak self] in
|
|
607
|
+
self?.loadPlaylistInternal(playlistId: playlistId)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
535
611
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
612
|
+
private func loadPlaylistInternal(playlistId: String) {
|
|
613
|
+
print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
|
|
614
|
+
print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
|
|
615
|
+
print(" Playlist ID: \(playlistId)")
|
|
616
|
+
|
|
617
|
+
// Clear temporary tracks when loading new playlist
|
|
618
|
+
self.playNextStack.removeAll()
|
|
619
|
+
self.upNextQueue.removeAll()
|
|
620
|
+
self.currentTemporaryType = .none
|
|
621
|
+
print(" 🧹 Cleared temporary tracks")
|
|
622
|
+
|
|
623
|
+
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
624
|
+
if let playlist = playlist {
|
|
625
|
+
print(" ✅ Found playlist: \(playlist.name)")
|
|
626
|
+
print(" 📋 Contains \(playlist.tracks.count) tracks:")
|
|
627
|
+
for (index, track) in playlist.tracks.enumerated() {
|
|
628
|
+
print(" [\(index + 1)] \(track.title) - \(track.artist)")
|
|
545
629
|
}
|
|
630
|
+
print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
631
|
+
|
|
632
|
+
self.currentPlaylistId = playlistId
|
|
633
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
634
|
+
// Emit initial state (paused/stopped before play)
|
|
635
|
+
self.emitStateChange()
|
|
636
|
+
} else {
|
|
637
|
+
print(" ❌ Playlist NOT FOUND")
|
|
638
|
+
print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
546
639
|
}
|
|
547
640
|
}
|
|
548
641
|
|
|
@@ -583,8 +676,8 @@ class TrackPlayerCore: NSObject {
|
|
|
583
676
|
}
|
|
584
677
|
|
|
585
678
|
print("🔔 TrackPlayerCore: Emitting state change: \(state)")
|
|
586
|
-
print("🔔 TrackPlayerCore: Callback exists: \(
|
|
587
|
-
|
|
679
|
+
print("🔔 TrackPlayerCore: Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
|
|
680
|
+
notifyPlaybackStateChange(state, reason)
|
|
588
681
|
mediaSessionManager?.onPlaybackStateChanged()
|
|
589
682
|
}
|
|
590
683
|
|
|
@@ -594,9 +687,40 @@ class TrackPlayerCore: NSObject {
|
|
|
594
687
|
private func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false)
|
|
595
688
|
-> AVPlayerItem?
|
|
596
689
|
{
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
690
|
+
// Get effective URL - uses local path if downloaded, otherwise remote URL
|
|
691
|
+
let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
|
|
692
|
+
|
|
693
|
+
// Create URL - use fileURLWithPath for local files, URL(string:) for remote
|
|
694
|
+
let url: URL
|
|
695
|
+
let isLocal = effectiveUrlString.hasPrefix("/")
|
|
696
|
+
|
|
697
|
+
if isLocal {
|
|
698
|
+
// Local file - use fileURLWithPath
|
|
699
|
+
print("📥 TrackPlayerCore: Using DOWNLOADED version for \(track.title)")
|
|
700
|
+
print(" Local path: \(effectiveUrlString)")
|
|
701
|
+
|
|
702
|
+
// Verify file exists
|
|
703
|
+
if FileManager.default.fileExists(atPath: effectiveUrlString) {
|
|
704
|
+
url = URL(fileURLWithPath: effectiveUrlString)
|
|
705
|
+
print(" File URL: \(url.absoluteString)")
|
|
706
|
+
print(" ✅ File verified to exist")
|
|
707
|
+
} else {
|
|
708
|
+
print(" ❌ Downloaded file does NOT exist at path!")
|
|
709
|
+
print(" Falling back to remote URL: \(track.url)")
|
|
710
|
+
guard let remoteUrl = URL(string: track.url) else {
|
|
711
|
+
print("❌ TrackPlayerCore: Invalid remote URL: \(track.url)")
|
|
712
|
+
return nil
|
|
713
|
+
}
|
|
714
|
+
url = remoteUrl
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
// Remote URL
|
|
718
|
+
guard let remoteUrl = URL(string: effectiveUrlString) else {
|
|
719
|
+
print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(effectiveUrlString)")
|
|
720
|
+
return nil
|
|
721
|
+
}
|
|
722
|
+
url = remoteUrl
|
|
723
|
+
print("🌐 TrackPlayerCore: Using REMOTE version for \(track.title)")
|
|
600
724
|
}
|
|
601
725
|
|
|
602
726
|
// Check if we have a preloaded asset for this track
|
|
@@ -625,6 +749,13 @@ class TrackPlayerCore: NSObject {
|
|
|
625
749
|
// Store track ID for later reference
|
|
626
750
|
item.trackId = track.id
|
|
627
751
|
|
|
752
|
+
// Apply equalizer audio mix to the player item
|
|
753
|
+
// This enables real-time EQ processing via MTAudioProcessingTap
|
|
754
|
+
// Apply equalizer audio mix to the player item
|
|
755
|
+
// This enables real-time EQ processing via MTAudioProcessingTap
|
|
756
|
+
EqualizerCore.shared.applyAudioMix(to: item)
|
|
757
|
+
print("🎛️ TrackPlayerCore: Requesting EQ audio mix application for \(track.title)")
|
|
758
|
+
|
|
628
759
|
// If this is a preload request, start loading asset keys asynchronously
|
|
629
760
|
if isPreload {
|
|
630
761
|
asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
|
|
@@ -654,10 +785,13 @@ class TrackPlayerCore: NSObject {
|
|
|
654
785
|
preloadQueue.async { [weak self] in
|
|
655
786
|
guard let self = self else { return }
|
|
656
787
|
|
|
657
|
-
|
|
788
|
+
// Capture currentTracks to avoid race condition with main thread
|
|
789
|
+
let tracks = self.currentTracks
|
|
790
|
+
let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
|
|
658
791
|
|
|
659
792
|
for i in startIndex..<endIndex {
|
|
660
|
-
|
|
793
|
+
guard i < tracks.count else { break }
|
|
794
|
+
let track = tracks[i]
|
|
661
795
|
|
|
662
796
|
// Skip if already preloaded
|
|
663
797
|
if self.preloadedAssets[track.id] != nil {
|
|
@@ -717,6 +851,141 @@ class TrackPlayerCore: NSObject {
|
|
|
717
851
|
}
|
|
718
852
|
}
|
|
719
853
|
|
|
854
|
+
// MARK: - Listener Registration
|
|
855
|
+
|
|
856
|
+
func addOnChangeTrackListener(
|
|
857
|
+
owner: AnyObject, _ listener: @escaping (TrackItem, Reason?) -> Void
|
|
858
|
+
) {
|
|
859
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
860
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
861
|
+
self?.onChangeTrackListeners.append(box)
|
|
862
|
+
print(
|
|
863
|
+
"🎯 TrackPlayerCore: Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))"
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
func addOnPlaybackStateChangeListener(
|
|
869
|
+
owner: AnyObject,
|
|
870
|
+
_ listener: @escaping (TrackPlayerState, Reason?) -> Void
|
|
871
|
+
) {
|
|
872
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
873
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
874
|
+
self?.onPlaybackStateChangeListeners.append(box)
|
|
875
|
+
print(
|
|
876
|
+
"🎯 TrackPlayerCore: Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))"
|
|
877
|
+
)
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
func addOnSeekListener(owner: AnyObject, _ listener: @escaping (Double, Double) -> Void) {
|
|
882
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
883
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
884
|
+
self?.onSeekListeners.append(box)
|
|
885
|
+
print("🎯 TrackPlayerCore: Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
func addOnPlaybackProgressChangeListener(
|
|
890
|
+
owner: AnyObject,
|
|
891
|
+
_ listener: @escaping (Double, Double, Bool?) -> Void
|
|
892
|
+
) {
|
|
893
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
894
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
895
|
+
self?.onPlaybackProgressChangeListeners.append(box)
|
|
896
|
+
print(
|
|
897
|
+
"🎯 TrackPlayerCore: Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))"
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// MARK: - Listener Notification Helpers
|
|
903
|
+
|
|
904
|
+
private func notifyTrackChange(_ track: TrackItem, _ reason: Reason?) {
|
|
905
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
906
|
+
guard let self = self else { return }
|
|
907
|
+
|
|
908
|
+
// Remove dead listeners
|
|
909
|
+
self.onChangeTrackListeners.removeAll { !$0.isAlive }
|
|
910
|
+
|
|
911
|
+
// Get live callbacks
|
|
912
|
+
let liveCallbacks = self.onChangeTrackListeners.compactMap {
|
|
913
|
+
$0.isAlive ? $0.callback : nil
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Call on main thread
|
|
917
|
+
if !liveCallbacks.isEmpty {
|
|
918
|
+
DispatchQueue.main.async {
|
|
919
|
+
for callback in liveCallbacks {
|
|
920
|
+
callback(track, reason)
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private func notifyPlaybackStateChange(_ state: TrackPlayerState, _ reason: Reason?) {
|
|
928
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
929
|
+
guard let self = self else { return }
|
|
930
|
+
|
|
931
|
+
self.onPlaybackStateChangeListeners.removeAll { !$0.isAlive }
|
|
932
|
+
|
|
933
|
+
let liveCallbacks = self.onPlaybackStateChangeListeners.compactMap {
|
|
934
|
+
$0.isAlive ? $0.callback : nil
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if !liveCallbacks.isEmpty {
|
|
938
|
+
DispatchQueue.main.async {
|
|
939
|
+
for callback in liveCallbacks {
|
|
940
|
+
callback(state, reason)
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private func notifySeek(_ position: Double, _ duration: Double) {
|
|
948
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
949
|
+
guard let self = self else { return }
|
|
950
|
+
|
|
951
|
+
self.onSeekListeners.removeAll { !$0.isAlive }
|
|
952
|
+
|
|
953
|
+
let liveCallbacks = self.onSeekListeners.compactMap {
|
|
954
|
+
$0.isAlive ? $0.callback : nil
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if !liveCallbacks.isEmpty {
|
|
958
|
+
DispatchQueue.main.async {
|
|
959
|
+
for callback in liveCallbacks {
|
|
960
|
+
callback(position, duration)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private func notifyPlaybackProgress(_ position: Double, _ duration: Double, _ isPlaying: Bool?) {
|
|
968
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
969
|
+
guard let self = self else { return }
|
|
970
|
+
|
|
971
|
+
self.onPlaybackProgressChangeListeners.removeAll { !$0.isAlive }
|
|
972
|
+
|
|
973
|
+
let liveCallbacks = self.onPlaybackProgressChangeListeners.compactMap {
|
|
974
|
+
$0.isAlive ? $0.callback : nil
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if !liveCallbacks.isEmpty {
|
|
978
|
+
DispatchQueue.main.async {
|
|
979
|
+
for callback in liveCallbacks {
|
|
980
|
+
callback(position, duration, isPlaying)
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// MARK: - State Management
|
|
988
|
+
|
|
720
989
|
// MARK: - Queue Management
|
|
721
990
|
|
|
722
991
|
private func updatePlayerQueue(tracks: [TrackItem]) {
|
|
@@ -724,9 +993,17 @@ class TrackPlayerCore: NSObject {
|
|
|
724
993
|
print("📋 TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
|
|
725
994
|
print(String(repeating: "=", count: Constants.separatorLineLength))
|
|
726
995
|
|
|
727
|
-
// Print the full playlist being fed
|
|
996
|
+
// Print the full playlist being fed and check download status
|
|
728
997
|
for (index, track) in tracks.enumerated() {
|
|
729
|
-
|
|
998
|
+
let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
|
|
999
|
+
let downloadStatus = isDownloaded ? "📥 DOWNLOADED" : "🌐 REMOTE"
|
|
1000
|
+
print(
|
|
1001
|
+
" [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
|
|
1002
|
+
if isDownloaded {
|
|
1003
|
+
if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
|
|
1004
|
+
print(" Local path: \(localPath)")
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
730
1007
|
}
|
|
731
1008
|
print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
732
1009
|
|
|
@@ -808,8 +1085,8 @@ class TrackPlayerCore: NSObject {
|
|
|
808
1085
|
// Notify track change
|
|
809
1086
|
if let firstTrack = tracks.first {
|
|
810
1087
|
print("🎵 TrackPlayerCore: Emitting track change: \(firstTrack.title)")
|
|
811
|
-
print("🎵 TrackPlayerCore: onChangeTrack
|
|
812
|
-
|
|
1088
|
+
print("🎵 TrackPlayerCore: onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
|
|
1089
|
+
notifyTrackChange(firstTrack, nil)
|
|
813
1090
|
mediaSessionManager?.onTrackChanged()
|
|
814
1091
|
}
|
|
815
1092
|
|
|
@@ -820,226 +1097,324 @@ class TrackPlayerCore: NSObject {
|
|
|
820
1097
|
}
|
|
821
1098
|
|
|
822
1099
|
func getCurrentTrack() -> TrackItem? {
|
|
1100
|
+
// If playing a temporary track, return that
|
|
1101
|
+
if currentTemporaryType != .none,
|
|
1102
|
+
let currentItem = player?.currentItem,
|
|
1103
|
+
let trackId = currentItem.trackId
|
|
1104
|
+
{
|
|
1105
|
+
if currentTemporaryType == .playNext {
|
|
1106
|
+
return playNextStack.first(where: { $0.id == trackId })
|
|
1107
|
+
} else if currentTemporaryType == .upNext {
|
|
1108
|
+
return upNextQueue.first(where: { $0.id == trackId })
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Otherwise return from original playlist
|
|
823
1113
|
guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else {
|
|
824
1114
|
return nil
|
|
825
1115
|
}
|
|
826
1116
|
return currentTracks[currentTrackIndex]
|
|
827
1117
|
}
|
|
828
1118
|
|
|
1119
|
+
func getActualQueue() -> [TrackItem] {
|
|
1120
|
+
// Called from Promise.async background thread
|
|
1121
|
+
// Schedule on main thread and wait for result
|
|
1122
|
+
if Thread.isMainThread {
|
|
1123
|
+
return getActualQueueInternal()
|
|
1124
|
+
} else {
|
|
1125
|
+
var queue: [TrackItem] = []
|
|
1126
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1127
|
+
queue = self?.getActualQueueInternal() ?? []
|
|
1128
|
+
}
|
|
1129
|
+
return queue
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
private func getActualQueueInternal() -> [TrackItem] {
|
|
1134
|
+
var queue: [TrackItem] = []
|
|
1135
|
+
|
|
1136
|
+
// Add tracks before current (original playlist)
|
|
1137
|
+
if currentTrackIndex > 0 {
|
|
1138
|
+
queue.append(contentsOf: Array(currentTracks[0..<currentTrackIndex]))
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Add current track
|
|
1142
|
+
if let current = getCurrentTrack() {
|
|
1143
|
+
queue.append(current)
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1147
|
+
// Stack is already in correct order since we insert at position 0
|
|
1148
|
+
queue.append(contentsOf: playNextStack)
|
|
1149
|
+
|
|
1150
|
+
// Add upNext queue (in order, FIFO)
|
|
1151
|
+
queue.append(contentsOf: upNextQueue)
|
|
1152
|
+
|
|
1153
|
+
// Add remaining original tracks
|
|
1154
|
+
if currentTrackIndex + 1 < currentTracks.count {
|
|
1155
|
+
queue.append(contentsOf: Array(currentTracks[(currentTrackIndex + 1)...]))
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return queue
|
|
1159
|
+
}
|
|
1160
|
+
|
|
829
1161
|
func play() {
|
|
830
1162
|
print("▶️ TrackPlayerCore: play() called")
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1163
|
+
if Thread.isMainThread {
|
|
1164
|
+
playInternal()
|
|
1165
|
+
} else {
|
|
1166
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1167
|
+
self?.playInternal()
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private func playInternal() {
|
|
1173
|
+
print("▶️ TrackPlayerCore: Calling player.play()")
|
|
1174
|
+
if let player = self.player {
|
|
1175
|
+
print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
|
|
1176
|
+
if let currentItem = player.currentItem {
|
|
1177
|
+
print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
|
|
1178
|
+
if let error = currentItem.error {
|
|
1179
|
+
print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
|
|
848
1180
|
}
|
|
849
|
-
} else {
|
|
850
|
-
print("❌ TrackPlayerCore: No player available")
|
|
851
1181
|
}
|
|
1182
|
+
player.play()
|
|
1183
|
+
// Emit state change immediately for responsive UI
|
|
1184
|
+
// KVO will also fire, but this ensures immediate feedback
|
|
1185
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
|
|
1186
|
+
[weak self] in
|
|
1187
|
+
self?.emitStateChange()
|
|
1188
|
+
}
|
|
1189
|
+
} else {
|
|
1190
|
+
print("❌ TrackPlayerCore: No player available")
|
|
852
1191
|
}
|
|
853
1192
|
}
|
|
854
1193
|
|
|
855
1194
|
func pause() {
|
|
856
1195
|
print("⏸️ TrackPlayerCore: pause() called")
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
self?.emitStateChange()
|
|
1196
|
+
if Thread.isMainThread {
|
|
1197
|
+
pauseInternal()
|
|
1198
|
+
} else {
|
|
1199
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1200
|
+
self?.pauseInternal()
|
|
863
1201
|
}
|
|
864
1202
|
}
|
|
865
1203
|
}
|
|
866
1204
|
|
|
867
|
-
func
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
)
|
|
1205
|
+
private func pauseInternal() {
|
|
1206
|
+
self.player?.pause()
|
|
1207
|
+
// Emit state change immediately for responsive UI
|
|
1208
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
|
|
1209
|
+
self?.emitStateChange()
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
871
1212
|
|
|
1213
|
+
func playSong(songId: String, fromPlaylist: String?) {
|
|
872
1214
|
DispatchQueue.main.async { [weak self] in
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
var songIndex: Int = -1
|
|
1215
|
+
self?.playSongInternal(songId: songId, fromPlaylist: fromPlaylist)
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
877
1218
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1219
|
+
private func playSongInternal(songId: String, fromPlaylist: String?) {
|
|
1220
|
+
// Clear temporary tracks when directly playing a song
|
|
1221
|
+
self.playNextStack.removeAll()
|
|
1222
|
+
self.upNextQueue.removeAll()
|
|
1223
|
+
self.currentTemporaryType = .none
|
|
1224
|
+
print(" 🧹 Cleared temporary tracks")
|
|
1225
|
+
|
|
1226
|
+
var targetPlaylistId: String?
|
|
1227
|
+
var songIndex: Int = -1
|
|
1228
|
+
|
|
1229
|
+
// Case 1: If fromPlaylist is provided, use that playlist
|
|
1230
|
+
if let playlistId = fromPlaylist {
|
|
1231
|
+
print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
|
|
1232
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
1233
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1234
|
+
targetPlaylistId = playlistId
|
|
1235
|
+
songIndex = index
|
|
1236
|
+
print("✅ Found song at index \(index) in playlist \(playlistId)")
|
|
890
1237
|
} else {
|
|
891
|
-
print("⚠️
|
|
1238
|
+
print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
|
|
892
1239
|
return
|
|
893
1240
|
}
|
|
1241
|
+
} else {
|
|
1242
|
+
print("⚠️ Playlist \(playlistId) not found")
|
|
1243
|
+
return
|
|
894
1244
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1245
|
+
}
|
|
1246
|
+
// Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
|
|
1247
|
+
else {
|
|
1248
|
+
print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
|
|
1249
|
+
|
|
1250
|
+
// Check if song exists in currently loaded playlist
|
|
1251
|
+
if let currentId = self.currentPlaylistId,
|
|
1252
|
+
let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
|
|
1253
|
+
{
|
|
1254
|
+
if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1255
|
+
targetPlaylistId = currentId
|
|
1256
|
+
songIndex = index
|
|
1257
|
+
print("✅ Found song at index \(index) in current playlist \(currentId)")
|
|
908
1258
|
}
|
|
1259
|
+
}
|
|
909
1260
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1261
|
+
// If not found in current playlist, search in all playlists
|
|
1262
|
+
if songIndex == -1 {
|
|
1263
|
+
print("🔍 Song not found in current playlist, searching all playlists...")
|
|
1264
|
+
let allPlaylists = self.playlistManager.getAllPlaylists()
|
|
914
1265
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
1266
|
+
for playlist in allPlaylists {
|
|
1267
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1268
|
+
targetPlaylistId = playlist.id
|
|
1269
|
+
songIndex = index
|
|
1270
|
+
print("✅ Found song at index \(index) in playlist \(playlist.id)")
|
|
1271
|
+
break
|
|
922
1272
|
}
|
|
1273
|
+
}
|
|
923
1274
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1275
|
+
// If still not found, just use the first playlist if available
|
|
1276
|
+
if songIndex == -1 && !allPlaylists.isEmpty {
|
|
1277
|
+
targetPlaylistId = allPlaylists[0].id
|
|
1278
|
+
songIndex = 0
|
|
1279
|
+
print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
|
|
930
1280
|
}
|
|
931
1281
|
}
|
|
1282
|
+
}
|
|
932
1283
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1284
|
+
// Now play the song
|
|
1285
|
+
guard let playlistId = targetPlaylistId, songIndex >= 0 else {
|
|
1286
|
+
print("❌ Could not determine playlist or song index")
|
|
1287
|
+
return
|
|
1288
|
+
}
|
|
938
1289
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
1290
|
+
// Load playlist if it's different from current
|
|
1291
|
+
if self.currentPlaylistId != playlistId {
|
|
1292
|
+
print("🔄 Loading new playlist: \(playlistId)")
|
|
1293
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
1294
|
+
self.currentPlaylistId = playlistId
|
|
1295
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
946
1296
|
}
|
|
947
|
-
|
|
948
|
-
// Play from the found index
|
|
949
|
-
print("▶️ Playing from index: \(songIndex)")
|
|
950
|
-
self.playFromIndex(index: songIndex)
|
|
951
1297
|
}
|
|
1298
|
+
|
|
1299
|
+
// Play from the found index
|
|
1300
|
+
print("▶️ Playing from index: \(songIndex)")
|
|
1301
|
+
self.playFromIndex(index: songIndex)
|
|
952
1302
|
}
|
|
953
1303
|
|
|
954
1304
|
func skipToNext() {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1305
|
+
if Thread.isMainThread {
|
|
1306
|
+
skipToNextInternal()
|
|
1307
|
+
} else {
|
|
1308
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1309
|
+
self?.skipToNextInternal()
|
|
960
1310
|
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
961
1313
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
print(" currentTrackIndex: \(self.currentTrackIndex)")
|
|
965
|
-
print(" Total tracks in currentTracks: \(self.currentTracks.count)")
|
|
966
|
-
print(" Items in player queue: \(queuePlayer.items().count)")
|
|
1314
|
+
private func skipToNextInternal() {
|
|
1315
|
+
guard let queuePlayer = self.player else { return }
|
|
967
1316
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1317
|
+
print("\n⏭️ TrackPlayerCore: SKIP TO NEXT")
|
|
1318
|
+
print(" BEFORE:")
|
|
1319
|
+
print(" currentTrackIndex: \(self.currentTrackIndex)")
|
|
1320
|
+
print(" Total tracks in currentTracks: \(self.currentTracks.count)")
|
|
1321
|
+
print(" Items in player queue: \(queuePlayer.items().count)")
|
|
1322
|
+
|
|
1323
|
+
if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
|
|
1324
|
+
if let track = self.currentTracks.first(where: { $0.id == trackId }) {
|
|
1325
|
+
print(" Currently playing: \(track.title) (ID: \(track.id))")
|
|
972
1326
|
}
|
|
1327
|
+
}
|
|
973
1328
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1329
|
+
// Check if there are more items in the queue
|
|
1330
|
+
if self.currentTrackIndex + 1 < self.currentTracks.count {
|
|
1331
|
+
print(" 🔄 Calling advanceToNextItem()...")
|
|
1332
|
+
queuePlayer.advanceToNextItem()
|
|
978
1333
|
|
|
979
|
-
|
|
980
|
-
|
|
1334
|
+
// NOTE: Don't manually update currentTrackIndex here!
|
|
1335
|
+
// The KVO observer (currentItemDidChange) will update it automatically
|
|
981
1336
|
|
|
982
|
-
|
|
983
|
-
|
|
1337
|
+
print(" AFTER advanceToNextItem():")
|
|
1338
|
+
print(" Items in player queue: \(queuePlayer.items().count)")
|
|
984
1339
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
}
|
|
1340
|
+
if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
|
|
1341
|
+
if let track = self.currentTracks.first(where: { $0.id == trackId }) {
|
|
1342
|
+
print(" New current item: \(track.title) (ID: \(track.id))")
|
|
989
1343
|
}
|
|
990
|
-
|
|
991
|
-
print(" ⏳ Waiting for KVO observer to update index...")
|
|
992
|
-
} else {
|
|
993
|
-
print(" ⚠️ No more tracks in playlist")
|
|
994
|
-
// At end of playlist - stop or loop
|
|
995
|
-
queuePlayer.pause()
|
|
996
|
-
self.onPlaybackStateChange?(.stopped, .end)
|
|
997
1344
|
}
|
|
1345
|
+
|
|
1346
|
+
print(" ⏳ Waiting for KVO observer to update index...")
|
|
1347
|
+
} else {
|
|
1348
|
+
print(" ⚠️ No more tracks in playlist")
|
|
1349
|
+
// At end of playlist - stop or loop
|
|
1350
|
+
queuePlayer.pause()
|
|
1351
|
+
self.notifyPlaybackStateChange(.stopped, .end)
|
|
998
1352
|
}
|
|
999
1353
|
}
|
|
1000
1354
|
|
|
1001
1355
|
func skipToPrevious() {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1356
|
+
if Thread.isMainThread {
|
|
1357
|
+
skipToPreviousInternal()
|
|
1358
|
+
} else {
|
|
1359
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1360
|
+
self?.skipToPreviousInternal()
|
|
1007
1361
|
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1008
1364
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1365
|
+
private func skipToPreviousInternal() {
|
|
1366
|
+
guard let queuePlayer = self.player else { return }
|
|
1367
|
+
|
|
1368
|
+
print("\n⏮️ TrackPlayerCore: SKIP TO PREVIOUS")
|
|
1369
|
+
print(" Current index: \(self.currentTrackIndex)")
|
|
1370
|
+
print(" Temporary type: \(self.currentTemporaryType)")
|
|
1371
|
+
print(" Current time: \(queuePlayer.currentTime().seconds)s")
|
|
1372
|
+
|
|
1373
|
+
let currentTime = queuePlayer.currentTime()
|
|
1374
|
+
if currentTime.seconds > Constants.skipToPreviousThreshold {
|
|
1375
|
+
// If more than threshold seconds in, restart current track
|
|
1376
|
+
print(
|
|
1377
|
+
" 🔄 More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
|
|
1378
|
+
queuePlayer.seek(to: .zero)
|
|
1379
|
+
} else if self.currentTemporaryType != .none {
|
|
1380
|
+
// Playing temporary track - just restart it (temps are not navigable backwards)
|
|
1381
|
+
print(" 🔄 Playing temporary track - restarting it (temps not navigable backwards)")
|
|
1382
|
+
queuePlayer.seek(to: .zero)
|
|
1383
|
+
} else if self.currentTrackIndex > 0 {
|
|
1384
|
+
// Go to previous track in original playlist
|
|
1385
|
+
let previousIndex = self.currentTrackIndex - 1
|
|
1386
|
+
print(" ⏮️ Going to previous track at index \(previousIndex)")
|
|
1387
|
+
self.playFromIndex(index: previousIndex)
|
|
1388
|
+
} else {
|
|
1389
|
+
// Already at first track, restart it
|
|
1390
|
+
print(" 🔄 Already at first track, restarting it")
|
|
1391
|
+
queuePlayer.seek(to: .zero)
|
|
1029
1392
|
}
|
|
1030
1393
|
}
|
|
1031
1394
|
|
|
1032
1395
|
func seek(position: Double) {
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1396
|
+
if Thread.isMainThread {
|
|
1397
|
+
seekInternal(position: position)
|
|
1398
|
+
} else {
|
|
1399
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1400
|
+
self?.seekInternal(position: position)
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
private func seekInternal(position: Double) {
|
|
1406
|
+
guard let player = self.player else { return }
|
|
1407
|
+
|
|
1408
|
+
self.isManuallySeeked = true
|
|
1409
|
+
let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
1410
|
+
player.seek(to: time) { [weak self] completed in
|
|
1411
|
+
// Always update now playing info to restore playback rate after seek
|
|
1412
|
+
// This ensures the scrubber animation resumes correctly
|
|
1413
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
1414
|
+
|
|
1415
|
+
if completed {
|
|
1416
|
+
let duration = player.currentItem?.duration.seconds ?? 0.0
|
|
1417
|
+
self?.notifySeek(position, duration)
|
|
1043
1418
|
}
|
|
1044
1419
|
}
|
|
1045
1420
|
}
|
|
@@ -1048,13 +1423,41 @@ class TrackPlayerCore: NSObject {
|
|
|
1048
1423
|
|
|
1049
1424
|
func setRepeatMode(mode: RepeatMode) -> Bool {
|
|
1050
1425
|
print("🔁 TrackPlayerCore: setRepeatMode called with mode: \(mode)")
|
|
1051
|
-
|
|
1426
|
+
if Thread.isMainThread {
|
|
1427
|
+
self.repeatMode = mode
|
|
1428
|
+
} else {
|
|
1429
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1430
|
+
self?.repeatMode = mode
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1052
1433
|
return true
|
|
1053
1434
|
}
|
|
1054
1435
|
|
|
1055
|
-
// MARK: - State Management
|
|
1056
|
-
|
|
1057
1436
|
func getState() -> PlayerState {
|
|
1437
|
+
// Called from Promise.async background thread
|
|
1438
|
+
// Schedule on main thread and wait for result
|
|
1439
|
+
if Thread.isMainThread {
|
|
1440
|
+
return getStateInternal()
|
|
1441
|
+
} else {
|
|
1442
|
+
var state: PlayerState!
|
|
1443
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1444
|
+
state =
|
|
1445
|
+
self?.getStateInternal()
|
|
1446
|
+
?? PlayerState(
|
|
1447
|
+
currentTrack: nil,
|
|
1448
|
+
currentPosition: 0.0,
|
|
1449
|
+
totalDuration: 0.0,
|
|
1450
|
+
currentState: .stopped,
|
|
1451
|
+
currentPlaylistId: nil,
|
|
1452
|
+
currentIndex: -1.0,
|
|
1453
|
+
currentPlayingType: .notPlaying
|
|
1454
|
+
)
|
|
1455
|
+
}
|
|
1456
|
+
return state
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private func getStateInternal() -> PlayerState {
|
|
1058
1461
|
guard let player = player else {
|
|
1059
1462
|
return PlayerState(
|
|
1060
1463
|
currentTrack: nil,
|
|
@@ -1062,7 +1465,8 @@ class TrackPlayerCore: NSObject {
|
|
|
1062
1465
|
totalDuration: 0.0,
|
|
1063
1466
|
currentState: .stopped,
|
|
1064
1467
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1065
|
-
currentIndex: -1.0
|
|
1468
|
+
currentIndex: -1.0,
|
|
1469
|
+
currentPlayingType: .notPlaying
|
|
1066
1470
|
)
|
|
1067
1471
|
}
|
|
1068
1472
|
|
|
@@ -1082,13 +1486,29 @@ class TrackPlayerCore: NSObject {
|
|
|
1082
1486
|
// Get current index
|
|
1083
1487
|
let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
|
|
1084
1488
|
|
|
1489
|
+
// Map internal temporary type to CurrentPlayingType
|
|
1490
|
+
let currentPlayingType: CurrentPlayingType
|
|
1491
|
+
if currentTrack == nil {
|
|
1492
|
+
currentPlayingType = .notPlaying
|
|
1493
|
+
} else {
|
|
1494
|
+
switch currentTemporaryType {
|
|
1495
|
+
case .none:
|
|
1496
|
+
currentPlayingType = .playlist
|
|
1497
|
+
case .playNext:
|
|
1498
|
+
currentPlayingType = .playNext
|
|
1499
|
+
case .upNext:
|
|
1500
|
+
currentPlayingType = .upNext
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1085
1504
|
return PlayerState(
|
|
1086
1505
|
currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
|
|
1087
1506
|
currentPosition: currentPosition,
|
|
1088
1507
|
totalDuration: totalDuration,
|
|
1089
1508
|
currentState: currentState,
|
|
1090
1509
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1091
|
-
currentIndex: currentIndex
|
|
1510
|
+
currentIndex: currentIndex,
|
|
1511
|
+
currentPlayingType: currentPlayingType
|
|
1092
1512
|
)
|
|
1093
1513
|
}
|
|
1094
1514
|
|
|
@@ -1133,72 +1553,374 @@ class TrackPlayerCore: NSObject {
|
|
|
1133
1553
|
}
|
|
1134
1554
|
|
|
1135
1555
|
func playFromIndex(index: Int) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
return
|
|
1556
|
+
if Thread.isMainThread {
|
|
1557
|
+
playFromIndexInternal(index: index)
|
|
1558
|
+
} else {
|
|
1559
|
+
DispatchQueue.main.async { [weak self] in
|
|
1560
|
+
self?.playFromIndexInternal(index: index)
|
|
1142
1561
|
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1143
1564
|
|
|
1144
|
-
|
|
1145
|
-
print(" Total tracks in playlist: \(self.currentTracks.count)")
|
|
1146
|
-
print(" Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
1565
|
+
// MARK: - Skip to Index in Actual Queue
|
|
1147
1566
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1567
|
+
func skipToIndex(index: Int) -> Bool {
|
|
1568
|
+
if Thread.isMainThread {
|
|
1569
|
+
return skipToIndexInternal(index: index)
|
|
1570
|
+
} else {
|
|
1571
|
+
var result = false
|
|
1572
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1573
|
+
result = self?.skipToIndexInternal(index: index) ?? false
|
|
1574
|
+
}
|
|
1575
|
+
return result
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1150
1578
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1579
|
+
private func skipToIndexInternal(index: Int) -> Bool {
|
|
1580
|
+
print("\n🎯 TrackPlayerCore: SKIP TO INDEX \(index)")
|
|
1153
1581
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1582
|
+
// Get actual queue to validate index and determine position
|
|
1583
|
+
let actualQueue = getActualQueueInternal()
|
|
1584
|
+
let totalQueueSize = actualQueue.count
|
|
1585
|
+
|
|
1586
|
+
// Validate index
|
|
1587
|
+
guard index >= 0 && index < totalQueueSize else {
|
|
1588
|
+
print(" ❌ Invalid index \(index), queue size is \(totalQueueSize)")
|
|
1589
|
+
return false
|
|
1590
|
+
}
|
|
1160
1591
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1592
|
+
// Calculate queue section boundaries
|
|
1593
|
+
// ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
|
|
1594
|
+
let currentPos = currentTrackIndex
|
|
1595
|
+
let playNextStart = currentPos + 1
|
|
1596
|
+
let playNextEnd = playNextStart + playNextStack.count
|
|
1597
|
+
let upNextStart = playNextEnd
|
|
1598
|
+
let upNextEnd = upNextStart + upNextQueue.count
|
|
1599
|
+
let originalRemainingStart = upNextEnd
|
|
1600
|
+
|
|
1601
|
+
print(" Queue structure:")
|
|
1602
|
+
print(" currentPos: \(currentPos)")
|
|
1603
|
+
print(" playNextStart: \(playNextStart), playNextEnd: \(playNextEnd)")
|
|
1604
|
+
print(" upNextStart: \(upNextStart), upNextEnd: \(upNextEnd)")
|
|
1605
|
+
print(" originalRemainingStart: \(originalRemainingStart)")
|
|
1606
|
+
print(" totalQueueSize: \(totalQueueSize)")
|
|
1607
|
+
|
|
1608
|
+
// Case 1: Target is before current - use playFromIndex on original
|
|
1609
|
+
if index < currentPos {
|
|
1610
|
+
print(" 📍 Target is before current, jumping to original playlist index \(index)")
|
|
1611
|
+
playFromIndexInternal(index: index)
|
|
1612
|
+
return true
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// Case 2: Target is current - seek to beginning
|
|
1616
|
+
if index == currentPos {
|
|
1617
|
+
print(" 📍 Target is current track, seeking to beginning")
|
|
1618
|
+
player?.seek(to: .zero)
|
|
1619
|
+
return true
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// Case 3: Target is in playNext section
|
|
1623
|
+
if index >= playNextStart && index < playNextEnd {
|
|
1624
|
+
let playNextIndex = index - playNextStart
|
|
1625
|
+
print(" 📍 Target is in playNext section at position \(playNextIndex)")
|
|
1626
|
+
|
|
1627
|
+
// Remove tracks before the target from playNext (they're being skipped)
|
|
1628
|
+
if playNextIndex > 0 {
|
|
1629
|
+
playNextStack.removeFirst(playNextIndex)
|
|
1630
|
+
print(" Removed \(playNextIndex) tracks from playNext stack")
|
|
1166
1631
|
}
|
|
1167
1632
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1633
|
+
// Rebuild queue and advance
|
|
1634
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1635
|
+
player?.advanceToNextItem()
|
|
1636
|
+
return true
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
// Case 4: Target is in upNext section
|
|
1640
|
+
if index >= upNextStart && index < upNextEnd {
|
|
1641
|
+
let upNextIndex = index - upNextStart
|
|
1642
|
+
print(" 📍 Target is in upNext section at position \(upNextIndex)")
|
|
1643
|
+
|
|
1644
|
+
// Clear all playNext tracks (they're being skipped)
|
|
1645
|
+
playNextStack.removeAll()
|
|
1646
|
+
print(" Cleared all playNext tracks")
|
|
1647
|
+
|
|
1648
|
+
// Remove tracks before target from upNext
|
|
1649
|
+
if upNextIndex > 0 {
|
|
1650
|
+
upNextQueue.removeFirst(upNextIndex)
|
|
1651
|
+
print(" Removed \(upNextIndex) tracks from upNext queue")
|
|
1171
1652
|
}
|
|
1172
1653
|
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1654
|
+
// Rebuild queue and advance
|
|
1655
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1656
|
+
player?.advanceToNextItem()
|
|
1657
|
+
return true
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Case 5: Target is in remaining original tracks
|
|
1661
|
+
if index >= originalRemainingStart {
|
|
1662
|
+
// Get the target track directly from actualQueue
|
|
1663
|
+
let targetTrack = actualQueue[index]
|
|
1664
|
+
|
|
1665
|
+
print(" 📍 Case 5: Target is in remaining original tracks")
|
|
1666
|
+
print(" targetTrack.id: \(targetTrack.id)")
|
|
1667
|
+
print(" currentTracks.count: \(currentTracks.count)")
|
|
1668
|
+
print(" currentTracks IDs: \(currentTracks.map { $0.id })")
|
|
1669
|
+
|
|
1670
|
+
// Find this track's index in the original playlist
|
|
1671
|
+
guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
|
|
1672
|
+
print(" ❌ Could not find track \(targetTrack.id) in original playlist")
|
|
1673
|
+
print(" Available tracks: \(currentTracks.map { $0.id })")
|
|
1674
|
+
return false
|
|
1177
1675
|
}
|
|
1178
1676
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1677
|
+
print(" originalIndex found: \(originalIndex)")
|
|
1678
|
+
|
|
1679
|
+
// Clear all temporary tracks (they're being skipped)
|
|
1680
|
+
playNextStack.removeAll()
|
|
1681
|
+
upNextQueue.removeAll()
|
|
1682
|
+
currentTemporaryType = .none
|
|
1683
|
+
print(" Cleared all temporary tracks")
|
|
1684
|
+
|
|
1685
|
+
// Play from the original playlist index
|
|
1686
|
+
let success = playFromIndexInternalWithResult(index: originalIndex)
|
|
1687
|
+
return success
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
print(" ❌ Unexpected case, index \(index) not handled")
|
|
1691
|
+
return false
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
private func playFromIndexInternal(index: Int) {
|
|
1695
|
+
_ = playFromIndexInternalWithResult(index: index)
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
private func playFromIndexInternalWithResult(index: Int) -> Bool {
|
|
1699
|
+
guard index >= 0 && index < self.currentTracks.count else {
|
|
1700
|
+
print(
|
|
1701
|
+
"❌ TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)"
|
|
1702
|
+
)
|
|
1703
|
+
return false
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
|
|
1707
|
+
print(" Total tracks in playlist: \(self.currentTracks.count)")
|
|
1708
|
+
print(" Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
1709
|
+
|
|
1710
|
+
// Clear temporary tracks when jumping to specific index
|
|
1711
|
+
self.playNextStack.removeAll()
|
|
1712
|
+
self.upNextQueue.removeAll()
|
|
1713
|
+
self.currentTemporaryType = .none
|
|
1714
|
+
print(" 🧹 Cleared temporary tracks")
|
|
1715
|
+
|
|
1716
|
+
// Store the full playlist
|
|
1717
|
+
let fullPlaylist = self.currentTracks
|
|
1718
|
+
|
|
1719
|
+
// Update currentTrackIndex BEFORE updating queue
|
|
1720
|
+
self.currentTrackIndex = index
|
|
1721
|
+
|
|
1722
|
+
// Recreate the queue starting from the target index
|
|
1723
|
+
// This ensures all remaining tracks are in the queue
|
|
1724
|
+
let tracksToPlay = Array(fullPlaylist[index...])
|
|
1725
|
+
print(
|
|
1726
|
+
" 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
|
|
1727
|
+
)
|
|
1728
|
+
|
|
1729
|
+
// Create gapless-optimized player items
|
|
1730
|
+
let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
|
|
1731
|
+
// First few items get preload treatment for faster playback
|
|
1732
|
+
let isPreload = offset < Constants.gaplessPreloadCount
|
|
1733
|
+
return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
guard let player = self.player, !items.isEmpty else {
|
|
1737
|
+
print("❌ No player or no items to play")
|
|
1738
|
+
return false
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Remove old boundary observer
|
|
1742
|
+
if let boundaryObserver = self.boundaryTimeObserver {
|
|
1743
|
+
player.removeTimeObserver(boundaryObserver)
|
|
1744
|
+
self.boundaryTimeObserver = nil
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Clear and rebuild queue
|
|
1748
|
+
player.removeAllItems()
|
|
1749
|
+
var lastItem: AVPlayerItem? = nil
|
|
1750
|
+
for item in items {
|
|
1751
|
+
player.insert(item, after: lastItem)
|
|
1752
|
+
lastItem = item
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// Restore the full playlist reference (don't slice it!)
|
|
1756
|
+
self.currentTracks = fullPlaylist
|
|
1757
|
+
|
|
1758
|
+
print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
|
|
1759
|
+
if let track = self.getCurrentTrack() {
|
|
1760
|
+
print(" 🎵 Playing: \(track.title)")
|
|
1761
|
+
notifyTrackChange(track, .skip)
|
|
1762
|
+
self.mediaSessionManager?.onTrackChanged()
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Start preloading upcoming tracks for gapless playback
|
|
1766
|
+
self.preloadUpcomingTracks(from: index + 1)
|
|
1767
|
+
|
|
1768
|
+
player.play()
|
|
1769
|
+
return true
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// MARK: - Temporary Track Management
|
|
1773
|
+
|
|
1774
|
+
/**
|
|
1775
|
+
* Add a track to the up-next queue (FIFO - first added plays first)
|
|
1776
|
+
* Track will be inserted after currently playing track and any playNext tracks
|
|
1777
|
+
*/
|
|
1778
|
+
func addToUpNext(trackId: String) {
|
|
1779
|
+
DispatchQueue.main.async { [weak self] in
|
|
1780
|
+
self?.addToUpNextInternal(trackId: trackId)
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
private func addToUpNextInternal(trackId: String) {
|
|
1785
|
+
print("📋 TrackPlayerCore: addToUpNext(\(trackId))")
|
|
1786
|
+
|
|
1787
|
+
// Find the track from current playlist or all playlists
|
|
1788
|
+
guard let track = self.findTrackById(trackId) else {
|
|
1789
|
+
print("❌ TrackPlayerCore: Track \(trackId) not found")
|
|
1790
|
+
return
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// Add to end of upNext queue (FIFO)
|
|
1794
|
+
self.upNextQueue.append(track)
|
|
1795
|
+
print(" ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
|
|
1796
|
+
|
|
1797
|
+
// Rebuild the player queue if actively playing
|
|
1798
|
+
if self.player?.currentItem != nil {
|
|
1799
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* Add a track to play next (LIFO - last added plays first)
|
|
1805
|
+
* Track will be inserted immediately after currently playing track
|
|
1806
|
+
*/
|
|
1807
|
+
func playNext(trackId: String) {
|
|
1808
|
+
DispatchQueue.main.async { [weak self] in
|
|
1809
|
+
self?.playNextInternal(trackId: trackId)
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
private func playNextInternal(trackId: String) {
|
|
1814
|
+
print("⏭️ TrackPlayerCore: playNext(\(trackId))")
|
|
1815
|
+
|
|
1816
|
+
// Find the track from current playlist or all playlists
|
|
1817
|
+
guard let track = self.findTrackById(trackId) else {
|
|
1818
|
+
print("❌ TrackPlayerCore: Track \(trackId) not found")
|
|
1819
|
+
return
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// Insert at beginning of playNext stack (LIFO)
|
|
1823
|
+
self.playNextStack.insert(track, at: 0)
|
|
1824
|
+
print(" ✅ Added '\(track.title)' to playNext stack (position: 1)")
|
|
1825
|
+
|
|
1826
|
+
// Rebuild the player queue if actively playing
|
|
1827
|
+
if self.player?.currentItem != nil {
|
|
1828
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
/**
|
|
1833
|
+
* Rebuild the AVQueuePlayer from current position with temporary tracks
|
|
1834
|
+
* Order: [current] + [playNext stack reversed] + [upNext queue] + [remaining original]
|
|
1835
|
+
*/
|
|
1836
|
+
private func rebuildAVQueueFromCurrentPosition() {
|
|
1837
|
+
guard let player = self.player else { return }
|
|
1838
|
+
|
|
1839
|
+
print("\n🔄 TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
|
|
1840
|
+
print(" playNext stack: \(playNextStack.count) tracks")
|
|
1841
|
+
print(" upNext queue: \(upNextQueue.count) tracks")
|
|
1842
|
+
|
|
1843
|
+
// Don't interrupt currently playing item
|
|
1844
|
+
let currentItem = player.currentItem
|
|
1845
|
+
let playingItems = player.items()
|
|
1846
|
+
|
|
1847
|
+
// Build new queue order:
|
|
1848
|
+
// [playNext stack] + [upNext queue] + [remaining original tracks]
|
|
1849
|
+
var newQueueTracks: [TrackItem] = []
|
|
1850
|
+
|
|
1851
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1852
|
+
// Stack is already in correct order since we insert at position 0
|
|
1853
|
+
newQueueTracks.append(contentsOf: playNextStack)
|
|
1854
|
+
|
|
1855
|
+
// Add upNext queue (in order, FIFO)
|
|
1856
|
+
newQueueTracks.append(contentsOf: upNextQueue)
|
|
1857
|
+
|
|
1858
|
+
// Add remaining original tracks
|
|
1859
|
+
if currentTrackIndex + 1 < currentTracks.count {
|
|
1860
|
+
let remainingOriginal = Array(currentTracks[(currentTrackIndex + 1)...])
|
|
1861
|
+
newQueueTracks.append(contentsOf: remainingOriginal)
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
print(" New queue: \(newQueueTracks.count) tracks total")
|
|
1865
|
+
|
|
1866
|
+
// Remove all items from player EXCEPT the currently playing one
|
|
1867
|
+
for item in playingItems where item != currentItem {
|
|
1868
|
+
player.remove(item)
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Insert new items in order
|
|
1872
|
+
var lastItem = currentItem
|
|
1873
|
+
for track in newQueueTracks {
|
|
1874
|
+
if let item = createGaplessPlayerItem(for: track, isPreload: false) {
|
|
1183
1875
|
player.insert(item, after: lastItem)
|
|
1184
1876
|
lastItem = item
|
|
1185
1877
|
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
print(" ✅ Queue rebuilt successfully")
|
|
1881
|
+
}
|
|
1186
1882
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1883
|
+
/**
|
|
1884
|
+
* Find a track by ID from current playlist or all playlists
|
|
1885
|
+
*/
|
|
1886
|
+
private func findTrackById(_ trackId: String) -> TrackItem? {
|
|
1887
|
+
// First check current playlist
|
|
1888
|
+
if let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
1889
|
+
return track
|
|
1890
|
+
}
|
|
1189
1891
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1892
|
+
// Then check all playlists
|
|
1893
|
+
let allPlaylists = playlistManager.getAllPlaylists()
|
|
1894
|
+
for playlist in allPlaylists {
|
|
1895
|
+
if let track = playlist.tracks.first(where: { $0.id == trackId }) {
|
|
1896
|
+
return track
|
|
1195
1897
|
}
|
|
1898
|
+
}
|
|
1196
1899
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1900
|
+
return nil
|
|
1901
|
+
}
|
|
1199
1902
|
|
|
1200
|
-
|
|
1903
|
+
/**
|
|
1904
|
+
* Determine what type of track is currently playing
|
|
1905
|
+
*/
|
|
1906
|
+
private func determineCurrentTemporaryType() -> TemporaryType {
|
|
1907
|
+
guard let currentItem = player?.currentItem,
|
|
1908
|
+
let trackId = currentItem.trackId
|
|
1909
|
+
else {
|
|
1910
|
+
return .none
|
|
1201
1911
|
}
|
|
1912
|
+
|
|
1913
|
+
// Check if in playNext stack
|
|
1914
|
+
if playNextStack.contains(where: { $0.id == trackId }) {
|
|
1915
|
+
return .playNext
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Check if in upNext queue
|
|
1919
|
+
if upNextQueue.contains(where: { $0.id == trackId }) {
|
|
1920
|
+
return .upNext
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
return .none
|
|
1202
1924
|
}
|
|
1203
1925
|
|
|
1204
1926
|
// MARK: - Cleanup
|