From 096ee2c41d7ad1ad3ec670499e6908c9d9ccf2f7 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 24 Jul 2026 14:19:03 +0200 Subject: [PATCH] feat(chromecast): support custom subtitle broadcast --- bun.lock | 11 +++++++++++ package.json | 1 + src/player.web.tsx | 30 +++++++++++++----------------- src/provider.web.tsx | 38 ++++++++++++++++++++++++++++++++++++++ tsconfig.json | 1 + 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/bun.lock b/bun.lock index b5306e1..2dfcf56 100644 --- a/bun.lock +++ b/bun.lock @@ -12,6 +12,7 @@ }, "devDependencies": { "@biomejs/biome": "2.5.3", + "@types/chromecast-caf-sender": "^1.0.11", "@types/jest": "^30.0.0", "@types/react": "19.2.17", "nitrogen": "0.36.1", @@ -596,6 +597,10 @@ "@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="], + "@types/chrome": ["@types/chrome@0.2.2", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-8rSMZ4cvo2xmaSyQg0sN5yRL7oiDkntLoiHxUhfwQnv1mvnkrdoZ25SlNrKWmYKaeP50WvrfWj1pmc02+U9KKw=="], + + "@types/chromecast-caf-sender": ["@types/chromecast-caf-sender@1.0.11", "", { "dependencies": { "@types/chrome": "*" } }, "sha512-Pv3xvNYtxD/cTM/tKfuZRlLasvpxAm+CFni0GJd6Cp8XgiZS9g9tMZkR1uymsi5fIFv057SZKKAWVFFgy7fJtw=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/connect-history-api-fallback": ["@types/connect-history-api-fallback@1.5.4", "", { "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw=="], @@ -606,6 +611,12 @@ "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="], + "@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="], + + "@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="], + + "@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="], + "@types/html-minifier-terser": ["@types/html-minifier-terser@6.1.0", "", {}, "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="], "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], diff --git a/package.json b/package.json index 8c027c7..7a26535 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ }, "devDependencies": { "@biomejs/biome": "2.5.3", + "@types/chromecast-caf-sender": "^1.0.11", "@types/jest": "^30.0.0", "@types/react": "19.2.17", "nitrogen": "0.36.1", diff --git a/src/player.web.tsx b/src/player.web.tsx index 6167e6a..ed16cce 100644 --- a/src/player.web.tsx +++ b/src/player.web.tsx @@ -244,26 +244,22 @@ export class WebOmniPlayer implements OmniPlayer { this.setOverlaySubtitle(overlay ?? null); if (this.castStatus === "connected") { - try { - // if the cast receiver handles overlay subtitles like us, send this message. - const ctx = ( - window as unknown as { - // biome-ignore lint/suspicious/noExplicitAny: cast sender SDK global - cast?: { framework?: { CastContext?: { getInstance(): any } } }; - } - ).cast?.framework?.CastContext?.getInstance?.(); - ctx - ?.getCurrentSession?.() - ?.sendMessage("urn:x-cast:dev.zoriya.omni", { - subtitle: overlay?.id ?? null, - }) - ?.catch?.(() => {}); - } catch { - // no active cast session / sender SDK not loaded - } + window.cast.framework.CastContext.getInstance() + .getCurrentSession() + ?.sendMessage("urn:x-cast:dev.zoriya.omni", { + subtitle: overlay?.id ?? null, + }) + .catch(() => {}); } } + // To be used on a callback of a chromecast session, it only changes the local state + applyRemoteSubtitle(id: string | null): void { + this.setOverlaySubtitle( + id ? (this.overlaySubtitles.find((s) => s.id === id) ?? null) : null, + ); + } + private setOverlaySubtitle(sub: Subtitle | null): void { if (this.overlaySubtitle === sub) return; this.overlaySubtitle = sub; diff --git a/src/provider.web.tsx b/src/provider.web.tsx index e972276..9afed44 100644 --- a/src/provider.web.tsx +++ b/src/provider.web.tsx @@ -72,6 +72,44 @@ const PlayerInitializer = ({ player.showNotification = showNotification; }, [showNotification]); + useEffect(() => { + const ctx = window.cast?.framework?.CastContext?.getInstance(); + if (!window.cast?.framework || !ctx) return; + + let attached: cast.framework.CastSession | null = null; + + const onSessionChange = () => { + const session = ctx.getCurrentSession(); + if (!session || session === attached) return; + attached = session; + session.addMessageListener( + "urn:x-cast:dev.zoriya.omni", + (_ns: string, message: string) => { + let data: unknown; + try { + data = JSON.parse(message); + } catch { + return; + } + if (data && typeof data === "object" && "subtitle" in data) { + const id = (data as { subtitle?: unknown }).subtitle; + player.applyRemoteSubtitle(typeof id === "string" ? id : null); + } + }, + ); + }; + onSessionChange(); + ctx.addEventListener( + window.cast?.framework.CastContextEventType.SESSION_STATE_CHANGED, + onSessionChange, + ); + return () => + ctx.removeEventListener( + window.cast?.framework.CastContextEventType.SESSION_STATE_CHANGED, + onSessionChange, + ); + }, []); + return {children}; }; diff --git a/tsconfig.json b/tsconfig.json index d66df93..ae315d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "skipLibCheck": true, "strict": true, "target": "esnext", + "types": ["chromecast-caf-sender"], "verbatimModuleSyntax": true }, "exclude": [