From 8730281d739d9ed1d6f2d366772582eea8ba0294 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 21 Aug 2026 13:52:11 +0200 Subject: feat: LAN Wi-Fi casting to Chromecast via local HTTP relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-serve decrypted fMP4 video over HTTP on the LAN so a Chromecast can play the stream. The relay runs in the Electron main process — same trust boundary as downloads and MSE playback. - cast-relay.js: HTTP server with BoxAccumulator (reassembles WebRTC chunks into moof+mdat pairs), ring buffer, backpressure, finish() for clean end-of-stream, fixed port range 19550-19553 - cast-chromecast.js: mDNS discovery (bonjour-service) + CASTV2 protocol (castv2-client), connect/reload/disconnect lifecycle - Seek-aware: relay restarts on every seek, Chromecast reloads new URL; generation counter prevents stale async errors from killing active restarts; landingPlayheadRef suppresses programmatic seeking events - Device picker in video top bar with scan, device selection, copy-URL fallback, and cast status indicator - IPC bridge (main/preload/platform) for start/push/stop/finish/status/ discover/chromecastConnect/chromecastReload/chromecastDisconnect - Phase 3 design doc for DLNA/Smart TV in docs/cast-smart-tv.md Co-Authored-By: Claude Opus 4.6 --- packages/meshbay-client/src/main.js | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'packages/meshbay-client/src/main.js') diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index f2ec645..45e8462 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -351,6 +351,11 @@ function createWindow() { // renderer parses decrypted content from nodes, which is attacker-controlled // input, so it is treated as hostile even though it is our own code. +const CastRelay = require('./cast-relay.js'); +const castRelay = new CastRelay(); +const CastChromecast = require('./cast-chromecast.js'); +const castChromecast = new CastChromecast(); + function registerBridge() { ipcMain.handle('hub:set', async (_e, base) => { const url = String(base || '').trim().replace(/\/+$/, ''); @@ -738,6 +743,60 @@ function registerBridge() { _nodePairingCode = code || null; return true; }); + + // ── LAN cast relay ────────────────────────────────────────────────────── + // + // A local HTTP server that re-serves decrypted fMP4 segments so a + // Chromecast or Smart TV on the same Wi-Fi can stream the video. The + // renderer feeds it segments via IPC; the relay serves them over HTTP. + // Same trust boundary as the MSE player and the download-to-disk path. + + ipcMain.handle('cast:start', async (_e, opts) => { + return castRelay.start({ + codec: opts.codec, + initSegment: opts.initSegment ? Buffer.from(opts.initSegment) : null, + }); + }); + + ipcMain.handle('cast:push', async (_e, data) => { + castRelay.pushSegment(Buffer.from(data)); + return true; + }); + + ipcMain.handle('cast:stop', async () => { + await castRelay.stop(); + return true; + }); + + ipcMain.handle('cast:finish', async () => { + castRelay.finish(); + return true; + }); + + ipcMain.handle('cast:status', async () => ({ + active: castRelay.active, + url: castRelay.url, + chromecast: castChromecast.getStatus(), + })); + + // ── Chromecast discovery + control ────────────────────────────────────── + + ipcMain.handle('cast:discover', async () => { + return castChromecast.discover(); + }); + + ipcMain.handle('cast:chromecast:connect', async (_e, { deviceId, mediaUrl }) => { + return castChromecast.connect(deviceId, mediaUrl); + }); + + ipcMain.handle('cast:chromecast:reload', async (_e, { mediaUrl }) => { + return castChromecast.reload(mediaUrl); + }); + + ipcMain.handle('cast:chromecast:disconnect', async () => { + await castChromecast.disconnect(); + return true; + }); } /** -- cgit v1.2.3