Files
siftlode/frontend/src/lib/playerVolume.ts
T
peter 36653886d5 build(frontend)!: knip drops --production; plain knip stays
The production pass (7ed2ac4e, E32) asked "is this export used by what
ships?". No real hit: its only findings were 37 exports a test imports,
answered with 36 `@internal` JSDoc tags (E34 S2) — test-only exports, not
defects. Since then it has only ever asked for another tag. The tags go
with it (36, no longer read by anything), and so do the production-only
`!…!` exclusions in knip.jsonc and their register rows. Plain knip (unused
files, exports and dependencies) keeps running in the gate.
2026-10-05 00:19:38 +02:00

71 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Volume arithmetic for BOTH players, kept out of the components so it can be unit-tested. Every
// rule here was established by a bug: a wheel spin that lost notches to the iframe's lagging
// getVolume, a partial stored object that became NaN and persisted as `null`, and a "corrupt →
// default" fallback that jumped an already-quiet session to full blast.
//
// The two players run on different native scales — the YouTube iframe API takes 0–100 integers, the
// HTML5 <video> element a 0..1 float with a separate `.muted` flag — so the pure core below is
// scale-agnostic (a `VolumeScale` parameterises the range, default, and integer rounding) and each
// player binds it to its own scale. The YouTube-bound wrappers keep the original 0–100 API.
/** A player's volume scale. `round` is true for the integer YouTube API, false for the 0..1 float. */
export interface VolumeScale {
min: number;
max: number;
/** Level used when nothing trustworthy is available. */
default: number;
round: boolean;
}
/**
* YouTube iframe API: 0–100 integers, no mute flag (level 0 IS muted).
*/
export const YT_SCALE: VolumeScale = { min: 0, max: 100, default: 100, round: true };
/** HTML5 <video>: a 0..1 float (mute is a separate flag the caller carries). */
export const PLEX_SCALE: VolumeScale = { min: 0, max: 1, default: 1, round: false };
function clampTo(n: number, s: VolumeScale): number {
const c = Math.max(s.min, Math.min(s.max, n));
return s.round ? Math.round(c) : c;
}
/**
* Coerce anything that claims to be a volume into a usable level ON `scale`.
*
* Only a real, finite `number` is accepted: the stored blob is JSON, so a level is a number or it
* is corrupt — `null`, `undefined`, `"80"` and `NaN` all mean "we don't know", and `Number(null)`
* being `0` (silently muting the player) is exactly the coercion to avoid. `fallback` is the last
* level known to work; it is itself validated, so a corrupt value can never travel through it.
*/
export function normalizeVolumeIn(raw: unknown, fallback: unknown, scale: VolumeScale): number {
if (typeof raw === "number" && Number.isFinite(raw)) return clampTo(raw, scale);
if (typeof fallback === "number" && Number.isFinite(fallback)) return clampTo(fallback, scale);
return scale.default;
}
/** One volume step (wheel notch / arrow key) from `current`, on `scale`.
*
* `delta` and `fallback` ride in an options object on purpose: as two adjacent `number`
* parameters they were transposable without a type error, and swapping them silently steps the
* volume BY the current level. */
export function stepVolumeIn(
current: unknown,
opts: { delta: number; fallback: unknown },
scale: VolumeScale,
): number {
return clampTo(normalizeVolumeIn(current, opts.fallback, scale) + opts.delta, scale);
}
// --- YouTube-bound convenience wrappers (0–100 integers) — the original API, behaviour unchanged. ---
/** Level used when nothing trustworthy is available (YouTube scale). */
export const DEFAULT_VOLUME = YT_SCALE.default;
export function normalizeVolume(raw: unknown, fallback: unknown = DEFAULT_VOLUME): number {
return normalizeVolumeIn(raw, fallback, YT_SCALE);
}
export function stepVolume(current: unknown, opts: { delta: number; fallback: unknown }): number {
return stepVolumeIn(current, opts, YT_SCALE);
}