react-native-nitro-player 0.5.9-alpha.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,28 @@ class TrackPlayerCore private constructor(
41
41
  private val handler = android.os.Handler(android.os.Looper.getMainLooper())
42
42
  private lateinit var player: ExoPlayer
43
43
  private val playlistManager = PlaylistManager.getInstance(context)
44
+
45
+ // Named Runnable so handler.removeCallbacks() can coalesce rapid playlist
46
+ // mutations (e.g. N individual removes followed by a batch add during shuffle)
47
+ // into a single player update, preventing audio gaps on Android.
48
+ private val updateCurrentPlaylistRunnable = Runnable {
49
+ val playlistId = currentPlaylistId ?: return@Runnable
50
+ val playlist = playlistManager.getPlaylist(playlistId) ?: return@Runnable
51
+
52
+ // Always update the canonical track list first.
53
+ currentTracks = playlist.tracks
54
+
55
+ if (::player.isInitialized && player.currentMediaItem != null && player.currentMediaItemIndex >= 0) {
56
+ // Something is actively playing — rebuild only the items AFTER the
57
+ // current position using surgical removeMediaItems/addMediaItems.
58
+ // This avoids setMediaItems() which replaces the entire ExoPlayer
59
+ // queue (including the current item) and causes an audible gap.
60
+ rebuildQueueFromCurrentPosition()
61
+ } else {
62
+ // Nothing playing yet — safe to do a full replace.
63
+ updatePlayerQueue(playlist.tracks)
64
+ }
65
+ }
44
66
  private val downloadManager = DownloadManagerCore.getInstance(context)
45
67
  private val mediaLibraryManager = MediaLibraryManager.getInstance(context)
46
68
  private var mediaSessionManager: MediaSessionManager? = null
@@ -434,14 +456,13 @@ class TrackPlayerCore private constructor(
434
456
  * Update the player queue when playlist changes
435
457
  */
436
458
  fun updatePlaylist(playlistId: String) {
437
- handler.post {
438
- if (currentPlaylistId == playlistId) {
439
- val playlist = playlistManager.getPlaylist(playlistId)
440
- if (playlist != null) {
441
- updatePlayerQueue(playlist.tracks)
442
- }
443
- }
444
- }
459
+ // Debounce: rapid back-to-back calls (e.g. removing N tracks then adding
460
+ // the shuffled replacement) are coalesced into a single setMediaItems call.
461
+ // removeCallbacks cancels any pending-but-not-yet-executed callback so only
462
+ // the final playlist state triggers a player rebuild.
463
+ if (currentPlaylistId != playlistId) return
464
+ handler.removeCallbacks(updateCurrentPlaylistRunnable)
465
+ handler.post(updateCurrentPlaylistRunnable)
445
466
  }
446
467
 
447
468
  /**
@@ -1180,6 +1201,20 @@ class TrackPlayerCore private constructor(
1180
1201
  val currentIndex = player.currentMediaItemIndex
1181
1202
  if (currentIndex < 0) return
1182
1203
 
1204
+ // Handle removed-current-track case: if the currently playing media item is no longer
1205
+ // in currentTracks (e.g. the user removed it while it was playing), delegate to
1206
+ // playFromIndexInternal so the player immediately starts the next track.
1207
+ val currentTrackId = player.currentMediaItem?.mediaId?.let { extractTrackId(it) }
1208
+ if (currentTrackId != null && currentTracks.none { it.id == currentTrackId }) {
1209
+ val targetIndex = when {
1210
+ currentTracks.isEmpty() -> return
1211
+ currentTrackIndex < currentTracks.size -> currentTrackIndex
1212
+ else -> currentTracks.size - 1
1213
+ }
1214
+ playFromIndexInternal(targetIndex)
1215
+ return
1216
+ }
1217
+
1183
1218
  val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
1184
1219
 
1185
1220
  // Add playNext stack (LIFO - most recently added plays first)
@@ -1588,6 +1623,35 @@ class TrackPlayerCore private constructor(
1588
1623
  if (currentPlaylistId != null && affectedPlaylists.containsKey(currentPlaylistId)) {
1589
1624
  NitroPlayerLogger.log("TrackPlayerCore") { "🔄 Rebuilding queue - ${affectedPlaylists[currentPlaylistId]} tracks updated in current playlist" }
1590
1625
 
1626
+ // PlaylistManager.updateTracks() creates a new Playlist via .copy(tracks = newTracks),
1627
+ // so our currentTracks reference still points at the old list with empty URLs.
1628
+ // Refresh it now so rebuildQueueFromCurrentPosition builds MediaItems with the
1629
+ // resolved URLs, allowing ExoPlayer to pre-buffer the next track for gapless playback.
1630
+ val refreshedPlaylist = playlistManager.getPlaylist(currentPlaylistId!!)
1631
+ if (refreshedPlaylist != null) {
1632
+ currentTracks = refreshedPlaylist.tracks
1633
+
1634
+ // Also reconcile any queued items that still reference old TrackItem instances
1635
+ // from this playlist, so that gapless pre-buffering uses tracks with resolved URLs.
1636
+ val updatedTrackById = currentTracks.associateBy { it.id }
1637
+
1638
+ // Update playNextStack entries to point at the refreshed TrackItem objects.
1639
+ playNextStack.forEachIndexed { index, track ->
1640
+ val updated = updatedTrackById[track.id]
1641
+ if (updated != null && updated !== track) {
1642
+ playNextStack[index] = updated
1643
+ }
1644
+ }
1645
+
1646
+ // Update upNextQueue entries to point at the refreshed TrackItem objects.
1647
+ upNextQueue.forEachIndexed { index, track ->
1648
+ val updated = updatedTrackById[track.id]
1649
+ if (updated != null && updated !== track) {
1650
+ upNextQueue[index] = updated
1651
+ }
1652
+ }
1653
+ }
1654
+
1591
1655
  // This method preserves current item and gapless buffering
1592
1656
  rebuildQueueFromCurrentPosition()
1593
1657
 
@@ -48,6 +48,10 @@ class TrackPlayerCore: NSObject {
48
48
  private var currentPlaylistId: String?
49
49
  private var currentTrackIndex: Int = -1
50
50
  private var currentTracks: [TrackItem] = []
51
+ // Debounce work item — rapid playlist mutations (e.g. N individual removes
52
+ // during shuffle) are coalesced into a single rebuildAVQueueFromCurrentPosition
53
+ // call, preventing audio gaps/interruptions on iOS.
54
+ private var pendingPlaylistUpdateWorkItem: DispatchWorkItem?
51
55
  private var isManuallySeeked = false
52
56
  private var currentRepeatMode: RepeatMode = .off
53
57
  private var lookaheadCount: Int = 5 // Number of tracks to preload ahead
@@ -649,7 +653,13 @@ class TrackPlayerCore: NSObject {
649
653
  }
650
654
 
651
655
  func updatePlaylist(playlistId: String) {
652
- DispatchQueue.main.async { [weak self] in
656
+ guard currentPlaylistId == playlistId else { return }
657
+
658
+ // Cancel any pending rebuild so back-to-back calls (e.g. N individual removes
659
+ // during shuffle) collapse into a single rebuild at the end.
660
+ pendingPlaylistUpdateWorkItem?.cancel()
661
+
662
+ let workItem = DispatchWorkItem { [weak self] in
653
663
  guard let self = self else { return }
654
664
  guard self.currentPlaylistId == playlistId,
655
665
  let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
@@ -667,6 +677,9 @@ class TrackPlayerCore: NSObject {
667
677
  // Rebuild only the items after the currently playing item
668
678
  self.rebuildAVQueueFromCurrentPosition()
669
679
  }
680
+
681
+ pendingPlaylistUpdateWorkItem = workItem
682
+ DispatchQueue.main.async(execute: workItem)
670
683
  }
671
684
 
672
685
  // MARK: - Public Methods
@@ -1918,6 +1931,21 @@ class TrackPlayerCore: NSObject {
1918
1931
  let currentItem = player.currentItem
1919
1932
  let playingItems = player.items()
1920
1933
 
1934
+ // ---- Handle removed-current-track case ----
1935
+ // If the currently playing AVPlayerItem is no longer in currentTracks (e.g. the user
1936
+ // removed it while it was playing), delegate to rebuildQueueFromPlaylistIndex so the
1937
+ // player immediately starts what is now at currentTrackIndex in the updated list.
1938
+ if let playingTrackId = currentItem?.trackId,
1939
+ !currentTracks.contains(where: { $0.id == playingTrackId }) {
1940
+ let targetIndex = currentTrackIndex < currentTracks.count
1941
+ ? currentTrackIndex
1942
+ : currentTracks.count - 1
1943
+ if targetIndex >= 0 {
1944
+ _ = rebuildQueueFromPlaylistIndex(index: targetIndex)
1945
+ }
1946
+ return
1947
+ }
1948
+
1921
1949
  // ---- Build the desired upcoming track list ----
1922
1950
 
1923
1951
  var newQueueTracks: [TrackItem] = []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.5.9-alpha.1",
3
+ "version": "0.6.1",
4
4
  "description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",