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,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,221 @@
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 the currently playing track by ID (already added as current)
90
+ let currentId = player?.currentItem?.trackId
91
+ if currentTemporaryType == .playNext, let currentId = currentId {
92
+ var skipped = false
93
+ for track in playNextStack {
94
+ if !skipped && track.id == currentId { skipped = true; continue }
95
+ queue.append(track)
96
+ }
97
+ } else if currentTemporaryType != .playNext {
98
+ queue.append(contentsOf: playNextStack)
99
+ }
100
+
101
+ // Add upNext queue — skip the currently playing track by ID (already added as current)
102
+ if currentTemporaryType == .upNext, let currentId = currentId {
103
+ var skipped = false
104
+ for track in upNextQueue {
105
+ if !skipped && track.id == currentId { skipped = true; continue }
106
+ queue.append(track)
107
+ }
108
+ } else if currentTemporaryType != .upNext {
109
+ queue.append(contentsOf: upNextQueue)
110
+ }
111
+
112
+ // Add remaining original tracks
113
+ if currentTrackIndex + 1 < currentTracks.count {
114
+ queue.append(contentsOf: currentTracks[(currentTrackIndex + 1)...])
115
+ }
116
+ return queue
117
+ }
118
+
119
+ func getCurrentTrack() -> TrackItem? {
120
+ if currentTemporaryType != .none,
121
+ let currentItem = player?.currentItem,
122
+ let trackId = currentItem.trackId
123
+ {
124
+ if currentTemporaryType == .playNext { return playNextStack.first(where: { $0.id == trackId }) }
125
+ if currentTemporaryType == .upNext { return upNextQueue.first(where: { $0.id == trackId }) }
126
+ }
127
+ guard currentTrackIndex >= 0 && currentTrackIndex < currentTracks.count else { return nil }
128
+ return currentTracks[currentTrackIndex]
129
+ }
130
+
131
+ @discardableResult
132
+ func skipToIndexInternal(index: Int) -> Bool {
133
+ let actualQueue = getActualQueueInternal()
134
+ guard index >= 0 && index < actualQueue.count else { return false }
135
+
136
+ // Calculate queue section boundaries using effective sizes
137
+ // (reduced by 1 when current track is from that temp list)
138
+ let currentPos = currentTemporaryType != .none
139
+ ? currentTrackIndex + 1 : currentTrackIndex
140
+ let effectivePlayNextSize = currentTemporaryType == .playNext
141
+ ? max(0, playNextStack.count - 1) : playNextStack.count
142
+ let effectiveUpNextSize = currentTemporaryType == .upNext
143
+ ? max(0, upNextQueue.count - 1) : upNextQueue.count
144
+
145
+ let playNextStart = currentPos + 1
146
+ let playNextEnd = playNextStart + effectivePlayNextSize
147
+ let upNextStart = playNextEnd
148
+ let upNextEnd = upNextStart + effectiveUpNextSize
149
+ let originalRemainingStart = upNextEnd
150
+
151
+ // Case 1: Target is before current - rebuild from that playlist index
152
+ if index < currentPos {
153
+ _ = rebuildQueueFromPlaylistIndex(index: index)
154
+ return true
155
+ }
156
+
157
+ // Case 2: Target is current - seek to beginning
158
+ if index == currentPos {
159
+ player?.seek(to: .zero)
160
+ return true
161
+ }
162
+
163
+ // Case 3: Target is in playNext section
164
+ if index >= playNextStart && index < playNextEnd {
165
+ let targetTrack = actualQueue[index]
166
+ // Remove all playNext tracks before the target (by ID lookup, not position)
167
+ if let targetIdx = playNextStack.firstIndex(where: { $0.id == targetTrack.id }), targetIdx > 0 {
168
+ // Remove tracks before target, but keep the currently playing track
169
+ // (rebuildAVQueueFromCurrentPosition will skip it by ID)
170
+ playNextStack.removeSubrange(0..<targetIdx)
171
+ }
172
+ rebuildAVQueueFromCurrentPosition()
173
+ player?.advanceToNextItem()
174
+ return true
175
+ }
176
+
177
+ // Case 4: Target is in upNext section
178
+ if index >= upNextStart && index < upNextEnd {
179
+ let targetTrack = actualQueue[index]
180
+ playNextStack.removeAll()
181
+ // Remove all upNext tracks before the target (by ID lookup, not position)
182
+ if let targetIdx = upNextQueue.firstIndex(where: { $0.id == targetTrack.id }), targetIdx > 0 {
183
+ upNextQueue.removeSubrange(0..<targetIdx)
184
+ }
185
+ rebuildAVQueueFromCurrentPosition()
186
+ player?.advanceToNextItem()
187
+ return true
188
+ }
189
+
190
+ // Case 5: Target is in remaining original tracks
191
+ if index >= originalRemainingStart {
192
+ let targetTrack = actualQueue[index]
193
+ guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else { return false }
194
+
195
+ playNextStack.removeAll()
196
+ upNextQueue.removeAll()
197
+ currentTemporaryType = .none
198
+
199
+ let result = rebuildQueueFromPlaylistIndex(index: originalIndex)
200
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
201
+ return result
202
+ }
203
+
204
+ checkUpcomingTracksForUrls(lookahead: lookaheadCount)
205
+ return false
206
+ }
207
+
208
+ func playFromIndexInternal(index: Int) {
209
+ playNextStack.removeAll()
210
+ upNextQueue.removeAll()
211
+ currentTemporaryType = .none
212
+ _ = rebuildQueueFromPlaylistIndex(index: index)
213
+ }
214
+
215
+ func determineCurrentTemporaryType() -> TemporaryType {
216
+ guard let trackId = player?.currentItem?.trackId else { return .none }
217
+ if playNextStack.contains(where: { $0.id == trackId }) { return .playNext }
218
+ if upNextQueue.contains(where: { $0.id == trackId }) { return .upNext }
219
+ return .none
220
+ }
221
+ }