Skip to content

[draft] Cast: move route socket operations off main thread#3470

Draft
emulatronicGIT wants to merge 5 commits into
microg:masterfrom
emulatronicGIT:codex/cast-route-main-thread-fix
Draft

[draft] Cast: move route socket operations off main thread#3470
emulatronicGIT wants to merge 5 commits into
microg:masterfrom
emulatronicGIT:codex/cast-route-main-thread-fix

Conversation

@emulatronicGIT

Copy link
Copy Markdown

This is a small follow-up branch for #3351, based on the current master branch and including the existing Cast connection lifecycle commits from that PR plus one additional fix for the crash reported in #3351 (comment).

The additional change moves CastMediaRouteController route connect/disconnect/release work onto a single background executor. That avoids calling ChromeCast.connect() from the media route provider handler/main path, which is what triggers the reported NetworkOnMainThreadException when selecting a Cast route.

What changed in the follow-up commit:

  • onSelect() schedules chromecast.connect() on a single-thread executor.
  • onUnselect() and onRelease() schedule disconnect on the same executor.
  • late work submitted after release is ignored instead of crashing.

Validation:

  • git diff --check fork/master..HEAD
  • JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home ANDROID_HOME=/opt/homebrew/share/android-commandlinetools ./gradlew :play-services-cast-core:compileDebugJavaWithJavac

I cannot test actual Chromecast playback from this machine. The useful runtime check is whether the LogFox NetworkOnMainThreadException is gone and whether YouTube progresses past route selection.

FloodExLLC and others added 5 commits May 13, 2026 13:04
The ChromeCast object was created but connect() was never called
before launchApp(), sendRawRequest() etc., causing all outgoing
operations to fail with IOException because no socket existed.

- Add ensureConnected() helper; call it at the top of every
  outgoing operation (launchApplication, joinApplication,
  sendMessage, stopApplication)
- Fix launchApplication() to guard against a null Application
  response instead of NPE-ing on app.sessionId
- Implement joinApplication() properly: query device status,
  join an already-running matching session (wasLaunched=false),
  and only fall back to launching when the app is absent
chromecast.connect() throws both IOException and GeneralSecurityException
(a checked exception). The ensureConnected() helper only declared
throws IOException, causing a compile error.

Wrap the connect() call to catch GeneralSecurityException and rethrow
as IOException so all callers remain unchanged.

Fixes: microg#3351
onSelect() and onUnselect() were stubs. The Cast device connection
lifecycle must mirror the route selection lifecycle so the socket
is open before the Cast session begins and closed when the user
switches away.

- onSelect(): open TCP/TLS connection to the Cast device
- onUnselect() / onUnselect(int): close the connection
- onRelease(): close the connection when the controller is destroyed
….onSelect()

Same fix as CastDeviceControllerImpl: chromecast.connect() throws both
IOException and GeneralSecurityException. Update the catch clause to
handle both using a multi-catch.
@peterhel

Copy link
Copy Markdown

This is clean — easily the most polished of the open Cast PRs I've looked at. Things I liked: moving the route-controller connect/disconnect onto a single-thread executor (and guarding RejectedExecutionException after shutdown()) is exactly the right shape for the main-thread problem; ensureConnected() correctly wraps GeneralSecurityException (a couple of the other Cast PRs miss that and don't compile); and the joinApplication join-vs-relaunch plus the null-checks are nice touches.

Builds cleanly for me (:play-services-core:assembleVtmDefaultDebug).

One bit of context, since it's relevant to where this slots in: the CastDeviceControllerImpl changes here are complementary to the connectionless (Cast.API_CXLESS) work in #3502 / #3570 — those add setListener/connect/onConnectedWithResult to the same class for the modern Cast-SDK clients (Prime/YouTube), while this hardens the existing launch/join/send paths and the route-controller threading. They compose cleanly (your ensureConnected() is idempotent via isConnected(), so an explicit connectionless connect() on top is a no-op). Combined you'd get a robust and modern-app-capable controller — might be worth coordinating so they land together rather than colliding on CastDeviceControllerImpl.

Know it's still a draft, so no rush — happy to hardware-test once you want eyes on it. Nice work.

@emulatronicGIT

Copy link
Copy Markdown
Author

Thanks, @peterhel, really useful context, especially around #3502 / #3570.

The current draft is ready for hardware testing from my side. If you are still up for it, the main thing I would like checked is the original route-selection failure path: select a Cast route from YouTube and confirm whether the NetworkOnMainThreadException is gone and whether the app progresses past route selection.

If you have time, it would also be useful to sanity-check how this branch behaves alongside the modern connectionless Cast path from #3570, since your note makes sense: this PR hardens the existing launch/join/send and route-controller threading, while #3570 covers the Cast.API_CXLESS path used by newer clients.

I will keep this as draft while we coordinate so it does not collide unnecessarily with the other Cast controller work.

@peterhel

Copy link
Copy Markdown

Ran it on hardware (Pixel 2 + real Chromecast, driving Prime). No NetworkOnMainThreadException anywhere, builds + deploys clean. But I couldn't exercise the onSelect threading end-to-end: on current master the framework SessionManagerImpl.addSessionManagerListener is still a stub (it spams unimplemented Method when the app starts a session), so the session never starts, CastMediaRouteController.onSelect is never reached, and Prime just keeps playing in-app. The route is selected (Selecting route: Vardagsrum), but the route-controller path your PR hardens isn't hit standalone.

Looks like more evidence for landing #3470 alongside the session-management work (#3567 / #3570 / #3502) — once one of those is in I'll re-run and confirm the threaded connect + no NetworkOnMainThread for real.

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.

3 participants