From 18e985622704cbb420cea6be1955d4f2cab1d205 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:13:51 -0400 Subject: [PATCH] fix(web): detect HDR via video-dynamic-range media query for Firefox (#259) * fix(web): detect HDR via video-dynamic-range media query for Firefox Firefox's (dynamic-range: high) query reflects the browser canvas and reports standard even on HDR-enabled displays, so the player capability check always sent hdr=false. Firefox 116+ exposes the video plane via (video-dynamic-range: high); accept either query. Unknown media features parse as non-matching, so the extra query is safe in other browsers. Closes #206 Co-Authored-By: Claude Opus 4.8 * fix(web): keep Window receiver when passing matchMedia to HDR helper Invoking a detached matchMedia reference throws (Illegal invocation / TypeError) in some browsers, crashing capability detection instead of reporting HDR. Wrap it in an arrow so the Window receiver is preserved. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 --- .../player/hooks/useCodecDetection.test.ts | 23 ++++++++++++++++++- web/src/player/hooks/useCodecDetection.ts | 22 ++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) 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 }; }, []);