From bcfa8c6d635af454c1d2714b31ee34490c29ea7f Mon Sep 17 00:00:00 2001 From: Uruk Date: Thu, 21 May 2026 02:13:31 +0200 Subject: [PATCH] feat(casting): add Chromecast capability detection --- utils/casting/capabilities.test.ts | 69 ++++++++++++++++++++ utils/casting/capabilities.ts | 101 +++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 utils/casting/capabilities.test.ts create mode 100644 utils/casting/capabilities.ts diff --git a/utils/casting/capabilities.test.ts b/utils/casting/capabilities.test.ts new file mode 100644 index 000000000..cee80b006 --- /dev/null +++ b/utils/casting/capabilities.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "bun:test"; +import { CONSERVATIVE_CAPABILITIES, detectCapabilities } from "./capabilities"; + +describe("detectCapabilities", () => { + test("unknown device falls back to the conservative baseline", () => { + const caps = detectCapabilities( + { modelName: "Some Unknown TV" }, + { profileMode: "auto" }, + ); + expect(caps).toEqual(CONSERVATIVE_CAPABILITIES); + }); + + test("null device falls back to the conservative baseline", () => { + const caps = detectCapabilities(null, { profileMode: "auto" }); + expect(caps).toEqual(CONSERVATIVE_CAPABILITIES); + }); + + test('plain "Chromecast" (gen 1/2/3) gets the conservative baseline', () => { + const caps = detectCapabilities( + { modelName: "Chromecast" }, + { profileMode: "auto" }, + ); + expect(caps.hevc).toBe(false); + expect(caps.maxResolution).toBe(1080); + expect(caps.maxAudioChannels).toBe(2); + }); + + test("Chromecast Ultra is recognised with HEVC + 4K", () => { + const caps = detectCapabilities( + { modelName: "Chromecast Ultra" }, + { profileMode: "auto" }, + ); + expect(caps.hevc).toBe(true); + expect(caps.maxResolution).toBe(2160); + }); + + test('"force-h264" override disables HEVC even on a capable device', () => { + const caps = detectCapabilities( + { modelName: "Chromecast Ultra" }, + { profileMode: "force-h264" }, + ); + expect(caps.hevc).toBe(false); + expect(caps.hevc10bit).toBe(false); + }); + + test('"force-hevc" override enables HEVC on the conservative baseline', () => { + const caps = detectCapabilities( + { modelName: "Chromecast" }, + { profileMode: "force-hevc" }, + ); + expect(caps.hevc).toBe(true); + }); + + test("maxBitrate override clamps but never raises the bitrate", () => { + const lowered = detectCapabilities( + { modelName: "Chromecast" }, + { profileMode: "auto", maxBitrate: 3_000_000 }, + ); + expect(lowered.maxVideoBitrate).toBe(3_000_000); + + const raised = detectCapabilities( + { modelName: "Chromecast" }, + { profileMode: "auto", maxBitrate: 999_000_000 }, + ); + expect(raised.maxVideoBitrate).toBe( + CONSERVATIVE_CAPABILITIES.maxVideoBitrate, + ); + }); +}); diff --git a/utils/casting/capabilities.ts b/utils/casting/capabilities.ts new file mode 100644 index 000000000..d6e62ea71 --- /dev/null +++ b/utils/casting/capabilities.ts @@ -0,0 +1,101 @@ +/** + * Chromecast device capability detection. + * + * The Cast SDK exposes a device's `modelName` but no codec-level capability API. + * We map known model names to a capability profile and fall back to a conservative + * baseline (H.264 / 1080p / stereo) for anything unrecognised — a baseline that + * cannot produce an unplayable stream on any Cast receiver. + */ + +/** Profile selection mode, surfaced as an advanced setting. */ +export type ChromecastProfileMode = "auto" | "force-hevc" | "force-h264"; + +export interface ChromecastCapabilities { + /** HEVC 8-bit (Main profile) decode support. */ + hevc: boolean; + /** HEVC 10-bit (Main10) decode support. */ + hevc10bit: boolean; + /** Maximum video resolution height. */ + maxResolution: 1080 | 2160; + /** Maximum video bitrate in bits per second. */ + maxVideoBitrate: number; + /** Maximum audio channels the receiver can output. */ + maxAudioChannels: number; +} + +/** Minimal shape we need from the Cast SDK `Device` — keeps this module import-free. */ +interface DeviceLike { + modelName?: string; +} + +/** Overrides derived from user settings. */ +export interface CapabilityOverrides { + profileMode: ChromecastProfileMode; + /** Optional manual cap in bits per second. */ + maxBitrate?: number; +} + +/** + * Baseline for a 1st/2nd/3rd-gen Chromecast and any unrecognised device. + * `maxVideoBitrate` is an initial estimate — see docs/chromecast-test-matrix.md. + */ +export const CONSERVATIVE_CAPABILITIES: ChromecastCapabilities = { + hevc: false, + hevc10bit: false, + maxResolution: 1080, + maxVideoBitrate: 8_000_000, + maxAudioChannels: 2, +}; + +/** Known Cast devices keyed by `Device.modelName`. Unlisted models stay conservative. */ +const CHROMECAST_REGISTRY: Record = { + "Chromecast Ultra": { + hevc: true, + hevc10bit: false, + maxResolution: 2160, + maxVideoBitrate: 20_000_000, + maxAudioChannels: 6, + }, + "Chromecast with Google TV": { + hevc: true, + hevc10bit: true, + maxResolution: 2160, + maxVideoBitrate: 20_000_000, + maxAudioChannels: 6, + }, + "Google TV Streamer": { + hevc: true, + hevc10bit: true, + maxResolution: 2160, + maxVideoBitrate: 25_000_000, + maxAudioChannels: 8, + }, +}; + +/** + * Resolve the effective capabilities for a Cast device. + * Registry lookup → conservative fallback → user overrides applied last. + */ +export const detectCapabilities = ( + device: DeviceLike | null, + overrides: CapabilityOverrides, +): ChromecastCapabilities => { + const base = + (device?.modelName && CHROMECAST_REGISTRY[device.modelName]) || + CONSERVATIVE_CAPABILITIES; + + const caps: ChromecastCapabilities = { ...base }; + + if (overrides.profileMode === "force-hevc") { + caps.hevc = true; + } else if (overrides.profileMode === "force-h264") { + caps.hevc = false; + caps.hevc10bit = false; + } + + if (overrides.maxBitrate && overrides.maxBitrate > 0) { + caps.maxVideoBitrate = Math.min(caps.maxVideoBitrate, overrides.maxBitrate); + } + + return caps; +};