diff --git a/frontend/src/lib/playerVolume.test.ts b/frontend/src/lib/playerVolume.test.ts index 5a941757..795573d3 100644 --- a/frontend/src/lib/playerVolume.test.ts +++ b/frontend/src/lib/playerVolume.test.ts @@ -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); }); diff --git a/frontend/src/lib/plexPlayerHooks.ts b/frontend/src/lib/plexPlayerHooks.ts index 3a92ccca..8c684a55 100644 --- a/frontend/src/lib/plexPlayerHooks.ts +++ b/frontend/src/lib/plexPlayerHooks.ts @@ -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; 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