react-native-nitro-player 0.7.0 โ†’ 0.7.1-alpha.1

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.
Files changed (110) hide show
  1. package/README.md +47 -46
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +179 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +170 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +150 -135
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  23. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  24. package/ios/HybridDownloadManager.swift +32 -26
  25. package/ios/HybridEqualizer.swift +48 -35
  26. package/ios/HybridTrackPlayer.swift +127 -102
  27. package/ios/core/ListenerRegistry.swift +60 -0
  28. package/ios/core/TrackPlayerCore.swift +130 -2356
  29. package/ios/core/TrackPlayerListener.swift +395 -0
  30. package/ios/core/TrackPlayerNotify.swift +52 -0
  31. package/ios/core/TrackPlayerPlayback.swift +274 -0
  32. package/ios/core/TrackPlayerQueue.swift +221 -0
  33. package/ios/core/TrackPlayerQueueBuild.swift +493 -0
  34. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  35. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  36. package/ios/equalizer/EqualizerCore.swift +63 -123
  37. package/ios/media/MediaSessionManager.swift +32 -49
  38. package/ios/playlist/PlaylistManager.swift +2 -9
  39. package/ios/queue/HybridPlayerQueue.swift +69 -66
  40. package/lib/hooks/useDownloadedTracks.js +16 -13
  41. package/lib/hooks/useEqualizer.d.ts +4 -4
  42. package/lib/hooks/useEqualizer.js +22 -17
  43. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  44. package/lib/hooks/useEqualizerPresets.js +12 -18
  45. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  46. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  47. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  48. package/lib/specs/Equalizer.nitro.d.ts +10 -10
  49. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  50. package/lib/types/EqualizerTypes.d.ts +3 -3
  51. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
  52. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  53. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
  54. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
  55. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
  56. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
  57. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
  58. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
  59. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
  60. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
  61. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
  62. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
  63. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
  64. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
  72. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  73. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  74. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  75. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  76. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  77. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  78. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  79. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  80. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  81. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  82. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  83. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  84. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  85. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  86. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  87. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  88. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  89. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  90. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  91. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  92. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  93. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  94. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  95. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  96. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  97. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  98. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  99. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  100. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  101. package/package.json +5 -5
  102. package/src/hooks/useDownloadedTracks.ts +17 -13
  103. package/src/hooks/useEqualizer.ts +26 -21
  104. package/src/hooks/useEqualizerPresets.ts +15 -21
  105. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  106. package/src/specs/AudioDevices.nitro.ts +2 -2
  107. package/src/specs/DownloadManager.nitro.ts +10 -10
  108. package/src/specs/Equalizer.nitro.ts +10 -10
  109. package/src/specs/TrackPlayer.nitro.ts +52 -16
  110. package/src/types/EqualizerTypes.ts +17 -13
@@ -0,0 +1,493 @@
1
+ //
2
+ // TrackPlayerQueueBuild.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+
8
+ import AVFoundation
9
+
10
+ import Foundation
11
+
12
+ extension TrackPlayerCore {
13
+
14
+ func updatePlayerQueue(tracks: [TrackItem]) {
15
+ NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "=", count: Constants.separatorLineLength))
16
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ“‹ UPDATE PLAYER QUEUE - Received \(tracks.count) tracks")
17
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength))
18
+
19
+ #if DEBUG
20
+ for (index, track) in tracks.enumerated() {
21
+ let isDownloaded = DownloadManagerCore.shared.isTrackDownloaded(trackId: track.id)
22
+ let downloadStatus = isDownloaded ? "๐Ÿ“ฅ DOWNLOADED" : "๐ŸŒ REMOTE"
23
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] ๐ŸŽต \(track.title) - \(track.artist) (ID: \(track.id)) - \(downloadStatus)")
24
+ if isDownloaded {
25
+ if let localPath = DownloadManagerCore.shared.getLocalPath(trackId: track.id) {
26
+ NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(localPath)")
27
+ }
28
+ }
29
+ }
30
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
31
+ #endif
32
+
33
+ // Store tracks for index tracking
34
+ currentTracks = tracks
35
+ currentTrackIndex = 0
36
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ”ข Reset currentTrackIndex to 0 (will be updated by KVO observer)")
37
+
38
+ // Remove old boundary observer if exists
39
+ if let boundaryObserver = boundaryTimeObserver, let currentPlayer = player {
40
+ currentPlayer.removeTimeObserver(boundaryObserver)
41
+ boundaryTimeObserver = nil
42
+ }
43
+
44
+ // Re-enable stall waiting for the new first track
45
+ player?.automaticallyWaitsToMinimizeStalling = true
46
+
47
+ // Clear old preloaded assets when loading new queue
48
+ preloadedAssets.removeAll()
49
+
50
+ guard let existingPlayer = self.player else {
51
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ No player available")
52
+ return
53
+ }
54
+
55
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ”„ Removing \(existingPlayer.items().count) old items from player")
56
+ existingPlayer.removeAllItems()
57
+
58
+ // Lazy-load mode: if any track has no URL AND is not downloaded locally,
59
+ // we can't create an AVPlayerItem for it and the queue order would be wrong.
60
+ let isLazyLoad = tracks.contains {
61
+ $0.url.isEmpty && !DownloadManagerCore.shared.isTrackDownloaded(trackId: $0.id)
62
+ }
63
+ if isLazyLoad {
64
+ NitroPlayerLogger.log("TrackPlayerCore", "โณ Lazy-load mode โ€” player cleared, awaiting URL resolution")
65
+ return
66
+ }
67
+
68
+ let items = tracks.enumerated().compactMap { (index, track) -> AVPlayerItem? in
69
+ let isPreload = index < Constants.gaplessPreloadCount
70
+ return createGaplessPlayerItem(for: track, isPreload: isPreload)
71
+ }
72
+
73
+ NitroPlayerLogger.log("TrackPlayerCore", "๐ŸŽต Created \(items.count) gapless-optimized player items")
74
+
75
+ guard !items.isEmpty else {
76
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ No valid items to play")
77
+ return
78
+ }
79
+
80
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ”„ Adding \(items.count) new items to player")
81
+
82
+ var lastItem: AVPlayerItem? = nil
83
+ for (index, item) in items.enumerated() {
84
+ existingPlayer.insert(item, after: lastItem)
85
+ lastItem = item
86
+
87
+ #if DEBUG
88
+ if let trackId = item.trackId, let track = tracks.first(where: { $0.id == trackId }) {
89
+ NitroPlayerLogger.log("TrackPlayerCore", " โž• Added to player queue [\(index + 1)]: \(track.title)")
90
+ }
91
+ #endif
92
+ }
93
+
94
+ #if DEBUG
95
+ let trackById = Dictionary(uniqueKeysWithValues: tracks.map { ($0.id, $0) })
96
+ NitroPlayerLogger.log("TrackPlayerCore", "\n๐Ÿ” VERIFICATION - Player now has \(existingPlayer.items().count) items:")
97
+ for (index, item) in existingPlayer.items().enumerated() {
98
+ if let trackId = item.trackId, let track = trackById[trackId] {
99
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] โœ“ \(track.title) - \(track.artist) (ID: \(track.id))")
100
+ } else {
101
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] โš ๏ธ Unknown item (no trackId)")
102
+ }
103
+ }
104
+ if let currentItem = existingPlayer.currentItem,
105
+ let trackId = currentItem.trackId,
106
+ let track = trackById[trackId]
107
+ {
108
+ NitroPlayerLogger.log("TrackPlayerCore", "โ–ถ๏ธ Current item: \(track.title)")
109
+ }
110
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "=", count: Constants.separatorLineLength) + "\n")
111
+ #endif
112
+
113
+ // Notify track change
114
+ if let firstTrack = tracks.first {
115
+ NitroPlayerLogger.log("TrackPlayerCore", "๐ŸŽต Emitting track change: \(firstTrack.title)")
116
+ notifyTrackChange(firstTrack, nil)
117
+ }
118
+
119
+ // Start preloading upcoming tracks for gapless playback
120
+ preloadUpcomingTracks(from: 1)
121
+
122
+ NitroPlayerLogger.log("TrackPlayerCore", "โœ… Queue updated with \(items.count) gapless-optimized tracks")
123
+ }
124
+
125
+ /// Clears temporary tracks, rebuilds AVQueuePlayer from `index` in the original playlist,
126
+ /// and resumes playback only if the player was already playing (preserves paused state).
127
+ @discardableResult
128
+ func rebuildQueueFromPlaylistIndex(index: Int) -> Bool {
129
+ guard index >= 0 && index < self.currentTracks.count else {
130
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ rebuildQueueFromPlaylistIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)")
131
+ return false
132
+ }
133
+
134
+ NitroPlayerLogger.log("TrackPlayerCore", "\n๐ŸŽฏ REBUILD QUEUE FROM PLAYLIST INDEX \(index)")
135
+ NitroPlayerLogger.log("TrackPlayerCore", " Total tracks in playlist: \(self.currentTracks.count)")
136
+ NitroPlayerLogger.log("TrackPlayerCore", " Current index: \(self.currentTrackIndex), target index: \(index)")
137
+
138
+ // Preserve playback state โ€” only resume if already playing.
139
+ let wasPlaying = self.player?.rate ?? 0 > 0
140
+
141
+ // Clear temporary tracks when jumping to specific index
142
+ self.playNextStack.removeAll()
143
+ self.upNextQueue.removeAll()
144
+ self.currentTemporaryType = .none
145
+ NitroPlayerLogger.log("TrackPlayerCore", " ๐Ÿงน Cleared temporary tracks")
146
+
147
+ let fullPlaylist = self.currentTracks
148
+
149
+ // Update currentTrackIndex BEFORE updating queue
150
+ self.currentTrackIndex = index
151
+
152
+ // Lazy-load guard: if the target track has no URL AND is not downloaded locally,
153
+ // the queue can't be built. Defer to updateTracks once URL resolution completes.
154
+ let targetTrack = fullPlaylist[index]
155
+ let isLazyLoad = targetTrack.url.isEmpty
156
+ && !DownloadManagerCore.shared.isTrackDownloaded(trackId: targetTrack.id)
157
+ if isLazyLoad {
158
+ NitroPlayerLogger.log("TrackPlayerCore", " โณ Lazy-load โ€” deferring AVQueuePlayer setup; emitting track change for index \(index)")
159
+ self.currentTracks = fullPlaylist
160
+ if let track = self.currentTracks[safe: index] {
161
+ notifyTrackChange(track, .skip)
162
+ }
163
+ return true
164
+ }
165
+
166
+ let tracksToPlay = Array(fullPlaylist[index...])
167
+ NitroPlayerLogger.log("TrackPlayerCore", " ๐Ÿ”„ Creating gapless queue with \(tracksToPlay.count) tracks starting from index \(index)")
168
+
169
+ let items = tracksToPlay.enumerated().compactMap { (offset, track) -> AVPlayerItem? in
170
+ let isPreload = offset < Constants.gaplessPreloadCount
171
+ return self.createGaplessPlayerItem(for: track, isPreload: isPreload)
172
+ }
173
+
174
+ guard let player = self.player, !items.isEmpty else {
175
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ No player or no items to play")
176
+ return false
177
+ }
178
+
179
+ // Remove old boundary observer
180
+ if let boundaryObserver = self.boundaryTimeObserver {
181
+ player.removeTimeObserver(boundaryObserver)
182
+ self.boundaryTimeObserver = nil
183
+ }
184
+
185
+ // Re-enable stall waiting for the new first track
186
+ player.automaticallyWaitsToMinimizeStalling = true
187
+
188
+ player.removeAllItems()
189
+ var lastItem: AVPlayerItem? = nil
190
+ for item in items {
191
+ player.insert(item, after: lastItem)
192
+ lastItem = item
193
+ }
194
+
195
+ // Restore the full playlist reference (don't slice it!)
196
+ self.currentTracks = fullPlaylist
197
+
198
+ NitroPlayerLogger.log("TrackPlayerCore", " โœ… Gapless queue recreated. Now at index: \(self.currentTrackIndex)")
199
+ if let track = self.getCurrentTrack() {
200
+ NitroPlayerLogger.log("TrackPlayerCore", " ๐ŸŽต Playing: \(track.title)")
201
+ notifyTrackChange(track, .skip)
202
+ }
203
+
204
+ self.preloadUpcomingTracks(from: index + 1)
205
+
206
+ if wasPlaying { player.play() }
207
+ return true
208
+ }
209
+
210
+ /// Rebuilds the AVQueuePlayer from the current position with temporary tracks.
211
+ /// Order: [current] + [playNext stack] + [upNext queue] + [remaining original]
212
+ ///
213
+ /// - Parameter changedTrackIds: When non-nil, performs a surgical update:
214
+ /// only AVPlayerItems whose track ID is in this set are removed and re-created.
215
+ func rebuildAVQueueFromCurrentPosition(changedTrackIds: Set<String>? = nil) {
216
+ guard let player = self.player else { return }
217
+
218
+ let currentItem = player.currentItem
219
+ let playingItems = player.items()
220
+
221
+ // If the currently playing AVPlayerItem is no longer in currentTracks,
222
+ // delegate to rebuildQueueFromPlaylistIndex so the player immediately
223
+ // starts what is now at currentTrackIndex in the updated list.
224
+ if let playingTrackId = currentItem?.trackId,
225
+ !currentTracks.contains(where: { $0.id == playingTrackId }) {
226
+ let targetIndex = currentTrackIndex < currentTracks.count
227
+ ? currentTrackIndex : currentTracks.count - 1
228
+ if targetIndex >= 0 {
229
+ _ = rebuildQueueFromPlaylistIndex(index: targetIndex)
230
+ }
231
+ return
232
+ }
233
+
234
+ // Build the desired upcoming track list
235
+ var newQueueTracks: [TrackItem] = []
236
+ let currentId = currentItem?.trackId
237
+
238
+ // PlayNext stack: skip the currently playing track by ID (not position)
239
+ if currentTemporaryType == .playNext, let currentId = currentId {
240
+ var skipped = false
241
+ for track in playNextStack {
242
+ if !skipped && track.id == currentId { skipped = true; continue }
243
+ newQueueTracks.append(track)
244
+ }
245
+ } else if currentTemporaryType != .playNext {
246
+ newQueueTracks.append(contentsOf: playNextStack)
247
+ }
248
+
249
+ // UpNext queue: skip the currently playing track by ID (not position)
250
+ if currentTemporaryType == .upNext, let currentId = currentId {
251
+ var skipped = false
252
+ for track in upNextQueue {
253
+ if !skipped && track.id == currentId { skipped = true; continue }
254
+ newQueueTracks.append(track)
255
+ }
256
+ } else if currentTemporaryType != .upNext {
257
+ newQueueTracks.append(contentsOf: upNextQueue)
258
+ }
259
+
260
+ if currentTrackIndex + 1 < currentTracks.count {
261
+ newQueueTracks.append(contentsOf: currentTracks[(currentTrackIndex + 1)...])
262
+ }
263
+
264
+ // Collect existing upcoming AVPlayerItems
265
+ let upcomingItems: [AVPlayerItem]
266
+ if let ci = currentItem, let ciIndex = playingItems.firstIndex(of: ci) {
267
+ upcomingItems = Array(playingItems.suffix(from: playingItems.index(after: ciIndex)))
268
+ } else {
269
+ upcomingItems = []
270
+ }
271
+
272
+ let existingIds = upcomingItems.compactMap { $0.trackId }
273
+ let desiredIds = newQueueTracks.map { $0.id }
274
+
275
+ // Fast-path: nothing to do if queue already matches
276
+ if existingIds == desiredIds {
277
+ if let changedIds = changedTrackIds {
278
+ if Set(existingIds).isDisjoint(with: changedIds) {
279
+ NitroPlayerLogger.log("TrackPlayerCore",
280
+ "โœ… Queue matches & no buffered URLs changed โ€” preserving \(existingIds.count) items for gapless")
281
+ return
282
+ }
283
+ } else {
284
+ NitroPlayerLogger.log("TrackPlayerCore",
285
+ "โœ… Queue already matches desired order โ€” preserving \(existingIds.count) items for gapless")
286
+ return
287
+ }
288
+ }
289
+
290
+ // Surgical path (changedTrackIds provided, e.g. from updateTracks)
291
+ if let changedIds = changedTrackIds {
292
+ var reusableByTrackId: [String: AVPlayerItem] = [:]
293
+ for item in upcomingItems {
294
+ if let trackId = item.trackId, !changedIds.contains(trackId) {
295
+ reusableByTrackId[trackId] = item
296
+ }
297
+ }
298
+
299
+ let desiredIdSet = Set(desiredIds)
300
+ for item in upcomingItems {
301
+ guard let trackId = item.trackId else { continue }
302
+ if changedIds.contains(trackId) || !desiredIdSet.contains(trackId) {
303
+ player.remove(item)
304
+ }
305
+ }
306
+
307
+ var lastAnchor: AVPlayerItem? = currentItem
308
+ for (offset, trackId) in desiredIds.enumerated() {
309
+ if let reusable = reusableByTrackId[trackId] {
310
+ lastAnchor = reusable
311
+ } else if let track = newQueueTracks.first(where: { $0.id == trackId }),
312
+ let newItem = createGaplessPlayerItem(for: track, isPreload: offset < Constants.gaplessPreloadCount)
313
+ {
314
+ player.insert(newItem, after: lastAnchor)
315
+ lastAnchor = newItem
316
+ }
317
+ }
318
+
319
+ let preserved = reusableByTrackId.count
320
+ let inserted = desiredIds.count - preserved
321
+ NitroPlayerLogger.log("TrackPlayerCore",
322
+ "๐Ÿ”„ Surgical rebuild: preserved \(preserved) buffered items, inserted \(inserted) new items")
323
+ return
324
+ }
325
+
326
+ // Full rebuild path (no changedTrackIds โ€” skip, reorder, etc.)
327
+ for item in playingItems where item != currentItem {
328
+ player.remove(item)
329
+ }
330
+
331
+ var lastItem = currentItem
332
+ for (offset, track) in newQueueTracks.enumerated() {
333
+ let isPreload = offset < Constants.gaplessPreloadCount
334
+ if let item = createGaplessPlayerItem(for: track, isPreload: isPreload) {
335
+ player.insert(item, after: lastItem)
336
+ lastItem = item
337
+ }
338
+ }
339
+ }
340
+
341
+ /// Creates a gapless-optimized AVPlayerItem with proper buffering configuration
342
+ func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false) -> AVPlayerItem? {
343
+ let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
344
+
345
+ let url: URL
346
+ let isLocal = effectiveUrlString.hasPrefix("/")
347
+
348
+ if isLocal {
349
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ“ฅ Using DOWNLOADED version for \(track.title)")
350
+ NitroPlayerLogger.log("TrackPlayerCore", " Local path: \(effectiveUrlString)")
351
+
352
+ if FileManager.default.fileExists(atPath: effectiveUrlString) {
353
+ url = URL(fileURLWithPath: effectiveUrlString)
354
+ NitroPlayerLogger.log("TrackPlayerCore", " File URL: \(url.absoluteString)")
355
+ NitroPlayerLogger.log("TrackPlayerCore", " โœ… File verified to exist")
356
+ } else {
357
+ NitroPlayerLogger.log("TrackPlayerCore", " โŒ Downloaded file does NOT exist at path!")
358
+ NitroPlayerLogger.log("TrackPlayerCore", " Falling back to remote URL: \(track.url)")
359
+ guard let remoteUrl = URL(string: track.url) else {
360
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ Invalid remote URL: \(track.url)")
361
+ return nil
362
+ }
363
+ url = remoteUrl
364
+ }
365
+ } else {
366
+ guard let remoteUrl = URL(string: effectiveUrlString) else {
367
+ NitroPlayerLogger.log("TrackPlayerCore", "โŒ Invalid URL for track: \(track.title) - \(effectiveUrlString)")
368
+ return nil
369
+ }
370
+ url = remoteUrl
371
+ NitroPlayerLogger.log("TrackPlayerCore", "๐ŸŒ Using REMOTE version for \(track.title)")
372
+ }
373
+
374
+ let asset: AVURLAsset
375
+ if let preloadedAsset = preloadedAssets[track.id] {
376
+ asset = preloadedAsset
377
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿš€ Using preloaded asset for \(track.title)")
378
+ } else {
379
+ asset = AVURLAsset(url: url, options: [
380
+ AVURLAssetPreferPreciseDurationAndTimingKey: true
381
+ ])
382
+ }
383
+
384
+ let item = AVPlayerItem(asset: asset)
385
+
386
+ // Let the system choose the optimal forward buffer size (0 = automatic).
387
+ item.preferredForwardBufferDuration = 0
388
+
389
+ item.canUseNetworkResourcesForLiveStreamingWhilePaused = true
390
+ item.trackId = track.id
391
+
392
+ if isPreload {
393
+ asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) {
394
+ var allKeysLoaded = true
395
+ for key in Constants.preloadAssetKeys {
396
+ var error: NSError?
397
+ let status = asset.statusOfValue(forKey: key, error: &error)
398
+ if status == .failed {
399
+ NitroPlayerLogger.log("TrackPlayerCore", "โš ๏ธ Failed to load key '\(key)' for \(track.title): \(error?.localizedDescription ?? "unknown")")
400
+ allKeysLoaded = false
401
+ }
402
+ }
403
+ if allKeysLoaded {
404
+ NitroPlayerLogger.log("TrackPlayerCore", "โœ… All asset keys preloaded for \(track.title)")
405
+ }
406
+ // "tracks" key is now loaded โ€” EQ tap attaches synchronously
407
+ EqualizerCore.shared.applyAudioMix(to: item)
408
+ }
409
+ } else {
410
+ EqualizerCore.shared.applyAudioMix(to: item)
411
+ }
412
+
413
+ return item
414
+ }
415
+
416
+ /// Preloads assets for upcoming tracks to enable gapless playback
417
+ func preloadUpcomingTracks(from startIndex: Int) {
418
+ // Capture the set of track IDs that already have AVPlayerItems in the queue.
419
+ let queuedTrackIds = Set(player?.items().compactMap { $0.trackId } ?? [])
420
+
421
+ preloadQueue.async { [weak self] in
422
+ guard let self else { return }
423
+
424
+ let tracks = self.currentTracks
425
+ let endIndex = min(startIndex + Constants.gaplessPreloadCount, tracks.count)
426
+
427
+ for i in startIndex..<endIndex {
428
+ guard i < tracks.count else { break }
429
+ let track = tracks[i]
430
+
431
+ if self.preloadedAssets[track.id] != nil || queuedTrackIds.contains(track.id) {
432
+ continue
433
+ }
434
+
435
+ let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
436
+ let isLocal = effectiveUrlString.hasPrefix("/")
437
+
438
+ let url: URL
439
+ if isLocal {
440
+ url = URL(fileURLWithPath: effectiveUrlString)
441
+ } else {
442
+ guard let remoteUrl = URL(string: effectiveUrlString) else { continue }
443
+ url = remoteUrl
444
+ }
445
+
446
+ let asset = AVURLAsset(url: url, options: [
447
+ AVURLAssetPreferPreciseDurationAndTimingKey: true
448
+ ])
449
+
450
+ asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) { [weak self] in
451
+ var allKeysLoaded = true
452
+ for key in Constants.preloadAssetKeys {
453
+ var error: NSError?
454
+ let status = asset.statusOfValue(forKey: key, error: &error)
455
+ if status != .loaded {
456
+ allKeysLoaded = false
457
+ break
458
+ }
459
+ }
460
+
461
+ if allKeysLoaded {
462
+ self?.playerQueue.async {
463
+ self?.preloadedAssets[track.id] = asset
464
+ NitroPlayerLogger.log("TrackPlayerCore", "๐ŸŽฏ Preloaded asset for upcoming track: \(track.title)")
465
+ }
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ /// Clears preloaded assets that are no longer needed
473
+ func cleanupPreloadedAssets(keepingFrom currentIndex: Int) {
474
+ // Already on playerQueue โ€” access preloadedAssets directly
475
+ let keepRange =
476
+ currentIndex..<min(
477
+ currentIndex + Constants.gaplessPreloadCount + 1, self.currentTracks.count)
478
+ let keepIds = Set(keepRange.compactMap { self.currentTracks[safe: $0]?.id })
479
+
480
+ let assetsToRemove = self.preloadedAssets.keys.filter { !keepIds.contains($0) }
481
+ for id in assetsToRemove {
482
+ self.preloadedAssets.removeValue(forKey: id)
483
+ }
484
+
485
+ if !assetsToRemove.isEmpty {
486
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿงน Cleaned up \(assetsToRemove.count) preloaded assets")
487
+ }
488
+ }
489
+
490
+ func getAllPlaylists() -> [Playlist] {
491
+ playlistManager.getAllPlaylists().map { $0.toGeneratedPlaylist() }
492
+ }
493
+ }
@@ -0,0 +1,167 @@
1
+ //
2
+ // TrackPlayerTempQueue.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+ import Foundation
8
+
9
+ extension TrackPlayerCore {
10
+
11
+ func loadPlaylist(playlistId: String) async {
12
+ await withPlayerQueueNoThrow {
13
+ self.playNextStack.removeAll()
14
+ self.upNextQueue.removeAll()
15
+ self.currentTemporaryType = .none
16
+
17
+ NitroPlayerLogger.log("TrackPlayerCore", "\n" + String(repeating: "๐ŸŽผ", count: Constants.playlistSeparatorLength))
18
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ“‚ LOAD PLAYLIST REQUEST")
19
+ NitroPlayerLogger.log("TrackPlayerCore", " Playlist ID: \(playlistId)")
20
+ NitroPlayerLogger.log("TrackPlayerCore", " ๐Ÿงน Cleared temporary tracks")
21
+
22
+ guard let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) else {
23
+ NitroPlayerLogger.log("TrackPlayerCore", " โŒ Playlist NOT FOUND")
24
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "๐ŸŽผ", count: Constants.playlistSeparatorLength) + "\n")
25
+ return
26
+ }
27
+
28
+ NitroPlayerLogger.log("TrackPlayerCore", " โœ… Found playlist: \(playlist.name)")
29
+ NitroPlayerLogger.log("TrackPlayerCore", " ๐Ÿ“‹ Contains \(playlist.tracks.count) tracks:")
30
+ for (index, track) in playlist.tracks.enumerated() {
31
+ NitroPlayerLogger.log("TrackPlayerCore", " [\(index + 1)] \(track.title) - \(track.artist)")
32
+ }
33
+ NitroPlayerLogger.log("TrackPlayerCore", String(repeating: "๐ŸŽผ", count: Constants.playlistSeparatorLength) + "\n")
34
+
35
+ self.currentPlaylistId = playlistId
36
+ self.updatePlayerQueue(tracks: playlist.tracks)
37
+ self.emitStateChange()
38
+ self.checkUpcomingTracksForUrls(lookahead: self.lookaheadCount)
39
+ self.notifyTemporaryQueueChange()
40
+ }
41
+ }
42
+
43
+ func updatePlaylist(playlistId: String) {
44
+ guard currentPlaylistId == playlistId else { return }
45
+
46
+ // Cancel any pending rebuild so back-to-back calls collapse into a single rebuild.
47
+ pendingPlaylistUpdateWorkItem?.cancel()
48
+
49
+ let workItem = DispatchWorkItem { [weak self] in
50
+ guard let self, self.currentPlaylistId == playlistId,
51
+ let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) else { return }
52
+
53
+ // If nothing is playing yet, do a full load
54
+ guard self.player?.currentItem != nil else {
55
+ self.updatePlayerQueue(tracks: playlist.tracks)
56
+ return
57
+ }
58
+
59
+ // Update tracks list without interrupting playback
60
+ self.currentTracks = playlist.tracks
61
+ self.rebuildAVQueueFromCurrentPosition()
62
+ }
63
+
64
+ pendingPlaylistUpdateWorkItem = workItem
65
+ playerQueue.async(execute: workItem)
66
+ }
67
+
68
+ func playNext(trackId: String) async throws {
69
+ try await withPlayerQueue {
70
+ guard let track = self.findTrackById(trackId) else {
71
+ throw NSError(domain: "NitroPlayer", code: 404, userInfo: [NSLocalizedDescriptionKey: "Track \(trackId) not found"])
72
+ }
73
+ NitroPlayerLogger.log("TrackPlayerCore", "โญ๏ธ playNext(\(trackId))")
74
+ self.playNextStack.insert(track, at: 0)
75
+ NitroPlayerLogger.log("TrackPlayerCore", " โœ… Added '\(track.title)' to playNext stack (position: 1)")
76
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
77
+ self.notifyTemporaryQueueChange()
78
+ }
79
+ }
80
+
81
+ func addToUpNext(trackId: String) async throws {
82
+ try await withPlayerQueue {
83
+ guard let track = self.findTrackById(trackId) else {
84
+ throw NSError(domain: "NitroPlayer", code: 404, userInfo: [NSLocalizedDescriptionKey: "Track \(trackId) not found"])
85
+ }
86
+ NitroPlayerLogger.log("TrackPlayerCore", "๐Ÿ“‹ addToUpNext(\(trackId))")
87
+ self.upNextQueue.append(track)
88
+ NitroPlayerLogger.log("TrackPlayerCore", " โœ… Added '\(track.title)' to upNext queue (position: \(self.upNextQueue.count))")
89
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
90
+ self.notifyTemporaryQueueChange()
91
+ }
92
+ }
93
+
94
+ func removeFromPlayNext(trackId: String) async -> Bool {
95
+ await withPlayerQueueNoThrow {
96
+ guard let idx = self.playNextStack.firstIndex(where: { $0.id == trackId }) else { return false }
97
+ self.playNextStack.remove(at: idx)
98
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
99
+ self.notifyTemporaryQueueChange()
100
+ return true
101
+ }
102
+ }
103
+
104
+ func removeFromUpNext(trackId: String) async -> Bool {
105
+ await withPlayerQueueNoThrow {
106
+ guard let idx = self.upNextQueue.firstIndex(where: { $0.id == trackId }) else { return false }
107
+ self.upNextQueue.remove(at: idx)
108
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
109
+ self.notifyTemporaryQueueChange()
110
+ return true
111
+ }
112
+ }
113
+
114
+ func clearPlayNext() async {
115
+ await withPlayerQueueNoThrow {
116
+ self.playNextStack.removeAll()
117
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
118
+ self.notifyTemporaryQueueChange()
119
+ }
120
+ }
121
+
122
+ func clearUpNext() async {
123
+ await withPlayerQueueNoThrow {
124
+ self.upNextQueue.removeAll()
125
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
126
+ self.notifyTemporaryQueueChange()
127
+ }
128
+ }
129
+
130
+ func reorderTemporaryTrack(trackId: String, newIndex: Int) async -> Bool {
131
+ await withPlayerQueueNoThrow {
132
+ var combined = self.playNextStack + self.upNextQueue
133
+ guard let fromIdx = combined.firstIndex(where: { $0.id == trackId }) else { return false }
134
+ let track = combined.remove(at: fromIdx)
135
+ let clamped = newIndex.clamped(to: 0...combined.count)
136
+ combined.insert(track, at: clamped)
137
+ let pnSize = self.playNextStack.count
138
+ self.playNextStack = Array(combined.prefix(pnSize))
139
+ self.upNextQueue = Array(combined.dropFirst(pnSize))
140
+ if self.player?.currentItem != nil { self.rebuildAVQueueFromCurrentPosition() }
141
+ self.notifyTemporaryQueueChange()
142
+ return true
143
+ }
144
+ }
145
+
146
+ func getPlayNextQueue() async -> [TrackItem] {
147
+ await withPlayerQueueNoThrow { self.playNextStack }
148
+ }
149
+
150
+ func getUpNextQueue() async -> [TrackItem] {
151
+ await withPlayerQueueNoThrow { self.upNextQueue }
152
+ }
153
+
154
+ func findTrackById(_ trackId: String) -> TrackItem? {
155
+ if let t = currentTracks.first(where: { $0.id == trackId }) { return t }
156
+ for playlist in playlistManager.getAllPlaylists() {
157
+ if let t = playlist.tracks.first(where: { $0.id == trackId }) { return t }
158
+ }
159
+ return nil
160
+ }
161
+ }
162
+
163
+ private extension Comparable {
164
+ func clamped(to range: ClosedRange<Self>) -> Self {
165
+ min(max(self, range.lowerBound), range.upperBound)
166
+ }
167
+ }