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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|