react-native-nitro-player 0.7.0 → 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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrary.kt +9 -13
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridAudioDevices.kt +45 -90
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridDownloadManager.kt +48 -182
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridEqualizer.kt +21 -77
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridPlayerQueue.kt +55 -104
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +113 -123
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +82 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ListenerRegistry.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerAndroidAuto.kt +62 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +153 -1887
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerListener.kt +122 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerNotify.kt +44 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerPlayback.kt +162 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueue.kt +165 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +161 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerSetup.kt +28 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerTempQueue.kt +121 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +98 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/download/DownloadDatabase.kt +27 -18
- package/android/src/main/java/com/margelo/nitro/nitroplayer/equalizer/EqualizerCore.kt +11 -58
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/MediaSessionManager.kt +13 -30
- package/android/src/main/java/com/margelo/nitro/nitroplayer/playlist/PlaylistManager.kt +102 -162
- package/ios/HybridDownloadManager.swift +32 -26
- package/ios/HybridEqualizer.swift +48 -35
- package/ios/HybridTrackPlayer.swift +127 -102
- package/ios/core/ListenerRegistry.swift +60 -0
- package/ios/core/TrackPlayerCore.swift +130 -2356
- package/ios/core/TrackPlayerListener.swift +395 -0
- package/ios/core/TrackPlayerNotify.swift +52 -0
- package/ios/core/TrackPlayerPlayback.swift +274 -0
- package/ios/core/TrackPlayerQueue.swift +212 -0
- package/ios/core/TrackPlayerQueueBuild.swift +482 -0
- package/ios/core/TrackPlayerTempQueue.swift +167 -0
- package/ios/core/TrackPlayerUrlLoader.swift +169 -0
- package/ios/equalizer/EqualizerCore.swift +24 -89
- package/ios/media/MediaSessionManager.swift +32 -49
- package/ios/playlist/PlaylistManager.swift +2 -9
- package/ios/queue/HybridPlayerQueue.swift +69 -66
- package/lib/hooks/useDownloadedTracks.js +16 -13
- package/lib/hooks/useEqualizer.d.ts +4 -4
- package/lib/hooks/useEqualizer.js +12 -12
- package/lib/hooks/useEqualizerPresets.d.ts +3 -3
- package/lib/hooks/useEqualizerPresets.js +12 -18
- package/lib/specs/AndroidAutoMediaLibrary.nitro.d.ts +2 -2
- package/lib/specs/AudioDevices.nitro.d.ts +2 -2
- package/lib/specs/DownloadManager.nitro.d.ts +10 -10
- package/lib/specs/Equalizer.nitro.d.ts +9 -9
- package/lib/specs/TrackPlayer.nitro.d.ts +38 -16
- package/nitrogen/generated/android/NitroPlayerOnLoad.cpp +2 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_TrackItem__std__vector_TrackItem_.hpp +122 -0
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.cpp +31 -6
- package/nitrogen/generated/android/c++/JHybridAndroidAutoMediaLibrarySpec.hpp +2 -2
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.cpp +16 -3
- package/nitrogen/generated/android/c++/JHybridAudioDevicesSpec.hpp +1 -1
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.cpp +154 -44
- package/nitrogen/generated/android/c++/JHybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.cpp +130 -34
- package/nitrogen/generated/android/c++/JHybridEqualizerSpec.hpp +9 -9
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.cpp +115 -24
- package/nitrogen/generated/android/c++/JHybridPlayerQueueSpec.hpp +8 -8
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +243 -24
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +16 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/Func_void_std__vector_TrackItem__std__vector_TrackItem_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAndroidAutoMediaLibrarySpec.kt +3 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridAudioDevicesSpec.kt +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridDownloadManagerSpec.kt +10 -10
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridEqualizerSpec.kt +10 -9
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridPlayerQueueSpec.kt +9 -8
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +45 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +74 -18
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +380 -151
- package/nitrogen/generated/ios/c++/HybridDownloadManagerSpecSwift.hpp +10 -10
- package/nitrogen/generated/ios/c++/HybridEqualizerSpecSwift.hpp +12 -9
- package/nitrogen/generated/ios/c++/HybridPlayerQueueSpecSwift.hpp +23 -8
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +82 -8
- package/nitrogen/generated/ios/swift/Func_void_EqualizerState.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedPlaylist_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__DownloadedTrack_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__variant_nitro__NullType__std__string_.swift +58 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedPlaylist_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_DownloadedTrack_.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_EqualizerBand_.swift +5 -5
- package/nitrogen/generated/ios/swift/Func_void_std__vector_TrackItem__std__vector_TrackItem_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec.swift +10 -10
- package/nitrogen/generated/ios/swift/HybridDownloadManagerSpec_cxx.swift +141 -71
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec.swift +9 -9
- package/nitrogen/generated/ios/swift/HybridEqualizerSpec_cxx.swift +105 -41
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec.swift +8 -8
- package/nitrogen/generated/ios/swift/HybridPlayerQueueSpec_cxx.swift +95 -32
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +16 -8
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +267 -32
- package/nitrogen/generated/shared/c++/HybridAndroidAutoMediaLibrarySpec.hpp +3 -2
- package/nitrogen/generated/shared/c++/HybridAudioDevicesSpec.hpp +2 -1
- package/nitrogen/generated/shared/c++/HybridDownloadManagerSpec.hpp +10 -10
- package/nitrogen/generated/shared/c++/HybridEqualizerSpec.hpp +10 -9
- package/nitrogen/generated/shared/c++/HybridPlayerQueueSpec.hpp +9 -8
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +8 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +16 -8
- package/package.json +1 -1
- package/src/hooks/useDownloadedTracks.ts +17 -13
- package/src/hooks/useEqualizer.ts +16 -16
- package/src/hooks/useEqualizerPresets.ts +15 -21
- package/src/specs/AndroidAutoMediaLibrary.nitro.ts +2 -2
- package/src/specs/AudioDevices.nitro.ts +2 -2
- package/src/specs/DownloadManager.nitro.ts +10 -10
- package/src/specs/Equalizer.nitro.ts +9 -9
- 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
|
+
}
|