Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1465,9 +1465,7 @@ internal class CrossfadeExoPlayerAdapter(
startPositionMs: Long,
shouldPlay: Boolean,
) {
if (index !in playlist.indices) return

val mediaItem = playlist[index]
val mediaItem = playlist.getOrNull(index) ?: return
val videoId = mediaItem.mediaId

// While casting, playback starts on the receiver — never on a local ExoPlayer.
Expand Down Expand Up @@ -1496,34 +1494,31 @@ internal class CrossfadeExoPlayerAdapter(
)
}

// Use precached player if available
// Resolve player instance (promote precached player if available, else instantiate fresh)
val cachedPlayerEntry = precachedPlayers.remove(videoId)
val player: ExoPlayer
val playerFilter: CrossfadeFilterAudioProcessor?
if (cachedPlayerEntry?.player != null) {
Logger.d(TAG, "Using precached player for $videoId")
player = cachedPlayerEntry.player
playerFilter = cachedPlayerEntry.filter
} else {
Logger.d(TAG, "Creating new player for $videoId")
val pwf = createExoPlayerInstance()
player = pwf.player
playerFilter = pwf.filter
player.setMediaItem(mediaItem.toMedia3MediaItem())
player.prepare()
}

// === CAREFUL ORDER for ForwardingPlayer integration ===
val (player, playerFilter) =
if (cachedPlayerEntry?.player != null) {
Logger.d(TAG, "Promoting precached player for $videoId")
cachedPlayerEntry.player to cachedPlayerEntry.filter
} else {
Logger.d(TAG, "Instantiating fresh player for $videoId")
val pwf = createExoPlayerInstance()
pwf.player.apply {
setMediaItem(mediaItem.toMedia3MediaItem())
prepare()
}
pwf.player to pwf.filter
}

// 1. Remove our active listener from old player
// 1. Teardown active listeners and ongoing crossfade jobs before swapping
cleanupPlayerListenerInternal()
stopPositionUpdates()
crossfadeJob?.cancel()
crossfadeJob = null
setCrossfading(false)

// 2. Save old player reference
val oldPlayer = currentPlayer
// 2. Save previous player reference
val previousPlayer = currentPlayer

// 3. Set new player as current
currentPlayer = player
Expand All @@ -1532,41 +1527,24 @@ internal class CrossfadeExoPlayerAdapter(
// 4. Setup our listener on new player
setupPlayerListenerInternal(player)

// 5. Swap ForwardingPlayer delegate (moves MediaSession's listeners from old to new)
forwardingPlayer.swapDelegate(player)

// 5b. Notify MediaSession about the new media item
// The MediaItem was set before the swap (either during precache or above),
// so MediaSession's listener missed the onMediaItemTransition event.
// play() below will trigger onIsPlayingChanged which causes MediaSession
// to re-query metadata, but this explicit notify is safer and ensures
// the notification updates immediately even if play() is delayed.
forwardingPlayer.notifyMediaItemChanged()

// 6. NOW release old player (it has no listeners anymore)
if (oldPlayer != null && oldPlayer !== player) {
try {
oldPlayer.stop()
oldPlayer.release()
} catch (e: Exception) {
Logger.w(TAG, "Error releasing old player: ${e.message}")
// 5. Swap between previous and current

// Configure playback parameters/initial play state prior to delegate swapping.
// Swapping the delegate before starting playback causes MediaSession to observe an idle state,
// which arms the Android foreground service demotion watchdog.
player.apply {
// Apply settings
volume = internalVolume
playbackParameters = PlaybackParameters(internalPlaybackSpeed, internalPlaybackPitch)
skipSilenceEnabled = internalSkipSilence

// Seek if needed
if (startPositionMs > 0) {
seekTo(startPositionMs)
cachedPosition = startPositionMs
}
}

// Audio focus is held at the adapter level (see Audio Focus section),
// not per-player, so it survives this swap (#2155).

// Apply settings
player.volume = internalVolume
player.playbackParameters = PlaybackParameters(internalPlaybackSpeed, internalPlaybackPitch)
player.skipSilenceEnabled = internalSkipSilence

// Seek if needed
if (startPositionMs > 0) {
player.seekTo(startPositionMs)
cachedPosition = startPositionMs
}

// Auto-play if requested
if (shouldPlay) {
requestAudioFocusInternal()
Expand All @@ -1577,9 +1555,20 @@ internal class CrossfadeExoPlayerAdapter(
transitionToState(InternalState.READY)
}

forwardingPlayer.suppressPlaybackEnded = false
forwardingPlayer.swapDelegate(player)
forwardingPlayer.notifyMediaItemChanged()

// Asynchronously release the old player instance to prevent blocking the transition
if (previousPlayer != null && previousPlayer !== player) {
runCatching {
previousPlayer.stop()
previousPlayer.release()
}.onFailure {
Logger.w(TAG, "Failed to release previous player: ${it.message}")
}
}

// Start position updates
forwardingPlayer.suppressPlaybackEnded = false
startPositionUpdates()

// Eagerly load audio metadata for auto crossfade calculations
Expand All @@ -1592,8 +1581,7 @@ internal class CrossfadeExoPlayerAdapter(
triggerPrecachingInternal()
} catch (e: Exception) {
if (e is CancellationException) throw e
Logger.e(TAG, "Load track error: ${e.message}", e)
forwardingPlayer.suppressPlaybackEnded = false
Logger.e(TAG, "Failed to load and play track: ${e.message}", e)
transitionToState(InternalState.ERROR)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,18 +150,10 @@ internal class SimpleMediaService :
notification: Notification,
ongoing: Boolean,
) {
fun startFg() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
} else {
startForeground(notificationId, notification)
}
}
coroutineScope.launch {
while (coroutineScope.isActive) {
startFg()
delay(30.seconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
} else {
startForeground(notificationId, notification)
}
}
},
Expand Down Expand Up @@ -197,7 +189,11 @@ internal class SimpleMediaService :
session: MediaSession,
startInForegroundRequired: Boolean,
) {
super.onUpdateNotification(session, startInForegroundRequired)
val player = session.player
val isActivelyPlaying =
player.playWhenReady &&
(player.playbackState == Player.STATE_READY || player.playbackState == Player.STATE_BUFFERING)
super.onUpdateNotification(session, startInForegroundRequired || isActivelyPlaying)
}

@UnstableApi
Expand Down