Cast: Implement connectionless (Cast.API_CXLESS) device controller#3570
Open
peterhel wants to merge 3 commits into
Open
Cast: Implement connectionless (Cast.API_CXLESS) device controller#3570peterhel wants to merge 3 commits into
peterhel wants to merge 3 commits into
Conversation
CastContextImpl never registered a route-selection callback, so selecting a Chromecast via the Cast SDK (CastContext/SessionManager) did nothing: discovery worked but no session was ever started and the app's SessionManagerListener never fired. MediaRouterCallbackImpl already had the correct start-on-selection logic but was never instantiated or registered with the router. Register MediaRouterCallbackImpl via IMediaRouter (registerMediaRouterCallbackImpl + addCallback) so route selection starts a session. This goes through the IMediaRouter binder rather than touching androidx MediaRouter from the dynamite, which would throw Resources$NotFoundException (the dynamite uses the app's Resources but microG's resource IDs). Also resolve defaultSessionProvider by category prefix: the provider map is keyed by the full control category (with namespace/flag suffixes such as .../CC1AD845///ALLOW_IPV6), so the exact categoryForCast(appId) lookup missed and left defaultSessionProvider null. Harden MediaRouterCallbackImpl.onRouteSelected against a missing provider / non-SessionImpl and fall back to the route extras. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Modern Cast SDK clients — and apps such as Amazon Prime Video — drive the Cast device over the connectionless API instead of the classic connection-based path. They bind the existing device-controller service, but unlike the classic client they deliver their listener out-of-band via setListener() and then call connect(), and they wait for the service to reply ICastDeviceControllerListener.onConnectedWithResult before launching an application. microG only implemented the classic path, so these clients bound the service and then stalled forever; withholding the cxless features instead made the SDK report microG as too old (ConnectionResult=2). Add the missing connectionless surface: - ICastDeviceController: connect() (txn 17), setListener() (18) and unregisterListener() (19). The other transaction numbers already match the framework; these methods were simply absent. - ICastDeviceControllerListener: onConnectedWithResult() (txn 14), the readiness signal the connectionless client blocks on. - CastDeviceControllerImpl: open the CastV2 channel on connect() and emit onConnectedWithResult(SUCCESS); store the listener delivered via setListener(); report onSendMessageSuccess from the outgoing send keyed by its requestId (reporting it from inbound messages completed a non-existent client task and threw a RemoteException); and disconnect from the device if the client process dies (linkToDeath) so the connection and its reader thread are not leaked. - CastDeviceControllerService: advertise the requested API features via ConnectionInfo so the client's availability check passes. Tested on-device: the real Amazon Prime Video app launches its own receiver and plays a DRM title end to end through this path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 20, 2026
Draft
Connectionless senders (Netflix/Prime, Cast.API_CXLESS) bind BIND_CAST_DEVICE_CONTROLLER_SERVICE with serviceId CAST_API (161), not CAST (10). The broker resolves the id to a service via its supportedServices; with only CAST declared it matches none and rejects the bind with "Service not supported", which the sender surfaces as "No devices found". Add CAST_API to this service's supported ids. The same CastDeviceControllerImpl serves the request -- its connectionless connect/setListener path already handles it -- so no new service or implementation is needed. Verified on a Pixel 2 against a Chromecast: the real Netflix app now selects the device, launches its receiver, and plays a DRM title end to end.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cast: Implement connectionless (Cast.API_CXLESS) device controller
Summary
Modern Cast SDK clients — and real apps such as Amazon Prime Video, YouTube,
Netflix — drive the Cast device over the connectionless Cast API
(
Cast.API_CXLESS) rather than the classic connection-based path that microGimplemented. As a result, casting from these apps never worked on microG: the
session would start and then stall indefinitely.
This PR implements the connectionless device-controller surface. With it, the
real Amazon Prime Video and Netflix apps launch their own receiver and play a
DRM title end to end on a Chromecast, cast from a de-Googled phone.
Builds on #3567 (which makes the framework start a session when a Cast route is
selected); that session is the trigger that binds the device controller this PR
completes.
The problem
The connectionless client binds the same
ICastDeviceControllerservice, butits handshake differs from the classic client:
GetServiceRequest"listener"extra and connects lazily on the first launch/sendMessage.
setListener(), then callsconnect(), and waits for the service to callICastDeviceControllerListener.onConnectedWithResultbefore it will launch anapplication. It is otherwise purely reactive.
microG implemented only the classic path. So a connectionless client bound the
service, called
setListener/connect(transactions microG didn't handle, andwhich were silently dropped because they are
oneway), never receivedonConnectedWithResult, and stayed "not connected" forever. Withholding thecxless_*features to force the classic path instead just made the SDK decidemicroG was too old and fail with
ConnectionResult=2.There is also a second gate, at the bind itself. The connectionless client
issues its
GetServiceRequestwithserviceId = CAST_API(161), notCAST(10). The broker resolves that id against each service's supported ids;the device-controller service declared only
CAST, so the id matched nothingand the broker rejected the bind with "Service not supported" — which the
sender surfaces to the user as "No devices found", before any of the
handshake above can even run.
The fix
The transaction numbers in microG's AIDL already matched the framework — the
connectionless methods were simply absent. The change is additive:
ICastDeviceController.aidl— addconnect()(txn 17),setListener(ICastDeviceControllerListener)(18),unregisterListener()(19).ICastDeviceControllerListener.aidl— addonConnectedWithResult(int)(txn 14), the readiness signal.
CastDeviceControllerImpl.java:connect()opens the CastV2 channel and emitsonConnectedWithResult(SUCCESS).setListener()stores the out-of-band listener.onSendMessageSuccessis now reported from the outgoingsendMessage,keyed by that send's
requestId. Previously it was reported from inboundmessages using the response's id, which completed a non-existent client task
and threw a
RemoteException. Inbound messages now go solely throughonTextMessageReceived.linkToDeath; if the clientprocess dies without a clean
disconnect(), the device connection (and itsreader thread) is torn down instead of leaked.
CastDeviceControllerService.java:ConnectionInfoso the client'savailability check passes.
serviceIdCAST_API(161) on the bind, alongsideCAST(10),so the broker no longer rejects the connectionless client with "Service not
supported". The same
CastDeviceControllerImplserves both ids — theconnectionless
connect/setListenerpath handles the request — so no newservice or implementation is introduced.
Connectionless handshake (for reference)
Testing
Validated on a Pixel 2 (LineageOS-for-microG, Android 15) against a Chromecast:
launches Prime's own receiver (appId
17608BC8), Prime completes itsproprietary registration/settings handshake on
urn:x-cast:com.amazon.primevideo.cast, LOADs a DRM title, and the titleplays on the TV — with zero failed listener callbacks.
CAST_APIbind, drives the connectionlesssetListener/connecthandshaketo
onConnectedWithResult(0), launches Netflix's receiver, relays itsprivate-namespace traffic, LOADs a DRM title, and the title plays on the
TV.
connectionless path, launches its receiver, and the title plays on the
TV.
(
playerState=PLAYING, currentTime advancing).linkToDeathcleanup(device disconnected, no leaked connection).
Notes / scope
CastMediaRouteController.onSelectremains a stub; it is a separate legacyroute-control path and does not gate the connectionless flow (the framework's
session manager binds the device controller directly).
CAST_API(161) id is accepted at the bind (see above), but noadditional
CAST_API-specific surface (settings/relay/logging) isimplemented. Netflix and Prime carry their app traffic over the device
controller's namespaces, so that surface was not needed for casting in
testing;
registerNamespace/unregisterNamespaceremain logged no-ops anddid not block playback.