fix(cast): improve playback synchronization with Chromecast receivers - #29
Hitomatito wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed Cast handoff/volume sync correctness issues (race on disconnect during background appends, and volume polling/sync being treated as local changes causing debounce + redundant device writes) that should be fixed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves Google Cast (Chromecast) playback handoff and synchronization by speeding up initial remote playback start, keeping UI metadata/volume state in sync, and reducing redundant or incorrect remote transition updates.
Changes:
- Optimize Cast handoff by pushing the first item immediately, appending the rest asynchronously, adjusting window sizing/look-ahead, and adding remote recovery backoff.
- Add Cast volume synchronization (connect-time + polling) and route volume changes to the Cast device when casting.
- Reduce redundant work and stale UI by reusing cached stream format data, refining Cast state modeling, and parallelizing selected DB updates in now-playing handling.
File summaries
| File | Description |
|---|---|
| media/media3/src/main/java/com/maxrave/media3/service/download/DownloadUtils.kt | Improves failure behavior when a DataSpec is missing a media id by throwing an IOException. |
| media/media3/src/main/java/com/maxrave/media3/exoplayer/CrossfadeExoPlayerAdapter.kt | Routes volume changes to Cast device during remote playback and adds a “connecting” notification hook. |
| media/media3/src/main/java/com/maxrave/media3/di/Media3ServiceModule.kt | Aligns DataSpec missing-media-id handling with IOException in resolving data source factory. |
| media/media3/src/main/java/com/maxrave/media3/cast/CastStreamResolver.kt | Refactors cached-vs-fresh Cast stream resolution flow and MIME type selection. |
| media/media3/src/main/java/com/maxrave/media3/cast/CastHandoffManager.kt | Core Cast handoff latency/sync improvements: immediate first push, background appends, look-ahead, volume polling, and retry backoff. |
| domain/src/commonMain/kotlin/com/maxrave/domain/data/player/GenericCastState.kt | Extends Cast state to include “connecting” and adds isActive helper. |
| data/src/androidMain/kotlin/com/maxrave/data/mediaservice/MediaServiceHandlerImpl.kt | Clears stale song metadata promptly, parallelizes certain DB updates, and wires volume updates through player/control state. |
Review details
Suppressed comments (2)
media/media3/src/main/java/com/maxrave/media3/cast/CastHandoffManager.kt:215
- The background append path checks
isRemotebefore switching toDispatchers.Main, but the Cast session can end between that check and thewithContext(Main)block, causingremoteToPlaylistandsessionPlayer.addMediaItem(...)to run after disconnect (potentially corrupting local playback state). Re-checkisRemoteinside the Main block immediately before mutating state.
// --- Remaining tracks: resolve and append in background ---
if (!adapter.shuffleModeEnabled) {
for (offset in 1 until minOf(INITIAL_WINDOW_SIZE, itemCount - startIndex)) {
val idx = startIndex + offset
val item = adapter.getMediaItemAt(idx) ?: continue
launch(Dispatchers.IO) {
try {
resolver.resolve(item.mediaId)?.let { stream ->
if (!isRemote) return@let
withContext(Dispatchers.Main) {
remoteToPlaylist = remoteToPlaylist + idx
sessionPlayer.addMediaItem(item.toCastMediaItem(stream))
}
media/media3/src/main/java/com/maxrave/media3/cast/CastHandoffManager.kt:368
- In
startVolumePolling, syncing UI viaadapter.volume = deviceVolumewill currently be treated as a local volume change (because CrossfadeExoPlayerAdapter.volume invokescastLocalVolumeChangeCallback+setCastDeviceVolume). That means remote-driven volume changes get debounced and may get redundantly written back to the Cast device, reducing how well the poll keeps the slider in sync. Use an adapter API that updates the local/UI volume without triggering the local-change callback or setting Cast volume.
private fun startVolumePolling() {
volumePollJob?.cancel()
volumePollJob =
coroutineScope.launch {
while (isActive) {
delay(VOLUME_POLL_INTERVAL_MS)
val now = System.currentTimeMillis()
val timeSinceLocalChange = now - lastLocalVolumeChangeMs
if (timeSinceLocalChange < VOLUME_LOCAL_DEBOUNCE_MS) {
Logger.d(TAG, "Volume poll: SKIPPED (local change ${timeSinceLocalChange}ms ago, debounce=${VOLUME_LOCAL_DEBOUNCE_MS}ms)")
continue
}
val deviceVolume = getCastDeviceVolume() ?: continue
if (deviceVolume != adapter.volume) {
Logger.d(TAG, "Volume poll: device=${deviceVolume} adapter=${adapter.volume} — syncing")
adapter.volume = deviceVolume
}
- Files reviewed: 7/7 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- CastHandoffManager: push first track immediately to receiver, resolve remaining tracks in background and append via addMediaItem (cuts connect-to-playback from ~2-4s to ~200ms) - CastHandoffManager: reduce INITIAL_WINDOW_SIZE from 5 to 2 for faster first-track delivery - CastHandoffManager: update remoteToPlaylist on background appends so onRemoteTransition finds the correct index - CastHandoffManager: trigger look-ahead at lastIndex-1 instead of lastIndex to avoid gap before auto-advance - CastStreamResolver: reuse cached Stream entity instead of re-extracting on every cast (reduces latency and CPU) - CrossfadeExoPlayerAdapter: restore duplicate-index guard in notifyRemoteTransition to prevent redundant UI updates - MediaServiceHandlerImpl: parallelize DB writes in getDataOfNowPlayingState (updateThumbnails, updateVideoType, updateSongInLibrary, updateListenCount run concurrently) - MediaServiceHandlerImpl: clear songEntity when videoId changes so stale metadata from previous track is never shown in UI - Media3ServiceModule: bind CastStreamResolver as singleton - GenericCastState: add computed isActive property
a76c8fa to
b0ae612
Compare
Summary
Improves Google Cast playback synchronization by reducing connect-to-playback latency, fixing stale metadata in UI, and preventing duplicate transition notifications.
Changes
CastHandoffManager (core fix):
onRemoteTransitioncouldn't find index for tracks appended after initial push → UI stalled.CastStreamResolver:
Streamentity instead of re-extracting on every cast.CrossfadeExoPlayerAdapter:
notifyRemoteTransitionto prevent redundant UI updates.MediaServiceHandlerImpl:
getDataOfNowPlayingState(updateThumbnails,updateVideoType,updateSongInLibrary,updateListenCountrun concurrently). DB time: ~200-500ms → ~37-62ms.songEntitywhenvideoIdchanges to prevent stale metadata in UI.Media3ServiceModule: Bind
CastStreamResolveras singleton.GenericCastState: Add
isActivecomputed property.Depends on: none. Required before maxrave-dev/SimpMusic#2440 can be merged (parent repo submodule must point to this commit after merge).