Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions apps/web/src/components/player-defaults.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -74,7 +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 === defaultHeight,
(o) => defaultHeight !== null && qualityOptionHeight(o) === defaultHeight,
);
const match = exactMatch ?? heightMatch;
if (!match) return;
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/components/quality-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,7 +24,8 @@ const QUALITY_OPTIONS = { sort: "descending" } as const;
type QualityOption = ReturnType<typeof useVideoQualityOptions>[number];

function qualityValue(option: QualityOption): string {
return String(option.quality?.height ?? option.label);
const height = qualityOptionHeight(option);
return height !== null ? String(height) : option.label;
}

function collectResolutionOptions(options: QualityOption[]): QualityOption[] {
Expand Down Expand Up @@ -131,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) => (o.quality?.height ?? 0) === 0)) return null;
if (filteredOptions.every((o) => (qualityOptionHeight(o) ?? 0) === 0)) return null;

if (!selected) return null;
const current = selected.label;
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/lib/player-quality.ts
Original file line number Diff line number Diff line change
@@ -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);
}
17 changes: 17 additions & 0 deletions apps/web/tests/player-quality.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});