react-native-nitro-player 0.3.0-alpha.12 ā 0.3.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +7 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +232 -7
- package/ios/HybridTrackPlayer.swift +6 -0
- package/ios/core/TrackPlayerCore.swift +158 -6
- package/lib/hooks/useNowPlaying.js +1 -0
- package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
- package/lib/types/PlayerQueue.d.ts +2 -0
- package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +20 -0
- package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JPlayerState.hpp +9 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -8
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +46 -22
- package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +11 -0
- package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +19 -0
- package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
- package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
- package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
- package/package.json +1 -1
- package/src/hooks/useNowPlaying.ts +1 -0
- package/src/specs/TrackPlayer.nitro.ts +1 -0
- package/src/types/PlayerQueue.ts +6 -0
package/README.md
CHANGED
|
@@ -52,6 +52,7 @@ npm install react-native-nitro-modules
|
|
|
52
52
|
| `playNext(id)` | Both | **Async**. Adds a track to the "play next" stack (LIFO). |
|
|
53
53
|
| `getActualQueue()` | Both | **Async**. Gets the full playback queue including temporary tracks. |
|
|
54
54
|
| `getState()` | Both | **Async**. Gets the current player state immediately. |
|
|
55
|
+
| `skipToIndex(index)` | Both | **Async**. Skips to a specific index in the actual queue. |
|
|
55
56
|
| `configure(config)` | Both | Configures player settings (Android Auto, etc.). |
|
|
56
57
|
| `isAndroidAutoConnected()` | Both | Checks if Android Auto is currently connected. |
|
|
57
58
|
|
|
@@ -270,6 +271,29 @@ Temporary tracks are automatically cleared when:
|
|
|
270
271
|
- `PlayerQueue.loadPlaylist()` is called
|
|
271
272
|
- `TrackPlayer.playFromIndex()` is called
|
|
272
273
|
|
|
274
|
+
### `skipToIndex(index: number): Promise<boolean>`
|
|
275
|
+
|
|
276
|
+
Skips to a specific index in the **actual queue** (the combined queue with temporary tracks).
|
|
277
|
+
|
|
278
|
+
**Behavior:**
|
|
279
|
+
|
|
280
|
+
- Takes an index into the actual queue structure
|
|
281
|
+
- If the target is a temporary track (playNext or upNext), plays that track
|
|
282
|
+
- If the target is beyond temporary tracks (in the remaining original playlist), clears all temporary tracks and plays from the original playlist
|
|
283
|
+
- Returns `true` if successful, `false` if the index is invalid
|
|
284
|
+
|
|
285
|
+
**Example:**
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Queue: [track1(0), track2(1, current), playNext-A(2), upNext-B(3), track3(4), track4(5)]
|
|
289
|
+
|
|
290
|
+
// Skip to playNext track
|
|
291
|
+
await TrackPlayer.skipToIndex(2) // Plays playNext-A
|
|
292
|
+
|
|
293
|
+
// Skip to original playlist track (clears temporary tracks)
|
|
294
|
+
await TrackPlayer.skipToIndex(4) // Clears temps, plays track3
|
|
295
|
+
```
|
|
296
|
+
|
|
273
297
|
### Getting the Actual Queue
|
|
274
298
|
|
|
275
299
|
Use `useActualQueue()` hook to see the complete queue including temporary tracks:
|
|
@@ -298,6 +322,31 @@ function QueueView() {
|
|
|
298
322
|
- `refreshQueue: () => void` - Manually refresh the queue
|
|
299
323
|
- `isLoading: boolean` - Whether the queue is currently loading
|
|
300
324
|
|
|
325
|
+
## CurrentPlayingType
|
|
326
|
+
|
|
327
|
+
The `currentPlayingType` field in `PlayerState` indicates the source of the currently playing track:
|
|
328
|
+
|
|
329
|
+
| Value | Description |
|
|
330
|
+
| -------------- | -------------------------------------------------------- |
|
|
331
|
+
| `'playlist'` | Playing from the original playlist |
|
|
332
|
+
| `'play-next'` | Playing a track added via `playNext()` (LIFO stack) |
|
|
333
|
+
| `'up-next'` | Playing a track added via `addToUpNext()` (FIFO queue) |
|
|
334
|
+
| `'not-playing'`| No track is currently playing |
|
|
335
|
+
|
|
336
|
+
**Example:**
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
const state = await TrackPlayer.getState()
|
|
340
|
+
|
|
341
|
+
if (state.currentPlayingType === 'play-next') {
|
|
342
|
+
console.log('Playing a play-next track')
|
|
343
|
+
} else if (state.currentPlayingType === 'up-next') {
|
|
344
|
+
console.log('Playing an up-next track')
|
|
345
|
+
} else if (state.currentPlayingType === 'playlist') {
|
|
346
|
+
console.log('Playing from the original playlist')
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
301
350
|
## Core Concepts
|
|
302
351
|
|
|
303
352
|
### PlayerQueue
|
|
@@ -390,6 +439,7 @@ Returns the complete current player state (same as `TrackPlayer.getState()`). Th
|
|
|
390
439
|
- `currentState: TrackPlayerState` - Current playback state (`'playing'`, `'paused'`, or `'stopped'`)
|
|
391
440
|
- `currentPlaylistId: string | null` - ID of the currently loaded playlist, or `null` if no playlist is loaded
|
|
392
441
|
- `currentIndex: number` - Index of the current track in the playlist (-1 if no track is playing)
|
|
442
|
+
- `currentPlayingType: CurrentPlayingType` - Source of the current track (`'playlist'`, `'play-next'`, `'up-next'`, or `'not-playing'`)
|
|
393
443
|
|
|
394
444
|
**Note:** This hook is equivalent to calling `TrackPlayer.getState()` but provides reactive updates. It listens to track changes and playback state changes to update automatically. Also dont rely on progress from this hook
|
|
395
445
|
|
|
@@ -489,7 +539,7 @@ import { AudioDevices } from 'react-native-nitro-player'
|
|
|
489
539
|
|
|
490
540
|
if (AudioDevices) {
|
|
491
541
|
const devices = AudioDevices.getAudioDevices()
|
|
492
|
-
devices.forEach(device => {
|
|
542
|
+
devices.forEach((device) => {
|
|
493
543
|
console.log(`${device.name} - Active: ${device.isActive}`)
|
|
494
544
|
})
|
|
495
545
|
}
|
|
@@ -736,7 +786,7 @@ TrackPlayer.onPlaybackProgressChange(
|
|
|
736
786
|
)
|
|
737
787
|
|
|
738
788
|
// Listen to Android Auto connection changes
|
|
739
|
-
TrackPlayer.onAndroidAutoConnectionChange(connected => {
|
|
789
|
+
TrackPlayer.onAndroidAutoConnectionChange((connected) => {
|
|
740
790
|
console.log('Android Auto:', connected ? 'Connected' : 'Disconnected')
|
|
741
791
|
})
|
|
742
792
|
```
|
|
@@ -754,6 +804,7 @@ console.log(state.totalDuration) // total duration in seconds
|
|
|
754
804
|
console.log(state.currentTrack) // current TrackItem or null
|
|
755
805
|
console.log(state.currentPlaylistId) // current playlist ID or null
|
|
756
806
|
console.log(state.currentIndex) // current track index in playlist
|
|
807
|
+
console.log(state.currentPlayingType) // 'playlist' | 'play-next' | 'up-next' | 'not-playing'
|
|
757
808
|
```
|
|
758
809
|
|
|
759
810
|
## Track Item Structure
|
|
@@ -978,6 +1029,7 @@ import type {
|
|
|
978
1029
|
Playlist,
|
|
979
1030
|
PlayerState,
|
|
980
1031
|
TrackPlayerState,
|
|
1032
|
+
CurrentPlayingType,
|
|
981
1033
|
QueueOperation,
|
|
982
1034
|
Reason,
|
|
983
1035
|
PlayerConfig,
|
|
@@ -114,4 +114,11 @@ class HybridTrackPlayer : HybridTrackPlayerSpec() {
|
|
|
114
114
|
@DoNotStrip
|
|
115
115
|
@Keep
|
|
116
116
|
override fun setVolume(volume: Double): Boolean = core.setVolume(volume)
|
|
117
|
+
|
|
118
|
+
@DoNotStrip
|
|
119
|
+
@Keep
|
|
120
|
+
override fun skipToIndex(index: Double): Promise<Boolean> =
|
|
121
|
+
Promise.async {
|
|
122
|
+
core.skipToIndex(index.toInt())
|
|
123
|
+
}
|
|
117
124
|
}
|
|
@@ -12,6 +12,7 @@ import androidx.media3.common.Player
|
|
|
12
12
|
import androidx.media3.exoplayer.DefaultLoadControl
|
|
13
13
|
import androidx.media3.exoplayer.ExoPlayer
|
|
14
14
|
import com.margelo.nitro.core.NullType
|
|
15
|
+
import com.margelo.nitro.nitroplayer.CurrentPlayingType
|
|
15
16
|
import com.margelo.nitro.nitroplayer.NitroPlayerPackage
|
|
16
17
|
import com.margelo.nitro.nitroplayer.PlayerState
|
|
17
18
|
import com.margelo.nitro.nitroplayer.Reason
|
|
@@ -84,6 +85,7 @@ class TrackPlayerCore private constructor(
|
|
|
84
85
|
private var upNextQueue: MutableList<TrackItem> = mutableListOf() // FIFO - first added plays first
|
|
85
86
|
private var currentTemporaryType: TemporaryType = TemporaryType.NONE
|
|
86
87
|
private var currentTracks: List<TrackItem> = emptyList()
|
|
88
|
+
private var currentTrackIndex: Int = -1 // Index in the original playlist (currentTracks)
|
|
87
89
|
|
|
88
90
|
// Enum to track what type of track is currently playing
|
|
89
91
|
private enum class TemporaryType {
|
|
@@ -228,6 +230,16 @@ class TrackPlayerCore private constructor(
|
|
|
228
230
|
currentTemporaryType = determineCurrentTemporaryType()
|
|
229
231
|
println(" Updated currentTemporaryType: $currentTemporaryType")
|
|
230
232
|
|
|
233
|
+
// Update currentTrackIndex when we land on an original playlist track
|
|
234
|
+
if (currentTemporaryType == TemporaryType.NONE && mediaItem != null) {
|
|
235
|
+
val trackId = extractTrackId(mediaItem.mediaId)
|
|
236
|
+
val newIndex = currentTracks.indexOfFirst { it.id == trackId }
|
|
237
|
+
if (newIndex >= 0 && newIndex != currentTrackIndex) {
|
|
238
|
+
println(" š Updating currentTrackIndex from $currentTrackIndex to $newIndex")
|
|
239
|
+
currentTrackIndex = newIndex
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
// Handle playlist switching if needed
|
|
232
244
|
mediaItem?.mediaId?.let { mediaId ->
|
|
233
245
|
if (mediaId.contains(':')) {
|
|
@@ -596,10 +608,13 @@ class TrackPlayerCore private constructor(
|
|
|
596
608
|
if (currentTemporaryType != TemporaryType.NONE) {
|
|
597
609
|
println("š TrackPlayerCore: Playing temporary track - seeking to beginning")
|
|
598
610
|
player.seekTo(0)
|
|
599
|
-
} else if (
|
|
600
|
-
|
|
611
|
+
} else if (currentTrackIndex > 0) {
|
|
612
|
+
// Go to previous track in original playlist
|
|
613
|
+
println("š TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
|
|
614
|
+
playFromIndexInternal(currentTrackIndex - 1)
|
|
601
615
|
} else {
|
|
602
616
|
// Already at first track, seek to beginning
|
|
617
|
+
println("š TrackPlayerCore: Already at first track, seeking to beginning")
|
|
603
618
|
player.seekTo(0)
|
|
604
619
|
}
|
|
605
620
|
}
|
|
@@ -692,6 +707,18 @@ class TrackPlayerCore private constructor(
|
|
|
692
707
|
-1.0
|
|
693
708
|
}
|
|
694
709
|
|
|
710
|
+
// Map internal temporary type to CurrentPlayingType
|
|
711
|
+
val currentPlayingTypeValue =
|
|
712
|
+
if (track == null) {
|
|
713
|
+
CurrentPlayingType.NOT_PLAYING
|
|
714
|
+
} else {
|
|
715
|
+
when (currentTemporaryType) {
|
|
716
|
+
TemporaryType.NONE -> CurrentPlayingType.PLAYLIST
|
|
717
|
+
TemporaryType.PLAY_NEXT -> CurrentPlayingType.PLAY_NEXT
|
|
718
|
+
TemporaryType.UP_NEXT -> CurrentPlayingType.UP_NEXT
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
695
722
|
PlayerState(
|
|
696
723
|
currentTrack = currentTrack,
|
|
697
724
|
currentPosition = currentPosition,
|
|
@@ -699,6 +726,7 @@ class TrackPlayerCore private constructor(
|
|
|
699
726
|
currentState = currentState,
|
|
700
727
|
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
701
728
|
currentIndex = currentIndex,
|
|
729
|
+
currentPlayingType = currentPlayingTypeValue,
|
|
702
730
|
)
|
|
703
731
|
} else {
|
|
704
732
|
// Return default state if player is not initialized
|
|
@@ -709,6 +737,7 @@ class TrackPlayerCore private constructor(
|
|
|
709
737
|
currentState = TrackPlayerState.STOPPED,
|
|
710
738
|
currentPlaylistId = currentPlaylistId?.let { Variant_NullType_String.create(it) },
|
|
711
739
|
currentIndex = -1.0,
|
|
740
|
+
currentPlayingType = CurrentPlayingType.NOT_PLAYING,
|
|
712
741
|
)
|
|
713
742
|
}
|
|
714
743
|
|
|
@@ -778,6 +807,159 @@ class TrackPlayerCore private constructor(
|
|
|
778
807
|
}
|
|
779
808
|
}
|
|
780
809
|
|
|
810
|
+
// MARK: - Skip to Index in Actual Queue
|
|
811
|
+
|
|
812
|
+
fun skipToIndex(index: Int): Boolean {
|
|
813
|
+
// Check if we're already on the main thread
|
|
814
|
+
if (android.os.Looper.myLooper() == handler.looper) {
|
|
815
|
+
return skipToIndexInternal(index)
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Use CountDownLatch to wait for the result on the main thread
|
|
819
|
+
val latch = CountDownLatch(1)
|
|
820
|
+
var result = false
|
|
821
|
+
|
|
822
|
+
handler.post {
|
|
823
|
+
try {
|
|
824
|
+
result = skipToIndexInternal(index)
|
|
825
|
+
} finally {
|
|
826
|
+
latch.countDown()
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
// Wait up to 5 seconds for the result
|
|
832
|
+
latch.await(5, TimeUnit.SECONDS)
|
|
833
|
+
} catch (e: InterruptedException) {
|
|
834
|
+
Thread.currentThread().interrupt()
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
return result
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
private fun skipToIndexInternal(index: Int): Boolean {
|
|
841
|
+
println("\nšÆ TrackPlayerCore: SKIP TO INDEX $index")
|
|
842
|
+
|
|
843
|
+
if (!::player.isInitialized) {
|
|
844
|
+
println(" ā Player not initialized")
|
|
845
|
+
return false
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Get actual queue to validate index and determine position
|
|
849
|
+
val actualQueue = getActualQueueInternal()
|
|
850
|
+
val totalQueueSize = actualQueue.size
|
|
851
|
+
|
|
852
|
+
// Validate index
|
|
853
|
+
if (index < 0 || index >= totalQueueSize) {
|
|
854
|
+
println(" ā Invalid index $index, queue size is $totalQueueSize")
|
|
855
|
+
return false
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Calculate queue section boundaries
|
|
859
|
+
// ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
|
|
860
|
+
// Use our internal tracking instead of player.currentMediaItemIndex (which is relative to ExoPlayer's subset queue)
|
|
861
|
+
val currentPos = currentTrackIndex
|
|
862
|
+
val playNextStart = currentPos + 1
|
|
863
|
+
val playNextEnd = playNextStart + playNextStack.size
|
|
864
|
+
val upNextStart = playNextEnd
|
|
865
|
+
val upNextEnd = upNextStart + upNextQueue.size
|
|
866
|
+
val originalRemainingStart = upNextEnd
|
|
867
|
+
|
|
868
|
+
println(" Queue structure:")
|
|
869
|
+
println(" currentPos: $currentPos")
|
|
870
|
+
println(" playNextStart: $playNextStart, playNextEnd: $playNextEnd")
|
|
871
|
+
println(" upNextStart: $upNextStart, upNextEnd: $upNextEnd")
|
|
872
|
+
println(" originalRemainingStart: $originalRemainingStart")
|
|
873
|
+
println(" totalQueueSize: $totalQueueSize")
|
|
874
|
+
|
|
875
|
+
// Case 1: Target is before current - use playFromIndex on original
|
|
876
|
+
if (index < currentPos) {
|
|
877
|
+
println(" š Target is before current, jumping to original playlist index $index")
|
|
878
|
+
playFromIndexInternal(index)
|
|
879
|
+
return true
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Case 2: Target is current - seek to beginning
|
|
883
|
+
if (index == currentPos) {
|
|
884
|
+
println(" š Target is current track, seeking to beginning")
|
|
885
|
+
player.seekTo(0)
|
|
886
|
+
return true
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Case 3: Target is in playNext section
|
|
890
|
+
if (index >= playNextStart && index < playNextEnd) {
|
|
891
|
+
val playNextIndex = index - playNextStart
|
|
892
|
+
println(" š Target is in playNext section at position $playNextIndex")
|
|
893
|
+
|
|
894
|
+
// Remove tracks before the target from playNext (they're being skipped)
|
|
895
|
+
if (playNextIndex > 0) {
|
|
896
|
+
repeat(playNextIndex) { playNextStack.removeAt(0) }
|
|
897
|
+
println(" Removed $playNextIndex tracks from playNext stack")
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Rebuild queue and advance
|
|
901
|
+
rebuildQueueFromCurrentPosition()
|
|
902
|
+
player.seekToNextMediaItem()
|
|
903
|
+
return true
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Case 4: Target is in upNext section
|
|
907
|
+
if (index >= upNextStart && index < upNextEnd) {
|
|
908
|
+
val upNextIndex = index - upNextStart
|
|
909
|
+
println(" š Target is in upNext section at position $upNextIndex")
|
|
910
|
+
|
|
911
|
+
// Clear all playNext tracks (they're being skipped)
|
|
912
|
+
playNextStack.clear()
|
|
913
|
+
println(" Cleared all playNext tracks")
|
|
914
|
+
|
|
915
|
+
// Remove tracks before target from upNext
|
|
916
|
+
if (upNextIndex > 0) {
|
|
917
|
+
repeat(upNextIndex) { upNextQueue.removeAt(0) }
|
|
918
|
+
println(" Removed $upNextIndex tracks from upNext queue")
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Rebuild queue and advance
|
|
922
|
+
rebuildQueueFromCurrentPosition()
|
|
923
|
+
player.seekToNextMediaItem()
|
|
924
|
+
return true
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Case 5: Target is in remaining original tracks
|
|
928
|
+
if (index >= originalRemainingStart) {
|
|
929
|
+
// Get the target track directly from actualQueue
|
|
930
|
+
val targetTrack = actualQueue[index]
|
|
931
|
+
|
|
932
|
+
println(" š Case 5: Target is in remaining original tracks")
|
|
933
|
+
println(" targetTrack.id: ${targetTrack.id}")
|
|
934
|
+
println(" currentTracks.count: ${currentTracks.size}")
|
|
935
|
+
println(" currentTracks IDs: ${currentTracks.map { it.id }}")
|
|
936
|
+
|
|
937
|
+
// Find this track's index in the original playlist
|
|
938
|
+
val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
|
|
939
|
+
if (originalIndex == -1) {
|
|
940
|
+
println(" ā Could not find track ${targetTrack.id} in original playlist")
|
|
941
|
+
println(" Available tracks: ${currentTracks.map { it.id }}")
|
|
942
|
+
return false
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
println(" originalIndex found: $originalIndex")
|
|
946
|
+
|
|
947
|
+
// Clear all temporary tracks (they're being skipped)
|
|
948
|
+
playNextStack.clear()
|
|
949
|
+
upNextQueue.clear()
|
|
950
|
+
currentTemporaryType = TemporaryType.NONE
|
|
951
|
+
println(" Cleared all temporary tracks")
|
|
952
|
+
|
|
953
|
+
// IMPORTANT: Rebuild the ExoPlayer queue without temporary tracks, then seek
|
|
954
|
+
// We need to rebuild from the target index, not just seek
|
|
955
|
+
rebuildQueueAndPlayFromIndex(originalIndex)
|
|
956
|
+
return true
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
println(" ā Unexpected case, index $index not handled")
|
|
960
|
+
return false
|
|
961
|
+
}
|
|
962
|
+
|
|
781
963
|
private fun playFromIndexInternal(index: Int) {
|
|
782
964
|
// Clear temporary tracks when jumping to specific index
|
|
783
965
|
playNextStack.clear()
|
|
@@ -785,10 +967,51 @@ class TrackPlayerCore private constructor(
|
|
|
785
967
|
currentTemporaryType = TemporaryType.NONE
|
|
786
968
|
println(" š§¹ Cleared temporary tracks")
|
|
787
969
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
970
|
+
rebuildQueueAndPlayFromIndex(index)
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Rebuild the entire ExoPlayer queue from the original playlist starting at the given index
|
|
975
|
+
* This clears all temporary tracks and rebuilds the queue fresh
|
|
976
|
+
*/
|
|
977
|
+
private fun rebuildQueueAndPlayFromIndex(index: Int) {
|
|
978
|
+
if (!::player.isInitialized) {
|
|
979
|
+
println(" ā Player not initialized")
|
|
980
|
+
return
|
|
791
981
|
}
|
|
982
|
+
|
|
983
|
+
if (index < 0 || index >= currentTracks.size) {
|
|
984
|
+
println(" ā Invalid index $index for currentTracks size ${currentTracks.size}")
|
|
985
|
+
return
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
println("\nš TrackPlayerCore: REBUILD QUEUE AND PLAY FROM INDEX $index")
|
|
989
|
+
println(" currentTracks.size: ${currentTracks.size}")
|
|
990
|
+
println(" currentTracks IDs: ${currentTracks.map { it.id }}")
|
|
991
|
+
|
|
992
|
+
// Build queue from the target index onwards
|
|
993
|
+
val tracksToPlay = currentTracks.subList(index, currentTracks.size)
|
|
994
|
+
println(" tracksToPlay (${tracksToPlay.size}): ${tracksToPlay.map { it.id }}")
|
|
995
|
+
|
|
996
|
+
val playlistId = currentPlaylistId ?: ""
|
|
997
|
+
val mediaItems =
|
|
998
|
+
tracksToPlay.map { track ->
|
|
999
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
1000
|
+
track.toMediaItem(mediaId)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Update our internal tracking of the position in original playlist
|
|
1004
|
+
currentTrackIndex = index
|
|
1005
|
+
println(" Setting currentTrackIndex to $index")
|
|
1006
|
+
|
|
1007
|
+
// Clear the entire player queue and set new items
|
|
1008
|
+
player.clearMediaItems()
|
|
1009
|
+
player.setMediaItems(mediaItems)
|
|
1010
|
+
player.seekToDefaultPosition(0) // Seek to first item (which is our target track)
|
|
1011
|
+
player.playWhenReady = true
|
|
1012
|
+
player.prepare()
|
|
1013
|
+
|
|
1014
|
+
println(" ā
Queue rebuilt with ${player.mediaItemCount} items, playing from index 0 (track ${tracksToPlay.firstOrNull()?.id})")
|
|
792
1015
|
}
|
|
793
1016
|
|
|
794
1017
|
// MARK: - Temporary Track Management
|
|
@@ -1153,6 +1376,7 @@ class TrackPlayerCore private constructor(
|
|
|
1153
1376
|
println(" playNextStack size: ${playNextStack.size}, tracks: ${playNextStack.map { it.id }}")
|
|
1154
1377
|
println(" upNextQueue size: ${upNextQueue.size}, tracks: ${upNextQueue.map { it.id }}")
|
|
1155
1378
|
println(" currentTracks size: ${currentTracks.size}, tracks: ${currentTracks.map { it.id }}")
|
|
1379
|
+
println(" currentTrackIndex: $currentTrackIndex")
|
|
1156
1380
|
|
|
1157
1381
|
val queue = mutableListOf<TrackItem>()
|
|
1158
1382
|
|
|
@@ -1161,8 +1385,9 @@ class TrackPlayerCore private constructor(
|
|
|
1161
1385
|
return emptyList()
|
|
1162
1386
|
}
|
|
1163
1387
|
|
|
1164
|
-
|
|
1165
|
-
|
|
1388
|
+
// Use our internal tracking of position in original playlist
|
|
1389
|
+
val currentIndex = currentTrackIndex
|
|
1390
|
+
println(" Using currentTrackIndex: $currentIndex")
|
|
1166
1391
|
if (currentIndex < 0) {
|
|
1167
1392
|
println(" ā currentIndex < 0, returning empty")
|
|
1168
1393
|
return emptyList()
|
|
@@ -124,6 +124,12 @@ final class HybridTrackPlayer: HybridTrackPlayerSpec {
|
|
|
124
124
|
return false
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
func skipToIndex(index: Double) throws -> Promise<Bool> {
|
|
128
|
+
return Promise.async {
|
|
129
|
+
return self.core.skipToIndex(index: Int(index))
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
// MARK: - Volume Control
|
|
128
134
|
|
|
129
135
|
func setVolume(volume: Double) throws -> Bool {
|
|
@@ -1436,7 +1436,8 @@ class TrackPlayerCore: NSObject {
|
|
|
1436
1436
|
totalDuration: 0.0,
|
|
1437
1437
|
currentState: .stopped,
|
|
1438
1438
|
currentPlaylistId: nil,
|
|
1439
|
-
currentIndex: -1.0
|
|
1439
|
+
currentIndex: -1.0,
|
|
1440
|
+
currentPlayingType: .notPlaying
|
|
1440
1441
|
)
|
|
1441
1442
|
}
|
|
1442
1443
|
return state
|
|
@@ -1451,7 +1452,8 @@ class TrackPlayerCore: NSObject {
|
|
|
1451
1452
|
totalDuration: 0.0,
|
|
1452
1453
|
currentState: .stopped,
|
|
1453
1454
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1454
|
-
currentIndex: -1.0
|
|
1455
|
+
currentIndex: -1.0,
|
|
1456
|
+
currentPlayingType: .notPlaying
|
|
1455
1457
|
)
|
|
1456
1458
|
}
|
|
1457
1459
|
|
|
@@ -1471,13 +1473,29 @@ class TrackPlayerCore: NSObject {
|
|
|
1471
1473
|
// Get current index
|
|
1472
1474
|
let currentIndex: Double = currentTrackIndex >= 0 ? Double(currentTrackIndex) : -1.0
|
|
1473
1475
|
|
|
1476
|
+
// Map internal temporary type to CurrentPlayingType
|
|
1477
|
+
let currentPlayingType: CurrentPlayingType
|
|
1478
|
+
if currentTrack == nil {
|
|
1479
|
+
currentPlayingType = .notPlaying
|
|
1480
|
+
} else {
|
|
1481
|
+
switch currentTemporaryType {
|
|
1482
|
+
case .none:
|
|
1483
|
+
currentPlayingType = .playlist
|
|
1484
|
+
case .playNext:
|
|
1485
|
+
currentPlayingType = .playNext
|
|
1486
|
+
case .upNext:
|
|
1487
|
+
currentPlayingType = .upNext
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1474
1491
|
return PlayerState(
|
|
1475
1492
|
currentTrack: currentTrack.map { Variant_NullType_TrackItem.second($0) },
|
|
1476
1493
|
currentPosition: currentPosition,
|
|
1477
1494
|
totalDuration: totalDuration,
|
|
1478
1495
|
currentState: currentState,
|
|
1479
1496
|
currentPlaylistId: currentPlaylistId.map { Variant_NullType_String.second($0) },
|
|
1480
|
-
currentIndex: currentIndex
|
|
1497
|
+
currentIndex: currentIndex,
|
|
1498
|
+
currentPlayingType: currentPlayingType
|
|
1481
1499
|
)
|
|
1482
1500
|
}
|
|
1483
1501
|
|
|
@@ -1531,10 +1549,143 @@ class TrackPlayerCore: NSObject {
|
|
|
1531
1549
|
}
|
|
1532
1550
|
}
|
|
1533
1551
|
|
|
1552
|
+
// MARK: - Skip to Index in Actual Queue
|
|
1553
|
+
|
|
1554
|
+
func skipToIndex(index: Int) -> Bool {
|
|
1555
|
+
if Thread.isMainThread {
|
|
1556
|
+
return skipToIndexInternal(index: index)
|
|
1557
|
+
} else {
|
|
1558
|
+
var result = false
|
|
1559
|
+
DispatchQueue.main.sync { [weak self] in
|
|
1560
|
+
result = self?.skipToIndexInternal(index: index) ?? false
|
|
1561
|
+
}
|
|
1562
|
+
return result
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
private func skipToIndexInternal(index: Int) -> Bool {
|
|
1567
|
+
print("\nšÆ TrackPlayerCore: SKIP TO INDEX \(index)")
|
|
1568
|
+
|
|
1569
|
+
// Get actual queue to validate index and determine position
|
|
1570
|
+
let actualQueue = getActualQueueInternal()
|
|
1571
|
+
let totalQueueSize = actualQueue.count
|
|
1572
|
+
|
|
1573
|
+
// Validate index
|
|
1574
|
+
guard index >= 0 && index < totalQueueSize else {
|
|
1575
|
+
print(" ā Invalid index \(index), queue size is \(totalQueueSize)")
|
|
1576
|
+
return false
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Calculate queue section boundaries
|
|
1580
|
+
// ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
|
|
1581
|
+
let currentPos = currentTrackIndex
|
|
1582
|
+
let playNextStart = currentPos + 1
|
|
1583
|
+
let playNextEnd = playNextStart + playNextStack.count
|
|
1584
|
+
let upNextStart = playNextEnd
|
|
1585
|
+
let upNextEnd = upNextStart + upNextQueue.count
|
|
1586
|
+
let originalRemainingStart = upNextEnd
|
|
1587
|
+
|
|
1588
|
+
print(" Queue structure:")
|
|
1589
|
+
print(" currentPos: \(currentPos)")
|
|
1590
|
+
print(" playNextStart: \(playNextStart), playNextEnd: \(playNextEnd)")
|
|
1591
|
+
print(" upNextStart: \(upNextStart), upNextEnd: \(upNextEnd)")
|
|
1592
|
+
print(" originalRemainingStart: \(originalRemainingStart)")
|
|
1593
|
+
print(" totalQueueSize: \(totalQueueSize)")
|
|
1594
|
+
|
|
1595
|
+
// Case 1: Target is before current - use playFromIndex on original
|
|
1596
|
+
if index < currentPos {
|
|
1597
|
+
print(" š Target is before current, jumping to original playlist index \(index)")
|
|
1598
|
+
playFromIndexInternal(index: index)
|
|
1599
|
+
return true
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
// Case 2: Target is current - seek to beginning
|
|
1603
|
+
if index == currentPos {
|
|
1604
|
+
print(" š Target is current track, seeking to beginning")
|
|
1605
|
+
player?.seek(to: .zero)
|
|
1606
|
+
return true
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Case 3: Target is in playNext section
|
|
1610
|
+
if index >= playNextStart && index < playNextEnd {
|
|
1611
|
+
let playNextIndex = index - playNextStart
|
|
1612
|
+
print(" š Target is in playNext section at position \(playNextIndex)")
|
|
1613
|
+
|
|
1614
|
+
// Remove tracks before the target from playNext (they're being skipped)
|
|
1615
|
+
if playNextIndex > 0 {
|
|
1616
|
+
playNextStack.removeFirst(playNextIndex)
|
|
1617
|
+
print(" Removed \(playNextIndex) tracks from playNext stack")
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Rebuild queue and advance
|
|
1621
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1622
|
+
player?.advanceToNextItem()
|
|
1623
|
+
return true
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Case 4: Target is in upNext section
|
|
1627
|
+
if index >= upNextStart && index < upNextEnd {
|
|
1628
|
+
let upNextIndex = index - upNextStart
|
|
1629
|
+
print(" š Target is in upNext section at position \(upNextIndex)")
|
|
1630
|
+
|
|
1631
|
+
// Clear all playNext tracks (they're being skipped)
|
|
1632
|
+
playNextStack.removeAll()
|
|
1633
|
+
print(" Cleared all playNext tracks")
|
|
1634
|
+
|
|
1635
|
+
// Remove tracks before target from upNext
|
|
1636
|
+
if upNextIndex > 0 {
|
|
1637
|
+
upNextQueue.removeFirst(upNextIndex)
|
|
1638
|
+
print(" Removed \(upNextIndex) tracks from upNext queue")
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// Rebuild queue and advance
|
|
1642
|
+
rebuildAVQueueFromCurrentPosition()
|
|
1643
|
+
player?.advanceToNextItem()
|
|
1644
|
+
return true
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Case 5: Target is in remaining original tracks
|
|
1648
|
+
if index >= originalRemainingStart {
|
|
1649
|
+
// Get the target track directly from actualQueue
|
|
1650
|
+
let targetTrack = actualQueue[index]
|
|
1651
|
+
|
|
1652
|
+
print(" š Case 5: Target is in remaining original tracks")
|
|
1653
|
+
print(" targetTrack.id: \(targetTrack.id)")
|
|
1654
|
+
print(" currentTracks.count: \(currentTracks.count)")
|
|
1655
|
+
print(" currentTracks IDs: \(currentTracks.map { $0.id })")
|
|
1656
|
+
|
|
1657
|
+
// Find this track's index in the original playlist
|
|
1658
|
+
guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
|
|
1659
|
+
print(" ā Could not find track \(targetTrack.id) in original playlist")
|
|
1660
|
+
print(" Available tracks: \(currentTracks.map { $0.id })")
|
|
1661
|
+
return false
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
print(" originalIndex found: \(originalIndex)")
|
|
1665
|
+
|
|
1666
|
+
// Clear all temporary tracks (they're being skipped)
|
|
1667
|
+
playNextStack.removeAll()
|
|
1668
|
+
upNextQueue.removeAll()
|
|
1669
|
+
currentTemporaryType = .none
|
|
1670
|
+
print(" Cleared all temporary tracks")
|
|
1671
|
+
|
|
1672
|
+
// Play from the original playlist index
|
|
1673
|
+
let success = playFromIndexInternalWithResult(index: originalIndex)
|
|
1674
|
+
return success
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
print(" ā Unexpected case, index \(index) not handled")
|
|
1678
|
+
return false
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1534
1681
|
private func playFromIndexInternal(index: Int) {
|
|
1682
|
+
_ = playFromIndexInternalWithResult(index: index)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
private func playFromIndexInternalWithResult(index: Int) -> Bool {
|
|
1535
1686
|
guard index >= 0 && index < self.currentTracks.count else {
|
|
1536
|
-
print("ā TrackPlayerCore: playFromIndex - invalid index \(index)")
|
|
1537
|
-
return
|
|
1687
|
+
print("ā TrackPlayerCore: playFromIndex - invalid index \(index), currentTracks.count = \(self.currentTracks.count)")
|
|
1688
|
+
return false
|
|
1538
1689
|
}
|
|
1539
1690
|
|
|
1540
1691
|
print("\nšÆ TrackPlayerCore: PLAY FROM INDEX \(index)")
|
|
@@ -1569,7 +1720,7 @@ class TrackPlayerCore: NSObject {
|
|
|
1569
1720
|
|
|
1570
1721
|
guard let player = self.player, !items.isEmpty else {
|
|
1571
1722
|
print("ā No player or no items to play")
|
|
1572
|
-
return
|
|
1723
|
+
return false
|
|
1573
1724
|
}
|
|
1574
1725
|
|
|
1575
1726
|
// Remove old boundary observer
|
|
@@ -1600,6 +1751,7 @@ class TrackPlayerCore: NSObject {
|
|
|
1600
1751
|
self.preloadUpcomingTracks(from: index + 1)
|
|
1601
1752
|
|
|
1602
1753
|
player.play()
|
|
1754
|
+
return true
|
|
1603
1755
|
}
|
|
1604
1756
|
|
|
1605
1757
|
// MARK: - Temporary Track Management
|
|
@@ -27,6 +27,7 @@ export interface TrackPlayer extends HybridObject<{
|
|
|
27
27
|
pause(): void;
|
|
28
28
|
playSong(songId: string, fromPlaylist?: string): Promise<void>;
|
|
29
29
|
skipToNext(): void;
|
|
30
|
+
skipToIndex(index: number): Promise<boolean>;
|
|
30
31
|
skipToPrevious(): void;
|
|
31
32
|
seek(position: number): void;
|
|
32
33
|
addToUpNext(trackId: string): Promise<void>;
|