Skip to content

Cast: Implement connectionless (Cast.API_CXLESS) device controller#3570

Open
peterhel wants to merge 2 commits into
microg:masterfrom
peterhel:feat/cast-connectionless
Open

Cast: Implement connectionless (Cast.API_CXLESS) device controller#3570
peterhel wants to merge 2 commits into
microg:masterfrom
peterhel:feat/cast-connectionless

Conversation

@peterhel

Copy link
Copy Markdown

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 microG
implemented. 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 app launches its own receiver and plays 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 ICastDeviceController service, but
its handshake differs from the classic client:

  • The classic client passes its listener in the GetServiceRequest "listener"
    extra and connects lazily on the first launch/sendMessage.
  • The connectionless client instead delivers its listener out-of-band via
    setListener(), then calls connect(), and waits for the service to call
    ICastDeviceControllerListener.onConnectedWithResult before it will launch an
    application.
    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, and
which were silently dropped because they are oneway), never received
onConnectedWithResult, and stayed "not connected" forever. Withholding the
cxless_* features to force the classic path instead just made the SDK decide
microG was too old and fail with ConnectionResult=2.

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 — add connect() (txn 17),
    setListener(ICastDeviceControllerListener) (18), unregisterListener() (19).
  • ICastDeviceControllerListener.aidl — add onConnectedWithResult(int)
    (txn 14), the readiness signal.
  • CastDeviceControllerImpl.java:
    • connect() opens the CastV2 channel and emits
      onConnectedWithResult(SUCCESS).
    • setListener() stores the out-of-band listener.
    • onSendMessageSuccess is now reported from the outgoing sendMessage,
      keyed by that send's requestId. Previously it was reported from inbound
      messages using the response's id, which completed a non-existent client task
      and threw a RemoteException. Inbound messages now go solely through
      onTextMessageReceived.
    • The client's listener binder is watched with linkToDeath; if the client
      process dies without a clean disconnect(), the device connection (and its
      reader thread) is torn down instead of leaked.
  • CastDeviceControllerService.java — advertise the requested API features
    via ConnectionInfo so the client's availability check passes.

Connectionless handshake (for reference)

bind CAST service (cxless features, EXTRA_CAST_DEVICE, connectionless_client_record_id)
  → setListener(listener)              [txn 18]
  → connect()                          [txn 17]
  ← onConnectedWithResult(0)           [listener txn 14]   // session becomes "connected"
  → launchApplication(appId, opts)     [txn 13]
  ← onApplicationConnectionSuccess     [listener txn 2]
  → sendMessage(LOAD, media namespace) [txn 9]             // + per-app namespaces
  ← onSendMessageSuccess / onTextMessageReceived

Testing

Validated on a Pixel 2 (LineageOS-for-microG, Android 15) against a Chromecast:

  • The real, logged-in Amazon Prime Video app selects the device, microG
    launches Prime's own receiver (appId 17608BC8), Prime completes its
    proprietary registration/settings handshake on
    urn:x-cast:com.amazon.primevideo.cast, LOADs a DRM title, and the title
    plays on the TV — with zero failed listener callbacks.
  • A minimal first-party Cast sender plays a clip end to end
    (playerState=PLAYING, currentTime advancing).
  • Force-killing the sender mid-session triggers the linkToDeath cleanup
    (device disconnected, no leaked connection).

Notes / scope

  • CastMediaRouteController.onSelect remains a stub; it is a separate legacy
    route-control path and does not gate the connectionless flow (the framework's
    session manager binds the device controller directly).
  • The auxiliary CAST_API service (id 161 — settings/relay/logging) is still
    unimplemented; it is not on the media path and its absence did not affect
    casting in testing.

peterhel and others added 2 commits June 17, 2026 00:59
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>
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.

1 participant