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.
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +89 -115
- package/ios/core/TrackPlayerCore.swift +99 -113
- package/lib/hooks/callbackManager.d.ts +18 -0
- package/lib/hooks/callbackManager.js +66 -0
- package/lib/hooks/useNowPlaying.js +30 -18
- package/lib/hooks/useOnPlaybackProgressChange.js +2 -2
- package/package.json +1 -1
- package/src/hooks/callbackManager.ts +87 -0
- package/src/hooks/useNowPlaying.ts +31 -19
- package/src/hooks/useOnPlaybackProgressChange.ts +2 -2
|
@@ -625,10 +625,32 @@ class TrackPlayerCore private constructor(
|
|
|
625
625
|
|
|
626
626
|
fun skipToPrevious() {
|
|
627
627
|
handler.post {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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
|
-
|
|
876
|
-
|
|
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 +
|
|
905
|
+
val playNextEnd = playNextStart + effectivePlayNextSize
|
|
885
906
|
val upNextStart = playNextEnd
|
|
886
|
-
val upNextEnd = upNextStart +
|
|
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
|
-
|
|
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 (
|
|
917
|
-
repeat(
|
|
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
|
-
|
|
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 (
|
|
938
|
-
repeat(
|
|
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
|
-
//
|
|
1122
|
-
|
|
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
|
-
|
|
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 (
|
|
1129
|
-
val remaining = currentTracks.subList(
|
|
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
|
-
|
|
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
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
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
|
-
//
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
643
|
+
guard self.currentPlaylistId == playlistId,
|
|
646
644
|
let playlist = self.playlistManager.getPlaylist(playlistId: playlistId)
|
|
647
|
-
|
|
648
|
-
|
|
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
|
-
|
|
1138
|
-
|
|
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
|
-
//
|
|
1148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
|
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
|
|
1381
|
-
|
|
1382
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
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 +
|
|
1605
|
+
let playNextEnd = playNextStart + effectivePlayNextSize
|
|
1597
1606
|
let upNextStart = playNextEnd
|
|
1598
|
-
let upNextEnd = upNextStart +
|
|
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
|
-
|
|
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
|
|
1629
|
-
playNextStack.removeFirst(
|
|
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
|
-
|
|
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
|
|
1650
|
-
upNextQueue.removeFirst(
|
|
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
|
-
|
|
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
|
-
//
|
|
1853
|
-
|
|
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
|
-
|
|
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
|
|
50
|
+
const fetchFullState = useCallback(async () => {
|
|
51
51
|
if (!isMounted.current)
|
|
52
52
|
return;
|
|
53
53
|
try {
|
|
54
54
|
const newState = await TrackPlayer.getState();
|
|
55
|
-
|
|
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
|
-
|
|
66
|
+
fetchFullState();
|
|
65
67
|
return () => {
|
|
66
68
|
isMounted.current = false;
|
|
67
69
|
};
|
|
68
|
-
}, [
|
|
69
|
-
// Subscribe to track changes
|
|
70
|
+
}, [fetchFullState]);
|
|
71
|
+
// Subscribe to track changes ā full refresh
|
|
70
72
|
useEffect(() => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
return callbackManager.subscribeToTrackChange(() => {
|
|
74
|
+
fetchFullState();
|
|
73
75
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
}, [updateState]);
|
|
78
|
-
// Subscribe to playback state changes
|
|
76
|
+
}, [fetchFullState]);
|
|
77
|
+
// Subscribe to playback state changes ā full refresh
|
|
79
78
|
useEffect(() => {
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
return callbackManager.subscribeToPlaybackState(() => {
|
|
80
|
+
fetchFullState();
|
|
82
81
|
});
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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 {
|
|
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
|
-
|
|
12
|
+
return callbackManager.subscribeToPlaybackProgressChange((newPosition, newTotalDuration, newIsManuallySeeked) => {
|
|
13
13
|
setPosition(newPosition);
|
|
14
14
|
setTotalDuration(newTotalDuration);
|
|
15
15
|
setIsManuallySeeked(newIsManuallySeeked);
|
package/package.json
CHANGED
|
@@ -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
|
|
54
|
+
const fetchFullState = useCallback(async () => {
|
|
55
55
|
if (!isMounted.current) return
|
|
56
56
|
|
|
57
57
|
try {
|
|
58
58
|
const newState = await TrackPlayer.getState()
|
|
59
|
-
|
|
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
|
-
|
|
70
|
+
fetchFullState()
|
|
69
71
|
|
|
70
72
|
return () => {
|
|
71
73
|
isMounted.current = false
|
|
72
74
|
}
|
|
73
|
-
}, [
|
|
75
|
+
}, [fetchFullState])
|
|
74
76
|
|
|
75
|
-
// Subscribe to track changes
|
|
77
|
+
// Subscribe to track changes ā full refresh
|
|
76
78
|
useEffect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
return callbackManager.subscribeToTrackChange(() => {
|
|
80
|
+
fetchFullState()
|
|
79
81
|
})
|
|
82
|
+
}, [fetchFullState])
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
unsubscribe()
|
|
83
|
-
}
|
|
84
|
-
}, [updateState])
|
|
85
|
-
|
|
86
|
-
// Subscribe to playback state changes
|
|
84
|
+
// Subscribe to playback state changes ā full refresh
|
|
87
85
|
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
return callbackManager.subscribeToPlaybackState(() => {
|
|
87
|
+
fetchFullState()
|
|
90
88
|
})
|
|
89
|
+
}, [fetchFullState])
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 {
|
|
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
|
-
|
|
20
|
+
return callbackManager.subscribeToPlaybackProgressChange(
|
|
21
21
|
(newPosition, newTotalDuration, newIsManuallySeeked) => {
|
|
22
22
|
setPosition(newPosition)
|
|
23
23
|
setTotalDuration(newTotalDuration)
|