Skip to content

Add casting: Chromecast + DLNA receivers#67

Open
savvystartagency wants to merge 4 commits into
truelockmc:mainfrom
savvystartagency:feat/casting-chromecast-dlna
Open

Add casting: Chromecast + DLNA receivers#67
savvystartagency wants to merge 4 commits into
truelockmc:mainfrom
savvystartagency:feat/casting-chromecast-dlna

Conversation

@savvystartagency
Copy link
Copy Markdown

Summary

Adds a Cast button to Movie / TV / Downloads pages plus a device-picker modal and a mini-controller. Streams anime mp4, anime Sl-Hls, vidsrc / videasy / 2embed HLS captures, and completed local downloads to any Chromecast-built-in device (incl. XGIMI Android TV projectors) or DLNA renderer on the LAN.

Highlights

  • src/ipc/cast.js — main-process subsystem. Discovery via bonjour-service (_googlecast._tcp) + dlnacasts3 SSDP. Single active session (mirrors PIP) via castv2-client for Chromecast and dlnacasts3 for DLNA. Clean disconnect on before-quit.
  • LAN-routable HTTP server bound 0.0.0.0:<random> (separate from the 127.0.0.1 allmanga player server):
    • /file/:token — range-supported local file serve
    • /proxy/:token — header-injecting proxy with on-the-fly HLS playlist rewriting (segments + EXT-X-KEY / EXT-X-MAP child tokens) so receivers always speak to a Referer-injecting proxy
    • /sub/:token — VTT with CORS, inline SRT→VTT conversion
  • Preloadcast:* IPC surface modelled on existing PIP naming.
  • RendereruseCast() hook, CastPickerModal, CastMiniController (play/pause/scrubber/CC/volume/stop), CastIcon + CastingIcon. Wired into MoviePage, TVPage, DownloadsPage.
  • Settings — new "Cast & Devices" section: auto-discover, include-DLNA, auto-stop-on-player-stop, preferred device, rescan, limitations notes.
  • Storage — five new CAST_* keys.

Dependencies

castv2-client, bonjour-service, dlnacasts3 — all pure JS, no native deps.

Test plan

  • Settings → Cast & Devices: Chromecast / XGIMI Android TV appears within ~5s
  • Movie page → anime mp4 (AllManga) → Cast → plays on receiver
  • Movie page → vidsrc m3u8 → Cast → plays (proxy injects Referer)
  • TV page → episode → Cast → episode title + poster show on receiver
  • Downloads page → completed mp4 → Cast → plays; range seek works
  • Mini-controller play / pause / seek / volume / CC toggle / stop all work
  • App quit while casting → receiver returns to idle in ~2s
  • Disable DLNA in settings → DLNA devices vanish from picker

Known limitations (surfaced in Settings notes box)

  • One-time signed segment URLs may invalidate during long sessions
  • DRM (Widevine / PlayReady) won't play on receivers
  • Non-AndroidTV DLNA-only renderers usually reject HLS
  • Mid-segment seeks on rewritten HLS take 2–3s
  • Single active session at a time
  • Proxy injects Referer + User-Agent only — sources requiring cookies will not work

Adds a Cast button to Movie, TV and Downloads pages plus a device-picker
modal and mini-controller. Streams anime mp4, anime Sl-Hls, vidsrc /
videasy / 2embed HLS captures, and completed local downloads to any
Chromecast-built-in device (incl. XGIMI Android TV projectors) or DLNA
renderer on the LAN.

Highlights
- New main-process module src/ipc/cast.js
  - Discovery: bonjour-service (_googlecast._tcp) + dlnacasts3 SSDP
  - LAN-routable HTTP server bound 0.0.0.0:<random>
    - /file/:token  range-supported local file serve
    - /proxy/:token header-injecting proxy with on-the-fly HLS playlist
      rewriting (segments + EXT-X-KEY / EXT-X-MAP child tokens) so
      receivers always speak to a Referer-injecting proxy
    - /sub/:token   VTT with CORS, inline SRT->VTT conversion
  - Single active session (mirrors PIP). castv2-client for Chromecast,
    dlnacasts3 for DLNA. Clean disconnect on before-quit.
- Preload bridge: cast:* IPC surface modeled on existing PIP naming.
- Renderer
  - useCast() hook (src/utils/useCast.js) for device list + session state
  - CastPickerModal with recent / available device lists, CAST/DLNA badges
  - CastMiniController with play/pause/scrubber/CC toggle/volume/stop
  - CastIcon + CastingIcon in Icons.jsx
  - Wired into MoviePage, TVPage, DownloadsPage; Sidebar status via
    the global mini-controller variant
- Settings: new "Cast & Devices" section (auto-discover, include-DLNA,
  auto-stop, preferred device, rescan, limitations notes).
- Storage: CAST_* keys for the above preferences.

Dependencies
- castv2-client    (Chromecast control)
- bonjour-service  (mDNS for _googlecast._tcp)
- dlnacasts3       (DLNA SSDP + AVTransport)

All pure JS, no native deps.
@truelockmc
Copy link
Copy Markdown
Owner

truelockmc commented May 18, 2026

Looks interesting, I suppose you have a Device to test this?

Because I do not xD...

Also, do you understand the Code you shared here?
Because the Text and the comment's look pretty AI generated 😬

@savvystartagency
Copy link
Copy Markdown
Author

yes, I am using it on my projectors and my TV :) ;)

@savvystartagency
Copy link
Copy Markdown
Author

I am just adding some controls, will push shortly

@savvystartagency savvystartagency force-pushed the feat/casting-chromecast-dlna branch from c831caf to 5df2c8a Compare May 22, 2026 11:06
daniel-avand and others added 3 commits May 23, 2026 10:42
…dded controls

Session / stop reliability
- tearDown quits the DefaultMediaReceiver via client.stop(player) before
  closing the socket (1.5s fallback), so Stop returns the device to its
  home screen instead of leaving the cast splash up.
- On Chromecast load failure, tear down the session + emit session-ended
  so the receiver doesn't sit on the splash and the UI resets.
- Force-HLS hint: hlsRemote parent token and rewritten variant-playlist
  child tokens carry isHls so a mislabeled / extensionless m3u8 is still
  rewritten through the proxy (segments keep the injected Referer).

In-app playback while casting
- When a device is connected, the in-app <webview> is suspended
  (src -> about:blank) so playback doesn't double up with the receiver.
  A centred overlay shows "Playing on <device>" with the full controller.

Device controls
- CastMiniController gains a stacked "modal" variant (device label, seek,
  back/forward 10s, play/pause, subtitle toggle, volume, stop) embedded
  directly in the casting overlay and in the picker popup when connected.

Discovery noise / efficiency
- Discovery is on-demand: pages no longer scan on mount. The picker and
  the Settings > Cast section start discovery on open and stop it on close.
- The connected device is tracked independently of the live discovery
  list, so controls keep working while discovery is stopped.
- DLNA SSDP is gated by the "Include DLNA" setting and throttled to once
  per 8s; the dlnacasts "[DLNACASTS] querying ssdp" debug line is silenced.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Discovery (root cause of "no devices found")
- Bind the mDNS socket to the LAN interface (new Bonjour({ interface }))
  via getLocalIPv4(). On machines with an active VPN or a second adapter
  the default bind landed on the wrong interface and received zero
  responses, so no Chromecast / Chromecast-built-in (e.g. XGIMI) devices
  ever appeared. Rebuild the instance if the routable IP changes (VPN
  toggled / network switched) so discovery recovers without a restart.
- Keep the mDNS browser alive for the session and re-query on reopen
  instead of tearing it down on every picker/settings close (the teardown
  made already-found devices disappear).
- Restore page-mount discovery (autoDiscover) now that it's cheap: passive
  mDNS listener, DLNA SSDP gated by setting + throttled to 8s, and the
  dlnacasts "[DLNACASTS] querying ssdp" debug line silenced.
- Hold the "scanning" state for a ~5s response window so the picker shows
  "Scanning the network…" instead of flashing "No devices found" (the
  start-discovery IPC resolves immediately while responses trickle in).

Serving correctness (from code review)
- getLocalIPv4 is now subnet-aware: when a receiver is connected it picks
  the local interface on the receiver's subnet, falling back to a
  preferred adapter, then any non-internal IPv4, then loopback. Prevents
  handing the receiver an unreachable URL on multi-NIC setups.
- Dedup proxy tokens by target+referer (makeProxyToken) so HLS playlist
  re-fetches (seeks/variant switches) reuse tokens instead of growing
  _serveTokens unbounded; gcServeTokens prunes the dedup index too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The in-app player paused when the laptop display slept or the window was
occluded because the main window used backgroundThrottling: true and
nothing held a power-save blocker (pre-existing, unrelated to casting).

- Hold powerSaveBlocker("prevent-display-sleep") while in-app playback is
  active; release on pause/stop, on window close, and on app quit.
- Release the blocker while casting — playback then lives on the receiver
  and the laptop is free to sleep.
- Set backgroundThrottling: false on the main and pop-out windows so
  occlusion / alt-tab no longer throttles or pauses media.

Wired via a new playback-keepawake IPC + setPlaybackKeepAwake preload
bridge, driven from MoviePage/TVPage on `playing && !cast.currentDevice`.

Co-Authored-By: Claude Opus 4.7 <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.

3 participants