diff --git a/web/src/player/hooks/useCodecDetection.test.ts b/web/src/player/hooks/useCodecDetection.test.ts index 69d98168..4c6f365d 100644 --- a/web/src/player/hooks/useCodecDetection.test.ts +++ b/web/src/player/hooks/useCodecDetection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { detectMaxResolutionFromScreen } from "./useCodecDetection"; +import { detectHDRFromMatchMedia, detectMaxResolutionFromScreen } from "./useCodecDetection"; describe("detectMaxResolutionFromScreen", () => { it("treats a 2560x1440 display as above the 720p bucket", () => { @@ -10,3 +10,24 @@ describe("detectMaxResolutionFromScreen", () => { expect(detectMaxResolutionFromScreen(1280, 720)).toBe("720p"); }); }); + +describe("detectHDRFromMatchMedia", () => { + const fakeMatchMedia = (matching: string[]) => + ((query: string) => ({ matches: matching.includes(query) })) as unknown as typeof matchMedia; + + it("returns false when matchMedia is unavailable", () => { + expect(detectHDRFromMatchMedia(undefined)).toBe(false); + }); + + it("returns true when dynamic-range reports high", () => { + expect(detectHDRFromMatchMedia(fakeMatchMedia(["(dynamic-range: high)"]))).toBe(true); + }); + + it("returns true when only video-dynamic-range reports high (Firefox)", () => { + expect(detectHDRFromMatchMedia(fakeMatchMedia(["(video-dynamic-range: high)"]))).toBe(true); + }); + + it("returns false when neither query matches", () => { + expect(detectHDRFromMatchMedia(fakeMatchMedia([]))).toBe(false); + }); +}); diff --git a/web/src/player/hooks/useCodecDetection.ts b/web/src/player/hooks/useCodecDetection.ts index 774f8a9e..69ca0730 100644 --- a/web/src/player/hooks/useCodecDetection.ts +++ b/web/src/player/hooks/useCodecDetection.ts @@ -82,6 +82,21 @@ export function getPlaybackEnvironmentSnapshot(): PlaybackEnvironmentSnapshot | }; } +/** + * Detects HDR display support (best effort). Firefox's `dynamic-range` query + * reflects the browser canvas and reports `standard` even on HDR displays; + * the video plane is exposed via `video-dynamic-range` (Firefox 116+), so + * accept either. Browsers treat unknown media features as non-matching, so + * querying both is safe everywhere. + */ +export function detectHDRFromMatchMedia(matchMediaFn: typeof matchMedia | undefined): boolean { + if (!matchMediaFn) return false; + return ( + matchMediaFn("(dynamic-range: high)").matches || + matchMediaFn("(video-dynamic-range: high)").matches + ); +} + function testCodec(mimeWithCodec: string): boolean { if (typeof MediaSource === "undefined") return false; try { @@ -170,8 +185,11 @@ export function useCodecDetection() { // Detect max resolution via reported screen dimensions. const max_resolution = detectMaxResolutionFromScreen(screen.width, screen.height); - // HDR detection (best effort). - const hdr = typeof matchMedia !== "undefined" && matchMedia("(dynamic-range: high)").matches; + // HDR detection (best effort). Wrap matchMedia so it keeps its Window + // receiver — invoking a detached reference throws in some browsers. + const hdr = detectHDRFromMatchMedia( + typeof matchMedia !== "undefined" ? (query) => matchMedia(query) : undefined, + ); return { codecs_video, codecs_audio, containers, max_resolution, hdr }; }, []);