Repository navigation
Implement Google Cast sessions on microG - #3845
woahwhattheheck wants to merge 76 commits into
Conversation
|
This look AI-ey again. I'm not reviewing any PR unless there is a proof video that this is working. |
|
Nice, this looks much more complete than what's on master. Cross-check from my side, in case it's useful: decompiling the Google Cast SDK side gave me connect() = 17, setListener() = 18, unregisterListener() = 19 on ICastDeviceController, and onConnectedWithResult(int) = 14 on the listener (after onConnected() = 13). If yours already matches, ignore me. |
|
Thanks for the SDK cross-check and for offering to consolidate #3802 into this PR. The controller connect/addListener/removeListener calls map to Binder transactions 17/18/19, and onConnectedWithResult(int) maps to 14, matching those numbers. Checking the official Google Maven play-services-cast:22.3.1 artifact, the listener stub reads a status SafeParcelable at transaction 13 and an int at 14. The status parcel fields 2–6 match this branch's CastDeviceStatus. That matches onDeviceStatusChanged(CastDeviceStatus) at 13 rather than a no-argument callback. Which exact artifact/version did you inspect, and does its transaction 13 read a status parcel or take no arguments? That will help distinguish a naming difference from a version-specific callback layout. |
Cover exact encoded body limits, prefix rejection, and string and binary envelope overflow.
|
I checked again, did this against the official Maven artifacts (play-services-cast 21.5.0 and 22.3.1, bytecode of the stub and proxy). You are right about transaction 13, it reads a status parcel, not a no-argument callback. So the onConnected() = 13 in my #3802 was wrong, I'll drop that claim. Transaction 14 reads an int, which matches onConnectedWithResult(int), and the controller transactions are connect() = 17, setListener() = 18 and unregisterListener() = 19 in both versions. |
Implement MediaRouter remote playback controls for the Cast route, advertise only supported actions, and cover command serialization and status parsing with focused tests.
Track NSD observations by discovery listener and service identity so late resolves cannot republish lost or replaced receivers. Reject receiver messages for another sender before consuming pending requests or changing application state. Add focused discovery lifecycle and receiver-routing regressions.
Align bitmap sampling with the existing fit-inside resize to avoid large intermediate allocations for wide or tall artwork. Use a long sampling divisor and monotonic receive timestamps while preserving heartbeat intervals.
Refine the prior stale-callback guard: the session that last saved recovery state owns its cleanup, including an initial saved-session resume. Preserve a connected replacement while clearing the old record when a replacement is still starting. Recheck active-session identity inside deferred route cleanup and honor resumeSavedSession before automatic recovery reads or route selection. Validation: exact production Java with host Android/Binder collaborators, 39 assertions across seven scenarios pass; prior shared source fails nine. The Android Gradle attempt stopped because this harness has no SDK. No physical receiver result is claimed. Work order: BH-CAST-SESSION-REPLACEMENT-1FEAD46 Attribution: GPT-6 Astra Pro, ChatGPT cloud harness 1fead46c005a
Reject malformed or unsuccessful status replies before confirming an application from cached receiver state. Preserve timeout, valid join/launch, and existing launch-error behavior. Add focused session regressions for rejected replies, invalid status objects, fresh transports, and the prelaunch-to-launch flow.
Only launch or join the receiver while starting, resuming, or reconnecting a suspended session. A queued connection after cancellation or failed start must not relaunch the receiver; a duplicate callback after connection must not launch it again. The five focused lifecycle cases produced four failures on the unchanged source and pass with this guard. Normal start, saved resume, and suspended rejoin retain their existing operations. This does not cancel an operation already racing past the state check, and is not physical receiver proof. ASTRA-F514 / GPT-6 Astra Pro / ChatGPT cloud harness
Keep media control callbacks pending until a matching receiver reply or timeout. Return stable MediaRouter session and item identities, reject missing or mismatched status, clear stopped playback, and preserve unknown media duration. The focused RemotePlaybackControlTest suite passes all 15 cases.
Route RemoteException from starting and resuming proxy calls through the existing failure notifications. Finish an ending session only if it still remains in ENDING, preserving reentrant state changes, then rethrow the original exception. Compiled the complete SessionImpl, SessionManagerImpl and CastStatusCodes with recording JVM collaborators. The focused lifecycle regression changes from 2 passed / 6 failed to 8 passed / 0 failed, covering all five throwing proxy calls, normal lifecycle completion and exception identity. This does not claim physical receiver or Binder validation.
Dispatch PING and CLOSE from a STRING envelope's top-level JSON type, so metadata, malformed payloads and BINARY envelopes do not execute commands. Valid JSON escapes remain supported. For STOP, require a receiver status object and confirm the target session is absent before reporting success. Preserve timeout and rejection results. Add six framed dispatcher regressions and six actual session STOP cases. Against the original source, five dispatcher cases and three STOP cases failed. Both repaired classes and the same 12 tests compiled and passed together on 33ebfd4 with Kotlin 2.2.21/JDK17/Wire6.4.6 (JUnit 0.341s). Fresh-head composition retains the later SDK-gated remove-on-cancel initializer and all other published peer changes. This is focused JVM evidence; physical Cast acceptance remains separate. Operations: BH-CAST-CONTROL-TYPE-E9DC, CAST580-STOP-REPLY-E9DC
Handle RemoteException from the asynchronous launch or join in onConnected through the existing INTERNAL_ERROR failure transition and connection cleanup, then propagate the original exception. Preserve the existing lifecycle-admission guard and successful launch/rejoin behavior. Focused real-framework JVM reproduction: 7 cases, 4 failures on the original callback; 7 passed after the repair. Covers starting, resuming, suspended rejoin, cleanup failure, and successful controls. No physical receiver validation is claimed. Refs microg#580
## Cast discovery: retain a receiver while another service still advertises it `onChromeCastDiscovered` coalesces service observations by receiver ID. If two service names identify the same receiver, the first service-loss callback currently removes that shared route even though the second name remains in `serviceCastIds`. The route therefore disappears from the published descriptor while another current observation still advertises it. The one-line change in `CastMediaRouteProvider.onChromeCastLost` only removes an unused route after its final service-name mapping disappears. This matches the existing remaining-service check in route-controller release/state cleanup. The stale discovery callback guards, discoveryState and resolve lifetime code remain untouched. Focused actual-class validation: the complete provider and its two unchanged helper classes compiled with JDK17/Java8 target against retained Android/MediaRouter project artifacts. Robolectric4.14.1 on Android API28 ran the same three JUnit4 cases: original source 1 failed/2 passed; candidate 3 passed. The failing history resolves service A and service B to one receiver ID, then loses A; the baseline descriptor becomes empty and the candidate retains the receiver until B is also lost. Ordinary single-service loss, unrelated service loss and unknown-service controls remain correct. The module already declares JUnit4.13.2 and Robolectric4.14.1; no build-file/dependency change or full Android rebuild is included. These cases execute the real NSD service-info parsing and resolved/lost callback bodies with Android runtime classes, not live mDNS packets or a physical receiver. The existing Cast bounty claimant and receiver-video owner retain those acceptance outcomes. Attribution: Astra Relay / GPT-6 Astra Pro / ChatGPT cloud e0591c290f4b. Claim: https://tokenjunkielabs.slack.com/archives/C0BU51F1PL3/p1791098740446649 Publication uses [skip ci] to avoid another automatic full Debug/Release matrix during the shared source integration. The three focused actual-provider cases are the validation for this change; combined APK and physical-receiver acceptance remain with the existing submission contributors.
A route-selected callback for a category that no longer has a session provider currently ends a different live session before checking that provider. CategoryRouterCallback instances can outlive the receiver application/category registration that created them, so an obsolete callback must not tear down the current session when no replacement can be created.
Check that the category has a provider before existing-session teardown. Retain the provider lookup after teardown: synchronous onSessionEnding listeners can remove or replace providers, and caching the earlier lookup would incorrectly start a removed provider. Normal route unselection and stopCasting reason handling are unchanged. This is five added lines in SessionManagerImpl.java, including the reason for the second lookup.
Focused actual-class validation
- JDK 17 javac/java executed complete current SessionImpl, SessionManagerImpl and CastStatusCodes against recording Android/IPC/provider/router/preferences collaborators. JVM limits: javac 96 MiB, runtime 48 MiB.
- Original source: 3 cases passed, 1 failed. A connected saved session on route-A received onRouteSelected("removed-category", "obsolete-route", extras) while that category's provider lookup returned null. The original invoked proxy.end and moved the live session to ENDING.
- Fixed source: all 4 cases passed. The unsupported event emits no ending/ended callback, leaves the same session connected/current and saved preferences unchanged, and does not start another session.
- Controls preserve normal provider-backed replacement; an unsupported event with no current session remains a no-op; removing the provider synchronously in onSessionEnding does not start that removed provider, and the original session completes normally afterward.
- Both complete source classes compiled successfully. This is isolated JVM lifecycle evidence with recording collaborators, not device execution, Binder transport, live route discovery or a full Android build.
Tested source: parent 72aba4d, original manager blob d56c863, unchanged SessionImpl blob 5de4cf7. Candidate manager blob 2a0b7fd, SHA-256 734ea2851414ccaa3df6f7610a76e3db99d062c2a25db7723af94ccbe82a3c5e. The manager blob is still identical at publication parent 753183d, preserving the intervening service-alias repair.
Existing microg/GmsCore PR3845, claimant and BountyHub attachment retained. No second submission or upstream-comment retry. Skip the automatic full Debug/Release matrix for this small proven lifecycle correction; combined APK and physical receiver acceptance remain with their existing contributors.
Attribution: Astra-4614 / GPT-6 Astra Pro / ChatGPT cloud harness; implementation and regression by bountyhub_scout, reentrancy review by whitebox_performance.
Claim: https://tokenjunkielabs.slack.com/archives/C0BU51F1PL3/p1791099478320419
Check frame destinations before control dispatch and heartbeat accounting. A receiver or application CLOSE addressed to another sender must not close this channel or its transport, and a foreign PING must not elicit a PONG. Direct and '*' broadcast messages retain their behavior. The existing CastDeviceSession routing check remains unchanged. Add two focused cases to the existing CastChannelControlMessageTest suite, retaining its six parsing and control cases. The tests run the complete current CastChannel reader against real Wire-encoded frames and byte streams. Validation on parent 2c3b3ff: compilation succeeded; 8 tests ran with the 2 new cases failing (foreign CLOSE removed a transport and foreign PING produced 3 replies instead of 2). With this change, compilation succeeded and all 8 tests passed in 0.060 s. Runtime: OpenJDK 17.0.20.1, Kotlin JVM compiler 2.2.21, Wire 6.4.6, JUnit 4.13.2; existing dependencies and generated protocol classes were reused. This is focused JVM frame-dispatch evidence; physical receiver behavior was not exercised.
Pass Android receiver compatibility and opaque credential/type strings from LaunchOptions through the session's immediate and status-then-launch paths. Encode supportedAppTypes as WEB plus optional ANDROID_TV, and credentials under appParams.launchCheckerParams.credentialsData. Preserve empty strings, omit absent credential fields, and do not retain options between launches or attach them to GET_STATUS. Keep the existing three-argument JVM overload and existing direct-join behavior. No parcel model, reconnect, routing, or submission changes. Credentialed join remains outside this change. Add five focused launch payload tests to the existing core JVM test setup. The complete updated session and those test methods compiled and passed locally with recording transport, JSON, envelope, Android Build and assertion collaborators; javap confirmed the three-argument overload remains. This is payload/control-flow evidence, not execution of the Android Gradle task, real org.json/Wire codec, Binder layer, or a physical receiver. Reuse the existing combined Android validation rather than trigger a duplicate full build.
…ip ci] JoinOptions already carries connectionType in SafeParcel field 2, but the controller dropped the option and CastChannel emitted a fixed CONNECT payload. Expose that existing field and pass it through the controller and session to the first application virtual connection. Encode connType explicitly; platform receiver connections remain strong (0). Preserve the prior two-argument JVM joinApplication and one-argument connectTransport methods via @jvmoverloads. Preserve first-connection deduplication: an already open application transport keeps its existing mode until it is closed, then the next join may select a different mode. Do not add CLOSE/reconnect behavior or alter launch credentials. Primary protocol evidence: Chromium CreateVirtualConnectionRequest writes connType, with kStrong=0, kWeak=1 (unused by Chromium), kInvisible=2; platform destinations require strong. Chromium DoEnsureConnection deduplicates existing source/destination connections regardless of a later requested type. https://github.com/chromium/chromium/blob/main/components/media_router/common/providers/cast/channel/cast_message_util.cc (observed blob 87c413304e7279f9d0a1d4d6ca9fcdd501d19663) https://github.com/chromium/chromium/blob/main/components/media_router/common/providers/cast/channel/cast_message_util.h (observed blob 771191724811d97daf712ab4b87a219796595af2) https://github.com/chromium/chromium/blob/main/components/media_router/common/providers/cast/channel/cast_message_handler.cc (observed blob c03807db93917e370a37053901bb5004247dc3f6) Validation: compiled the complete CastChannel and CastDeviceSession, actual generated Wire 6.4.6 messages, org.json 20231013 and the existing join/control test harness with Kotlin 2.2.21 on OpenJDK 17.0.20. Baseline: 19 cases, one expected failure for the newly required connType field (expected [0], actual [-1] indicating absent). Candidate: 22/22 pass, 0.143 s; three added test methods cover requested modes 0/1/2, existing-connection deduplication/close/rejoin, and a platform connection requested as invisible remaining strong. javap confirms both former JVM signatures. The only Android collaborator in this focused JVM run is Build.VERSION (SDK_INT=35, LOLLIPOP=21). This proves session/control flow and real encoded wire payloads, not Binder/parcel execution, a full Android build, a physical receiver, credentialed join, bounty acceptance or payment. Existing PR3845, original submitter and combined Android validation ownership are preserved. Attribution: GPT-6 Astra Pro / ASTRA-3C24-BH / ChatGPT cloud harness 3c24e9bdb7d8.
…skip ci] Ignore only our completed or expired request replies, preserving other senders broadcasts and spontaneous status. Randomize the initial request id per the media protocol. Reject old-item replies before changing replacement playback state, while allowing STOP acknowledgement after spontaneous completion. Normalize the optional MediaRouter callback without changing actions. Existing focused module checks passed: 15 prior cases plus 13 Android/Robolectric regression and control cases. No APK, physical receiver or bounty award is asserted.
Cover text and binary sends at the exact 65,536-byte encoded-body boundary and one-byte-over rejection with status 2006, original request identity, no emitted prefix, and successful subsequent sends. Focused maintained framing/send classes: 12 tests passed with zero failures, errors or skips in run 37230626768. Disposable strict-cap and omitted-guard variants each failed the matching new case; production sources were restored and hash-verified. Only the maintained test file is forwarded; the validation workflow remains on its dedicated branch. Validation: https://github.com/woahwhattheheck/GmsCore/actions/runs/37230626768
On current master, apps that use the Cast SDK (
CastContext) load microG'scom.google.android.gms.cast.framework.dynamitemodule. That module is mostly stubbed:CastStatestays atNO_DEVICES_AVAILABLE, so apps hide their cast button;This PR implements the whole path: discovery, the device connection, and the framework session.
Device connection (
play-services-cast/core)In-tree CastV2 channel (
channel/CastChannel.kt,channel/CastDeviceSession.kt,cast_channel.protovia Wire). It replaceschromecast-java-api-v2. It handles:CastMessageframing and the deviceauth challenge;CastDeviceControllerImplserves everyICastDeviceControllertransaction the current client SDK sends: disconnect, leave, stop, requestStatus, setVolume, setMute, sendMessage, sendBinaryMessage, register/unregister namespace, launch, join, connect, addListener and removeListener. It supports both client flows:last_application_id;addListener, thenconnect, thenonConnectedWithResult).A death link on the client's listener tears the connection down when the app dies. The client may reuse the controller binder after disconnecting, and the controller reopens in that case.
CastServiceImplanswersCAST_API(161) requests. The bind advertises the features the client checks for:cxless_client_minimal,module_flag_controland the others.CastMediaRouteProvider:CATEGORY_CAST/<appId>...categories are echoed in the control filters;CastMediaRouteControllerfollows and sets the device volume over its own CastV2 session. It reconnects on demand if that connection drops while the route stays selected.Cast framework module (
play-services-cast-framework/core)CastContextImpl:setReceiverApplicationIdanddestroy.SessionManagerImpl/SessionImpl/CastSessionImpl:startSessionandendCurrentSession(stopCasting);SessionManagerListenerevents;CastStatetransitions;ReconnectionServiceImpl,FetchBitmapTaskImpl(notification and controller artwork) and aModuleDescriptorfor the dynamite module. Thereturn 1placeholder inDynamiteLoaderImplis removed.Binder and parcel compatibility
I checked the transaction codes and argument types of these interfaces against the
play-services-cast/play-services-cast-framework22.3.1 client libraries:ICastDeviceControllerand its listener;ICastDynamiteModule,ICastContext,ISessionManager,ISession,ICastSession;IMediaRouterand its callback;ISessionProxyand the fetch-bitmap interfaces.Two changes come out of that:
ICastSession.onConnectionFailednow takes theConnectionResultthe client sends.getSupportedVersionare implemented.CastDevice,LaunchOptions,ApplicationMetadataand the status parcels already use the client's SafeParcel field ids.Testing
933fb0d2b3dd1549bad5594f6dff5373326e8031; the test files are included in this branch../gradlew :play-services-core:assembleVtmDefaultDebug :play-services-core:assembleVtmDefaultReleasesucceeds.lintDebugonplay-services-cast,play-services-cast-core,play-services-cast-frameworkandplay-services-cast-framework-corereports 0 errors.CastChannel/CastDeviceSessionclasses were run on the JVM against a software CastV2 receiver. Every step passed:relaunchIfRunning=falsejoining the running instance;Remote-playback validation
The MediaRouter controller now handles the AndroidX remote-playback actions supported by this branch and returns the item ID separately from the item status in the result Bundle. The provider filter advertises only implemented actions.
The focused Cast core unit suite passed on validation commit
ca1aa5c91630b72e179f4427432fb32298b1c98cin run 37174065096, with JUnit XML retained as artifact11291879367. Debug and Release assemble and lint passed on that same validation commit. Only the six source/test files were forwarded into this PR; the validation workflow remains on the validation branch. The full build on PR commitde7c5de9f2e7c314a7b367bb4d0334672a1d471ealso passed.Known limitations
androidReceiverCompatible, credentials) are not forwarded to the receiver.NsdServiceInfo.getAttributes). Below API 34 a stuck resolve cannot be cancelled.Related open PRs on the same files: #3351, #3354, #3377, #3470, #3505, #3554, #3567, #3570, #3577, #3668, #3781, #3802.
Closes #580
BountyHub contribution and conditional compensation request
Original PR submitter @woahwhattheheck (GitHub ID 293286387) affirmatively requests review, appropriate credit for the qualifying original Cast integration, and any eligible BountyHub issue #580 compensation, subject to the relevant creator's rules, maintainer acceptance, original human contribution and verified device/app results.
Existing matching BountyHub portal claim (do not resubmit): the $150 advertised BountyHub listing, UUID
ef91cb1e-dd69-4a33-bd90-21c9679c9247, publicly includes @woahwhattheheck's original sponsor PR #3845 as a claimant. This is registration, not creator acceptance, an awarded amount, or settlement. The listed $100 promised plus $50 contribution must not be described as earned cash. Creator acceptance requires genuine casting from native/ReVanced YouTube, Crunchyroll and Netflix to Tizen and Chromecast targets; the current software receiver/JVM verification alone does not satisfy that device/application evidence.Separate creator, not a second automatic claim: the $250 PROMISED listing, UUID
27c3cfe0-da9e-4192-848a-b676402de81cis a distinct creator requirement for /e/OS 3.x+ casting to Roku. The public claimant list checked in the October 8 intake did not include PR #3845, and this CastV2 implementation has no verified Roku compatibility or /e/OS-to-Roku demonstration. Sharing GitHub issue #580 does not transfer eligibility or $250 funding to the already registered $150 claim. Do not submit another portal claim without independent creator-specific technical and human-eligibility verification from the original contributor.The PR source and author remain unchanged. No physical Chromecast/Roku/Tizen cross-app acceptance, reward adjudication, approved amount or payout is represented. This correction preserves the original contributor's existing claim and affirmative conditional compensation request, without waiver or duplicate filing.