react-native-nitro-player 0.4.0 → 0.4.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.
@@ -625,10 +625,32 @@ class TrackPlayerCore private constructor(
625
625
 
626
626
  fun skipToPrevious() {
627
627
  handler.post {
628
- // If playing temporary track, just seek to beginning (temps not navigable backwards)
629
- if (currentTemporaryType != TemporaryType.NONE) {
630
- println("šŸ”„ TrackPlayerCore: Playing temporary track - seeking to beginning")
628
+ val currentPosition = player.currentPosition // milliseconds
629
+
630
+ if (currentPosition > 2000) {
631
+ // More than 2 seconds in, restart current track
632
+ println("šŸ”„ TrackPlayerCore: Past threshold, restarting current track")
631
633
  player.seekTo(0)
634
+ } else if (currentTemporaryType != TemporaryType.NONE) {
635
+ // Playing temporary track within threshold — remove from its list, go back to original
636
+ println("šŸ”„ TrackPlayerCore: Removing temp track, going back to original")
637
+ val currentMediaItem = player.currentMediaItem
638
+ if (currentMediaItem != null) {
639
+ val trackId = extractTrackId(currentMediaItem.mediaId)
640
+ when (currentTemporaryType) {
641
+ TemporaryType.PLAY_NEXT -> {
642
+ val idx = playNextStack.indexOfFirst { it.id == trackId }
643
+ if (idx >= 0) playNextStack.removeAt(idx)
644
+ }
645
+ TemporaryType.UP_NEXT -> {
646
+ val idx = upNextQueue.indexOfFirst { it.id == trackId }
647
+ if (idx >= 0) upNextQueue.removeAt(idx)
648
+ }
649
+ else -> {}
650
+ }
651
+ }
652
+ currentTemporaryType = TemporaryType.NONE
653
+ playFromIndexInternal(currentTrackIndex)
632
654
  } else if (currentTrackIndex > 0) {
633
655
  // Go to previous track in original playlist
634
656
  println("šŸ”„ TrackPlayerCore: Going to previous track, currentTrackIndex: $currentTrackIndex -> ${currentTrackIndex - 1}")
@@ -859,50 +881,40 @@ class TrackPlayerCore private constructor(
859
881
  }
860
882
 
861
883
  private fun skipToIndexInternal(index: Int): Boolean {
862
- println("\nšŸŽÆ TrackPlayerCore: SKIP TO INDEX $index")
863
-
864
- if (!::player.isInitialized) {
865
- println(" āŒ Player not initialized")
866
- return false
867
- }
884
+ if (!::player.isInitialized) return false
868
885
 
869
886
  // Get actual queue to validate index and determine position
870
887
  val actualQueue = getActualQueueInternal()
871
888
  val totalQueueSize = actualQueue.size
872
889
 
873
890
  // Validate index
874
- if (index < 0 || index >= totalQueueSize) {
875
- println(" āŒ Invalid index $index, queue size is $totalQueueSize")
876
- return false
877
- }
891
+ if (index < 0 || index >= totalQueueSize) return false
892
+
893
+ // Calculate queue section boundaries using effective sizes
894
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
895
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
896
+ // so the current playing position shifts by 1
897
+ val currentPos = if (currentTemporaryType != TemporaryType.NONE)
898
+ currentTrackIndex + 1 else currentTrackIndex
899
+ val effectivePlayNextSize = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
900
+ maxOf(0, playNextStack.size - 1) else playNextStack.size
901
+ val effectiveUpNextSize = if (currentTemporaryType == TemporaryType.UP_NEXT)
902
+ maxOf(0, upNextQueue.size - 1) else upNextQueue.size
878
903
 
879
- // Calculate queue section boundaries
880
- // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
881
- // Use our internal tracking instead of player.currentMediaItemIndex (which is relative to ExoPlayer's subset queue)
882
- val currentPos = currentTrackIndex
883
904
  val playNextStart = currentPos + 1
884
- val playNextEnd = playNextStart + playNextStack.size
905
+ val playNextEnd = playNextStart + effectivePlayNextSize
885
906
  val upNextStart = playNextEnd
886
- val upNextEnd = upNextStart + upNextQueue.size
907
+ val upNextEnd = upNextStart + effectiveUpNextSize
887
908
  val originalRemainingStart = upNextEnd
888
909
 
889
- println(" Queue structure:")
890
- println(" currentPos: $currentPos")
891
- println(" playNextStart: $playNextStart, playNextEnd: $playNextEnd")
892
- println(" upNextStart: $upNextStart, upNextEnd: $upNextEnd")
893
- println(" originalRemainingStart: $originalRemainingStart")
894
- println(" totalQueueSize: $totalQueueSize")
895
-
896
910
  // Case 1: Target is before current - use playFromIndex on original
897
911
  if (index < currentPos) {
898
- println(" šŸ“ Target is before current, jumping to original playlist index $index")
899
912
  playFromIndexInternal(index)
900
913
  return true
901
914
  }
902
915
 
903
916
  // Case 2: Target is current - seek to beginning
904
917
  if (index == currentPos) {
905
- println(" šŸ“ Target is current track, seeking to beginning")
906
918
  player.seekTo(0)
907
919
  return true
908
920
  }
@@ -910,12 +922,13 @@ class TrackPlayerCore private constructor(
910
922
  // Case 3: Target is in playNext section
911
923
  if (index >= playNextStart && index < playNextEnd) {
912
924
  val playNextIndex = index - playNextStart
913
- println(" šŸ“ Target is in playNext section at position $playNextIndex")
925
+ // Offset by 1 if current is from playNext (index 0 is already playing)
926
+ val actualListIndex = if (currentTemporaryType == TemporaryType.PLAY_NEXT)
927
+ playNextIndex + 1 else playNextIndex
914
928
 
915
929
  // Remove tracks before the target from playNext (they're being skipped)
916
- if (playNextIndex > 0) {
917
- repeat(playNextIndex) { playNextStack.removeAt(0) }
918
- println(" Removed $playNextIndex tracks from playNext stack")
930
+ if (actualListIndex > 0) {
931
+ repeat(actualListIndex) { playNextStack.removeAt(0) }
919
932
  }
920
933
 
921
934
  // Rebuild queue and advance
@@ -927,16 +940,16 @@ class TrackPlayerCore private constructor(
927
940
  // Case 4: Target is in upNext section
928
941
  if (index >= upNextStart && index < upNextEnd) {
929
942
  val upNextIndex = index - upNextStart
930
- println(" šŸ“ Target is in upNext section at position $upNextIndex")
943
+ // Offset by 1 if current is from upNext (index 0 is already playing)
944
+ val actualListIndex = if (currentTemporaryType == TemporaryType.UP_NEXT)
945
+ upNextIndex + 1 else upNextIndex
931
946
 
932
947
  // Clear all playNext tracks (they're being skipped)
933
948
  playNextStack.clear()
934
- println(" Cleared all playNext tracks")
935
949
 
936
950
  // Remove tracks before target from upNext
937
- if (upNextIndex > 0) {
938
- repeat(upNextIndex) { upNextQueue.removeAt(0) }
939
- println(" Removed $upNextIndex tracks from upNext queue")
951
+ if (actualListIndex > 0) {
952
+ repeat(actualListIndex) { upNextQueue.removeAt(0) }
940
953
  }
941
954
 
942
955
  // Rebuild queue and advance
@@ -947,37 +960,21 @@ class TrackPlayerCore private constructor(
947
960
 
948
961
  // Case 5: Target is in remaining original tracks
949
962
  if (index >= originalRemainingStart) {
950
- // Get the target track directly from actualQueue
951
963
  val targetTrack = actualQueue[index]
952
964
 
953
- println(" šŸ“ Case 5: Target is in remaining original tracks")
954
- println(" targetTrack.id: ${targetTrack.id}")
955
- println(" currentTracks.count: ${currentTracks.size}")
956
- println(" currentTracks IDs: ${currentTracks.map { it.id }}")
957
-
958
965
  // Find this track's index in the original playlist
959
966
  val originalIndex = currentTracks.indexOfFirst { it.id == targetTrack.id }
960
- if (originalIndex == -1) {
961
- println(" āŒ Could not find track ${targetTrack.id} in original playlist")
962
- println(" Available tracks: ${currentTracks.map { it.id }}")
963
- return false
964
- }
965
-
966
- println(" originalIndex found: $originalIndex")
967
+ if (originalIndex == -1) return false
967
968
 
968
969
  // Clear all temporary tracks (they're being skipped)
969
970
  playNextStack.clear()
970
971
  upNextQueue.clear()
971
972
  currentTemporaryType = TemporaryType.NONE
972
- println(" Cleared all temporary tracks")
973
973
 
974
- // IMPORTANT: Rebuild the ExoPlayer queue without temporary tracks, then seek
975
- // We need to rebuild from the target index, not just seek
976
974
  rebuildQueueAndPlayFromIndex(originalIndex)
977
975
  return true
978
976
  }
979
977
 
980
- println(" āŒ Unexpected case, index $index not handled")
981
978
  return false
982
979
  }
983
980
 
@@ -986,7 +983,6 @@ class TrackPlayerCore private constructor(
986
983
  playNextStack.clear()
987
984
  upNextQueue.clear()
988
985
  currentTemporaryType = TemporaryType.NONE
989
- println(" 🧹 Cleared temporary tracks")
990
986
 
991
987
  rebuildQueueAndPlayFromIndex(index)
992
988
  }
@@ -1104,59 +1100,48 @@ class TrackPlayerCore private constructor(
1104
1100
  private fun rebuildQueueFromCurrentPosition() {
1105
1101
  if (!::player.isInitialized) return
1106
1102
 
1107
- println("\nšŸ”„ TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1108
- println(" currentIndex: ${player.currentMediaItemIndex}")
1109
- println(" currentMediaItem: ${player.currentMediaItem?.mediaId}")
1110
- println(" playNextStack (${playNextStack.size}): ${playNextStack.map { "${it.id}:${it.title}" }}")
1111
- println(" upNextQueue (${upNextQueue.size}): ${upNextQueue.map { "${it.id}:${it.title}" }}")
1112
-
1113
1103
  val currentIndex = player.currentMediaItemIndex
1114
1104
  if (currentIndex < 0) return
1115
1105
 
1116
- // Build new queue order:
1117
- // [playNext stack] + [upNext queue] + [remaining original tracks]
1118
1106
  val newQueueTracks = mutableListOf<TrackItem>()
1119
1107
 
1120
1108
  // Add playNext stack (LIFO - most recently added plays first)
1121
- // Stack is already in correct order since we insert at position 0
1122
- newQueueTracks.addAll(playNextStack)
1109
+ // Skip index 0 if current track is from playNext (it's already playing)
1110
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1111
+ newQueueTracks.addAll(playNextStack.subList(1, playNextStack.size))
1112
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1113
+ newQueueTracks.addAll(playNextStack)
1114
+ }
1123
1115
 
1124
1116
  // Add upNext queue (in order, FIFO)
1125
- newQueueTracks.addAll(upNextQueue)
1117
+ // Skip index 0 if current track is from upNext (it's already playing)
1118
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1119
+ newQueueTracks.addAll(upNextQueue.subList(1, upNextQueue.size))
1120
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1121
+ newQueueTracks.addAll(upNextQueue)
1122
+ }
1126
1123
 
1127
- // Add remaining original tracks
1128
- if (currentIndex + 1 < currentTracks.size) {
1129
- val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1130
- println(" remaining original (${remaining.size}): ${remaining.map { it.id }}")
1124
+ // Add remaining original tracks — use currentTrackIndex (original playlist position)
1125
+ if (currentTrackIndex + 1 < currentTracks.size) {
1126
+ val remaining = currentTracks.subList(currentTrackIndex + 1, currentTracks.size)
1131
1127
  newQueueTracks.addAll(remaining)
1132
1128
  }
1133
1129
 
1134
- println(" New queue total: ${newQueueTracks.size} tracks")
1135
- println(" Queue order: ${newQueueTracks.map { it.id }}")
1136
-
1137
1130
  // Create MediaItems for new tracks
1138
1131
  val playlistId = currentPlaylistId ?: ""
1139
1132
  val newMediaItems =
1140
1133
  newQueueTracks.map { track ->
1141
1134
  val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
1142
- println(" Creating MediaItem: mediaId=$mediaId, title=${track.title}")
1143
1135
  track.toMediaItem(mediaId)
1144
1136
  }
1145
1137
 
1146
1138
  // Remove all items after current
1147
- val removedCount = player.mediaItemCount - currentIndex - 1
1148
- println(" Removing $removedCount items after current")
1149
1139
  while (player.mediaItemCount > currentIndex + 1) {
1150
1140
  player.removeMediaItem(currentIndex + 1)
1151
1141
  }
1152
1142
 
1153
1143
  // Add new items
1154
1144
  player.addMediaItems(newMediaItems)
1155
-
1156
- println(" āœ… Queue rebuilt. Player now has ${player.mediaItemCount} items")
1157
- for (i in 0 until player.mediaItemCount) {
1158
- println(" [$i]: ${player.getMediaItemAt(i).mediaId}")
1159
- }
1160
1145
  }
1161
1146
 
1162
1147
  /**
@@ -1393,57 +1378,46 @@ class TrackPlayerCore private constructor(
1393
1378
  }
1394
1379
 
1395
1380
  private fun getActualQueueInternal(): List<TrackItem> {
1396
- println("\nšŸ” TrackPlayerCore: getActualQueueInternal() called")
1397
- println(" playNextStack size: ${playNextStack.size}, tracks: ${playNextStack.map { it.id }}")
1398
- println(" upNextQueue size: ${upNextQueue.size}, tracks: ${upNextQueue.map { it.id }}")
1399
- println(" currentTracks size: ${currentTracks.size}, tracks: ${currentTracks.map { it.id }}")
1400
- println(" currentTrackIndex: $currentTrackIndex")
1401
-
1402
1381
  val queue = mutableListOf<TrackItem>()
1403
1382
 
1404
- if (!::player.isInitialized) {
1405
- println(" āŒ Player not initialized, returning empty")
1406
- return emptyList()
1407
- }
1383
+ if (!::player.isInitialized) return emptyList()
1408
1384
 
1409
- // Use our internal tracking of position in original playlist
1410
1385
  val currentIndex = currentTrackIndex
1411
- println(" Using currentTrackIndex: $currentIndex")
1412
- if (currentIndex < 0) {
1413
- println(" āŒ currentIndex < 0, returning empty")
1414
- return emptyList()
1415
- }
1386
+ if (currentIndex < 0) return emptyList()
1416
1387
 
1417
1388
  // Add tracks before current (original playlist)
1418
- if (currentIndex > 0 && currentIndex <= currentTracks.size) {
1419
- val beforeCurrent = currentTracks.subList(0, currentIndex)
1420
- println(" Adding ${beforeCurrent.size} tracks before current")
1421
- queue.addAll(beforeCurrent)
1389
+ // When a temp track is playing, include the original track at currentTrackIndex
1390
+ // (it already played before the temp track started)
1391
+ val beforeEnd = if (currentTemporaryType != TemporaryType.NONE)
1392
+ minOf(currentIndex + 1, currentTracks.size) else currentIndex
1393
+ if (beforeEnd > 0) {
1394
+ queue.addAll(currentTracks.subList(0, beforeEnd))
1422
1395
  }
1423
1396
 
1424
- // Add current track
1425
- getCurrentTrack()?.let {
1426
- println(" Adding current track: ${it.id}")
1427
- queue.add(it)
1428
- }
1397
+ // Add current track (temp or original)
1398
+ getCurrentTrack()?.let { queue.add(it) }
1429
1399
 
1430
1400
  // Add playNext stack (LIFO - most recently added plays first)
1431
- // Stack is already in correct order since we insert at position 0
1432
- println(" Adding ${playNextStack.size} playNext tracks")
1433
- queue.addAll(playNextStack)
1401
+ // Skip index 0 if current track is from playNext (it's already added as current)
1402
+ if (currentTemporaryType == TemporaryType.PLAY_NEXT && playNextStack.size > 1) {
1403
+ queue.addAll(playNextStack.subList(1, playNextStack.size))
1404
+ } else if (currentTemporaryType != TemporaryType.PLAY_NEXT) {
1405
+ queue.addAll(playNextStack)
1406
+ }
1434
1407
 
1435
1408
  // Add upNext queue (in order, FIFO)
1436
- println(" Adding ${upNextQueue.size} upNext tracks")
1437
- queue.addAll(upNextQueue)
1409
+ // Skip index 0 if current track is from upNext (it's already added as current)
1410
+ if (currentTemporaryType == TemporaryType.UP_NEXT && upNextQueue.size > 1) {
1411
+ queue.addAll(upNextQueue.subList(1, upNextQueue.size))
1412
+ } else if (currentTemporaryType != TemporaryType.UP_NEXT) {
1413
+ queue.addAll(upNextQueue)
1414
+ }
1438
1415
 
1439
1416
  // Add remaining original tracks
1440
1417
  if (currentIndex + 1 < currentTracks.size) {
1441
- val remaining = currentTracks.subList(currentIndex + 1, currentTracks.size)
1442
- println(" Adding ${remaining.size} remaining tracks")
1443
- queue.addAll(remaining)
1418
+ queue.addAll(currentTracks.subList(currentIndex + 1, currentTracks.size))
1444
1419
  }
1445
1420
 
1446
- println(" āœ… Final queue size: ${queue.size}, tracks: ${queue.map { it.id }}")
1447
1421
  return queue
1448
1422
  }
1449
1423
  }
@@ -280,8 +280,7 @@ class TrackPlayerCore: NSObject {
280
280
  print("\nšŸ TrackPlayerCore: Track finished playing")
281
281
 
282
282
  guard let finishedItem = notification.object as? AVPlayerItem else {
283
- print("āš ļø Cannot identify finished item")
284
- skipToNext()
283
+ // Don't call skipToNext — AVQueuePlayer with actionAtItemEnd = .advance already auto-advances
285
284
  return
286
285
  }
287
286
 
@@ -352,7 +351,9 @@ class TrackPlayerCore: NSObject {
352
351
  print("šŸ” TrackPlayerCore: Repeat mode is OFF")
353
352
  }
354
353
 
355
- // Track ended naturally
354
+ // Track ended naturally — notify with .end reason
355
+ // AVQueuePlayer with actionAtItemEnd = .advance auto-advances to next item
356
+ // The KVO observer (currentItemDidChange) will handle the track change notification
356
357
  notifyTrackChange(
357
358
  getCurrentTrack()
358
359
  ?? TrackItem(
@@ -365,9 +366,6 @@ class TrackPlayerCore: NSObject {
365
366
  artwork: nil,
366
367
  extraPayload: nil
367
368
  ), .end)
368
-
369
- // Try to play next track
370
- skipToNext()
371
369
  }
372
370
 
373
371
  @objc private func playerItemFailedToPlayToEndTime(notification: Notification) {
@@ -642,12 +640,21 @@ class TrackPlayerCore: NSObject {
642
640
  func updatePlaylist(playlistId: String) {
643
641
  DispatchQueue.main.async { [weak self] in
644
642
  guard let self = self else { return }
645
- if self.currentPlaylistId == playlistId {
643
+ guard self.currentPlaylistId == playlistId,
646
644
  let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
647
- if let playlist = playlist {
648
- self.updatePlayerQueue(tracks: playlist.tracks)
649
- }
645
+ else { return }
646
+
647
+ // If nothing is playing yet, do a full load
648
+ guard let player = self.player, player.currentItem != nil else {
649
+ self.updatePlayerQueue(tracks: playlist.tracks)
650
+ return
650
651
  }
652
+
653
+ // Update tracks list without interrupting playback
654
+ self.currentTracks = playlist.tracks
655
+
656
+ // Rebuild only the items after the currently playing item
657
+ self.rebuildAVQueueFromCurrentPosition()
651
658
  }
652
659
  }
653
660
 
@@ -1134,21 +1141,34 @@ class TrackPlayerCore: NSObject {
1134
1141
  var queue: [TrackItem] = []
1135
1142
 
1136
1143
  // Add tracks before current (original playlist)
1137
- if currentTrackIndex > 0 {
1138
- queue.append(contentsOf: Array(currentTracks[0..<currentTrackIndex]))
1144
+ // When a temp track is playing, include the original track at currentTrackIndex
1145
+ // (it already played before the temp track started)
1146
+ let beforeEnd = currentTemporaryType != .none
1147
+ ? min(currentTrackIndex + 1, currentTracks.count) : currentTrackIndex
1148
+ if beforeEnd > 0 {
1149
+ queue.append(contentsOf: Array(currentTracks[0..<beforeEnd]))
1139
1150
  }
1140
1151
 
1141
- // Add current track
1152
+ // Add current track (temp or original)
1142
1153
  if let current = getCurrentTrack() {
1143
1154
  queue.append(current)
1144
1155
  }
1145
1156
 
1146
1157
  // Add playNext stack (LIFO - most recently added plays first)
1147
- // Stack is already in correct order since we insert at position 0
1148
- queue.append(contentsOf: playNextStack)
1158
+ // Skip index 0 if current track is from playNext (it's already added as current)
1159
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1160
+ queue.append(contentsOf: Array(playNextStack.dropFirst()))
1161
+ } else if currentTemporaryType != .playNext {
1162
+ queue.append(contentsOf: playNextStack)
1163
+ }
1149
1164
 
1150
1165
  // Add upNext queue (in order, FIFO)
1151
- queue.append(contentsOf: upNextQueue)
1166
+ // Skip index 0 if current track is from upNext (it's already added as current)
1167
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1168
+ queue.append(contentsOf: Array(upNextQueue.dropFirst()))
1169
+ } else if currentTemporaryType != .upNext {
1170
+ queue.append(contentsOf: upNextQueue)
1171
+ }
1152
1172
 
1153
1173
  // Add remaining original tracks
1154
1174
  if currentTrackIndex + 1 < currentTracks.count {
@@ -1314,39 +1334,23 @@ class TrackPlayerCore: NSObject {
1314
1334
  private func skipToNextInternal() {
1315
1335
  guard let queuePlayer = self.player else { return }
1316
1336
 
1317
- print("\nā­ļø TrackPlayerCore: SKIP TO NEXT")
1318
- print(" BEFORE:")
1319
- print(" currentTrackIndex: \(self.currentTrackIndex)")
1320
- print(" Total tracks in currentTracks: \(self.currentTracks.count)")
1321
- print(" Items in player queue: \(queuePlayer.items().count)")
1322
-
1323
- if let currentItem = queuePlayer.currentItem, let trackId = currentItem.trackId {
1324
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1325
- print(" Currently playing: \(track.title) (ID: \(track.id))")
1337
+ // Remove current temp track from its list before advancing
1338
+ if let trackId = queuePlayer.currentItem?.trackId {
1339
+ if currentTemporaryType == .playNext {
1340
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1341
+ playNextStack.remove(at: idx)
1342
+ }
1343
+ } else if currentTemporaryType == .upNext {
1344
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1345
+ upNextQueue.remove(at: idx)
1346
+ }
1326
1347
  }
1327
1348
  }
1328
1349
 
1329
- // Check if there are more items in the queue
1330
- if self.currentTrackIndex + 1 < self.currentTracks.count {
1331
- print(" šŸ”„ Calling advanceToNextItem()...")
1350
+ // Check if there are more items in the player queue
1351
+ if queuePlayer.items().count > 1 {
1332
1352
  queuePlayer.advanceToNextItem()
1333
-
1334
- // NOTE: Don't manually update currentTrackIndex here!
1335
- // The KVO observer (currentItemDidChange) will update it automatically
1336
-
1337
- print(" AFTER advanceToNextItem():")
1338
- print(" Items in player queue: \(queuePlayer.items().count)")
1339
-
1340
- if let newCurrentItem = queuePlayer.currentItem, let trackId = newCurrentItem.trackId {
1341
- if let track = self.currentTracks.first(where: { $0.id == trackId }) {
1342
- print(" New current item: \(track.title) (ID: \(track.id))")
1343
- }
1344
- }
1345
-
1346
- print(" ā³ Waiting for KVO observer to update index...")
1347
1353
  } else {
1348
- print(" āš ļø No more tracks in playlist")
1349
- // At end of playlist - stop or loop
1350
1354
  queuePlayer.pause()
1351
1355
  self.notifyPlaybackStateChange(.stopped, .end)
1352
1356
  }
@@ -1365,29 +1369,31 @@ class TrackPlayerCore: NSObject {
1365
1369
  private func skipToPreviousInternal() {
1366
1370
  guard let queuePlayer = self.player else { return }
1367
1371
 
1368
- print("\nā®ļø TrackPlayerCore: SKIP TO PREVIOUS")
1369
- print(" Current index: \(self.currentTrackIndex)")
1370
- print(" Temporary type: \(self.currentTemporaryType)")
1371
- print(" Current time: \(queuePlayer.currentTime().seconds)s")
1372
-
1373
1372
  let currentTime = queuePlayer.currentTime()
1374
1373
  if currentTime.seconds > Constants.skipToPreviousThreshold {
1375
1374
  // If more than threshold seconds in, restart current track
1376
- print(
1377
- " šŸ”„ More than \(Int(Constants.skipToPreviousThreshold))s in, restarting current track")
1378
1375
  queuePlayer.seek(to: .zero)
1379
1376
  } else if self.currentTemporaryType != .none {
1380
- // Playing temporary track - just restart it (temps are not navigable backwards)
1381
- print(" šŸ”„ Playing temporary track - restarting it (temps not navigable backwards)")
1382
- queuePlayer.seek(to: .zero)
1377
+ // Playing temporary track — remove from its list, then restart
1378
+ if let trackId = queuePlayer.currentItem?.trackId {
1379
+ if currentTemporaryType == .playNext {
1380
+ if let idx = playNextStack.firstIndex(where: { $0.id == trackId }) {
1381
+ playNextStack.remove(at: idx)
1382
+ }
1383
+ } else if currentTemporaryType == .upNext {
1384
+ if let idx = upNextQueue.firstIndex(where: { $0.id == trackId }) {
1385
+ upNextQueue.remove(at: idx)
1386
+ }
1387
+ }
1388
+ }
1389
+ // Go to current original track position (skip back from temp)
1390
+ self.playFromIndex(index: self.currentTrackIndex)
1383
1391
  } else if self.currentTrackIndex > 0 {
1384
1392
  // Go to previous track in original playlist
1385
1393
  let previousIndex = self.currentTrackIndex - 1
1386
- print(" ā®ļø Going to previous track at index \(previousIndex)")
1387
1394
  self.playFromIndex(index: previousIndex)
1388
1395
  } else {
1389
1396
  // Already at first track, restart it
1390
- print(" šŸ”„ Already at first track, restarting it")
1391
1397
  queuePlayer.seek(to: .zero)
1392
1398
  }
1393
1399
  }
@@ -1577,44 +1583,38 @@ class TrackPlayerCore: NSObject {
1577
1583
  }
1578
1584
 
1579
1585
  private func skipToIndexInternal(index: Int) -> Bool {
1580
- print("\nšŸŽÆ TrackPlayerCore: SKIP TO INDEX \(index)")
1581
-
1582
1586
  // Get actual queue to validate index and determine position
1583
1587
  let actualQueue = getActualQueueInternal()
1584
1588
  let totalQueueSize = actualQueue.count
1585
1589
 
1586
1590
  // Validate index
1587
- guard index >= 0 && index < totalQueueSize else {
1588
- print(" āŒ Invalid index \(index), queue size is \(totalQueueSize)")
1589
- return false
1590
- }
1591
+ guard index >= 0 && index < totalQueueSize else { return false }
1592
+
1593
+ // Calculate queue section boundaries using effective sizes
1594
+ // (reduced by 1 when current track is from that temp list, matching getActualQueueInternal)
1595
+ // When temp is playing, the original track at currentTrackIndex is included in "before",
1596
+ // so the current playing position shifts by 1
1597
+ let currentPos = currentTemporaryType != .none
1598
+ ? currentTrackIndex + 1 : currentTrackIndex
1599
+ let effectivePlayNextSize = currentTemporaryType == .playNext
1600
+ ? max(0, playNextStack.count - 1) : playNextStack.count
1601
+ let effectiveUpNextSize = currentTemporaryType == .upNext
1602
+ ? max(0, upNextQueue.count - 1) : upNextQueue.count
1591
1603
 
1592
- // Calculate queue section boundaries
1593
- // ActualQueue structure: [before_current] + [current] + [playNext] + [upNext] + [remaining_original]
1594
- let currentPos = currentTrackIndex
1595
1604
  let playNextStart = currentPos + 1
1596
- let playNextEnd = playNextStart + playNextStack.count
1605
+ let playNextEnd = playNextStart + effectivePlayNextSize
1597
1606
  let upNextStart = playNextEnd
1598
- let upNextEnd = upNextStart + upNextQueue.count
1607
+ let upNextEnd = upNextStart + effectiveUpNextSize
1599
1608
  let originalRemainingStart = upNextEnd
1600
1609
 
1601
- print(" Queue structure:")
1602
- print(" currentPos: \(currentPos)")
1603
- print(" playNextStart: \(playNextStart), playNextEnd: \(playNextEnd)")
1604
- print(" upNextStart: \(upNextStart), upNextEnd: \(upNextEnd)")
1605
- print(" originalRemainingStart: \(originalRemainingStart)")
1606
- print(" totalQueueSize: \(totalQueueSize)")
1607
-
1608
1610
  // Case 1: Target is before current - use playFromIndex on original
1609
1611
  if index < currentPos {
1610
- print(" šŸ“ Target is before current, jumping to original playlist index \(index)")
1611
1612
  playFromIndexInternal(index: index)
1612
1613
  return true
1613
1614
  }
1614
1615
 
1615
1616
  // Case 2: Target is current - seek to beginning
1616
1617
  if index == currentPos {
1617
- print(" šŸ“ Target is current track, seeking to beginning")
1618
1618
  player?.seek(to: .zero)
1619
1619
  return true
1620
1620
  }
@@ -1622,12 +1622,13 @@ class TrackPlayerCore: NSObject {
1622
1622
  // Case 3: Target is in playNext section
1623
1623
  if index >= playNextStart && index < playNextEnd {
1624
1624
  let playNextIndex = index - playNextStart
1625
- print(" šŸ“ Target is in playNext section at position \(playNextIndex)")
1625
+ // Offset by 1 if current is from playNext (index 0 is already playing)
1626
+ let actualListIndex = currentTemporaryType == .playNext
1627
+ ? playNextIndex + 1 : playNextIndex
1626
1628
 
1627
1629
  // Remove tracks before the target from playNext (they're being skipped)
1628
- if playNextIndex > 0 {
1629
- playNextStack.removeFirst(playNextIndex)
1630
- print(" Removed \(playNextIndex) tracks from playNext stack")
1630
+ if actualListIndex > 0 {
1631
+ playNextStack.removeFirst(actualListIndex)
1631
1632
  }
1632
1633
 
1633
1634
  // Rebuild queue and advance
@@ -1639,16 +1640,16 @@ class TrackPlayerCore: NSObject {
1639
1640
  // Case 4: Target is in upNext section
1640
1641
  if index >= upNextStart && index < upNextEnd {
1641
1642
  let upNextIndex = index - upNextStart
1642
- print(" šŸ“ Target is in upNext section at position \(upNextIndex)")
1643
+ // Offset by 1 if current is from upNext (index 0 is already playing)
1644
+ let actualListIndex = currentTemporaryType == .upNext
1645
+ ? upNextIndex + 1 : upNextIndex
1643
1646
 
1644
1647
  // Clear all playNext tracks (they're being skipped)
1645
1648
  playNextStack.removeAll()
1646
- print(" Cleared all playNext tracks")
1647
1649
 
1648
1650
  // Remove tracks before target from upNext
1649
- if upNextIndex > 0 {
1650
- upNextQueue.removeFirst(upNextIndex)
1651
- print(" Removed \(upNextIndex) tracks from upNext queue")
1651
+ if actualListIndex > 0 {
1652
+ upNextQueue.removeFirst(actualListIndex)
1652
1653
  }
1653
1654
 
1654
1655
  // Rebuild queue and advance
@@ -1659,35 +1660,21 @@ class TrackPlayerCore: NSObject {
1659
1660
 
1660
1661
  // Case 5: Target is in remaining original tracks
1661
1662
  if index >= originalRemainingStart {
1662
- // Get the target track directly from actualQueue
1663
1663
  let targetTrack = actualQueue[index]
1664
1664
 
1665
- print(" šŸ“ Case 5: Target is in remaining original tracks")
1666
- print(" targetTrack.id: \(targetTrack.id)")
1667
- print(" currentTracks.count: \(currentTracks.count)")
1668
- print(" currentTracks IDs: \(currentTracks.map { $0.id })")
1669
-
1670
1665
  // Find this track's index in the original playlist
1671
1666
  guard let originalIndex = currentTracks.firstIndex(where: { $0.id == targetTrack.id }) else {
1672
- print(" āŒ Could not find track \(targetTrack.id) in original playlist")
1673
- print(" Available tracks: \(currentTracks.map { $0.id })")
1674
1667
  return false
1675
1668
  }
1676
1669
 
1677
- print(" originalIndex found: \(originalIndex)")
1678
-
1679
1670
  // Clear all temporary tracks (they're being skipped)
1680
1671
  playNextStack.removeAll()
1681
1672
  upNextQueue.removeAll()
1682
1673
  currentTemporaryType = .none
1683
- print(" Cleared all temporary tracks")
1684
1674
 
1685
- // Play from the original playlist index
1686
- let success = playFromIndexInternalWithResult(index: originalIndex)
1687
- return success
1675
+ return playFromIndexInternalWithResult(index: originalIndex)
1688
1676
  }
1689
1677
 
1690
- print(" āŒ Unexpected case, index \(index) not handled")
1691
1678
  return false
1692
1679
  }
1693
1680
 
@@ -1836,24 +1823,26 @@ class TrackPlayerCore: NSObject {
1836
1823
  private func rebuildAVQueueFromCurrentPosition() {
1837
1824
  guard let player = self.player else { return }
1838
1825
 
1839
- print("\nšŸ”„ TrackPlayerCore: REBUILDING QUEUE FROM CURRENT POSITION")
1840
- print(" playNext stack: \(playNextStack.count) tracks")
1841
- print(" upNext queue: \(upNextQueue.count) tracks")
1842
-
1843
- // Don't interrupt currently playing item
1844
1826
  let currentItem = player.currentItem
1845
1827
  let playingItems = player.items()
1846
1828
 
1847
- // Build new queue order:
1848
- // [playNext stack] + [upNext queue] + [remaining original tracks]
1849
1829
  var newQueueTracks: [TrackItem] = []
1850
1830
 
1851
1831
  // Add playNext stack (LIFO - most recently added plays first)
1852
- // Stack is already in correct order since we insert at position 0
1853
- newQueueTracks.append(contentsOf: playNextStack)
1832
+ // Skip index 0 if current track is from playNext (it's already playing)
1833
+ if currentTemporaryType == .playNext && playNextStack.count > 1 {
1834
+ newQueueTracks.append(contentsOf: Array(playNextStack.dropFirst()))
1835
+ } else if currentTemporaryType != .playNext {
1836
+ newQueueTracks.append(contentsOf: playNextStack)
1837
+ }
1854
1838
 
1855
1839
  // Add upNext queue (in order, FIFO)
1856
- newQueueTracks.append(contentsOf: upNextQueue)
1840
+ // Skip index 0 if current track is from upNext (it's already playing)
1841
+ if currentTemporaryType == .upNext && upNextQueue.count > 1 {
1842
+ newQueueTracks.append(contentsOf: Array(upNextQueue.dropFirst()))
1843
+ } else if currentTemporaryType != .upNext {
1844
+ newQueueTracks.append(contentsOf: upNextQueue)
1845
+ }
1857
1846
 
1858
1847
  // Add remaining original tracks
1859
1848
  if currentTrackIndex + 1 < currentTracks.count {
@@ -1861,8 +1850,6 @@ class TrackPlayerCore: NSObject {
1861
1850
  newQueueTracks.append(contentsOf: remainingOriginal)
1862
1851
  }
1863
1852
 
1864
- print(" New queue: \(newQueueTracks.count) tracks total")
1865
-
1866
1853
  // Remove all items from player EXCEPT the currently playing one
1867
1854
  for item in playingItems where item != currentItem {
1868
1855
  player.remove(item)
@@ -1877,7 +1864,6 @@ class TrackPlayerCore: NSObject {
1877
1864
  }
1878
1865
  }
1879
1866
 
1880
- print(" āœ… Queue rebuilt successfully")
1881
1867
  }
1882
1868
 
1883
1869
  /**
@@ -1,6 +1,8 @@
1
1
  import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue';
2
2
  type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void;
3
3
  type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
4
+ type PlaybackProgressCallback = (position: number, totalDuration: number, isManuallySeeked?: boolean) => void;
5
+ type SeekCallback = (position: number, totalDuration: number) => void;
4
6
  /**
5
7
  * Internal subscription manager that allows multiple hooks to subscribe
6
8
  * to a single native callback. This solves the problem where registering
@@ -9,8 +11,12 @@ type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void;
9
11
  declare class CallbackSubscriptionManager {
10
12
  private playbackStateSubscribers;
11
13
  private trackChangeSubscribers;
14
+ private playbackProgressSubscribers;
15
+ private seekSubscribers;
12
16
  private isPlaybackStateRegistered;
13
17
  private isTrackChangeRegistered;
18
+ private isPlaybackProgressRegistered;
19
+ private isSeekRegistered;
14
20
  /**
15
21
  * Subscribe to playback state changes
16
22
  * @returns Unsubscribe function
@@ -23,6 +29,18 @@ declare class CallbackSubscriptionManager {
23
29
  subscribeToTrackChange(callback: TrackChangeCallback): () => void;
24
30
  private ensurePlaybackStateRegistered;
25
31
  private ensureTrackChangeRegistered;
32
+ /**
33
+ * Subscribe to playback progress changes
34
+ * @returns Unsubscribe function
35
+ */
36
+ subscribeToPlaybackProgressChange(callback: PlaybackProgressCallback): () => void;
37
+ /**
38
+ * Subscribe to seek events
39
+ * @returns Unsubscribe function
40
+ */
41
+ subscribeToSeek(callback: SeekCallback): () => void;
42
+ private ensurePlaybackProgressRegistered;
43
+ private ensureSeekRegistered;
26
44
  }
27
45
  export declare const callbackManager: CallbackSubscriptionManager;
28
46
  export {};
@@ -7,8 +7,12 @@ import { TrackPlayer } from '../index';
7
7
  class CallbackSubscriptionManager {
8
8
  playbackStateSubscribers = new Set();
9
9
  trackChangeSubscribers = new Set();
10
+ playbackProgressSubscribers = new Set();
11
+ seekSubscribers = new Set();
10
12
  isPlaybackStateRegistered = false;
11
13
  isTrackChangeRegistered = false;
14
+ isPlaybackProgressRegistered = false;
15
+ isSeekRegistered = false;
12
16
  /**
13
17
  * Subscribe to playback state changes
14
18
  * @returns Unsubscribe function
@@ -71,6 +75,68 @@ class CallbackSubscriptionManager {
71
75
  console.error('[CallbackManager] Failed to register track change callback:', error);
72
76
  }
73
77
  }
78
+ /**
79
+ * Subscribe to playback progress changes
80
+ * @returns Unsubscribe function
81
+ */
82
+ subscribeToPlaybackProgressChange(callback) {
83
+ this.playbackProgressSubscribers.add(callback);
84
+ this.ensurePlaybackProgressRegistered();
85
+ return () => {
86
+ this.playbackProgressSubscribers.delete(callback);
87
+ };
88
+ }
89
+ /**
90
+ * Subscribe to seek events
91
+ * @returns Unsubscribe function
92
+ */
93
+ subscribeToSeek(callback) {
94
+ this.seekSubscribers.add(callback);
95
+ this.ensureSeekRegistered();
96
+ return () => {
97
+ this.seekSubscribers.delete(callback);
98
+ };
99
+ }
100
+ ensurePlaybackProgressRegistered() {
101
+ if (this.isPlaybackProgressRegistered)
102
+ return;
103
+ try {
104
+ TrackPlayer.onPlaybackProgressChange((position, totalDuration, isManuallySeeked) => {
105
+ this.playbackProgressSubscribers.forEach((subscriber) => {
106
+ try {
107
+ subscriber(position, totalDuration, isManuallySeeked);
108
+ }
109
+ catch (error) {
110
+ console.error('[CallbackManager] Error in playback progress subscriber:', error);
111
+ }
112
+ });
113
+ });
114
+ this.isPlaybackProgressRegistered = true;
115
+ }
116
+ catch (error) {
117
+ console.error('[CallbackManager] Failed to register playback progress callback:', error);
118
+ }
119
+ }
120
+ ensureSeekRegistered() {
121
+ if (this.isSeekRegistered)
122
+ return;
123
+ try {
124
+ TrackPlayer.onSeek((position, totalDuration) => {
125
+ this.seekSubscribers.forEach((subscriber) => {
126
+ try {
127
+ subscriber(position, totalDuration);
128
+ }
129
+ catch (error) {
130
+ console.error('[CallbackManager] Error in seek subscriber:', error);
131
+ }
132
+ });
133
+ });
134
+ this.isSeekRegistered = true;
135
+ }
136
+ catch (error) {
137
+ console.error('[CallbackManager] Failed to register seek callback:', error);
138
+ }
139
+ }
74
140
  }
75
141
  // Export singleton instance
76
142
  export const callbackManager = new CallbackSubscriptionManager();
@@ -47,12 +47,14 @@ const DEFAULT_STATE = {
47
47
  export function useNowPlaying() {
48
48
  const [state, setState] = useState(DEFAULT_STATE);
49
49
  const isMounted = useRef(true);
50
- const updateState = useCallback(async () => {
50
+ const fetchFullState = useCallback(async () => {
51
51
  if (!isMounted.current)
52
52
  return;
53
53
  try {
54
54
  const newState = await TrackPlayer.getState();
55
- setState(newState);
55
+ if (isMounted.current) {
56
+ setState(newState);
57
+ }
56
58
  }
57
59
  catch (error) {
58
60
  console.error('[useNowPlaying] Error updating player state:', error);
@@ -61,28 +63,38 @@ export function useNowPlaying() {
61
63
  // Initialize with current state
62
64
  useEffect(() => {
63
65
  isMounted.current = true;
64
- updateState();
66
+ fetchFullState();
65
67
  return () => {
66
68
  isMounted.current = false;
67
69
  };
68
- }, [updateState]);
69
- // Subscribe to track changes
70
+ }, [fetchFullState]);
71
+ // Subscribe to track changes — full refresh
70
72
  useEffect(() => {
71
- const unsubscribe = callbackManager.subscribeToTrackChange(() => {
72
- updateState();
73
+ return callbackManager.subscribeToTrackChange(() => {
74
+ fetchFullState();
73
75
  });
74
- return () => {
75
- unsubscribe();
76
- };
77
- }, [updateState]);
78
- // Subscribe to playback state changes
76
+ }, [fetchFullState]);
77
+ // Subscribe to playback state changes — full refresh
79
78
  useEffect(() => {
80
- const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
81
- updateState();
79
+ return callbackManager.subscribeToPlaybackState(() => {
80
+ fetchFullState();
82
81
  });
83
- return () => {
84
- unsubscribe();
85
- };
86
- }, [updateState]);
82
+ }, [fetchFullState]);
83
+ // Subscribe to progress changes — lightweight position/duration update
84
+ useEffect(() => {
85
+ return callbackManager.subscribeToPlaybackProgressChange((currentPosition, totalDuration) => {
86
+ if (!isMounted.current)
87
+ return;
88
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }));
89
+ });
90
+ }, []);
91
+ // Subscribe to seek events — lightweight position/duration update
92
+ useEffect(() => {
93
+ return callbackManager.subscribeToSeek((currentPosition, totalDuration) => {
94
+ if (!isMounted.current)
95
+ return;
96
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }));
97
+ });
98
+ }, []);
87
99
  return state;
88
100
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react';
2
- import { TrackPlayer } from '../index';
2
+ import { callbackManager } from './callbackManager';
3
3
  /**
4
4
  * Hook to get the current playback progress
5
5
  * @returns Object with current position, total duration, and manual seek indicator
@@ -9,7 +9,7 @@ export function useOnPlaybackProgressChange() {
9
9
  const [totalDuration, setTotalDuration] = useState(0);
10
10
  const [isManuallySeeked, setIsManuallySeeked] = useState(undefined);
11
11
  useEffect(() => {
12
- TrackPlayer.onPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
12
+ return callbackManager.subscribeToPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
13
13
  setPosition(newPosition);
14
14
  setTotalDuration(newTotalDuration);
15
15
  setIsManuallySeeked(newIsManuallySeeked);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-alpha.0",
4
4
  "description": "react-native-nitro-player",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -3,6 +3,12 @@ import type { TrackItem, TrackPlayerState, Reason } from '../types/PlayerQueue'
3
3
 
4
4
  type PlaybackStateCallback = (state: TrackPlayerState, reason?: Reason) => void
5
5
  type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void
6
+ type PlaybackProgressCallback = (
7
+ position: number,
8
+ totalDuration: number,
9
+ isManuallySeeked?: boolean
10
+ ) => void
11
+ type SeekCallback = (position: number, totalDuration: number) => void
6
12
 
7
13
  /**
8
14
  * Internal subscription manager that allows multiple hooks to subscribe
@@ -12,8 +18,12 @@ type TrackChangeCallback = (track: TrackItem, reason?: Reason) => void
12
18
  class CallbackSubscriptionManager {
13
19
  private playbackStateSubscribers = new Set<PlaybackStateCallback>()
14
20
  private trackChangeSubscribers = new Set<TrackChangeCallback>()
21
+ private playbackProgressSubscribers = new Set<PlaybackProgressCallback>()
22
+ private seekSubscribers = new Set<SeekCallback>()
15
23
  private isPlaybackStateRegistered = false
16
24
  private isTrackChangeRegistered = false
25
+ private isPlaybackProgressRegistered = false
26
+ private isSeekRegistered = false
17
27
 
18
28
  /**
19
29
  * Subscribe to playback state changes
@@ -90,6 +100,83 @@ class CallbackSubscriptionManager {
90
100
  )
91
101
  }
92
102
  }
103
+
104
+ /**
105
+ * Subscribe to playback progress changes
106
+ * @returns Unsubscribe function
107
+ */
108
+ subscribeToPlaybackProgressChange(
109
+ callback: PlaybackProgressCallback
110
+ ): () => void {
111
+ this.playbackProgressSubscribers.add(callback)
112
+ this.ensurePlaybackProgressRegistered()
113
+
114
+ return () => {
115
+ this.playbackProgressSubscribers.delete(callback)
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Subscribe to seek events
121
+ * @returns Unsubscribe function
122
+ */
123
+ subscribeToSeek(callback: SeekCallback): () => void {
124
+ this.seekSubscribers.add(callback)
125
+ this.ensureSeekRegistered()
126
+
127
+ return () => {
128
+ this.seekSubscribers.delete(callback)
129
+ }
130
+ }
131
+
132
+ private ensurePlaybackProgressRegistered(): void {
133
+ if (this.isPlaybackProgressRegistered) return
134
+
135
+ try {
136
+ TrackPlayer.onPlaybackProgressChange(
137
+ (position, totalDuration, isManuallySeeked) => {
138
+ this.playbackProgressSubscribers.forEach((subscriber) => {
139
+ try {
140
+ subscriber(position, totalDuration, isManuallySeeked)
141
+ } catch (error) {
142
+ console.error(
143
+ '[CallbackManager] Error in playback progress subscriber:',
144
+ error
145
+ )
146
+ }
147
+ })
148
+ }
149
+ )
150
+ this.isPlaybackProgressRegistered = true
151
+ } catch (error) {
152
+ console.error(
153
+ '[CallbackManager] Failed to register playback progress callback:',
154
+ error
155
+ )
156
+ }
157
+ }
158
+
159
+ private ensureSeekRegistered(): void {
160
+ if (this.isSeekRegistered) return
161
+
162
+ try {
163
+ TrackPlayer.onSeek((position, totalDuration) => {
164
+ this.seekSubscribers.forEach((subscriber) => {
165
+ try {
166
+ subscriber(position, totalDuration)
167
+ } catch (error) {
168
+ console.error('[CallbackManager] Error in seek subscriber:', error)
169
+ }
170
+ })
171
+ })
172
+ this.isSeekRegistered = true
173
+ } catch (error) {
174
+ console.error(
175
+ '[CallbackManager] Failed to register seek callback:',
176
+ error
177
+ )
178
+ }
179
+ }
93
180
  }
94
181
 
95
182
  // Export singleton instance
@@ -51,12 +51,14 @@ export function useNowPlaying(): PlayerState {
51
51
  const [state, setState] = useState<PlayerState>(DEFAULT_STATE)
52
52
  const isMounted = useRef(true)
53
53
 
54
- const updateState = useCallback(async () => {
54
+ const fetchFullState = useCallback(async () => {
55
55
  if (!isMounted.current) return
56
56
 
57
57
  try {
58
58
  const newState = await TrackPlayer.getState()
59
- setState(newState)
59
+ if (isMounted.current) {
60
+ setState(newState)
61
+ }
60
62
  } catch (error) {
61
63
  console.error('[useNowPlaying] Error updating player state:', error)
62
64
  }
@@ -65,34 +67,44 @@ export function useNowPlaying(): PlayerState {
65
67
  // Initialize with current state
66
68
  useEffect(() => {
67
69
  isMounted.current = true
68
- updateState()
70
+ fetchFullState()
69
71
 
70
72
  return () => {
71
73
  isMounted.current = false
72
74
  }
73
- }, [updateState])
75
+ }, [fetchFullState])
74
76
 
75
- // Subscribe to track changes
77
+ // Subscribe to track changes — full refresh
76
78
  useEffect(() => {
77
- const unsubscribe = callbackManager.subscribeToTrackChange(() => {
78
- updateState()
79
+ return callbackManager.subscribeToTrackChange(() => {
80
+ fetchFullState()
79
81
  })
82
+ }, [fetchFullState])
80
83
 
81
- return () => {
82
- unsubscribe()
83
- }
84
- }, [updateState])
85
-
86
- // Subscribe to playback state changes
84
+ // Subscribe to playback state changes — full refresh
87
85
  useEffect(() => {
88
- const unsubscribe = callbackManager.subscribeToPlaybackState(() => {
89
- updateState()
86
+ return callbackManager.subscribeToPlaybackState(() => {
87
+ fetchFullState()
90
88
  })
89
+ }, [fetchFullState])
91
90
 
92
- return () => {
93
- unsubscribe()
94
- }
95
- }, [updateState])
91
+ // Subscribe to progress changes — lightweight position/duration update
92
+ useEffect(() => {
93
+ return callbackManager.subscribeToPlaybackProgressChange(
94
+ (currentPosition, totalDuration) => {
95
+ if (!isMounted.current) return
96
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }))
97
+ }
98
+ )
99
+ }, [])
100
+
101
+ // Subscribe to seek events — lightweight position/duration update
102
+ useEffect(() => {
103
+ return callbackManager.subscribeToSeek((currentPosition, totalDuration) => {
104
+ if (!isMounted.current) return
105
+ setState((prev) => ({ ...prev, currentPosition, totalDuration }))
106
+ })
107
+ }, [])
96
108
 
97
109
  return state
98
110
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState } from 'react'
2
- import { TrackPlayer } from '../index'
2
+ import { callbackManager } from './callbackManager'
3
3
 
4
4
  /**
5
5
  * Hook to get the current playback progress
@@ -17,7 +17,7 @@ export function useOnPlaybackProgressChange(): {
17
17
  )
18
18
 
19
19
  useEffect(() => {
20
- TrackPlayer.onPlaybackProgressChange(
20
+ return callbackManager.subscribeToPlaybackProgressChange(
21
21
  (newPosition, newTotalDuration, newIsManuallySeeked) => {
22
22
  setPosition(newPosition)
23
23
  setTotalDuration(newTotalDuration)