fix(player): hold last-known Plex volume on a corrupt read, normalise once (review)
- applyVolume/setVol now pass the element's CURRENT level as the normalize fallback instead of undefined, so a corrupt persisted volume holds the playing level rather than jumping to PLEX_SCALE.default=1 (full) — the "full blast" failure mode playerVolume.ts's header explicitly warns against. (The old bare clamp muted instead; both are wrong for corrupt data.) readMerged shallow-merges stored prefs without per-field validation, so a corrupt volume is reachable. - setVol normalises the level ONCE and both applies + persists that same value, so the <video> element and the stored pref can never disagree. - Test documents the hold-last-known contract (corrupt raw + valid fallback -> fallback; only an unusable fallback lands on the scale default). Gate green (tsc / eslint-0 / prettier / knip / vitest 83 / pytest 234).
This commit is contained in:
@@ -74,12 +74,16 @@ describe("scale-generic core (shared by both players)", () => {
|
||||
expect(normalizeVolumeIn(-0.2, undefined, PLEX_SCALE)).toBe(0); // clamps to min
|
||||
});
|
||||
|
||||
it("falls back to the scale default on a corrupt Plex volume instead of muting", () => {
|
||||
it("holds the last-known level on a corrupt Plex volume, never jumping to full", () => {
|
||||
// The Plex-side latent bug this consolidation fixes: a bare clamp turned a corrupt persisted
|
||||
// volume (null / "0.5" / NaN) into 0 — silently muting — where the shared core falls back to the
|
||||
// last-known level, else PLEX_SCALE.default (1).
|
||||
expect(normalizeVolumeIn(null, undefined, PLEX_SCALE)).toBe(PLEX_SCALE.default);
|
||||
// volume (null / "0.5" / NaN) into 0 — silently muting. The hook passes the element's CURRENT
|
||||
// level as the fallback, so a corrupt read HOLDS that level rather than muting (old bug) OR
|
||||
// jumping to PLEX_SCALE.default=1 / full (the failure mode the header warns against).
|
||||
expect(normalizeVolumeIn(null, 0.3, PLEX_SCALE)).toBeCloseTo(0.3, 10); // holds a quiet 0.3
|
||||
expect(normalizeVolumeIn("0.5", 0.3, PLEX_SCALE)).toBeCloseTo(0.3, 10);
|
||||
expect(normalizeVolumeIn(NaN, 0.42, PLEX_SCALE)).toBeCloseTo(0.42, 10);
|
||||
// Only when the fallback is ALSO unusable does it land on the scale default.
|
||||
expect(normalizeVolumeIn(null, undefined, PLEX_SCALE)).toBe(PLEX_SCALE.default);
|
||||
expect(normalizeVolumeIn(NaN, NaN, PLEX_SCALE)).toBe(PLEX_SCALE.default);
|
||||
});
|
||||
|
||||
|
||||
@@ -142,7 +142,8 @@ export function usePlexProgress(opts: {
|
||||
// default volume after a load/seek-restart). The 0..1 arithmetic is the shared, unit-tested core in
|
||||
// playerVolume.ts bound to PLEX_SCALE (the YouTube player binds the same core to YT_SCALE, 0–100); the
|
||||
// separate .muted flag stays here since it's Plex-only. Using normalizeVolumeIn (not a bare clamp)
|
||||
// also means a corrupt persisted volume falls back to the default instead of silently muting.
|
||||
// means a corrupt persisted volume falls back to the element's CURRENT level (last applied) rather
|
||||
// than either silently muting (the old bare clamp) or jumping to full — see playerVolume.ts's header.
|
||||
export function usePlayerVolume(opts: {
|
||||
videoRef: RefObject<HTMLVideoElement | null>;
|
||||
volume: number;
|
||||
@@ -162,18 +163,23 @@ export function usePlayerVolume(opts: {
|
||||
(vol: number, mute: boolean) => {
|
||||
const v = videoRef.current;
|
||||
if (!v) return;
|
||||
v.volume = normalizeVolumeIn(vol, undefined, PLEX_SCALE);
|
||||
// Fallback = the element's current level, so a corrupt `vol` HOLDS the playing level instead
|
||||
// of jumping to full (PLEX_SCALE.default). On a fresh element that current level is the
|
||||
// browser default, which is the sensible "nothing to restore" value anyway.
|
||||
v.volume = normalizeVolumeIn(vol, v.volume, PLEX_SCALE);
|
||||
v.muted = mute;
|
||||
},
|
||||
[videoRef],
|
||||
);
|
||||
// Set volume/mute on the <video> AND persist (survives F5).
|
||||
// Set volume/mute on the <video> AND persist (survives F5). Normalise ONCE so the element and the
|
||||
// stored pref can never disagree; a corrupt input holds the element's current level.
|
||||
const setVol = useCallback(
|
||||
(vol: number, mute: boolean) => {
|
||||
applyVolume(vol, mute);
|
||||
patchPrefs({ volume: normalizeVolumeIn(vol, undefined, PLEX_SCALE), muted: mute });
|
||||
const level = normalizeVolumeIn(vol, videoRef.current?.volume, PLEX_SCALE);
|
||||
applyVolume(level, mute);
|
||||
patchPrefs({ volume: level, muted: mute });
|
||||
},
|
||||
[applyVolume, patchPrefs],
|
||||
[applyVolume, patchPrefs, videoRef],
|
||||
);
|
||||
const nudgeVolume = useCallback(
|
||||
(d: number) => {
|
||||
|
||||
Reference in New Issue
Block a user