react-native-nitro-player 0.6.1 → 0.7.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.
Files changed (164) hide show
  1. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
  4. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
  5. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
  6. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
  7. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
  9. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
  10. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
  11. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
  12. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
  13. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
  14. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
  15. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
  16. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
  17. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
  18. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
  19. package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
  20. package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
  21. package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
  22. package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
  23. package/ios/HybridDownloadManager.swift +32 -26
  24. package/ios/HybridEqualizer.swift +48 -35
  25. package/ios/HybridTrackPlayer.swift +127 -102
  26. package/ios/core/ListenerRegistry.swift +60 -0
  27. package/ios/core/TrackPlayerCore.swift +130 -2356
  28. package/ios/core/TrackPlayerListener.swift +395 -0
  29. package/ios/core/TrackPlayerNotify.swift +52 -0
  30. package/ios/core/TrackPlayerPlayback.swift +274 -0
  31. package/ios/core/TrackPlayerQueue.swift +212 -0
  32. package/ios/core/TrackPlayerQueueBuild.swift +482 -0
  33. package/ios/core/TrackPlayerTempQueue.swift +167 -0
  34. package/ios/core/TrackPlayerUrlLoader.swift +169 -0
  35. package/ios/equalizer/EqualizerCore.swift +24 -89
  36. package/ios/media/MediaSessionManager.swift +32 -49
  37. package/ios/playlist/PlaylistManager.swift +2 -9
  38. package/ios/queue/HybridPlayerQueue.swift +69 -66
  39. package/lib/hooks/useDownloadedTracks.js +16 -13
  40. package/lib/hooks/useEqualizer.d.ts +4 -4
  41. package/lib/hooks/useEqualizer.js +12 -12
  42. package/lib/hooks/useEqualizerPresets.d.ts +3 -3
  43. package/lib/hooks/useEqualizerPresets.js +12 -18
  44. package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
  45. package/lib/specs/AudioDevices.nitro.d.ts +2 -2
  46. package/lib/specs/DownloadManager.nitro.d.ts +10 -10
  47. package/lib/specs/Equalizer.nitro.d.ts +9 -9
  48. package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
  49. package/nitro.json +44 -11
  50. package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +63 -24
  51. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +1 -1
  52. package/nitrogen/generated/android/c++/JDownloadConfig.hpp +1 -1
  53. package/nitrogen/generated/android/c++/JDownloadError.hpp +1 -1
  54. package/nitrogen/generated/android/c++/JDownloadErrorReason.hpp +1 -1
  55. package/nitrogen/generated/android/c++/JDownloadProgress.hpp +1 -1
  56. package/nitrogen/generated/android/c++/JDownloadQueueStatus.hpp +1 -1
  57. package/nitrogen/generated/android/c++/JDownloadState.hpp +1 -1
  58. package/nitrogen/generated/android/c++/JDownloadStorageInfo.hpp +1 -1
  59. package/nitrogen/generated/android/c++/JDownloadTask.hpp +1 -1
  60. package/nitrogen/generated/android/c++/JDownloadedPlaylist.hpp +1 -1
  61. package/nitrogen/generated/android/c++/JDownloadedTrack.hpp +1 -1
  62. package/nitrogen/generated/android/c++/JEqualizerBand.hpp +1 -1
  63. package/nitrogen/generated/android/c++/JEqualizerPreset.hpp +1 -1
  64. package/nitrogen/generated/android/c++/JEqualizerState.hpp +1 -1
  65. package/nitrogen/generated/android/c++/JFunc_void_DownloadProgress.hpp +2 -2
  66. package/nitrogen/generated/android/c++/JFunc_void_DownloadedTrack.hpp +2 -2
  67. package/nitrogen/generated/android/c++/JFunc_void_TrackItem_std__optional_Reason_.hpp +2 -2
  68. package/nitrogen/generated/android/c++/JFunc_void_TrackPlayerState_std__optional_Reason_.hpp +2 -2
  69. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +2 -2
  70. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +2 -2
  71. package/nitrogen/generated/android/c++/JFunc_void_double_double_std__optional_bool_.hpp +2 -2
  72. package/nitrogen/generated/android/c++/JFunc_void_std__optional_std__variant_nitro__NullType__std__string__.hpp +2 -2
  73. package/nitrogen/generated/android/c++/JFunc_void_std__string_Playlist_std__optional_QueueOperation_.hpp +2 -2
  74. package/nitrogen/generated/android/c++/JFunc_void_std__string_std__string_DownloadState_std__optional_DownloadError_.hpp +2 -2
  75. package/nitrogen/generated/android/c++/JFunc_void_std__vector_EqualizerBand_.hpp +2 -2
  76. package/nitrogen/generated/android/c++/JFunc_void_std__vector_Playlist__std__optional_QueueOperation_.hpp +2 -2
  77. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__double.hpp +2 -2
  78. package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
  79. package/nitrogen/generated/android/c++/JGainRange.hpp +1 -1
  80. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +49 -30
  81. package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +21 -24
  82. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +35 -28
  83. package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +20 -23
  84. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +197 -93
  85. package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +29 -32
  86. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +157 -67
  87. package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +28 -31
  88. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +138 -53
  89. package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +27 -30
  90. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +282 -69
  91. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +35 -30
  92. package/nitrogen/generated/android/c++/JPlaybackSource.hpp +1 -1
  93. package/nitrogen/generated/android/c++/JPlayerConfig.hpp +1 -1
  94. package/nitrogen/generated/android/c++/JPlayerState.hpp +1 -1
  95. package/nitrogen/generated/android/c++/JPlaylist.hpp +1 -1
  96. package/nitrogen/generated/android/c++/JPresetType.hpp +1 -1
  97. package/nitrogen/generated/android/c++/JQueueOperation.hpp +1 -1
  98. package/nitrogen/generated/android/c++/JReason.hpp +1 -1
  99. package/nitrogen/generated/android/c++/JRepeatMode.hpp +1 -1
  100. package/nitrogen/generated/android/c++/JStorageLocation.hpp +1 -1
  101. package/nitrogen/generated/android/c++/JTAudioDevice.hpp +1 -1
  102. package/nitrogen/generated/android/c++/JTrackItem.hpp +1 -1
  103. package/nitrogen/generated/android/c++/JTrackPlayerState.hpp +1 -1
  104. package/nitrogen/generated/android/c++/JVariant_NullType_Double.hpp +3 -3
  105. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadError.hpp +3 -3
  106. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadTask.hpp +3 -3
  107. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedPlaylist.hpp +3 -3
  108. package/nitrogen/generated/android/c++/JVariant_NullType_DownloadedTrack.hpp +3 -3
  109. package/nitrogen/generated/android/c++/JVariant_NullType_Playlist.hpp +3 -3
  110. package/nitrogen/generated/android/c++/JVariant_NullType_String.hpp +3 -3
  111. package/nitrogen/generated/android/c++/JVariant_NullType_TrackItem.hpp +3 -3
  112. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
  113. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +18 -20
  114. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +17 -19
  115. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +25 -28
  116. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +25 -27
  117. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +24 -26
  118. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +60 -26
  119. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Double.kt +0 -6
  120. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadError.kt +0 -6
  121. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadTask.kt +0 -6
  122. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedPlaylist.kt +0 -6
  123. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_DownloadedTrack.kt +0 -6
  124. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_Playlist.kt +0 -6
  125. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_String.kt +0 -6
  126. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Variant_NullType_TrackItem.kt +0 -6
  127. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
  128. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
  129. package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
  130. package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
  131. package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
  132. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
  133. package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
  134. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
  135. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
  136. package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
  137. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
  138. package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
  139. package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
  140. package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
  141. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
  142. package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
  143. package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
  144. package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
  145. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
  146. package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
  147. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
  148. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
  149. package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
  150. package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
  151. package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
  152. package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
  153. package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
  154. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
  155. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
  156. package/package.json +3 -3
  157. package/src/hooks/useDownloadedTracks.ts +17 -13
  158. package/src/hooks/useEqualizer.ts +16 -16
  159. package/src/hooks/useEqualizerPresets.ts +15 -21
  160. package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
  161. package/src/specs/AudioDevices.nitro.ts +2 -2
  162. package/src/specs/DownloadManager.nitro.ts +10 -10
  163. package/src/specs/Equalizer.nitro.ts +9 -9
  164. package/src/specs/TrackPlayer.nitro.ts +52 -16
@@ -0,0 +1,274 @@
1
+ //
2
+ // TrackPlayerPlayback.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+
8
+ import AVFoundation
9
+ import Foundation
10
+
11
+ extension TrackPlayerCore {
12
+
13
+ func play() async {
14
+ await withPlayerQueueNoThrow { self.playInternal() }
15
+ }
16
+
17
+ func pause() async {
18
+ await withPlayerQueueNoThrow { self.pauseInternal() }
19
+ }
20
+
21
+ func seek(position: Double) async {
22
+ await withPlayerQueueNoThrow { self.seekInternal(position: position) }
23
+ }
24
+
25
+ func skipToNext() async {
26
+ await withPlayerQueueNoThrow { self.skipToNextInternal() }
27
+ }
28
+
29
+ func skipToPrevious() async {
30
+ await withPlayerQueueNoThrow { self.skipToPreviousInternal() }
31
+ }
32
+
33
+ func setRepeatMode(mode: RepeatMode) async {
34
+ await withPlayerQueueNoThrow {
35
+ self.currentRepeatMode = mode
36
+ self.player?.actionAtItemEnd = (mode == .track) ? .none : .advance
37
+ NitroPlayerLogger.log("TrackPlayerCore", "🔁 setRepeatMode: \(mode)")
38
+ }
39
+ }
40
+
41
+ func setVolume(volume: Double) async {
42
+ await withPlayerQueueNoThrow {
43
+ let clamped = max(0.0, min(100.0, volume))
44
+ let normalized = Float(clamped / 100.0)
45
+ self.player?.volume = normalized
46
+ NitroPlayerLogger.log("TrackPlayerCore", "🔊 Volume set to \(Int(clamped))% (normalized: \(normalized))")
47
+ }
48
+ }
49
+
50
+ func configure(androidAutoEnabled: Bool?, carPlayEnabled: Bool?, showInNotification: Bool?, lookaheadCount: Int?) async {
51
+ await withPlayerQueueNoThrow {
52
+ if let la = lookaheadCount {
53
+ self.lookaheadCount = la
54
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Lookahead count set to: \(la)")
55
+ }
56
+ }
57
+ DispatchQueue.main.async { [weak self] in
58
+ self?.mediaSessionManager?.configure(
59
+ androidAutoEnabled: androidAutoEnabled,
60
+ carPlayEnabled: carPlayEnabled,
61
+ showInNotification: showInNotification
62
+ )
63
+ }
64
+ }
65
+
66
+ func setPlaybackSpeed(_ speed: Double) async {
67
+ await withPlayerQueueNoThrow {
68
+ self.currentPlaybackSpeed = speed
69
+ // Only update rate if currently playing; pause keeps rate at 0 until play() is called
70
+ if let player = self.player, player.rate != 0 {
71
+ player.rate = Float(speed)
72
+ }
73
+ }
74
+ }
75
+
76
+ func getPlaybackSpeed() async -> Double {
77
+ await withPlayerQueueNoThrow { self.currentPlaybackSpeed }
78
+ }
79
+
80
+ func playSong(songId: String, fromPlaylist: String?) async {
81
+ await withPlayerQueueNoThrow { self.playSongInternal(songId: songId, fromPlaylist: fromPlaylist) }
82
+ }
83
+
84
+ // MARK: - Internal (run on playerQueue)
85
+
86
+ func playInternal() {
87
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ play() called")
88
+ if let player = self.player {
89
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Player status: \(player.status.rawValue)")
90
+ if let currentItem = player.currentItem {
91
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Current item status: \(currentItem.status.rawValue)")
92
+ if let error = currentItem.error {
93
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Current item error: \(error.localizedDescription)")
94
+ }
95
+ }
96
+ player.rate = Float(currentPlaybackSpeed)
97
+ playerQueue.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
98
+ self?.emitStateChange()
99
+ }
100
+ } else {
101
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ No player available")
102
+ }
103
+ }
104
+
105
+ func pauseInternal() {
106
+ NitroPlayerLogger.log("TrackPlayerCore", "⏸️ pause() called")
107
+ self.player?.pause()
108
+ playerQueue.asyncAfter(deadline: .now() + Constants.stateChangeDelay) { [weak self] in
109
+ self?.emitStateChange()
110
+ }
111
+ }
112
+
113
+ func seekInternal(position: Double) {
114
+ guard let player = self.player else { return }
115
+ self.isManuallySeeked = true
116
+ let time = CMTime(seconds: position, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
117
+ player.seek(to: time) { [weak self] completed in
118
+ // Update now playing info to restore playback rate after seek
119
+ DispatchQueue.main.async { self?.mediaSessionManager?.refresh() }
120
+ if completed {
121
+ let duration = player.currentItem?.duration.seconds ?? 0.0
122
+ self?.notifySeek(position, duration)
123
+ }
124
+ }
125
+ }
126
+
127
+ func skipToNextInternal() {
128
+ guard let queuePlayer = self.player else { return }
129
+
130
+ // Lazy-load: AVQueuePlayer is empty because updatePlayerQueue deferred population.
131
+ if queuePlayer.items().isEmpty && !currentTracks.isEmpty {
132
+ let nextIndex = currentTrackIndex + 1
133
+ if nextIndex < currentTracks.count {
134
+ _ = skipToIndexInternal(index: nextIndex)
135
+ }
136
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
137
+ return
138
+ }
139
+
140
+ // Remove current temp track from its list before advancing
141
+ if let trackId = queuePlayer.currentItem?.trackId {
142
+ if currentTemporaryType == .playNext {
143
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
144
+ playNextStack.remove(at: idx)
145
+ notifyTemporaryQueueChange()
146
+ }
147
+ } else if currentTemporaryType == .upNext {
148
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
149
+ upNextQueue.remove(at: idx)
150
+ notifyTemporaryQueueChange()
151
+ }
152
+ }
153
+ }
154
+
155
+ if queuePlayer.items().count > 1 {
156
+ queuePlayer.advanceToNextItem()
157
+ } else {
158
+ queuePlayer.pause()
159
+ self.notifyPlaybackStateChange(.stopped, .end)
160
+ }
161
+
162
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
163
+ }
164
+
165
+ func skipToPreviousInternal() {
166
+ guard let queuePlayer = self.player else { return }
167
+
168
+ let currentTime = queuePlayer.currentTime()
169
+ if currentTime.seconds > Constants.skipToPreviousThreshold {
170
+ // If more than threshold seconds in, restart current track
171
+ queuePlayer.seek(to: .zero)
172
+ } else if self.currentTemporaryType != .none {
173
+ // Playing temporary track — remove from its list, then go back to original track
174
+ if let trackId = queuePlayer.currentItem?.trackId {
175
+ if currentTemporaryType == .playNext, let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
176
+ playNextStack.remove(at: idx)
177
+ notifyTemporaryQueueChange()
178
+ } else if currentTemporaryType == .upNext, let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
179
+ upNextQueue.remove(at: idx)
180
+ notifyTemporaryQueueChange()
181
+ }
182
+ }
183
+ // Go back to current original track position (skip back from temp)
184
+ _ = rebuildQueueFromPlaylistIndex(index: self.currentTrackIndex)
185
+ } else if self.currentTrackIndex > 0 {
186
+ // Go to previous track in original playlist
187
+ _ = rebuildQueueFromPlaylistIndex(index: self.currentTrackIndex - 1)
188
+ } else {
189
+ // Already at first track, restart it
190
+ queuePlayer.seek(to: .zero)
191
+ }
192
+
193
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
194
+ }
195
+
196
+ func playSongInternal(songId: String, fromPlaylist: String?) {
197
+ // Clear temporary tracks when directly playing a song
198
+ self.playNextStack.removeAll()
199
+ self.upNextQueue.removeAll()
200
+ self.currentTemporaryType = .none
201
+ NitroPlayerLogger.log("TrackPlayerCore", " 🧹 Cleared temporary tracks")
202
+
203
+ var targetPlaylistId: String?
204
+ var songIndex: Int = -1
205
+
206
+ // Case 1: If fromPlaylist is provided, use that playlist
207
+ if let playlistId = fromPlaylist {
208
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 Looking for song in specified playlist: \(playlistId)")
209
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
210
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
211
+ targetPlaylistId = playlistId
212
+ songIndex = index
213
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in playlist \(playlistId)")
214
+ } else {
215
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Song \(songId) not found in specified playlist \(playlistId)")
216
+ return
217
+ }
218
+ } else {
219
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Playlist \(playlistId) not found")
220
+ return
221
+ }
222
+ }
223
+ // Case 2: If fromPlaylist is not provided, search in current/loaded playlist first
224
+ else {
225
+ NitroPlayerLogger.log("TrackPlayerCore", "🎵 No playlist specified, checking current playlist")
226
+
227
+ if let currentId = self.currentPlaylistId,
228
+ let currentPlaylist = self.playlistManager.getPlaylist(playlistId: currentId)
229
+ {
230
+ if let index = currentPlaylist.tracks.firstIndex(where: { $0.id == songId }) {
231
+ targetPlaylistId = currentId
232
+ songIndex = index
233
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in current playlist \(currentId)")
234
+ }
235
+ }
236
+
237
+ if songIndex == -1 {
238
+ NitroPlayerLogger.log("TrackPlayerCore", "🔍 Song not found in current playlist, searching all playlists...")
239
+ let allPlaylists = self.playlistManager.getAllPlaylists()
240
+
241
+ for playlist in allPlaylists {
242
+ if let index = playlist.tracks.firstIndex(where: { $0.id == songId }) {
243
+ targetPlaylistId = playlist.id
244
+ songIndex = index
245
+ NitroPlayerLogger.log("TrackPlayerCore", "✅ Found song at index \(index) in playlist \(playlist.id)")
246
+ break
247
+ }
248
+ }
249
+
250
+ if songIndex == -1 && !allPlaylists.isEmpty {
251
+ targetPlaylistId = allPlaylists[0].id
252
+ songIndex = 0
253
+ NitroPlayerLogger.log("TrackPlayerCore", "⚠️ Song not found in any playlist, using first playlist and starting at index 0")
254
+ }
255
+ }
256
+ }
257
+
258
+ guard let playlistId = targetPlaylistId, songIndex >= 0 else {
259
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Could not determine playlist or song index")
260
+ return
261
+ }
262
+
263
+ if self.currentPlaylistId != playlistId {
264
+ NitroPlayerLogger.log("TrackPlayerCore", "🔄 Loading new playlist: \(playlistId)")
265
+ if let playlist = self.playlistManager.getPlaylist(playlistId: playlistId) {
266
+ self.currentPlaylistId = playlistId
267
+ self.updatePlayerQueue(tracks: playlist.tracks)
268
+ }
269
+ }
270
+
271
+ NitroPlayerLogger.log("TrackPlayerCore", "▶️ Playing from index: \(songIndex)")
272
+ self.playFromIndexInternal(index: songIndex)
273
+ }
274
+ }
@@ -0,0 +1,212 @@
1
+ //
2
+ // TrackPlayerQueue.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla on 25/03/26.
6
+ //
7
+
8
+ import AVFoundation
9
+ import Foundation
10
+
11
+ extension TrackPlayerCore {
12
+
13
+ func getState() async -> PlayerState {
14
+ await withPlayerQueueNoThrow { self.getStateInternal() }
15
+ }
16
+
17
+ func getActualQueue() async -> [TrackItem] {
18
+ await withPlayerQueueNoThrow { self.getActualQueueInternal() }
19
+ }
20
+
21
+ func skipToIndex(index: Int) async -> Bool {
22
+ await withPlayerQueueNoThrow { self.skipToIndexInternal(index: index) }
23
+ }
24
+
25
+ func playFromIndex(index: Int) async {
26
+ await withPlayerQueueNoThrow { self.playFromIndexInternal(index: index) }
27
+ }
28
+
29
+ func getCurrentTrackIndex() async -> Int {
30
+ await withPlayerQueueNoThrow { self.currentTrackIndex }
31
+ }
32
+
33
+ // MARK: - Internal (run on playerQueue)
34
+
35
+ func getStateInternal() -> PlayerState {
36
+ guard let player else {
37
+ return PlayerState(
38
+ currentTrack: nil, currentPosition: 0.0, totalDuration: 0.0,
39
+ currentState: .stopped,
40
+ currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
41
+ currentIndex: -1.0, currentPlayingType: .notPlaying
42
+ )
43
+ }
44
+ let currentTrack = getCurrentTrack()
45
+ let currentPosition = player.currentTime().seconds
46
+ let totalDuration = player.currentItem?.duration.seconds ?? 0.0
47
+
48
+ let state: TrackPlayerState
49
+ if player.rate == 0 { state = .paused }
50
+ else if player.timeControlStatus == .playing { state = .playing }
51
+ else { state = .stopped }
52
+
53
+ let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
54
+
55
+ let playingType: CurrentPlayingType
56
+ if currentTrack == nil { playingType = .notPlaying }
57
+ else {
58
+ switch currentTemporaryType {
59
+ case .none: playingType = .playlist
60
+ case .playNext: playingType = .playNext
61
+ case .upNext: playingType = .upNext
62
+ }
63
+ }
64
+
65
+ return PlayerState(
66
+ currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
67
+ currentPosition: currentPosition,
68
+ totalDuration: totalDuration,
69
+ currentState: state,
70
+ currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
71
+ currentIndex: currentIndex,
72
+ currentPlayingType: playingType
73
+ )
74
+ }
75
+
76
+ func getActualQueueInternal() -> [TrackItem] {
77
+ var queue: [TrackItem] = []
78
+ queue.reserveCapacity(currentTracks.count + playNextStack.count + upNextQueue.count)
79
+
80
+ // Add tracks before current (original playlist)
81
+ // When a temp track is playing, include the original track at currentTrackIndex
82
+ let beforeEnd = currentTemporaryType != .none
83
+ ? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
84
+ if beforeEnd > 0 { queue.append(contentsOf: currentTracks[0..<beforeEnd]) }
85
+
86
+ // Add current track (temp or original)
87
+ if let current = getCurrentTrack() { queue.append(current) }
88
+
89
+ // Add playNext stack — skip index 0 if current is from playNext (already added as current)
90
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
91
+ queue.append(contentsOf: playNextStack.dropFirst())
92
+ } else if currentTemporaryType != .playNext {
93
+ queue.append(contentsOf: playNextStack)
94
+ }
95
+
96
+ // Add upNext queue — skip index 0 if current is from upNext (already added as current)
97
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
98
+ queue.append(contentsOf: upNextQueue.dropFirst())
99
+ } else if currentTemporaryType != .upNext {
100
+ queue.append(contentsOf: upNextQueue)
101
+ }
102
+
103
+ // Add remaining original tracks
104
+ if currentTrackIndex + 1 < currentTracks.count {
105
+ queue.append(contentsOf: currentTracks[(currentTrackIndex + 1)...])
106
+ }
107
+ return queue
108
+ }
109
+
110
+ func getCurrentTrack() -> TrackItem? {
111
+ if currentTemporaryType != .none,
112
+ let currentItem = player?.currentItem,
113
+ let trackId = currentItem.trackId
114
+ {
115
+ if currentTemporaryType == .playNext { return playNextStack.first(where: { $0.id == trackId }) }
116
+ if currentTemporaryType == .upNext { return upNextQueue.first(where: { $0.id == trackId }) }
117
+ }
118
+ guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else { return nil }
119
+ return currentTracks[currentTrackIndex]
120
+ }
121
+
122
+ @discardableResult
123
+ func skipToIndexInternal(index: Int) -> Bool {
124
+ let actualQueue = getActualQueueInternal()
125
+ guard index >= 0 && index < actualQueue.count else { return false }
126
+
127
+ // Calculate queue section boundaries using effective sizes
128
+ // (reduced by 1 when current track is from that temp list)
129
+ let currentPos = currentTemporaryType != .none
130
+ ? currentTrackIndex + 1 : currentTrackIndex
131
+ let effectivePlayNextSize = currentTemporaryType == .playNext
132
+ ? max(0, playNextStack.count - 1) : playNextStack.count
133
+ let effectiveUpNextSize = currentTemporaryType == .upNext
134
+ ? max(0, upNextQueue.count - 1) : upNextQueue.count
135
+
136
+ let playNextStart = currentPos + 1
137
+ let playNextEnd = playNextStart + effectivePlayNextSize
138
+ let upNextStart = playNextEnd
139
+ let upNextEnd = upNextStart + effectiveUpNextSize
140
+ let originalRemainingStart = upNextEnd
141
+
142
+ // Case 1: Target is before current - rebuild from that playlist index
143
+ if index < currentPos {
144
+ _ = rebuildQueueFromPlaylistIndex(index: index)
145
+ return true
146
+ }
147
+
148
+ // Case 2: Target is current - seek to beginning
149
+ if index == currentPos {
150
+ player?.seek(to: .zero)
151
+ return true
152
+ }
153
+
154
+ // Case 3: Target is in playNext section
155
+ if index >= playNextStart && index < playNextEnd {
156
+ let playNextIndex = index - playNextStart
157
+ // Offset by 1 if current is from playNext (index 0 is already playing)
158
+ let actualListIndex = currentTemporaryType == .playNext
159
+ ? playNextIndex + 1 : playNextIndex
160
+
161
+ if actualListIndex > 0 { playNextStack.removeFirst(actualListIndex) }
162
+ rebuildAVQueueFromCurrentPosition()
163
+ player?.advanceToNextItem()
164
+ return true
165
+ }
166
+
167
+ // Case 4: Target is in upNext section
168
+ if index >= upNextStart && index < upNextEnd {
169
+ let upNextIndex = index - upNextStart
170
+ // Offset by 1 if current is from upNext (index 0 is already playing)
171
+ let actualListIndex = currentTemporaryType == .upNext
172
+ ? upNextIndex + 1 : upNextIndex
173
+
174
+ playNextStack.removeAll()
175
+ if actualListIndex > 0 { upNextQueue.removeFirst(actualListIndex) }
176
+ rebuildAVQueueFromCurrentPosition()
177
+ player?.advanceToNextItem()
178
+ return true
179
+ }
180
+
181
+ // Case 5: Target is in remaining original tracks
182
+ if index >= originalRemainingStart {
183
+ let targetTrack = actualQueue[index]
184
+ guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else { return false }
185
+
186
+ playNextStack.removeAll()
187
+ upNextQueue.removeAll()
188
+ currentTemporaryType = .none
189
+
190
+ let result = rebuildQueueFromPlaylistIndex(index: originalIndex)
191
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
192
+ return result
193
+ }
194
+
195
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
196
+ return false
197
+ }
198
+
199
+ func playFromIndexInternal(index: Int) {
200
+ playNextStack.removeAll()
201
+ upNextQueue.removeAll()
202
+ currentTemporaryType = .none
203
+ _ = rebuildQueueFromPlaylistIndex(index: index)
204
+ }
205
+
206
+ func determineCurrentTemporaryType() -> TemporaryType {
207
+ guard let trackId = player?.currentItem?.trackId else { return .none }
208
+ if playNextStack.contains(where: { $0.id == trackId }) { return .playNext }
209
+ if upNextQueue.contains(where: { $0.id == trackId }) { return .upNext }
210
+ return .none
211
+ }
212
+ }