fix(r5): address the fifth /code-review high round — 6 findings

- run_ffmpeg stops the process from ONE exit path (`except BaseException`), so
  an exception out of on_progress (a DB write) can no longer orphan a live
  ffmpeg after a 10s join on its pipes; _stop is now a no-op on a dead process
- both run_ffmpeg callbacks are throttled to ~1s (they were a DB round-trip per
  output line: ~10^5 queries on a long re-encode), mirroring the download hook
- volume arithmetic extracted to lib/playerVolume with unit tests; a corrupt
  level now falls back to the LAST GOOD one instead of full blast, and the
  sanitising happens where the stored value enters
- RSS is back to two except clauses (the shared tail already sits after the try)
- new tests: callback-exception cleanup, the two throttles; the fake Popen grew
  poll() and its wait count is now asserted

Mutation-checked: each new group goes red when its fix is reverted.
This commit is contained in:
2026-07-24 01:50:50 +02:00
parent fc82c8b47a
commit c4942eb590
8 changed files with 268 additions and 33 deletions
+13 -16
View File
@@ -35,6 +35,7 @@ import {
} from "../lib/format";
import { renderDescription } from "../lib/descriptionLinks";
import { useBackToClose } from "../lib/history";
import { DEFAULT_VOLUME, normalizeVolume, stepVolume } from "../lib/playerVolume";
import { LS, readAccountMerged, writeAccount } from "../lib/storage";
import { useScrollFade } from "../lib/useScrollFade";
@@ -188,12 +189,12 @@ export default function PlayerModal({
// (the on-player flash has its own `volumeUi` state), the once-bound wheel/key listeners need a
// ref anyway, and the hook's write lives inside a `setState` updater — which is both a second
// write on every settle and not something React must run while the component is unmounting.
// `readAccountMerged` (not readAccount) so a partial/older stored object still gets the default
// — a missing `volume` would otherwise travel as undefined into the arithmetic below. Read via
// a lazy `useState` initializer so it happens once, not on every render (the house pattern —
// see the frozen queue above).
const [storedVolume] = useState(
() => readAccountMerged(LS.ytPlayerPrefs, { volume: 100 }).volume,
// Sanitised HERE, at the boundary where the untrusted value enters (`normalizeVolume`, see
// lib/playerVolume): a stored `null` / `"80"` / missing key becomes the default once, instead of
// travelling into the arithmetic. Read via a lazy `useState` initializer so it happens once, not
// on every render (the house pattern — see the frozen queue above).
const [storedVolume] = useState(() =>
normalizeVolume(readAccountMerged(LS.ytPlayerPrefs, { volume: DEFAULT_VOLUME }).volume),
);
const volumeRef = useRef(storedVolume);
const volSaveTimerRef = useRef<number | undefined>(undefined);
@@ -227,10 +228,10 @@ export default function PlayerModal({
};
/** Push a level (0–100) to the player and remember it; 0 also mutes (a player left at zero
* volume but unmuted shows YouTube's speaker icon as ON, which reads as broken). */
const applyVolume = (level: number, flash = true) => {
// A non-finite level (a corrupt stored value, a player that answered NaN) must not become a
// clamped NaN: that would mute the player AND persist `null`, poisoning every later session.
const next = Number.isFinite(level) ? Math.max(0, Math.min(100, Math.round(level))) : 100;
const applyVolume = (level: unknown, flash = true) => {
// An unusable level (a player that answered NaN) falls back to the LAST GOOD one, never to the
// default: jumping a quiet session to full blast is the surprise this feature exists to avoid.
const next = normalizeVolume(level, volumeRef.current);
const p = playerRef.current;
if (p && typeof p.setVolume === "function") {
if (next > 0) p.unMute?.();
@@ -255,12 +256,8 @@ export default function PlayerModal({
const now = Date.now();
const continuing = now - lastNudgeAtRef.current < 1000;
lastNudgeAtRef.current = now;
const cur = continuing
? volumeRef.current
: p.isMuted?.()
? 0
: (p.getVolume?.() ?? volumeRef.current);
applyVolume(cur + delta);
const cur = continuing ? volumeRef.current : p.isMuted?.() ? 0 : p.getVolume?.();
applyVolume(stepVolume(cur, delta, volumeRef.current), true);
};
// The player can navigate to other videos (YouTube links in a description). The