From af67b139a76c796ff82e37bca5ca4c5e13b030de Mon Sep 17 00:00:00 2001 From: Situla <196762523+Aquarius-Situla@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:22:01 +0800 Subject: [PATCH 1/3] fix: improve quality resolution height resolution and fallback in QualitySelector --- apps/web/src/components/quality-selector.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/web/src/components/quality-selector.tsx b/apps/web/src/components/quality-selector.tsx index 4610d618..24db4adc 100644 --- a/apps/web/src/components/quality-selector.tsx +++ b/apps/web/src/components/quality-selector.tsx @@ -22,8 +22,24 @@ const QUALITY_OPTIONS = { sort: "descending" } as const; type QualityOption = ReturnType[number]; +function qualityLabelHeight(label: string): number | null { + const match = label.match(/(\d+)/); + if (!match) return null; + const height = Number(match[1]); + return Number.isFinite(height) ? height : null; +} + +function optionHeight(option: QualityOption): number | null { + if (option.quality?.height && option.quality.height > 0) return option.quality.height; + if ("height" in option && typeof (option as { height?: unknown }).height === "number") { + return (option as { height: number }).height; + } + return qualityLabelHeight(option.label); +} + function qualityValue(option: QualityOption): string { - return String(option.quality?.height ?? option.label); + const height = optionHeight(option); + return height !== null ? String(height) : option.label; } function collectResolutionOptions(options: QualityOption[]): QualityOption[] { @@ -131,7 +147,7 @@ export function QualitySelector() { const radioOptions = filteredOptions.map((o) => ({ label: o.label, value: qualityValue(o) })); if (filteredOptions.length <= 1) return null; - if (filteredOptions.every((o) => (o.quality?.height ?? 0) === 0)) return null; + if (filteredOptions.every((o) => (optionHeight(o) ?? 0) === 0)) return null; if (!selected) return null; const current = selected.label; From 8539848e55cf7a8876a70bbd9080edbe3fbc561c Mon Sep 17 00:00:00 2001 From: Situla <196762523+Aquarius-Situla@users.noreply.github.com> Date: Sat, 26 Sep 2026 20:22:23 +0800 Subject: [PATCH 2/3] fix: fallback to label height in PlayerDefaults quality matching --- apps/web/src/components/player-defaults.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/player-defaults.tsx b/apps/web/src/components/player-defaults.tsx index 6a6b4ee5..ed5c18b9 100644 --- a/apps/web/src/components/player-defaults.tsx +++ b/apps/web/src/components/player-defaults.tsx @@ -74,7 +74,11 @@ export function PlayerDefaults({ const defaultHeight = qualityLabelHeight(defaultQuality); const exactMatch = qualityOptions.find((o) => o.label === defaultQuality); const heightMatch = qualityOptions.find( - (o) => defaultHeight !== null && o.quality?.height === defaultHeight, + (o) => + defaultHeight !== null && + ((o.quality?.height && o.quality.height > 0 + ? o.quality.height + : qualityLabelHeight(o.label)) === defaultHeight), ); const match = exactMatch ?? heightMatch; if (!match) return; From 1b2f4b29c7611e3ffb7c394a955bfc470bb1128d Mon Sep 17 00:00:00 2001 From: Priveetee Date: Sat, 26 Sep 2026 18:00:38 +0000 Subject: [PATCH 3/3] fix: handle zero quality heights --- apps/web/src/components/player-defaults.tsx | 14 ++------------ apps/web/src/components/quality-selector.tsx | 20 +++----------------- apps/web/src/lib/player-quality.ts | 18 ++++++++++++++++++ apps/web/tests/player-quality.test.ts | 17 +++++++++++++++++ 4 files changed, 40 insertions(+), 29 deletions(-) create mode 100644 apps/web/src/lib/player-quality.ts create mode 100644 apps/web/tests/player-quality.test.ts diff --git a/apps/web/src/components/player-defaults.tsx b/apps/web/src/components/player-defaults.tsx index ed5c18b9..0e534525 100644 --- a/apps/web/src/components/player-defaults.tsx +++ b/apps/web/src/components/player-defaults.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from "react"; import { normalizeDefaultPlaybackSpeed } from "../lib/playback-speed"; +import { qualityLabelHeight, qualityOptionHeight } from "../lib/player-quality"; import type { SabrPlaybackRatePreference } from "../lib/sabr-playback-rate-preference"; import { useAudioOptions, @@ -30,13 +31,6 @@ type PlaybackSpeedDefaultProps = { preference?: SabrPlaybackRatePreference; }; -function qualityLabelHeight(label: string): number | null { - const match = label.match(/(\d+)/); - if (!match) return null; - const height = Number(match[1]); - return Number.isFinite(height) ? height : null; -} - export function PlayerDefaults({ defaultQuality, defaultPlaybackSpeed, @@ -74,11 +68,7 @@ export function PlayerDefaults({ const defaultHeight = qualityLabelHeight(defaultQuality); const exactMatch = qualityOptions.find((o) => o.label === defaultQuality); const heightMatch = qualityOptions.find( - (o) => - defaultHeight !== null && - ((o.quality?.height && o.quality.height > 0 - ? o.quality.height - : qualityLabelHeight(o.label)) === defaultHeight), + (o) => defaultHeight !== null && qualityOptionHeight(o) === defaultHeight, ); const match = exactMatch ?? heightMatch; if (!match) return; diff --git a/apps/web/src/components/quality-selector.tsx b/apps/web/src/components/quality-selector.tsx index 24db4adc..c9940ea7 100644 --- a/apps/web/src/components/quality-selector.tsx +++ b/apps/web/src/components/quality-selector.tsx @@ -3,6 +3,7 @@ import { useRef } from "react"; import { useInterfaceLocale } from "../hooks/use-interface-locale"; import { useDashPlayerSnapshot } from "../lib/dash-player-store"; import { dashQualityOptions, selectDashTrack, selectedDashHeight } from "../lib/dash-video"; +import { qualityOptionHeight } from "../lib/player-quality"; import { sabrResolutionOptions } from "../lib/sabr-quality-selection"; import type { DefaultLayoutIcon, MenuInstance } from "../lib/vidstack"; import { @@ -22,23 +23,8 @@ const QUALITY_OPTIONS = { sort: "descending" } as const; type QualityOption = ReturnType[number]; -function qualityLabelHeight(label: string): number | null { - const match = label.match(/(\d+)/); - if (!match) return null; - const height = Number(match[1]); - return Number.isFinite(height) ? height : null; -} - -function optionHeight(option: QualityOption): number | null { - if (option.quality?.height && option.quality.height > 0) return option.quality.height; - if ("height" in option && typeof (option as { height?: unknown }).height === "number") { - return (option as { height: number }).height; - } - return qualityLabelHeight(option.label); -} - function qualityValue(option: QualityOption): string { - const height = optionHeight(option); + const height = qualityOptionHeight(option); return height !== null ? String(height) : option.label; } @@ -147,7 +133,7 @@ export function QualitySelector() { const radioOptions = filteredOptions.map((o) => ({ label: o.label, value: qualityValue(o) })); if (filteredOptions.length <= 1) return null; - if (filteredOptions.every((o) => (optionHeight(o) ?? 0) === 0)) return null; + if (filteredOptions.every((o) => (qualityOptionHeight(o) ?? 0) === 0)) return null; if (!selected) return null; const current = selected.label; diff --git a/apps/web/src/lib/player-quality.ts b/apps/web/src/lib/player-quality.ts new file mode 100644 index 00000000..d1bdd62a --- /dev/null +++ b/apps/web/src/lib/player-quality.ts @@ -0,0 +1,18 @@ +type QualityOptionLike = { + label: string; + height?: number; + quality?: { height?: number | null } | null; +}; + +export function qualityLabelHeight(label: string): number | null { + const match = label.match(/(\d+)/); + if (!match) return null; + const height = Number(match[1]); + return Number.isFinite(height) && height > 0 ? height : null; +} + +export function qualityOptionHeight(option: QualityOptionLike): number | null { + if (option.quality?.height && option.quality.height > 0) return option.quality.height; + if (option.height && option.height > 0) return option.height; + return qualityLabelHeight(option.label); +} diff --git a/apps/web/tests/player-quality.test.ts b/apps/web/tests/player-quality.test.ts new file mode 100644 index 00000000..810a191f --- /dev/null +++ b/apps/web/tests/player-quality.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test"; +import { qualityLabelHeight, qualityOptionHeight } from "../src/lib/player-quality"; + +test("prefers the quality height and falls back to a positive option height", () => { + expect(qualityOptionHeight({ label: "720p", quality: { height: 1080 } })).toBe(1080); + expect(qualityOptionHeight({ label: "720p", height: 1080 })).toBe(1080); +}); + +test("falls back to the label when heights are missing or zero", () => { + expect(qualityOptionHeight({ label: "720p", quality: { height: 0 }, height: 0 })).toBe(720); + expect(qualityOptionHeight({ label: "1080p60" })).toBe(1080); +}); + +test("rejects labels without a positive resolution", () => { + expect(qualityLabelHeight("Auto")).toBeNull(); + expect(qualityLabelHeight("0p")).toBeNull(); +});