Skip to content

fix(cast): improve playback synchronization with Chromecast receivers - #29

Open
Hitomatito wants to merge 1 commit into
maxrave-dev:multiplatformfrom
Hitomatito:feat/cast-synchronization-improvements
Open

Hitomatito wants to merge 1 commit into
maxrave-dev:multiplatformfrom
Hitomatito:feat/cast-synchronization-improvements

Conversation

@Hitomatito

Copy link
Copy Markdown

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):

  • Push first track immediately: First resolved track is pushed to receiver right away, rest resolve in background. Cuts connect-to-playback from ~2-4s to ~200ms.
  • Reduce INITIAL_WINDOW_SIZE from 5→2: Faster initial delivery.
  • Update remoteToPlaylist on background appends: Without this, onRemoteTransition couldn't find index for tracks appended after initial push → UI stalled.
  • Look-ahead at lastIndex-1: Prevents gap before auto-advance.

CastStreamResolver:

  • Reuse cached Stream entity instead of re-extracting on every cast.

CrossfadeExoPlayerAdapter:

  • Restore duplicate-index guard in notifyRemoteTransition to prevent redundant UI updates.

MediaServiceHandlerImpl:

  • Parallelize DB writes in getDataOfNowPlayingState (updateThumbnails, updateVideoType, updateSongInLibrary, updateListenCount run concurrently). DB time: ~200-500ms → ~37-62ms.
  • Clear songEntity when videoId changes to prevent stale metadata in UI.

Media3ServiceModule: Bind CastStreamResolver as singleton.
GenericCastState: Add isActive computed property.

Depends on: none. Required before maxrave-dev/SimpMusic#2440 can be merged (parent repo submodule must point to this commit after merge).

Copilot AI lite review requested due to automatic review settings September 2, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 isRemote before switching to Dispatchers.Main, but the Cast session can end between that check and the withContext(Main) block, causing remoteToPlaylist and sessionPlayer.addMediaItem(...) to run after disconnect (potentially corrupting local playback state). Re-check isRemote inside 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 via adapter.volume = deviceVolume will currently be treated as a local volume change (because CrossfadeExoPlayerAdapter.volume invokes castLocalVolumeChangeCallback + 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.

Comment thread media/media3/src/main/java/com/maxrave/media3/cast/CastHandoffManager.kt Outdated
Comment thread media/media3/src/main/java/com/maxrave/media3/cast/CastStreamResolver.kt Outdated
- 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
@Hitomatito
Hitomatito force-pushed the feat/cast-synchronization-improvements branch from a76c8fa to b0ae612 Compare September 2, 2026 13:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants