react-native-nitro-player 1.5.0-alpha.0 → 1.5.0-alpha.3
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/README.md +3 -1
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/ExoPlayerCore.kt +2 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCast.kt +75 -37
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +3 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +106 -23
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerUrlLoader.kt +15 -3
- package/ios/core/CastSessionManager.swift +197 -56
- package/ios/core/TrackPlayerCast.swift +231 -10
- package/ios/core/TrackPlayerPlayback.swift +3 -1
- package/ios/core/TrackPlayerQueue.swift +27 -11
- package/ios/core/TrackPlayerQueueBuild.swift +6 -0
- package/ios/core/TrackPlayerUrlLoader.swift +26 -2
- package/lib/components/CastButton.js +68 -43
- package/package.json +1 -1
- package/src/components/CastButton.tsx +84 -47
package/README.md
CHANGED
|
@@ -677,7 +677,9 @@ Cast requires native configuration in your **app** (not just the library).
|
|
|
677
677
|
<key>NSBonjourServices</key>
|
|
678
678
|
<array>
|
|
679
679
|
<string>_googlecast._tcp</string>
|
|
680
|
-
<!--
|
|
680
|
+
<!-- Receiver-specific service. Use _CC1AD845._googlecast._tcp for the Default
|
|
681
|
+
Media Receiver, or _<YOUR_RECEIVER_APP_ID>._googlecast._tcp for a custom one. -->
|
|
682
|
+
<string>_CC1AD845._googlecast._tcp</string>
|
|
681
683
|
</array>
|
|
682
684
|
<!-- Only if you use Cast guest mode (full SDK): -->
|
|
683
685
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
|
@@ -80,6 +80,8 @@ class ExoPlayerCore(
|
|
|
80
80
|
}
|
|
81
81
|
val currentMediaItem: MediaItem? get() = player.currentMediaItem
|
|
82
82
|
val currentMediaItemIndex: Int get() = player.currentMediaItemIndex
|
|
83
|
+
|
|
84
|
+
fun getMediaItemAt(index: Int): MediaItem = player.getMediaItemAt(index)
|
|
83
85
|
val currentPosition: Long get() = player.currentPosition
|
|
84
86
|
val duration: Long get() = player.duration
|
|
85
87
|
val mediaItemCount: Int get() = player.mediaItemCount
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
package com.margelo.nitro.nitroplayer.core
|
|
4
4
|
|
|
5
5
|
import android.app.Activity
|
|
6
|
-
import androidx.media3.common.MediaItem
|
|
7
6
|
import androidx.media3.common.Player
|
|
8
7
|
import com.margelo.nitro.nitroplayer.CastState
|
|
8
|
+
import com.margelo.nitro.nitroplayer.TrackItem
|
|
9
9
|
import com.margelo.nitro.nitroplayer.media.NitroCastConfig
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -21,35 +21,91 @@ import com.margelo.nitro.nitroplayer.media.NitroCastConfig
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
// ── Backend switching ──────────────────────────────────────────────────────
|
|
24
|
+
// The queue is rebuilt for the target backend (not item-copied): cast needs remote URLs, local prefers download paths.
|
|
24
25
|
|
|
25
26
|
internal fun TrackPlayerCore.switchToCastPlayer() {
|
|
26
27
|
val controller = castSessionController ?: return
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (!isExoInitialized) return
|
|
29
|
+
val local = exo.player
|
|
30
|
+
if (local === controller.castPlayer) return
|
|
31
|
+
|
|
32
|
+
// Capture context BEFORE swapping — the queue helpers read `exo`, and the cast timeline is still empty.
|
|
33
|
+
val currentId = local.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
34
|
+
val currentTrack = getCurrentTrack() ?: currentTracks.getOrNull(currentTrackIndex)
|
|
35
|
+
val upcoming = buildUpcomingQueueTracks(currentId)
|
|
36
|
+
val positionMs = local.currentPosition.coerceAtLeast(0L)
|
|
37
|
+
val wasPlaying = local.playWhenReady
|
|
38
|
+
val repeat = local.repeatMode
|
|
39
|
+
|
|
40
|
+
playerListener?.let { local.removeListener(it) }
|
|
41
|
+
local.playWhenReady = false // silence local output — audio now comes from the device
|
|
42
|
+
|
|
43
|
+
isCastingField = true // before building items so makeMediaItem picks remote URLs
|
|
44
|
+
exo = ExoPlayerCore(controller.castPlayer)
|
|
45
|
+
playerListener?.let { controller.castPlayer.addListener(it) }
|
|
46
|
+
setMediaSessionPlayer(controller.castPlayer)
|
|
47
|
+
|
|
48
|
+
val castList = ArrayList<TrackItem>()
|
|
49
|
+
currentTrack?.let { castList.add(it) }
|
|
50
|
+
castList.addAll(upcoming)
|
|
51
|
+
val playable = castableUpcoming(castList)
|
|
52
|
+
|
|
53
|
+
if (playable.isNotEmpty()) {
|
|
54
|
+
lastCastWaitTrackId = null
|
|
55
|
+
val items = playable.map { makeMediaItem(it, mediaIdFor(it)) }
|
|
56
|
+
// Resume mid-track only when the receiver starts on the locally playing track.
|
|
57
|
+
val resumeFromCurrent = currentTrack != null && playable.first().id == currentTrack.id
|
|
58
|
+
controller.castPlayer.setMediaItems(items, 0, if (resumeFromCurrent) positionMs else 0L)
|
|
59
|
+
controller.castPlayer.repeatMode = repeat
|
|
60
|
+
controller.castPlayer.playWhenReady = wasPlaying
|
|
61
|
+
controller.castPlayer.prepare()
|
|
62
|
+
} else {
|
|
63
|
+
// Nothing castable yet (lazy queue) — request URLs; updateTracks will load.
|
|
64
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
65
|
+
}
|
|
66
|
+
|
|
29
67
|
notifyCastStateChange(CastState.CONNECTED, controller.currentDeviceName())
|
|
30
68
|
}
|
|
31
69
|
|
|
32
70
|
internal fun TrackPlayerCore.switchToLocalPlayer() {
|
|
33
71
|
val controller = castSessionController ?: return
|
|
34
|
-
switchToPlayer(controller.localPlayer)
|
|
35
|
-
isCastingField = false
|
|
36
|
-
notifyCastStateChange(controller.getCastState(), controller.currentDeviceName())
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
private fun TrackPlayerCore.switchToPlayer(target: Player) {
|
|
40
72
|
if (!isExoInitialized) return
|
|
41
|
-
val
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
val cast = exo.player
|
|
74
|
+
if (cast === controller.localPlayer) return
|
|
75
|
+
|
|
76
|
+
// Capture context before swapping (the cast timeline empties once the session ends).
|
|
77
|
+
val currentId = cast.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
78
|
+
val currentTrack = getCurrentTrack() ?: currentTracks.getOrNull(currentTrackIndex)
|
|
79
|
+
val upcoming = buildUpcomingQueueTracks(currentId)
|
|
80
|
+
val positionMs = cast.currentPosition.coerceAtLeast(0L)
|
|
81
|
+
val wasPlaying = cast.playWhenReady
|
|
82
|
+
val repeat = cast.repeatMode
|
|
83
|
+
|
|
84
|
+
playerListener?.let { cast.removeListener(it) }
|
|
85
|
+
cast.playWhenReady = false
|
|
86
|
+
|
|
87
|
+
isCastingField = false // before building items so makeMediaItem restores download paths
|
|
88
|
+
lastCastWaitTrackId = null
|
|
89
|
+
exo = ExoPlayerCore(controller.localPlayer)
|
|
90
|
+
playerListener?.let { controller.localPlayer.addListener(it) }
|
|
91
|
+
setMediaSessionPlayer(controller.localPlayer)
|
|
92
|
+
|
|
93
|
+
val localList = ArrayList<TrackItem>()
|
|
94
|
+
currentTrack?.let { localList.add(it) }
|
|
95
|
+
localList.addAll(upcoming)
|
|
96
|
+
|
|
97
|
+
if (localList.isNotEmpty()) {
|
|
98
|
+
val items = localList.map { makeMediaItem(it, mediaIdFor(it)) }
|
|
99
|
+
controller.localPlayer.setMediaItems(items, 0, positionMs)
|
|
100
|
+
controller.localPlayer.repeatMode = repeat
|
|
101
|
+
controller.localPlayer.playWhenReady = wasPlaying
|
|
102
|
+
controller.localPlayer.prepare()
|
|
103
|
+
}
|
|
49
104
|
|
|
50
|
-
|
|
51
|
-
|
|
105
|
+
notifyCastStateChange(controller.getCastState(), controller.currentDeviceName())
|
|
106
|
+
}
|
|
52
107
|
|
|
108
|
+
private fun TrackPlayerCore.setMediaSessionPlayer(target: Player) {
|
|
53
109
|
try {
|
|
54
110
|
castSessionController?.let { it.mediaSession.player = target }
|
|
55
111
|
} catch (e: Exception) {
|
|
@@ -57,24 +113,6 @@ private fun TrackPlayerCore.switchToPlayer(target: Player) {
|
|
|
57
113
|
}
|
|
58
114
|
}
|
|
59
115
|
|
|
60
|
-
/** Copy the queue, index, position, repeat mode and play-when-ready from one player to another. */
|
|
61
|
-
private fun transferPlaybackState(
|
|
62
|
-
from: Player,
|
|
63
|
-
to: Player,
|
|
64
|
-
) {
|
|
65
|
-
val count = from.mediaItemCount
|
|
66
|
-
if (count == 0) return
|
|
67
|
-
val items = ArrayList<MediaItem>(count)
|
|
68
|
-
for (i in 0 until count) items.add(from.getMediaItemAt(i))
|
|
69
|
-
val index = from.currentMediaItemIndex.coerceAtLeast(0)
|
|
70
|
-
val position = if (from.currentPosition >= 0) from.currentPosition else 0L
|
|
71
|
-
|
|
72
|
-
to.setMediaItems(items, index, position)
|
|
73
|
-
to.repeatMode = from.repeatMode
|
|
74
|
-
to.playWhenReady = from.playWhenReady
|
|
75
|
-
to.prepare()
|
|
76
|
-
}
|
|
77
|
-
|
|
78
116
|
// ── Notifications ──────────────────────────────────────────────────────────
|
|
79
117
|
|
|
80
118
|
internal fun TrackPlayerCore.notifyCastStateChange(
|
|
@@ -100,6 +100,9 @@ class TrackPlayerCore private constructor(
|
|
|
100
100
|
/** True while the active backend is the CastPlayer (audio routed to a remote device). */
|
|
101
101
|
@Volatile internal var isCastingField: Boolean = false
|
|
102
102
|
|
|
103
|
+
/** Last track announced from a cast "waiting for URL" state — dedups onChangeTrack across updateTracks re-entries. */
|
|
104
|
+
internal var lastCastWaitTrackId: String? = null
|
|
105
|
+
|
|
103
106
|
// ── Progress & playlist-update runnables ───────────────────────────────
|
|
104
107
|
internal val progressUpdateRunnable =
|
|
105
108
|
object : Runnable {
|
|
@@ -6,6 +6,7 @@ import android.net.Uri
|
|
|
6
6
|
import androidx.media3.common.MediaItem
|
|
7
7
|
import androidx.media3.common.MediaMetadata
|
|
8
8
|
import androidx.media3.common.Player
|
|
9
|
+
import com.margelo.nitro.nitroplayer.Reason
|
|
9
10
|
import com.margelo.nitro.nitroplayer.TrackItem
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -15,17 +16,63 @@ import com.margelo.nitro.nitroplayer.TrackItem
|
|
|
15
16
|
* when jumping to a specific index.
|
|
16
17
|
*/
|
|
17
18
|
|
|
19
|
+
// ── Cast-safety helpers ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/** A track the Cast receiver can actually load: non-empty remote (non-local) URL. */
|
|
22
|
+
internal fun TrackPlayerCore.isTrackCastable(track: TrackItem): Boolean =
|
|
23
|
+
track.url.isNotEmpty() &&
|
|
24
|
+
!track.url.startsWith("/") &&
|
|
25
|
+
!track.url.startsWith("file:")
|
|
26
|
+
|
|
27
|
+
/** Receiver-safe run: keeps castable tracks, skips local-only ones, stops at the first lazy (empty-URL) track whose resolution is pending — receivers auto-skip failed loads instead of waiting like ExoPlayer. */
|
|
28
|
+
internal fun TrackPlayerCore.castableUpcoming(tracks: List<TrackItem>): List<TrackItem> {
|
|
29
|
+
val result = ArrayList<TrackItem>(tracks.size)
|
|
30
|
+
for (track in tracks) {
|
|
31
|
+
when {
|
|
32
|
+
isTrackCastable(track) -> result.add(track)
|
|
33
|
+
track.url.isEmpty() -> break
|
|
34
|
+
// local-only → skip and keep scanning
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
internal fun TrackPlayerCore.mediaIdFor(track: TrackItem): String {
|
|
41
|
+
val playlistId = currentPlaylistId ?: ""
|
|
42
|
+
return if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
// ── Full rebuild (jump to index) ───────────────────────────────────────────
|
|
19
46
|
|
|
20
47
|
internal fun TrackPlayerCore.rebuildQueueAndPlayFromIndex(index: Int) {
|
|
21
48
|
if (!isExoInitialized) return
|
|
22
49
|
if (index < 0 || index >= currentTracks.size) return
|
|
23
50
|
|
|
24
|
-
|
|
51
|
+
if (isCastingField) {
|
|
52
|
+
currentTrackIndex = index
|
|
53
|
+
val target = currentTracks[index]
|
|
54
|
+
val castTracks = castableUpcoming(currentTracks.subList(index, currentTracks.size))
|
|
55
|
+
if (castTracks.isEmpty()) {
|
|
56
|
+
// Lazy target — silence the receiver and wait for updateTracks to reload (dedup so re-entries don't spam JS).
|
|
57
|
+
if (exo.mediaItemCount > 0) exo.clearMediaItems()
|
|
58
|
+
if (lastCastWaitTrackId != target.id) {
|
|
59
|
+
lastCastWaitTrackId = target.id
|
|
60
|
+
notifyTrackChange(target, Reason.SKIP)
|
|
61
|
+
}
|
|
62
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
lastCastWaitTrackId = null
|
|
66
|
+
// Single atomic queueLoad — no separate clear (each queue op is a receiver RPC).
|
|
67
|
+
exo.setMediaItems(castTracks.map { makeMediaItem(it, mediaIdFor(it)) }, true)
|
|
68
|
+
exo.prepare()
|
|
69
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
val mediaItems =
|
|
26
74
|
currentTracks.subList(index, currentTracks.size).map { track ->
|
|
27
|
-
|
|
28
|
-
makeMediaItem(track, mediaId)
|
|
75
|
+
makeMediaItem(track, mediaIdFor(track))
|
|
29
76
|
}
|
|
30
77
|
|
|
31
78
|
currentTrackIndex = index
|
|
@@ -63,8 +110,38 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
63
110
|
}
|
|
64
111
|
}
|
|
65
112
|
|
|
66
|
-
val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
|
|
67
113
|
val currentId = exo.currentMediaItem?.mediaId?.let { extractTrackId(it) }
|
|
114
|
+
var newQueueTracks: List<TrackItem> = buildUpcomingQueueTracks(currentId)
|
|
115
|
+
|
|
116
|
+
if (isCastingField) {
|
|
117
|
+
newQueueTracks = castableUpcoming(newQueueTracks)
|
|
118
|
+
|
|
119
|
+
// Every queue edit is a receiver RPC — skip the rebuild when the remote queue already matches.
|
|
120
|
+
val itemCount = exo.mediaItemCount
|
|
121
|
+
if (itemCount - currentIndex - 1 == newQueueTracks.size) {
|
|
122
|
+
var inSync = true
|
|
123
|
+
for (i in newQueueTracks.indices) {
|
|
124
|
+
val existingId = extractTrackId(exo.getMediaItemAt(currentIndex + 1 + i).mediaId)
|
|
125
|
+
if (existingId != newQueueTracks[i].id) {
|
|
126
|
+
inSync = false
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (inSync) return
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
val newMediaItems = newQueueTracks.map { makeMediaItem(it, mediaIdFor(it)) }
|
|
135
|
+
|
|
136
|
+
if (exo.mediaItemCount > currentIndex + 1) {
|
|
137
|
+
exo.removeMediaItems(currentIndex + 1, exo.mediaItemCount)
|
|
138
|
+
}
|
|
139
|
+
exo.addMediaItems(newMediaItems)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Desired upcoming list after the current track: playNext + upNext + remaining originals, with the playing temp track ([currentId]) skipped. */
|
|
143
|
+
internal fun TrackPlayerCore.buildUpcomingQueueTracks(currentId: String?): List<TrackItem> {
|
|
144
|
+
val newQueueTracks = ArrayList<TrackItem>(playNextStack.size + upNextQueue.size + currentTracks.size)
|
|
68
145
|
|
|
69
146
|
// playNext stack — skip the currently playing track by ID (not position)
|
|
70
147
|
if (currentTemporaryType == TrackPlayerCore.TemporaryType.PLAY_NEXT && currentId != null) {
|
|
@@ -94,34 +171,39 @@ internal fun TrackPlayerCore.rebuildQueueFromCurrentPosition() {
|
|
|
94
171
|
newQueueTracks.addAll(upNextQueue)
|
|
95
172
|
}
|
|
96
173
|
|
|
97
|
-
// Remaining original tracks (after currentTrackIndex, not after
|
|
174
|
+
// Remaining original tracks (after currentTrackIndex, not after the player's currentIndex)
|
|
98
175
|
if (currentTrackIndex + 1 < currentTracks.size) {
|
|
99
176
|
newQueueTracks.addAll(currentTracks.subList(currentTrackIndex + 1, currentTracks.size))
|
|
100
177
|
}
|
|
101
|
-
|
|
102
|
-
val playlistId = currentPlaylistId ?: ""
|
|
103
|
-
val newMediaItems =
|
|
104
|
-
newQueueTracks.map { track ->
|
|
105
|
-
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${track.id}" else track.id
|
|
106
|
-
makeMediaItem(track, mediaId)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (exo.mediaItemCount > currentIndex + 1) {
|
|
110
|
-
exo.removeMediaItems(currentIndex + 1, exo.mediaItemCount)
|
|
111
|
-
}
|
|
112
|
-
exo.addMediaItems(newMediaItems)
|
|
178
|
+
return newQueueTracks
|
|
113
179
|
}
|
|
114
180
|
|
|
115
181
|
// ── Full queue set (initial load or no active item) ───────────────────────
|
|
116
182
|
|
|
117
183
|
internal fun TrackPlayerCore.updatePlayerQueue(tracks: List<TrackItem>) {
|
|
118
184
|
currentTracks = tracks
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
185
|
+
|
|
186
|
+
if (isCastingField) {
|
|
187
|
+
currentTrackIndex = 0
|
|
188
|
+
val castTracks = castableUpcoming(tracks)
|
|
189
|
+
if (castTracks.isEmpty()) {
|
|
190
|
+
// Lazy first track — silence the receiver and wait for updateTracks to supply URLs.
|
|
191
|
+
if (exo.mediaItemCount > 0) exo.clearMediaItems()
|
|
192
|
+
val first = tracks.firstOrNull()
|
|
193
|
+
if (first != null && lastCastWaitTrackId != first.id) {
|
|
194
|
+
lastCastWaitTrackId = first.id
|
|
195
|
+
notifyTrackChange(first, null)
|
|
196
|
+
}
|
|
197
|
+
checkUpcomingTracksForUrls(lookaheadCount)
|
|
198
|
+
return
|
|
124
199
|
}
|
|
200
|
+
lastCastWaitTrackId = null
|
|
201
|
+
exo.setMediaItems(castTracks.map { makeMediaItem(it, mediaIdFor(it)) }, true)
|
|
202
|
+
exo.prepare()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
val mediaItems = tracks.map { makeMediaItem(it, mediaIdFor(it)) }
|
|
125
207
|
exo.setMediaItems(mediaItems, true)
|
|
126
208
|
if (exo.playbackState == Player.STATE_IDLE && mediaItems.isNotEmpty()) {
|
|
127
209
|
exo.prepare()
|
|
@@ -148,7 +230,8 @@ internal fun TrackPlayerCore.makeMediaItem(
|
|
|
148
230
|
}
|
|
149
231
|
}
|
|
150
232
|
|
|
151
|
-
|
|
233
|
+
// The receiver fetches media itself, so local download paths are unplayable there — cast uses the remote URL.
|
|
234
|
+
val effectiveUrl = if (isCastingField) track.url else downloadManager.getEffectiveUrl(track)
|
|
152
235
|
|
|
153
236
|
// Register custom HTTP headers (e.g. Authorization) from extraPayload.headers so they are
|
|
154
237
|
// injected into the request for this URL. Applies to remote streams only — see ExoPlayerBuilder.
|
|
@@ -36,12 +36,13 @@ suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
|
36
36
|
|
|
37
37
|
val affectedPlaylists: Map<String, Int> = playlistManager.updateTracks(safeTracks)
|
|
38
38
|
|
|
39
|
-
// Replace current
|
|
40
|
-
|
|
39
|
+
// Replace the current MediaItem when its URL just resolved (local only — the cast branch below reloads instead).
|
|
40
|
+
val currentTrackResolvedNow = currentTrackUpdate != null && currentTrackIsEmpty && currentTrackUpdate.url.isNotEmpty()
|
|
41
|
+
if (!isCastingField && currentTrackResolvedNow) {
|
|
41
42
|
val exoIndex = exo.currentMediaItemIndex
|
|
42
43
|
if (exoIndex >= 0) {
|
|
43
44
|
val playlistId = currentPlaylistId ?: ""
|
|
44
|
-
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate
|
|
45
|
+
val mediaId = if (playlistId.isNotEmpty()) "$playlistId:${currentTrackUpdate!!.id}" else currentTrackUpdate!!.id
|
|
45
46
|
exo.replaceMediaItem(exoIndex, makeMediaItem(currentTrackUpdate, mediaId))
|
|
46
47
|
if (exo.playbackState == Player.STATE_IDLE) exo.prepare()
|
|
47
48
|
}
|
|
@@ -55,6 +56,17 @@ suspend fun TrackPlayerCore.updateTracks(tracks: List<TrackItem>) =
|
|
|
55
56
|
playNextStack.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) playNextStack[i] = it } }
|
|
56
57
|
upNextQueue.forEachIndexed { i, t -> updatedById[t.id]?.let { if (it !== t) upNextQueue[i] = it } }
|
|
57
58
|
}
|
|
59
|
+
|
|
60
|
+
if (isCastingField) {
|
|
61
|
+
// Resolved current track (or empty receiver) → atomic reload from the current index; otherwise resync only the upcoming items.
|
|
62
|
+
if (currentTrackResolvedNow || exo.mediaItemCount == 0) {
|
|
63
|
+
rebuildQueueAndPlayFromIndex(currentTrackIndex)
|
|
64
|
+
} else {
|
|
65
|
+
rebuildQueueFromCurrentPosition()
|
|
66
|
+
}
|
|
67
|
+
return@withPlayerContext
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
rebuildQueueFromCurrentPosition()
|
|
59
71
|
}
|
|
60
72
|
}
|