react-native-nitro-player 0.3.0-alpha.9 → 0.4.1-alpha.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 +944 -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 +996 -288
- 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/callbackManager.d.ts +18 -0
- package/lib/hooks/callbackManager.js +66 -0
- 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 +32 -19
- package/lib/hooks/useOnChangeTrack.js +15 -12
- package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
- 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/callbackManager.ts +87 -0
- 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 +33 -20
- package/src/hooks/useOnChangeTrack.ts +15 -11
- package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
- 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
|
|
@@ -246,15 +280,26 @@ class TrackPlayerCore: NSObject {
|
|
|
246
280
|
print("\n🏁 TrackPlayerCore: Track finished playing")
|
|
247
281
|
|
|
248
282
|
guard let finishedItem = notification.object as? AVPlayerItem else {
|
|
249
|
-
|
|
250
|
-
skipToNext()
|
|
283
|
+
// Don't call skipToNext — AVQueuePlayer with actionAtItemEnd = .advance already auto-advances
|
|
251
284
|
return
|
|
252
285
|
}
|
|
253
286
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
// Determine what type of track just finished and remove it from temporary lists
|
|
288
|
+
if let trackId = finishedItem.trackId {
|
|
289
|
+
// Check if it was a playNext track
|
|
290
|
+
if let index = playNextStack.firstIndex(where: { $0.id == trackId }) {
|
|
291
|
+
let track = playNextStack.remove(at: index)
|
|
292
|
+
print("🏁 Finished playNext track: \(track.title) - removed from stack")
|
|
293
|
+
}
|
|
294
|
+
// Check if it was an upNext track
|
|
295
|
+
else if let index = upNextQueue.firstIndex(where: { $0.id == trackId }) {
|
|
296
|
+
let track = upNextQueue.remove(at: index)
|
|
297
|
+
print("🏁 Finished upNext track: \(track.title) - removed from queue")
|
|
298
|
+
}
|
|
299
|
+
// Otherwise it was from original playlist
|
|
300
|
+
else if let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
301
|
+
print("🏁 Finished original track: \(track.title)")
|
|
302
|
+
}
|
|
258
303
|
}
|
|
259
304
|
|
|
260
305
|
// Check remaining queue
|
|
@@ -269,23 +314,35 @@ class TrackPlayerCore: NSObject {
|
|
|
269
314
|
print("🔁 TrackPlayerCore: Repeat mode is TRACK - replaying current track")
|
|
270
315
|
DispatchQueue.main.async { [weak self] in
|
|
271
316
|
guard let self = self, let player = self.player else { return }
|
|
272
|
-
//
|
|
273
|
-
self.
|
|
317
|
+
// For temporary tracks, just seek to beginning
|
|
318
|
+
if self.currentTemporaryType != .none {
|
|
319
|
+
player.seek(to: .zero)
|
|
320
|
+
player.play()
|
|
321
|
+
} else {
|
|
322
|
+
// For original tracks, recreate via playFromIndex
|
|
323
|
+
self.playFromIndex(index: self.currentTrackIndex)
|
|
324
|
+
}
|
|
274
325
|
}
|
|
275
326
|
return
|
|
276
327
|
|
|
277
328
|
case .playlist:
|
|
278
|
-
// Check if we're at the end of the playlist
|
|
279
|
-
if currentTrackIndex >= currentTracks.count - 1 {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
329
|
+
// Check if we're at the end of the ORIGINAL playlist (ignore temps)
|
|
330
|
+
if currentTemporaryType == .none && currentTrackIndex >= currentTracks.count - 1 {
|
|
331
|
+
// Check if there are still temporary tracks
|
|
332
|
+
if !playNextStack.isEmpty || !upNextQueue.isEmpty {
|
|
333
|
+
print("🔁 TrackPlayerCore: Temporary tracks remaining, continuing...")
|
|
334
|
+
} else {
|
|
335
|
+
print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - restarting from beginning")
|
|
336
|
+
// Clear temps and restart
|
|
337
|
+
playNextStack.removeAll()
|
|
338
|
+
upNextQueue.removeAll()
|
|
339
|
+
DispatchQueue.main.async { [weak self] in
|
|
340
|
+
guard let self = self else { return }
|
|
341
|
+
self.playFromIndex(index: 0)
|
|
342
|
+
}
|
|
343
|
+
return
|
|
285
344
|
}
|
|
286
|
-
return
|
|
287
345
|
} else {
|
|
288
|
-
// Not at end, just continue to next track
|
|
289
346
|
print("🔁 TrackPlayerCore: Repeat mode is PLAYLIST - continuing to next track")
|
|
290
347
|
}
|
|
291
348
|
|
|
@@ -294,8 +351,10 @@ class TrackPlayerCore: NSObject {
|
|
|
294
351
|
print("🔁 TrackPlayerCore: Repeat mode is OFF")
|
|
295
352
|
}
|
|
296
353
|
|
|
297
|
-
// Track ended naturally
|
|
298
|
-
|
|
354
|
+
// Track ended naturally — notify with .end reason
|
|
355
|
+
// AVQueuePlayer with actionAtItemEnd = .advance auto-advances to next item
|
|
356
|
+
// The KVO observer (currentItemDidChange) will handle the track change notification
|
|
357
|
+
notifyTrackChange(
|
|
299
358
|
getCurrentTrack()
|
|
300
359
|
?? TrackItem(
|
|
301
360
|
id: "",
|
|
@@ -304,17 +363,15 @@ class TrackPlayerCore: NSObject {
|
|
|
304
363
|
album: "",
|
|
305
364
|
duration: 0,
|
|
306
365
|
url: "",
|
|
307
|
-
artwork: nil
|
|
366
|
+
artwork: nil,
|
|
367
|
+
extraPayload: nil
|
|
308
368
|
), .end)
|
|
309
|
-
|
|
310
|
-
// Try to play next track
|
|
311
|
-
skipToNext()
|
|
312
369
|
}
|
|
313
370
|
|
|
314
371
|
@objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
|
|
315
372
|
if let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error {
|
|
316
373
|
print("❌ TrackPlayerCore: Playback failed - \(error)")
|
|
317
|
-
|
|
374
|
+
notifyPlaybackStateChange(.stopped, .error)
|
|
318
375
|
}
|
|
319
376
|
}
|
|
320
377
|
|
|
@@ -346,7 +403,7 @@ class TrackPlayerCore: NSObject {
|
|
|
346
403
|
print("🎯 TrackPlayerCore: Time jumped (seek detected) - position: \(Int(position))s")
|
|
347
404
|
|
|
348
405
|
// Call onSeek callback immediately
|
|
349
|
-
|
|
406
|
+
notifySeek(position, duration)
|
|
350
407
|
|
|
351
408
|
// Mark that this was a manual seek
|
|
352
409
|
isManuallySeeked = true
|
|
@@ -371,7 +428,7 @@ class TrackPlayerCore: NSObject {
|
|
|
371
428
|
emitStateChange()
|
|
372
429
|
} else if player.status == .failed {
|
|
373
430
|
print("❌ TrackPlayerCore: Player failed")
|
|
374
|
-
|
|
431
|
+
notifyPlaybackStateChange(.stopped, .error)
|
|
375
432
|
}
|
|
376
433
|
} else if keyPath == "rate" {
|
|
377
434
|
print("👀 TrackPlayerCore: Rate changed to: \(player.rate)")
|
|
@@ -437,12 +494,34 @@ class TrackPlayerCore: NSObject {
|
|
|
437
494
|
// Setup KVO observers for current item
|
|
438
495
|
setupCurrentItemObservers(item: currentItem)
|
|
439
496
|
|
|
440
|
-
// Update track index
|
|
497
|
+
// Update track index and determine temporary type
|
|
441
498
|
if let trackId = currentItem.trackId {
|
|
442
499
|
print("🔍 TrackPlayerCore: Looking up trackId '\(trackId)' in currentTracks...")
|
|
443
500
|
print(" Current index BEFORE lookup: \(currentTrackIndex)")
|
|
444
501
|
|
|
445
|
-
|
|
502
|
+
// Update temporary type
|
|
503
|
+
currentTemporaryType = determineCurrentTemporaryType()
|
|
504
|
+
print(" 🎯 Track type: \(currentTemporaryType)")
|
|
505
|
+
|
|
506
|
+
// If it's a temporary track, don't update currentTrackIndex
|
|
507
|
+
if currentTemporaryType != .none {
|
|
508
|
+
// Find and emit the temporary track
|
|
509
|
+
var tempTrack: TrackItem? = nil
|
|
510
|
+
if currentTemporaryType == .playNext {
|
|
511
|
+
tempTrack = playNextStack.first(where: { $0.id == trackId })
|
|
512
|
+
} else if currentTemporaryType == .upNext {
|
|
513
|
+
tempTrack = upNextQueue.first(where: { $0.id == trackId })
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if let track = tempTrack {
|
|
517
|
+
print(" 🎵 Temporary track: \(track.title) - \(track.artist)")
|
|
518
|
+
print(" 📢 Emitting onChangeTrack for temporary track")
|
|
519
|
+
notifyTrackChange(track, .skip)
|
|
520
|
+
mediaSessionManager?.onTrackChanged()
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
// It's an original playlist track
|
|
524
|
+
else if let index = currentTracks.firstIndex(where: { $0.id == trackId }) {
|
|
446
525
|
print(" ✅ Found track at index: \(index)")
|
|
447
526
|
print(" Setting currentTrackIndex from \(currentTrackIndex) to \(index)")
|
|
448
527
|
|
|
@@ -456,7 +535,7 @@ class TrackPlayerCore: NSObject {
|
|
|
456
535
|
// This prevents duplicate emissions
|
|
457
536
|
if oldIndex != index {
|
|
458
537
|
print(" 📢 Emitting onChangeTrack (index changed from \(oldIndex) to \(index))")
|
|
459
|
-
|
|
538
|
+
notifyTrackChange(track, .skip)
|
|
460
539
|
mediaSessionManager?.onTrackChanged()
|
|
461
540
|
} else {
|
|
462
541
|
print(" ⏭️ Skipping onChangeTrack emission (index unchanged)")
|
|
@@ -485,14 +564,16 @@ class TrackPlayerCore: NSObject {
|
|
|
485
564
|
private func setupCurrentItemObservers(item: AVPlayerItem) {
|
|
486
565
|
print("📱 TrackPlayerCore: Setting up item observers")
|
|
487
566
|
|
|
488
|
-
// Observe status - recreate boundaries when ready
|
|
567
|
+
// Observe status - recreate boundaries when ready and update now playing info
|
|
489
568
|
let statusObserver = item.observe(\.status, options: [.new]) { [weak self] item, _ in
|
|
490
569
|
if item.status == .readyToPlay {
|
|
491
570
|
print("✅ TrackPlayerCore: Item ready, setting up boundaries")
|
|
492
571
|
self?.setupBoundaryTimeObserver()
|
|
572
|
+
// Update now playing info now that duration is available
|
|
573
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
493
574
|
} else if item.status == .failed {
|
|
494
575
|
print("❌ TrackPlayerCore: Item failed")
|
|
495
|
-
self?.
|
|
576
|
+
self?.notifyPlaybackStateChange(.stopped, .error)
|
|
496
577
|
}
|
|
497
578
|
}
|
|
498
579
|
currentItemObservers.append(statusObserver)
|
|
@@ -517,44 +598,63 @@ class TrackPlayerCore: NSObject {
|
|
|
517
598
|
// MARK: - Playlist Management
|
|
518
599
|
|
|
519
600
|
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")
|
|
601
|
+
if Thread.isMainThread {
|
|
602
|
+
loadPlaylistInternal(playlistId: playlistId)
|
|
603
|
+
} else {
|
|
604
|
+
DispatchQueue.main.sync { [weak self] in
|
|
605
|
+
self?.loadPlaylistInternal(playlistId: playlistId)
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
535
609
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
610
|
+
private func loadPlaylistInternal(playlistId: String) {
|
|
611
|
+
print("\n" + String(repeating: "🎼", count: Constants.playlistSeparatorLength))
|
|
612
|
+
print("📂 TrackPlayerCore: LOAD PLAYLIST REQUEST")
|
|
613
|
+
print(" Playlist ID: \(playlistId)")
|
|
614
|
+
|
|
615
|
+
// Clear temporary tracks when loading new playlist
|
|
616
|
+
self.playNextStack.removeAll()
|
|
617
|
+
self.upNextQueue.removeAll()
|
|
618
|
+
self.currentTemporaryType = .none
|
|
619
|
+
print(" 🧹 Cleared temporary tracks")
|
|
620
|
+
|
|
621
|
+
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
622
|
+
if let playlist = playlist {
|
|
623
|
+
print(" ✅ Found playlist: \(playlist.name)")
|
|
624
|
+
print(" 📋 Contains \(playlist.tracks.count) tracks:")
|
|
625
|
+
for (index, track) in playlist.tracks.enumerated() {
|
|
626
|
+
print(" [\(index + 1)] \(track.title) - \(track.artist)")
|
|
545
627
|
}
|
|
628
|
+
print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
629
|
+
|
|
630
|
+
self.currentPlaylistId = playlistId
|
|
631
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
632
|
+
// Emit initial state (paused/stopped before play)
|
|
633
|
+
self.emitStateChange()
|
|
634
|
+
} else {
|
|
635
|
+
print(" ❌ Playlist NOT FOUND")
|
|
636
|
+
print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n")
|
|
546
637
|
}
|
|
547
638
|
}
|
|
548
639
|
|
|
549
640
|
func updatePlaylist(playlistId: String) {
|
|
550
641
|
DispatchQueue.main.async { [weak self] in
|
|
551
642
|
guard let self = self else { return }
|
|
552
|
-
|
|
643
|
+
guard self.currentPlaylistId == playlistId,
|
|
553
644
|
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
645
|
+
else { return }
|
|
646
|
+
|
|
647
|
+
// If nothing is playing yet, do a full load
|
|
648
|
+
guard let player = self.player, player.currentItem != nil else {
|
|
649
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
650
|
+
return
|
|
557
651
|
}
|
|
652
|
+
|
|
653
|
+
// Update tracks list without interrupting playback
|
|
654
|
+
self.currentTracks = playlist.tracks
|
|
655
|
+
|
|
656
|
+
// Rebuild only the items after the currently playing item
|
|
657
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
558
658
|
}
|
|
559
659
|
}
|
|
560
660
|
|
|
@@ -583,8 +683,8 @@ class TrackPlayerCore: NSObject {
|
|
|
583
683
|
}
|
|
584
684
|
|
|
585
685
|
print("🔔 TrackPlayerCore: Emitting state change: \(state)")
|
|
586
|
-
print("🔔 TrackPlayerCore: Callback exists: \(
|
|
587
|
-
|
|
686
|
+
print("🔔 TrackPlayerCore: Callback exists: \(!onPlaybackStateChangeListeners.isEmpty)")
|
|
687
|
+
notifyPlaybackStateChange(state, reason)
|
|
588
688
|
mediaSessionManager?.onPlaybackStateChanged()
|
|
589
689
|
}
|
|
590
690
|
|
|
@@ -594,9 +694,40 @@ class TrackPlayerCore: NSObject {
|
|
|
594
694
|
private func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false)
|
|
595
695
|
-> AVPlayerItem?
|
|
596
696
|
{
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
697
|
+
// Get effective URL - uses local path if downloaded, otherwise remote URL
|
|
698
|
+
let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
|
|
699
|
+
|
|
700
|
+
// Create URL - use fileURLWithPath for local files, URL(string:) for remote
|
|
701
|
+
let url: URL
|
|
702
|
+
let isLocal = effectiveUrlString.hasPrefix("/")
|
|
703
|
+
|
|
704
|
+
if isLocal {
|
|
705
|
+
// Local file - use fileURLWithPath
|
|
706
|
+
print("📥 TrackPlayerCore: Using DOWNLOADED version for \(track.title)")
|
|
707
|
+
print(" Local path: \(effectiveUrlString)")
|
|
708
|
+
|
|
709
|
+
// Verify file exists
|
|
710
|
+
if FileManager.default.fileExists(atPath: effectiveUrlString) {
|
|
711
|
+
url = URL(fileURLWithPath: effectiveUrlString)
|
|
712
|
+
print(" File URL: \(url.absoluteString)")
|
|
713
|
+
print(" ✅ File verified to exist")
|
|
714
|
+
} else {
|
|
715
|
+
print(" ❌ Downloaded file does NOT exist at path!")
|
|
716
|
+
print(" Falling back to remote URL: \(track.url)")
|
|
717
|
+
guard let remoteUrl = URL(string: track.url) else {
|
|
718
|
+
print("❌ TrackPlayerCore: Invalid remote URL: \(track.url)")
|
|
719
|
+
return nil
|
|
720
|
+
}
|
|
721
|
+
url = remoteUrl
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Remote URL
|
|
725
|
+
guard let remoteUrl = URL(string: effectiveUrlString) else {
|
|
726
|
+
print("❌ TrackPlayerCore: Invalid URL for track: \(track.title) - \(effectiveUrlString)")
|
|
727
|
+
return nil
|
|
728
|
+
}
|
|
729
|
+
url = remoteUrl
|
|
730
|
+
print("🌐 TrackPlayerCore: Using REMOTE version for \(track.title)")
|
|
600
731
|
}
|
|
601
732
|
|
|
602
733
|
// Check if we have a preloaded asset for this track
|
|
@@ -625,6 +756,13 @@ class TrackPlayerCore: NSObject {
|
|
|
625
756
|
// Store track ID for later reference
|
|
626
757
|
item.trackId = track.id
|
|
627
758
|
|
|
759
|
+
// Apply equalizer audio mix to the player item
|
|
760
|
+
// This enables real-time EQ processing via MTAudioProcessingTap
|
|
761
|
+
// Apply equalizer audio mix to the player item
|
|
762
|
+
// This enables real-time EQ processing via MTAudioProcessingTap
|
|
763
|
+
EqualizerCore.shared.applyAudioMix(to: item)
|
|
764
|
+
print("🎛️ TrackPlayerCore: Requesting EQ audio mix application for \(track.title)")
|
|
765
|
+
|
|
628
766
|
// If this is a preload request, start loading asset keys asynchronously
|
|
629
767
|
if isPreload {
|
|
630
768
|
asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
|
|
@@ -654,10 +792,13 @@ class TrackPlayerCore: NSObject {
|
|
|
654
792
|
preloadQueue.async { [weak self] in
|
|
655
793
|
guard let self = self else { return }
|
|
656
794
|
|
|
657
|
-
|
|
795
|
+
// Capture currentTracks to avoid race condition with main thread
|
|
796
|
+
let tracks = self.currentTracks
|
|
797
|
+
let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
|
|
658
798
|
|
|
659
799
|
for i in startIndex..<endIndex {
|
|
660
|
-
|
|
800
|
+
guard i < tracks.count else { break }
|
|
801
|
+
let track = tracks[i]
|
|
661
802
|
|
|
662
803
|
// Skip if already preloaded
|
|
663
804
|
if self.preloadedAssets[track.id] != nil {
|
|
@@ -717,6 +858,141 @@ class TrackPlayerCore: NSObject {
|
|
|
717
858
|
}
|
|
718
859
|
}
|
|
719
860
|
|
|
861
|
+
// MARK: - Listener Registration
|
|
862
|
+
|
|
863
|
+
func addOnChangeTrackListener(
|
|
864
|
+
owner: AnyObject, _ listener: @escaping (TrackItem, Reason?) -> Void
|
|
865
|
+
) {
|
|
866
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
867
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
868
|
+
self?.onChangeTrackListeners.append(box)
|
|
869
|
+
print(
|
|
870
|
+
"🎯 TrackPlayerCore: Added onChangeTrack listener (total: \(self?.onChangeTrackListeners.count ?? 0))"
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
func addOnPlaybackStateChangeListener(
|
|
876
|
+
owner: AnyObject,
|
|
877
|
+
_ listener: @escaping (TrackPlayerState, Reason?) -> Void
|
|
878
|
+
) {
|
|
879
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
880
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
881
|
+
self?.onPlaybackStateChangeListeners.append(box)
|
|
882
|
+
print(
|
|
883
|
+
"🎯 TrackPlayerCore: Added onPlaybackStateChange listener (total: \(self?.onPlaybackStateChangeListeners.count ?? 0))"
|
|
884
|
+
)
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
func addOnSeekListener(owner: AnyObject, _ listener: @escaping (Double, Double) -> Void) {
|
|
889
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
890
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
891
|
+
self?.onSeekListeners.append(box)
|
|
892
|
+
print("🎯 TrackPlayerCore: Added onSeek listener (total: \(self?.onSeekListeners.count ?? 0))")
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
func addOnPlaybackProgressChangeListener(
|
|
897
|
+
owner: AnyObject,
|
|
898
|
+
_ listener: @escaping (Double, Double, Bool?) -> Void
|
|
899
|
+
) {
|
|
900
|
+
let box = WeakCallbackBox(owner: owner, callback: listener)
|
|
901
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
902
|
+
self?.onPlaybackProgressChangeListeners.append(box)
|
|
903
|
+
print(
|
|
904
|
+
"🎯 TrackPlayerCore: Added onPlaybackProgressChange listener (total: \(self?.onPlaybackProgressChangeListeners.count ?? 0))"
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// MARK: - Listener Notification Helpers
|
|
910
|
+
|
|
911
|
+
private func notifyTrackChange(_ track: TrackItem, _ reason: Reason?) {
|
|
912
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
913
|
+
guard let self = self else { return }
|
|
914
|
+
|
|
915
|
+
// Remove dead listeners
|
|
916
|
+
self.onChangeTrackListeners.removeAll { !$0.isAlive }
|
|
917
|
+
|
|
918
|
+
// Get live callbacks
|
|
919
|
+
let liveCallbacks = self.onChangeTrackListeners.compactMap {
|
|
920
|
+
$0.isAlive ? $0.callback : nil
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Call on main thread
|
|
924
|
+
if !liveCallbacks.isEmpty {
|
|
925
|
+
DispatchQueue.main.async {
|
|
926
|
+
for callback in liveCallbacks {
|
|
927
|
+
callback(track, reason)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
private func notifyPlaybackStateChange(_ state: TrackPlayerState, _ reason: Reason?) {
|
|
935
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
936
|
+
guard let self = self else { return }
|
|
937
|
+
|
|
938
|
+
self.onPlaybackStateChangeListeners.removeAll { !$0.isAlive }
|
|
939
|
+
|
|
940
|
+
let liveCallbacks = self.onPlaybackStateChangeListeners.compactMap {
|
|
941
|
+
$0.isAlive ? $0.callback : nil
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if !liveCallbacks.isEmpty {
|
|
945
|
+
DispatchQueue.main.async {
|
|
946
|
+
for callback in liveCallbacks {
|
|
947
|
+
callback(state, reason)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
private func notifySeek(_ position: Double, _ duration: Double) {
|
|
955
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
956
|
+
guard let self = self else { return }
|
|
957
|
+
|
|
958
|
+
self.onSeekListeners.removeAll { !$0.isAlive }
|
|
959
|
+
|
|
960
|
+
let liveCallbacks = self.onSeekListeners.compactMap {
|
|
961
|
+
$0.isAlive ? $0.callback : nil
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if !liveCallbacks.isEmpty {
|
|
965
|
+
DispatchQueue.main.async {
|
|
966
|
+
for callback in liveCallbacks {
|
|
967
|
+
callback(position, duration)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
private func notifyPlaybackProgress(_ position: Double, _ duration: Double, _ isPlaying: Bool?) {
|
|
975
|
+
listenersQueue.async(flags: .barrier) { [weak self] in
|
|
976
|
+
guard let self = self else { return }
|
|
977
|
+
|
|
978
|
+
self.onPlaybackProgressChangeListeners.removeAll { !$0.isAlive }
|
|
979
|
+
|
|
980
|
+
let liveCallbacks = self.onPlaybackProgressChangeListeners.compactMap {
|
|
981
|
+
$0.isAlive ? $0.callback : nil
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if !liveCallbacks.isEmpty {
|
|
985
|
+
DispatchQueue.main.async {
|
|
986
|
+
for callback in liveCallbacks {
|
|
987
|
+
callback(position, duration, isPlaying)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// MARK: - State Management
|
|
995
|
+
|
|
720
996
|
// MARK: - Queue Management
|
|
721
997
|
|
|
722
998
|
private func updatePlayerQueue(tracks: [TrackItem]) {
|
|
@@ -724,9 +1000,17 @@ class TrackPlayerCore: NSObject {
|
|
|
724
1000
|
print("📋 TrackPlayerCore: UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
|
|
725
1001
|
print(String(repeating: "=", count: Constants.separatorLineLength))
|
|
726
1002
|
|
|
727
|
-
// Print the full playlist being fed
|
|
1003
|
+
// Print the full playlist being fed and check download status
|
|
728
1004
|
for (index, track) in tracks.enumerated() {
|
|
729
|
-
|
|
1005
|
+
let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
|
|
1006
|
+
let downloadStatus = isDownloaded ? "📥 DOWNLOADED" : "🌐 REMOTE"
|
|
1007
|
+
print(
|
|
1008
|
+
" [\(index + 1)] 🎵 \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
|
|
1009
|
+
if isDownloaded {
|
|
1010
|
+
if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
|
|
1011
|
+
print(" Local path: \(localPath)")
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
730
1014
|
}
|
|
731
1015
|
print(String(repeating: "=", count: Constants.separatorLineLength) + "\n")
|
|
732
1016
|
|
|
@@ -808,8 +1092,8 @@ class TrackPlayerCore: NSObject {
|
|
|
808
1092
|
// Notify track change
|
|
809
1093
|
if let firstTrack = tracks.first {
|
|
810
1094
|
print("🎵 TrackPlayerCore: Emitting track change: \(firstTrack.title)")
|
|
811
|
-
print("🎵 TrackPlayerCore: onChangeTrack
|
|
812
|
-
|
|
1095
|
+
print("🎵 TrackPlayerCore: onChangeTrack callbacks count: \(onChangeTrackListeners.count)")
|
|
1096
|
+
notifyTrackChange(firstTrack, nil)
|
|
813
1097
|
mediaSessionManager?.onTrackChanged()
|
|
814
1098
|
}
|
|
815
1099
|
|
|
@@ -820,226 +1104,323 @@ class TrackPlayerCore: NSObject {
|
|
|
820
1104
|
}
|
|
821
1105
|
|
|
822
1106
|
func getCurrentTrack() -> TrackItem? {
|
|
1107
|
+
// If playing a temporary track, return that
|
|
1108
|
+
if currentTemporaryType != .none,
|
|
1109
|
+
let currentItem = player?.currentItem,
|
|
1110
|
+
let trackId = currentItem.trackId
|
|
1111
|
+
{
|
|
1112
|
+
if currentTemporaryType == .playNext {
|
|
1113
|
+
return playNextStack.first(where: { $0.id == trackId })
|
|
1114
|
+
} else if currentTemporaryType == .upNext {
|
|
1115
|
+
return upNextQueue.first(where: { $0.id == trackId })
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Otherwise return from original playlist
|
|
823
1120
|
guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else {
|
|
824
1121
|
return nil
|
|
825
1122
|
}
|
|
826
1123
|
return currentTracks[currentTrackIndex]
|
|
827
1124
|
}
|
|
828
1125
|
|
|
1126
|
+
func getActualQueue() -> [TrackItem] {
|
|
1127
|
+
// Called from Promise.async background thread
|
|
1128
|
+
// Schedule on main thread and wait for result
|
|
1129
|
+
if Thread.isMainThread {
|
|
1130
|
+
return getActualQueueInternal()
|
|
1131
|
+
} else {
|
|
1132
|
+
var queue: [TrackItem] = []
|
|
1133
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1134
|
+
queue = self?.getActualQueueInternal() ?? []
|
|
1135
|
+
}
|
|
1136
|
+
return queue
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private func getActualQueueInternal() -> [TrackItem] {
|
|
1141
|
+
var queue: [TrackItem] = []
|
|
1142
|
+
|
|
1143
|
+
// Add tracks before current (original playlist)
|
|
1144
|
+
// When a temp track is playing, include the original track at currentTrackIndex
|
|
1145
|
+
// (it already played before the temp track started)
|
|
1146
|
+
let beforeEnd = currentTemporaryType != .none
|
|
1147
|
+
? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
|
|
1148
|
+
if beforeEnd > 0 {
|
|
1149
|
+
queue.append(contentsOf: Array(currentTracks[0..<beforeEnd]))
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Add current track (temp or original)
|
|
1153
|
+
if let current = getCurrentTrack() {
|
|
1154
|
+
queue.append(current)
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1158
|
+
// Skip index 0 if current track is from playNext (it's already added as current)
|
|
1159
|
+
if currentTemporaryType == .playNext && playNextStack.count > 1 {
|
|
1160
|
+
queue.append(contentsOf: Array(playNextStack.dropFirst()))
|
|
1161
|
+
} else if currentTemporaryType != .playNext {
|
|
1162
|
+
queue.append(contentsOf: playNextStack)
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Add upNext queue (in order, FIFO)
|
|
1166
|
+
// Skip index 0 if current track is from upNext (it's already added as current)
|
|
1167
|
+
if currentTemporaryType == .upNext && upNextQueue.count > 1 {
|
|
1168
|
+
queue.append(contentsOf: Array(upNextQueue.dropFirst()))
|
|
1169
|
+
} else if currentTemporaryType != .upNext {
|
|
1170
|
+
queue.append(contentsOf: upNextQueue)
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Add remaining original tracks
|
|
1174
|
+
if currentTrackIndex + 1 < currentTracks.count {
|
|
1175
|
+
queue.append(contentsOf: Array(currentTracks[(currentTrackIndex + 1)...]))
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
return queue
|
|
1179
|
+
}
|
|
1180
|
+
|
|
829
1181
|
func play() {
|
|
830
1182
|
print("▶️ TrackPlayerCore: play() called")
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
1183
|
+
if Thread.isMainThread {
|
|
1184
|
+
playInternal()
|
|
1185
|
+
} else {
|
|
1186
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1187
|
+
self?.playInternal()
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private func playInternal() {
|
|
1193
|
+
print("▶️ TrackPlayerCore: Calling player.play()")
|
|
1194
|
+
if let player = self.player {
|
|
1195
|
+
print("▶️ TrackPlayerCore: Player status: \(player.status.rawValue)")
|
|
1196
|
+
if let currentItem = player.currentItem {
|
|
1197
|
+
print("▶️ TrackPlayerCore: Current item status: \(currentItem.status.rawValue)")
|
|
1198
|
+
if let error = currentItem.error {
|
|
1199
|
+
print("❌ TrackPlayerCore: Current item error: \(error.localizedDescription)")
|
|
848
1200
|
}
|
|
849
|
-
} else {
|
|
850
|
-
print("❌ TrackPlayerCore: No player available")
|
|
851
1201
|
}
|
|
1202
|
+
player.play()
|
|
1203
|
+
// Emit state change immediately for responsive UI
|
|
1204
|
+
// KVO will also fire, but this ensures immediate feedback
|
|
1205
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) {
|
|
1206
|
+
[weak self] in
|
|
1207
|
+
self?.emitStateChange()
|
|
1208
|
+
}
|
|
1209
|
+
} else {
|
|
1210
|
+
print("❌ TrackPlayerCore: No player available")
|
|
852
1211
|
}
|
|
853
1212
|
}
|
|
854
1213
|
|
|
855
1214
|
func pause() {
|
|
856
1215
|
print("⏸️ TrackPlayerCore: pause() called")
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
self?.emitStateChange()
|
|
1216
|
+
if Thread.isMainThread {
|
|
1217
|
+
pauseInternal()
|
|
1218
|
+
} else {
|
|
1219
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1220
|
+
self?.pauseInternal()
|
|
863
1221
|
}
|
|
864
1222
|
}
|
|
865
1223
|
}
|
|
866
1224
|
|
|
867
|
-
func
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
)
|
|
1225
|
+
private func pauseInternal() {
|
|
1226
|
+
self.player?.pause()
|
|
1227
|
+
// Emit state change immediately for responsive UI
|
|
1228
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
|
|
1229
|
+
self?.emitStateChange()
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
871
1232
|
|
|
1233
|
+
func playSong(songId: String, fromPlaylist: String?) {
|
|
872
1234
|
DispatchQueue.main.async { [weak self] in
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
var songIndex: Int = -1
|
|
1235
|
+
self?.playSongInternal(songId: songId, fromPlaylist: fromPlaylist)
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
877
1238
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1239
|
+
private func playSongInternal(songId: String, fromPlaylist: String?) {
|
|
1240
|
+
// Clear temporary tracks when directly playing a song
|
|
1241
|
+
self.playNextStack.removeAll()
|
|
1242
|
+
self.upNextQueue.removeAll()
|
|
1243
|
+
self.currentTemporaryType = .none
|
|
1244
|
+
print(" 🧹 Cleared temporary tracks")
|
|
1245
|
+
|
|
1246
|
+
var targetPlaylistId: String?
|
|
1247
|
+
var songIndex: Int = -1
|
|
1248
|
+
|
|
1249
|
+
// Case 1: If fromPlaylist is provided, use that playlist
|
|
1250
|
+
if let playlistId = fromPlaylist {
|
|
1251
|
+
print("🎵 TrackPlayerCore: Looking for song in specified playlist: \(playlistId)")
|
|
1252
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
1253
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1254
|
+
targetPlaylistId = playlistId
|
|
1255
|
+
songIndex = index
|
|
1256
|
+
print("✅ Found song at index \(index) in playlist \(playlistId)")
|
|
890
1257
|
} else {
|
|
891
|
-
print("⚠️
|
|
1258
|
+
print("⚠️ Song \(songId) not found in specified playlist \(playlistId)")
|
|
892
1259
|
return
|
|
893
1260
|
}
|
|
1261
|
+
} else {
|
|
1262
|
+
print("⚠️ Playlist \(playlistId) not found")
|
|
1263
|
+
return
|
|
894
1264
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1265
|
+
}
|
|
1266
|
+
// Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
|
|
1267
|
+
else {
|
|
1268
|
+
print("🎵 TrackPlayerCore: No playlist specified, checking current playlist")
|
|
1269
|
+
|
|
1270
|
+
// Check if song exists in currently loaded playlist
|
|
1271
|
+
if let currentId = self.currentPlaylistId,
|
|
1272
|
+
let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
|
|
1273
|
+
{
|
|
1274
|
+
if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1275
|
+
targetPlaylistId = currentId
|
|
1276
|
+
songIndex = index
|
|
1277
|
+
print("✅ Found song at index \(index) in current playlist \(currentId)")
|
|
908
1278
|
}
|
|
1279
|
+
}
|
|
909
1280
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1281
|
+
// If not found in current playlist, search in all playlists
|
|
1282
|
+
if songIndex == -1 {
|
|
1283
|
+
print("🔍 Song not found in current playlist, searching all playlists...")
|
|
1284
|
+
let allPlaylists = self.playlistManager.getAllPlaylists()
|
|
914
1285
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
}
|
|
1286
|
+
for playlist in allPlaylists {
|
|
1287
|
+
if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
|
|
1288
|
+
targetPlaylistId = playlist.id
|
|
1289
|
+
songIndex = index
|
|
1290
|
+
print("✅ Found song at index \(index) in playlist \(playlist.id)")
|
|
1291
|
+
break
|
|
922
1292
|
}
|
|
1293
|
+
}
|
|
923
1294
|
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
1295
|
+
// If still not found, just use the first playlist if available
|
|
1296
|
+
if songIndex == -1 && !allPlaylists.isEmpty {
|
|
1297
|
+
targetPlaylistId = allPlaylists[0].id
|
|
1298
|
+
songIndex = 0
|
|
1299
|
+
print("⚠️ Song not found in any playlist, using first playlist and starting at index 0")
|
|
930
1300
|
}
|
|
931
1301
|
}
|
|
1302
|
+
}
|
|
932
1303
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1304
|
+
// Now play the song
|
|
1305
|
+
guard let playlistId = targetPlaylistId, songIndex >= 0 else {
|
|
1306
|
+
print("❌ Could not determine playlist or song index")
|
|
1307
|
+
return
|
|
1308
|
+
}
|
|
938
1309
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
1310
|
+
// Load playlist if it's different from current
|
|
1311
|
+
if self.currentPlaylistId != playlistId {
|
|
1312
|
+
print("🔄 Loading new playlist: \(playlistId)")
|
|
1313
|
+
if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
|
|
1314
|
+
self.currentPlaylistId = playlistId
|
|
1315
|
+
self.updatePlayerQueue(tracks: playlist.tracks)
|
|
946
1316
|
}
|
|
947
|
-
|
|
948
|
-
// Play from the found index
|
|
949
|
-
print("▶️ Playing from index: \(songIndex)")
|
|
950
|
-
self.playFromIndex(index: songIndex)
|
|
951
1317
|
}
|
|
1318
|
+
|
|
1319
|
+
// Play from the found index
|
|
1320
|
+
print("▶️ Playing from index: \(songIndex)")
|
|
1321
|
+
self.playFromIndex(index: songIndex)
|
|
952
1322
|
}
|
|
953
1323
|
|
|
954
1324
|
func skipToNext() {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1325
|
+
if Thread.isMainThread {
|
|
1326
|
+
skipToNextInternal()
|
|
1327
|
+
} else {
|
|
1328
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1329
|
+
self?.skipToNextInternal()
|
|
960
1330
|
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
961
1333
|
|
|
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)")
|
|
1334
|
+
private func skipToNextInternal() {
|
|
1335
|
+
guard let queuePlayer = self.player else { return }
|
|
967
1336
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1337
|
+
// Remove current temp track from its list before advancing
|
|
1338
|
+
if let trackId = queuePlayer.currentItem?.trackId {
|
|
1339
|
+
if currentTemporaryType == .playNext {
|
|
1340
|
+
if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
|
|
1341
|
+
playNextStack.remove(at: idx)
|
|
1342
|
+
}
|
|
1343
|
+
} else if currentTemporaryType == .upNext {
|
|
1344
|
+
if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
|
|
1345
|
+
upNextQueue.remove(at: idx)
|
|
971
1346
|
}
|
|
972
1347
|
}
|
|
1348
|
+
}
|
|
973
1349
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1350
|
+
// Check if there are more items in the player queue
|
|
1351
|
+
if queuePlayer.items().count > 1 {
|
|
1352
|
+
queuePlayer.advanceToNextItem()
|
|
1353
|
+
} else {
|
|
1354
|
+
queuePlayer.pause()
|
|
1355
|
+
self.notifyPlaybackStateChange(.stopped, .end)
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
981
1358
|
|
|
982
|
-
|
|
983
|
-
|
|
1359
|
+
func skipToPrevious() {
|
|
1360
|
+
if Thread.isMainThread {
|
|
1361
|
+
skipToPreviousInternal()
|
|
1362
|
+
} else {
|
|
1363
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1364
|
+
self?.skipToPreviousInternal()
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
984
1368
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1369
|
+
private func skipToPreviousInternal() {
|
|
1370
|
+
guard let queuePlayer = self.player else { return }
|
|
1371
|
+
|
|
1372
|
+
let currentTime = queuePlayer.currentTime()
|
|
1373
|
+
if currentTime.seconds > Constants.skipToPreviousThreshold {
|
|
1374
|
+
// If more than threshold seconds in, restart current track
|
|
1375
|
+
queuePlayer.seek(to: .zero)
|
|
1376
|
+
} else if self.currentTemporaryType != .none {
|
|
1377
|
+
// Playing temporary track — remove from its list, then restart
|
|
1378
|
+
if let trackId = queuePlayer.currentItem?.trackId {
|
|
1379
|
+
if currentTemporaryType == .playNext {
|
|
1380
|
+
if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
|
|
1381
|
+
playNextStack.remove(at: idx)
|
|
1382
|
+
}
|
|
1383
|
+
} else if currentTemporaryType == .upNext {
|
|
1384
|
+
if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
|
|
1385
|
+
upNextQueue.remove(at: idx)
|
|
988
1386
|
}
|
|
989
1387
|
}
|
|
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
1388
|
}
|
|
1389
|
+
// Go to current original track position (skip back from temp)
|
|
1390
|
+
self.playFromIndex(index: self.currentTrackIndex)
|
|
1391
|
+
} else if self.currentTrackIndex > 0 {
|
|
1392
|
+
// Go to previous track in original playlist
|
|
1393
|
+
let previousIndex = self.currentTrackIndex - 1
|
|
1394
|
+
self.playFromIndex(index: previousIndex)
|
|
1395
|
+
} else {
|
|
1396
|
+
// Already at first track, restart it
|
|
1397
|
+
queuePlayer.seek(to: .zero)
|
|
998
1398
|
}
|
|
999
1399
|
}
|
|
1000
1400
|
|
|
1001
|
-
func
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
print("\n⏮️ TrackPlayerCore: SKIP TO PREVIOUS")
|
|
1010
|
-
print(" Current index: \(self.currentTrackIndex)")
|
|
1011
|
-
print(" Current time: \(queuePlayer.currentTime().seconds)s")
|
|
1012
|
-
|
|
1013
|
-
let currentTime = queuePlayer.currentTime()
|
|
1014
|
-
if currentTime.seconds > Constants.skipToPreviousThreshold {
|
|
1015
|
-
// If more than threshold seconds in, restart current track
|
|
1016
|
-
print(
|
|
1017
|
-
" 🔄 More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
|
|
1018
|
-
queuePlayer.seek(to: .zero)
|
|
1019
|
-
} else if self.currentTrackIndex > 0 {
|
|
1020
|
-
// Go to previous track
|
|
1021
|
-
let previousIndex = self.currentTrackIndex - 1
|
|
1022
|
-
print(" ⏮️ Going to previous track at index \(previousIndex)")
|
|
1023
|
-
self.playFromIndex(index: previousIndex)
|
|
1024
|
-
} else {
|
|
1025
|
-
// Already at first track, restart it
|
|
1026
|
-
print(" 🔄 Already at first track, restarting it")
|
|
1027
|
-
queuePlayer.seek(to: .zero)
|
|
1401
|
+
func seek(position: Double) {
|
|
1402
|
+
if Thread.isMainThread {
|
|
1403
|
+
seekInternal(position: position)
|
|
1404
|
+
} else {
|
|
1405
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1406
|
+
self?.seekInternal(position: position)
|
|
1028
1407
|
}
|
|
1029
1408
|
}
|
|
1030
1409
|
}
|
|
1031
1410
|
|
|
1032
|
-
func
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1411
|
+
private func seekInternal(position: Double) {
|
|
1412
|
+
guard let player = self.player else { return }
|
|
1413
|
+
|
|
1414
|
+
self.isManuallySeeked = true
|
|
1415
|
+
let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
|
|
1416
|
+
player.seek(to: time) { [weak self] completed in
|
|
1417
|
+
// Always update now playing info to restore playback rate after seek
|
|
1418
|
+
// This ensures the scrubber animation resumes correctly
|
|
1419
|
+
self?.mediaSessionManager?.updateNowPlayingInfo()
|
|
1420
|
+
|
|
1421
|
+
if completed {
|
|
1422
|
+
let duration = player.currentItem?.duration.seconds ?? 0.0
|
|
1423
|
+
self?.notifySeek(position, duration)
|
|
1043
1424
|
}
|
|
1044
1425
|
}
|
|
1045
1426
|
}
|
|
@@ -1048,13 +1429,41 @@ class TrackPlayerCore: NSObject {
|
|
|
1048
1429
|
|
|
1049
1430
|
func setRepeatMode(mode: RepeatMode) -> Bool {
|
|
1050
1431
|
print("🔁 TrackPlayerCore: setRepeatMode called with mode: \(mode)")
|
|
1051
|
-
|
|
1432
|
+
if Thread.isMainThread {
|
|
1433
|
+
self.repeatMode = mode
|
|
1434
|
+
} else {
|
|
1435
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1436
|
+
self?.repeatMode = mode
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1052
1439
|
return true
|
|
1053
1440
|
}
|
|
1054
1441
|
|
|
1055
|
-
// MARK: - State Management
|
|
1056
|
-
|
|
1057
1442
|
func getState() -> PlayerState {
|
|
1443
|
+
// Called from Promise.async background thread
|
|
1444
|
+
// Schedule on main thread and wait for result
|
|
1445
|
+
if Thread.isMainThread {
|
|
1446
|
+
return getStateInternal()
|
|
1447
|
+
} else {
|
|
1448
|
+
var state: PlayerState!
|
|
1449
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1450
|
+
state =
|
|
1451
|
+
self?.getStateInternal()
|
|
1452
|
+
?? PlayerState(
|
|
1453
|
+
currentTrack: nil,
|
|
1454
|
+
currentPosition: 0.0,
|
|
1455
|
+
totalDuration: 0.0,
|
|
1456
|
+
currentState: .stopped,
|
|
1457
|
+
currentPlaylistId: nil,
|
|
1458
|
+
currentIndex: -1.0,
|
|
1459
|
+
currentPlayingType: .notPlaying
|
|
1460
|
+
)
|
|
1461
|
+
}
|
|
1462
|
+
return state
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
private func getStateInternal() -> PlayerState {
|
|
1058
1467
|
guard let player = player else {
|
|
1059
1468
|
return PlayerState(
|
|
1060
1469
|
currentTrack: nil,
|
|
@@ -1062,7 +1471,8 @@ class TrackPlayerCore: NSObject {
|
|
|
1062
1471
|
totalDuration: 0.0,
|
|
1063
1472
|
currentState: .stopped,
|
|
1064
1473
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1065
|
-
currentIndex: -1.0
|
|
1474
|
+
currentIndex: -1.0,
|
|
1475
|
+
currentPlayingType: .notPlaying
|
|
1066
1476
|
)
|
|
1067
1477
|
}
|
|
1068
1478
|
|
|
@@ -1082,13 +1492,29 @@ class TrackPlayerCore: NSObject {
|
|
|
1082
1492
|
// Get current index
|
|
1083
1493
|
let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
|
|
1084
1494
|
|
|
1495
|
+
// Map internal temporary type to CurrentPlayingType
|
|
1496
|
+
let currentPlayingType: CurrentPlayingType
|
|
1497
|
+
if currentTrack == nil {
|
|
1498
|
+
currentPlayingType = .notPlaying
|
|
1499
|
+
} else {
|
|
1500
|
+
switch currentTemporaryType {
|
|
1501
|
+
case .none:
|
|
1502
|
+
currentPlayingType = .playlist
|
|
1503
|
+
case .playNext:
|
|
1504
|
+
currentPlayingType = .playNext
|
|
1505
|
+
case .upNext:
|
|
1506
|
+
currentPlayingType = .upNext
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1085
1510
|
return PlayerState(
|
|
1086
1511
|
currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
|
|
1087
1512
|
currentPosition: currentPosition,
|
|
1088
1513
|
totalDuration: totalDuration,
|
|
1089
1514
|
currentState: currentState,
|
|
1090
1515
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1091
|
-
currentIndex: currentIndex
|
|
1516
|
+
currentIndex: currentIndex,
|
|
1517
|
+
currentPlayingType: currentPlayingType
|
|
1092
1518
|
)
|
|
1093
1519
|
}
|
|
1094
1520
|
|
|
@@ -1133,72 +1559,354 @@ class TrackPlayerCore: NSObject {
|
|
|
1133
1559
|
}
|
|
1134
1560
|
|
|
1135
1561
|
func playFromIndex(index: Int) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
return
|
|
1562
|
+
if Thread.isMainThread {
|
|
1563
|
+
playFromIndexInternal(index: index)
|
|
1564
|
+
} else {
|
|
1565
|
+
DispatchQueue.main.async { [weak self] in
|
|
1566
|
+
self?.playFromIndexInternal(index: index)
|
|
1142
1567
|
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// MARK: - Skip to Index in Actual Queue
|
|
1143
1572
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1573
|
+
func skipToIndex(index: Int) -> Bool {
|
|
1574
|
+
if Thread.isMainThread {
|
|
1575
|
+
return skipToIndexInternal(index: index)
|
|
1576
|
+
} else {
|
|
1577
|
+
var result = false
|
|
1578
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1579
|
+
result = self?.skipToIndexInternal(index: index) ?? false
|
|
1580
|
+
}
|
|
1581
|
+
return result
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1147
1584
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1585
|
+
private func skipToIndexInternal(index: Int) -> Bool {
|
|
1586
|
+
// Get actual queue to validate index and determine position
|
|
1587
|
+
let actualQueue = getActualQueueInternal()
|
|
1588
|
+
let totalQueueSize = actualQueue.count
|
|
1589
|
+
|
|
1590
|
+
// Validate index
|
|
1591
|
+
guard index >= 0 && index < totalQueueSize else { return false }
|
|
1592
|
+
|
|
1593
|
+
// Calculate queue section boundaries using effective sizes
|
|
1594
|
+
// (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
|
|
1595
|
+
// When temp is playing, the original track at currentTrackIndex is included in "before",
|
|
1596
|
+
// so the current playing position shifts by 1
|
|
1597
|
+
let currentPos = currentTemporaryType != .none
|
|
1598
|
+
? currentTrackIndex + 1 : currentTrackIndex
|
|
1599
|
+
let effectivePlayNextSize = currentTemporaryType == .playNext
|
|
1600
|
+
? max(0, playNextStack.count - 1) : playNextStack.count
|
|
1601
|
+
let effectiveUpNextSize = currentTemporaryType == .upNext
|
|
1602
|
+
? max(0, upNextQueue.count - 1) : upNextQueue.count
|
|
1603
|
+
|
|
1604
|
+
let playNextStart = currentPos + 1
|
|
1605
|
+
let playNextEnd = playNextStart + effectivePlayNextSize
|
|
1606
|
+
let upNextStart = playNextEnd
|
|
1607
|
+
let upNextEnd = upNextStart + effectiveUpNextSize
|
|
1608
|
+
let originalRemainingStart = upNextEnd
|
|
1609
|
+
|
|
1610
|
+
// Case 1: Target is before current - use playFromIndex on original
|
|
1611
|
+
if index < currentPos {
|
|
1612
|
+
playFromIndexInternal(index: index)
|
|
1613
|
+
return true
|
|
1614
|
+
}
|
|
1150
1615
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1616
|
+
// Case 2: Target is current - seek to beginning
|
|
1617
|
+
if index == currentPos {
|
|
1618
|
+
player?.seek(to: .zero)
|
|
1619
|
+
return true
|
|
1620
|
+
}
|
|
1153
1621
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
let
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1622
|
+
// Case 3: Target is in playNext section
|
|
1623
|
+
if index >= playNextStart && index < playNextEnd {
|
|
1624
|
+
let playNextIndex = index - playNextStart
|
|
1625
|
+
// Offset by 1 if current is from playNext (index 0 is already playing)
|
|
1626
|
+
let actualListIndex = currentTemporaryType == .playNext
|
|
1627
|
+
? playNextIndex + 1 : playNextIndex
|
|
1160
1628
|
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
let isPreload = offset < Constants.gaplessPreloadCount
|
|
1165
|
-
return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
|
|
1629
|
+
// Remove tracks before the target from playNext (they're being skipped)
|
|
1630
|
+
if actualListIndex > 0 {
|
|
1631
|
+
playNextStack.removeFirst(actualListIndex)
|
|
1166
1632
|
}
|
|
1167
1633
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1634
|
+
// Rebuild queue and advance
|
|
1635
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1636
|
+
player?.advanceToNextItem()
|
|
1637
|
+
return true
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// Case 4: Target is in upNext section
|
|
1641
|
+
if index >= upNextStart && index < upNextEnd {
|
|
1642
|
+
let upNextIndex = index - upNextStart
|
|
1643
|
+
// Offset by 1 if current is from upNext (index 0 is already playing)
|
|
1644
|
+
let actualListIndex = currentTemporaryType == .upNext
|
|
1645
|
+
? upNextIndex + 1 : upNextIndex
|
|
1646
|
+
|
|
1647
|
+
// Clear all playNext tracks (they're being skipped)
|
|
1648
|
+
playNextStack.removeAll()
|
|
1649
|
+
|
|
1650
|
+
// Remove tracks before target from upNext
|
|
1651
|
+
if actualListIndex > 0 {
|
|
1652
|
+
upNextQueue.removeFirst(actualListIndex)
|
|
1171
1653
|
}
|
|
1172
1654
|
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1655
|
+
// Rebuild queue and advance
|
|
1656
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1657
|
+
player?.advanceToNextItem()
|
|
1658
|
+
return true
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Case 5: Target is in remaining original tracks
|
|
1662
|
+
if index >= originalRemainingStart {
|
|
1663
|
+
let targetTrack = actualQueue[index]
|
|
1664
|
+
|
|
1665
|
+
// Find this track's index in the original playlist
|
|
1666
|
+
guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
|
|
1667
|
+
return false
|
|
1177
1668
|
}
|
|
1178
1669
|
|
|
1179
|
-
// Clear
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1670
|
+
// Clear all temporary tracks (they're being skipped)
|
|
1671
|
+
playNextStack.removeAll()
|
|
1672
|
+
upNextQueue.removeAll()
|
|
1673
|
+
currentTemporaryType = .none
|
|
1674
|
+
|
|
1675
|
+
return playFromIndexInternalWithResult(index: originalIndex)
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
return false
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
private func playFromIndexInternal(index: Int) {
|
|
1682
|
+
_ = playFromIndexInternalWithResult(index: index)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
private func playFromIndexInternalWithResult(index: Int) -> Bool {
|
|
1686
|
+
guard index >= 0 && index < self.currentTracks.count else {
|
|
1687
|
+
print(
|
|
1688
|
+
"❌ TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)"
|
|
1689
|
+
)
|
|
1690
|
+
return false
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
print("\n🎯 TrackPlayerCore: PLAY FROM INDEX \(index)")
|
|
1694
|
+
print(" Total tracks in playlist: \(self.currentTracks.count)")
|
|
1695
|
+
print(" Current index: \(self.currentTrackIndex), target index: \(index)")
|
|
1696
|
+
|
|
1697
|
+
// Clear temporary tracks when jumping to specific index
|
|
1698
|
+
self.playNextStack.removeAll()
|
|
1699
|
+
self.upNextQueue.removeAll()
|
|
1700
|
+
self.currentTemporaryType = .none
|
|
1701
|
+
print(" 🧹 Cleared temporary tracks")
|
|
1702
|
+
|
|
1703
|
+
// Store the full playlist
|
|
1704
|
+
let fullPlaylist = self.currentTracks
|
|
1705
|
+
|
|
1706
|
+
// Update currentTrackIndex BEFORE updating queue
|
|
1707
|
+
self.currentTrackIndex = index
|
|
1708
|
+
|
|
1709
|
+
// Recreate the queue starting from the target index
|
|
1710
|
+
// This ensures all remaining tracks are in the queue
|
|
1711
|
+
let tracksToPlay = Array(fullPlaylist[index...])
|
|
1712
|
+
print(
|
|
1713
|
+
" 🔄 Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)"
|
|
1714
|
+
)
|
|
1715
|
+
|
|
1716
|
+
// Create gapless-optimized player items
|
|
1717
|
+
let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
|
|
1718
|
+
// First few items get preload treatment for faster playback
|
|
1719
|
+
let isPreload = offset < Constants.gaplessPreloadCount
|
|
1720
|
+
return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
guard let player = self.player, !items.isEmpty else {
|
|
1724
|
+
print("❌ No player or no items to play")
|
|
1725
|
+
return false
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Remove old boundary observer
|
|
1729
|
+
if let boundaryObserver = self.boundaryTimeObserver {
|
|
1730
|
+
player.removeTimeObserver(boundaryObserver)
|
|
1731
|
+
self.boundaryTimeObserver = nil
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Clear and rebuild queue
|
|
1735
|
+
player.removeAllItems()
|
|
1736
|
+
var lastItem: AVPlayerItem? = nil
|
|
1737
|
+
for item in items {
|
|
1738
|
+
player.insert(item, after: lastItem)
|
|
1739
|
+
lastItem = item
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Restore the full playlist reference (don't slice it!)
|
|
1743
|
+
self.currentTracks = fullPlaylist
|
|
1744
|
+
|
|
1745
|
+
print(" ✅ Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
|
|
1746
|
+
if let track = self.getCurrentTrack() {
|
|
1747
|
+
print(" 🎵 Playing: \(track.title)")
|
|
1748
|
+
notifyTrackChange(track, .skip)
|
|
1749
|
+
self.mediaSessionManager?.onTrackChanged()
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// Start preloading upcoming tracks for gapless playback
|
|
1753
|
+
self.preloadUpcomingTracks(from: index + 1)
|
|
1754
|
+
|
|
1755
|
+
player.play()
|
|
1756
|
+
return true
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// MARK: - Temporary Track Management
|
|
1760
|
+
|
|
1761
|
+
/**
|
|
1762
|
+
* Add a track to the up-next queue (FIFO - first added plays first)
|
|
1763
|
+
* Track will be inserted after currently playing track and any playNext tracks
|
|
1764
|
+
*/
|
|
1765
|
+
func addToUpNext(trackId: String) {
|
|
1766
|
+
DispatchQueue.main.async { [weak self] in
|
|
1767
|
+
self?.addToUpNextInternal(trackId: trackId)
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private func addToUpNextInternal(trackId: String) {
|
|
1772
|
+
print("📋 TrackPlayerCore: addToUpNext(\(trackId))")
|
|
1773
|
+
|
|
1774
|
+
// Find the track from current playlist or all playlists
|
|
1775
|
+
guard let track = self.findTrackById(trackId) else {
|
|
1776
|
+
print("❌ TrackPlayerCore: Track \(trackId) not found")
|
|
1777
|
+
return
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// Add to end of upNext queue (FIFO)
|
|
1781
|
+
self.upNextQueue.append(track)
|
|
1782
|
+
print(" ✅ Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
|
|
1783
|
+
|
|
1784
|
+
// Rebuild the player queue if actively playing
|
|
1785
|
+
if self.player?.currentItem != nil {
|
|
1786
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
/**
|
|
1791
|
+
* Add a track to play next (LIFO - last added plays first)
|
|
1792
|
+
* Track will be inserted immediately after currently playing track
|
|
1793
|
+
*/
|
|
1794
|
+
func playNext(trackId: String) {
|
|
1795
|
+
DispatchQueue.main.async { [weak self] in
|
|
1796
|
+
self?.playNextInternal(trackId: trackId)
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
private func playNextInternal(trackId: String) {
|
|
1801
|
+
print("⏭️ TrackPlayerCore: playNext(\(trackId))")
|
|
1802
|
+
|
|
1803
|
+
// Find the track from current playlist or all playlists
|
|
1804
|
+
guard let track = self.findTrackById(trackId) else {
|
|
1805
|
+
print("❌ TrackPlayerCore: Track \(trackId) not found")
|
|
1806
|
+
return
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// Insert at beginning of playNext stack (LIFO)
|
|
1810
|
+
self.playNextStack.insert(track, at: 0)
|
|
1811
|
+
print(" ✅ Added '\(track.title)' to playNext stack (position: 1)")
|
|
1812
|
+
|
|
1813
|
+
// Rebuild the player queue if actively playing
|
|
1814
|
+
if self.player?.currentItem != nil {
|
|
1815
|
+
self.rebuildAVQueueFromCurrentPosition()
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Rebuild the AVQueuePlayer from current position with temporary tracks
|
|
1821
|
+
* Order: [current] + [playNext stack reversed] + [upNext queue] + [remaining original]
|
|
1822
|
+
*/
|
|
1823
|
+
private func rebuildAVQueueFromCurrentPosition() {
|
|
1824
|
+
guard let player = self.player else { return }
|
|
1825
|
+
|
|
1826
|
+
let currentItem = player.currentItem
|
|
1827
|
+
let playingItems = player.items()
|
|
1828
|
+
|
|
1829
|
+
var newQueueTracks: [TrackItem] = []
|
|
1830
|
+
|
|
1831
|
+
// Add playNext stack (LIFO - most recently added plays first)
|
|
1832
|
+
// Skip index 0 if current track is from playNext (it's already playing)
|
|
1833
|
+
if currentTemporaryType == .playNext && playNextStack.count > 1 {
|
|
1834
|
+
newQueueTracks.append(contentsOf: Array(playNextStack.dropFirst()))
|
|
1835
|
+
} else if currentTemporaryType != .playNext {
|
|
1836
|
+
newQueueTracks.append(contentsOf: playNextStack)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
// Add upNext queue (in order, FIFO)
|
|
1840
|
+
// Skip index 0 if current track is from upNext (it's already playing)
|
|
1841
|
+
if currentTemporaryType == .upNext && upNextQueue.count > 1 {
|
|
1842
|
+
newQueueTracks.append(contentsOf: Array(upNextQueue.dropFirst()))
|
|
1843
|
+
} else if currentTemporaryType != .upNext {
|
|
1844
|
+
newQueueTracks.append(contentsOf: upNextQueue)
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Add remaining original tracks
|
|
1848
|
+
if currentTrackIndex + 1 < currentTracks.count {
|
|
1849
|
+
let remainingOriginal = Array(currentTracks[(currentTrackIndex + 1)...])
|
|
1850
|
+
newQueueTracks.append(contentsOf: remainingOriginal)
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Remove all items from player EXCEPT the currently playing one
|
|
1854
|
+
for item in playingItems where item != currentItem {
|
|
1855
|
+
player.remove(item)
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// Insert new items in order
|
|
1859
|
+
var lastItem = currentItem
|
|
1860
|
+
for track in newQueueTracks {
|
|
1861
|
+
if let item = createGaplessPlayerItem(for: track, isPreload: false) {
|
|
1183
1862
|
player.insert(item, after: lastItem)
|
|
1184
1863
|
lastItem = item
|
|
1185
1864
|
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
}
|
|
1186
1868
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1869
|
+
/**
|
|
1870
|
+
* Find a track by ID from current playlist or all playlists
|
|
1871
|
+
*/
|
|
1872
|
+
private func findTrackById(_ trackId: String) -> TrackItem? {
|
|
1873
|
+
// First check current playlist
|
|
1874
|
+
if let track = currentTracks.first(where: { $0.id == trackId }) {
|
|
1875
|
+
return track
|
|
1876
|
+
}
|
|
1189
1877
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1878
|
+
// Then check all playlists
|
|
1879
|
+
let allPlaylists = playlistManager.getAllPlaylists()
|
|
1880
|
+
for playlist in allPlaylists {
|
|
1881
|
+
if let track = playlist.tracks.first(where: { $0.id == trackId }) {
|
|
1882
|
+
return track
|
|
1195
1883
|
}
|
|
1884
|
+
}
|
|
1196
1885
|
|
|
1197
|
-
|
|
1198
|
-
|
|
1886
|
+
return nil
|
|
1887
|
+
}
|
|
1199
1888
|
|
|
1200
|
-
|
|
1889
|
+
/**
|
|
1890
|
+
* Determine what type of track is currently playing
|
|
1891
|
+
*/
|
|
1892
|
+
private func determineCurrentTemporaryType() -> TemporaryType {
|
|
1893
|
+
guard let currentItem = player?.currentItem,
|
|
1894
|
+
let trackId = currentItem.trackId
|
|
1895
|
+
else {
|
|
1896
|
+
return .none
|
|
1201
1897
|
}
|
|
1898
|
+
|
|
1899
|
+
// Check if in playNext stack
|
|
1900
|
+
if playNextStack.contains(where: { $0.id == trackId }) {
|
|
1901
|
+
return .playNext
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// Check if in upNext queue
|
|
1905
|
+
if upNextQueue.contains(where: { $0.id == trackId }) {
|
|
1906
|
+
return .upNext
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
return .none
|
|
1202
1910
|
}
|
|
1203
1911
|
|
|
1204
1912
|
// MARK: - Cleanup
|