Skip to content

Chromecast support (T-058): cast to TVs on the LAN, with discovery that works without mDNS - #11

Merged
df49b9cd merged 2 commits into
mainfrom
chromecast
Nov 4, 2026
Merged

df49b9cd merged 2 commits into
mainfrom
chromecast

Conversation

@df49b9cd

@df49b9cd df49b9cd commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Adds casting to a Chromecast or Google TV on the LAN. A Cast button in the player controls and next to the sidebar logo lists the TVs. Picking one moves what plays there: a movie or episode continues at its position, live TV starts at "now". With nothing playing, whatever is played next goes to the TV. The player stays the remote (play/pause, seek, TV volume/mute, zapping). Stopping the cast continues a movie or episode here, paused, where the TV was.

How it works (src-tauri/src/cast/)

  • castv2.rs: the Cast v2 protocol (protobuf frames over TLS to port 8009). Cast devices use Google's own CA, so like every sender only the handshake signature is checked, not the certificate chain.
  • mod.rs: discovery, one session task per TV, and the cast_* commands. The TV runs Google's Default Media Receiver (CC1AD845, no developer registration).
  • relay.rs: the TV never sees the provider URL, which carries the account's credentials. It gets http://<lan-ip>:<port>/cast/<token>/<n>.mp4: the token is random per item and served only to that TV's address. Each seek is a new URL whose start position the relay remembers.
  • remux.c, built by build.rs with cc against the embedded FFmpeg: repackages the stream as fragmented MP4. Video is copied (H.264/HEVC/VP9/AV1). AAC/MP3 audio is copied through aac_adtstoasc, which fMP4 doesn't insert by itself (the ffmpeg tool fails on TS input the same way). Anything else (AC-3, DTS, …) becomes stereo AAC. Live TV gets timestamp jumps re-based and dropped channels reloaded up to 3 times.
  • probe.rs: many routers drop multicast between Wi-Fi clients (AP isolation, IGMP snooping), so mDNS never arrives even though the TV is reachable. TVs are also found by re-checking remembered ones (saved on every successful connect), sweeping the local /24 networks for port 8009 (about 1 s, at most once a minute) and identifying hits via :8008/setup/eureka_info. The menu also has "Add a TV by IP address". A TV found both ways shows up once.
  • One provider stream at a time: local play and the version check end a cast session, and vice versa.

Reviewers should know

  • Media engine rebuild: scripts/build-media.sh now enables FFmpeg's aac encoder, needed for the audio conversion. Any edit to the script rebuilds the whole engine once, here, in CI and in the Flatpak build (which runs the same script).
  • Firewalls: the TV connects back to this computer for the stream, on a random port each launch. On firewalld desktops (Fedora's default public zone allows only ssh/dhcpv6), both mDNS and that connection are blocked by default. The network sweep works around discovery; the stream connection needs a firewall rule. A fixed, configurable port would make that rule a single port. It's recorded in WORKLOG as open, not done here.
  • Not cast: subtitles. Audio follows the player.audioLang setting. The in-player audio/subtitle menus, picture-in-picture, recording and stats are hidden while casting.
  • macOS: src-tauri/Info.plist adds NSLocalNetworkUsageDescription and NSBonjourServices, plus the network.server entitlement. Not run on a Mac.
  • New crates: mdns-sd, tokio-rustls (the same rustls/aws-lc as reqwest), if-addrs, getrandom, cc. THIRD_PARTY_NOTICES.md is regenerated.

Tested

  • On a real Chromecast Ultra, with mDNS blocked by the router: found by the network sweep in about 1 s. VOD from 5 s and 20 s (AC-3 MKV, converted to AAC) and an endless live TS both played. Pause/play, seek (to 40 s, and +30 s from the player controls) and TV volume worked. Stopping resumed locally, paused, at the TV's position (56 s). These were driven through the real Cast menu and player buttons.
  • Against a stand-in receiver (Python, Cast v2 over TLS, fetches the URL like the real one): the same flows plus TV switching; the output plays cleanly in ffprobe.
  • scripts/check.sh passes after merging main: 87 Rust tests (6 new: protobuf framing, relay URL parsing, network sweep, address parsing, id matching), clippy, tsc, 46 vitest (3 new), notices.

Merged main (7f5fa74): the Flatpak PR (#10) landed while this was open. Only README and WORKLOG conflicted; both sides are kept (Chromecast is T-058, Flatpak T-059). scripts/flatpak.sh pins still passes. Earlier, three code conflicts with #8/#9 (probe.rs, Layout.tsx, Player.tsx) were resolved by keeping main's new subtitle-key logic and mute badge and adding the casting guards on top.

Cast Live TV, movies and episodes to a Chromecast or Google TV on the LAN.
A Cast button in the player chrome and next to the sidebar logo picks the
TV; what plays moves there (VOD at its position, live at "now"), and the
player stays the remote (play/pause, seek, TV volume, zapping). Stopping
continues a movie/episode here, paused, where the TV was.

- src-tauri/src/cast/: Cast v2 over TLS (castv2.rs), session task and
  cast_* commands (mod.rs). Google's Default Media Receiver plays a URL
  from a LAN relay (relay.rs): provider URLs carry credentials and never
  reach the TV; each item gets a random token served only to that device.
- remux.c: the embedded FFmpeg remuxes to fragmented MP4 (video copied;
  AAC/MP3 copied via aac_adtstoasc, other audio -> stereo AAC). Needs
  FFmpeg's aac encoder, now enabled in build-media.sh (one rebuild).
- probe.rs: routers that drop multicast between Wi-Fi clients hide mDNS,
  so TVs are also found by re-checking remembered ones, sweeping the local
  /24 for port 8009 (identified via :8008/setup/eureka_info), or by IP.
- One provider stream at a time: local play and the version probe end a
  cast session.
- macOS: Info.plist local-network keys + network.server entitlement.

Verified on a real Chromecast Ultra with mDNS blocked by the router.
Docs only: README and WORKLOG keep both the Chromecast (T-058) and
Flatpak (T-059) entries. The Flatpak build runs build-media.sh, so it
picks up the AAC encoder casting needs.
@df49b9cd
df49b9cd merged commit 456cba2 into main Nov 4, 2026
6 checks passed
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