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:
2026-08-13 03:43:02 +02:00
parent cba992379c
commit 92ef6116f0
2 changed files with 20 additions and 10 deletions
+8 -4
View File
@@ -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);
});