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.
Files changed (32) hide show
  1. package/README.md +54 -2
  2. package/android/src/main/java/com/margelo/nitro/nitroplayer/HybridTrackPlayer.kt +7 -0
  3. package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +232 -7
  4. package/ios/HybridTrackPlayer.swift +6 -0
  5. package/ios/core/TrackPlayerCore.swift +158 -6
  6. package/lib/hooks/useNowPlaying.js +1 -0
  7. package/lib/specs/TrackPlayer.nitro.d.ts +1 -0
  8. package/lib/types/PlayerQueue.d.ts +2 -0
  9. package/nitrogen/generated/android/c++/JCurrentPlayingType.hpp +65 -0
  10. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.cpp +20 -0
  11. package/nitrogen/generated/android/c++/JHybridTrackPlayerSpec.hpp +1 -0
  12. package/nitrogen/generated/android/c++/JPlayerState.hpp +9 -3
  13. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/CurrentPlayingType.kt +23 -0
  14. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/HybridTrackPlayerSpec.kt +4 -0
  15. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroplayer/PlayerState.kt +6 -3
  16. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.cpp +8 -8
  17. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Bridge.hpp +46 -22
  18. package/nitrogen/generated/ios/NitroPlayer-Swift-Cxx-Umbrella.hpp +3 -0
  19. package/nitrogen/generated/ios/c++/HybridTrackPlayerSpecSwift.hpp +11 -0
  20. package/nitrogen/generated/ios/swift/CurrentPlayingType.swift +48 -0
  21. package/nitrogen/generated/ios/swift/Func_void_bool.swift +5 -5
  22. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec.swift +1 -0
  23. package/nitrogen/generated/ios/swift/HybridTrackPlayerSpec_cxx.swift +19 -0
  24. package/nitrogen/generated/ios/swift/PlayerState.swift +13 -2
  25. package/nitrogen/generated/shared/c++/CurrentPlayingType.hpp +84 -0
  26. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.cpp +1 -0
  27. package/nitrogen/generated/shared/c++/HybridTrackPlayerSpec.hpp +1 -0
  28. package/nitrogen/generated/shared/c++/PlayerState.hpp +9 -2
  29. package/package.json +1 -1
  30. package/src/hooks/useNowPlaying.ts +1 -0
  31. package/src/specs/TrackPlayer.nitro.ts +1 -0
  32. 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 (player.hasPreviousMediaItem()) {
600
- player.seekToPreviousMediaItem()
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
- if (::player.isInitialized && index >= 0 && index < player.mediaItemCount) {
789
- player.seekToDefaultPosition(index)
790
- player.playWhenReady = true
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
- val currentIndex = player.currentMediaItemIndex
1165
- println(" currentIndex: $currentIndex")
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
@@ -8,6 +8,7 @@ const DEFAULT_STATE = {
8
8
  currentState: 'stopped',
9
9
  currentPlaylistId: null,
10
10
  currentIndex: -1,
11
+ currentPlayingType: 'not-playing',
11
12
  };
12
13
  /**
13
14
  * Hook to get the current player state (same as TrackPlayer.getState())
@@ -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>;