fix(player): give YouTube's own fullscreen a working path + round-6 findings
The reported bug: after F, the embed's own fullscreen button is dead. Root cause
measured in Chrome — the browser's fullscreen lock sits on OUR wrapper, so
YouTube's button would need a nested request from a cross-origin frame. Handing
the lock to the iframe fixes it, but `iframe.requestFullscreen()` only resolves
from a CLICK (from a key press the promise stays pending forever, and awaiting
it spends the activation so a fallback is denied too).
So: F keeps fullscreening our wrapper (unchanged, our overlays stay visible) and
a new toolbar button hands the lock to the iframe — verified in real Chrome:
document.fullscreenElement is the IFRAME at 1920x1024, i.e. a plain YouTube
fullscreen where its own controls work.
Round-6 review findings, all fixed:
- one _rate_limiter + one _cancel_poll shared by the yt-dlp hook and run_ffmpeg
(was three hand-rolled throttles; cancel latency differed 2s vs 1s)
- stepVolume takes {delta, fallback} — two adjacent number args were silently
transposable
- volume gestures before the player is ready are honoured instead of dropped
- normalizeVolume's fallback is typed unknown (its guard was unreachable) and
applyVolume takes a number again
- the throttle tests inject a clock instead of patching global time.monotonic
This commit is contained in:
@@ -34,24 +34,25 @@ describe("normalizeVolume", () => {
|
||||
|
||||
it("uses the default when the fallback is unusable too", () => {
|
||||
expect(normalizeVolume(NaN, NaN)).toBe(DEFAULT_VOLUME);
|
||||
expect(normalizeVolume(null, undefined as unknown as number)).toBe(DEFAULT_VOLUME);
|
||||
expect(normalizeVolume(null, undefined)).toBe(DEFAULT_VOLUME);
|
||||
expect(normalizeVolume(null, "80")).toBe(DEFAULT_VOLUME);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stepVolume", () => {
|
||||
it("steps up and down from the current level", () => {
|
||||
expect(stepVolume(60, 5, 100)).toBe(65);
|
||||
expect(stepVolume(60, -5, 100)).toBe(55);
|
||||
expect(stepVolume(60, { delta: 5, fallback: 100 })).toBe(65);
|
||||
expect(stepVolume(60, { delta: -5, fallback: 100 })).toBe(55);
|
||||
});
|
||||
|
||||
it("clamps at both ends instead of wrapping", () => {
|
||||
expect(stepVolume(98, 5, 100)).toBe(100);
|
||||
expect(stepVolume(3, -5, 100)).toBe(0);
|
||||
expect(stepVolume(98, { delta: 5, fallback: 100 })).toBe(100);
|
||||
expect(stepVolume(3, { delta: -5, fallback: 100 })).toBe(0);
|
||||
});
|
||||
|
||||
it("steps from the fallback when the player answers garbage", () => {
|
||||
// A spin must not lose notches to a stale/unusable reading — it continues from what we last set.
|
||||
expect(stepVolume(undefined, -5, 90)).toBe(85);
|
||||
expect(stepVolume(NaN, -5, 90)).toBe(85);
|
||||
expect(stepVolume(undefined, { delta: -5, fallback: 90 })).toBe(85);
|
||||
expect(stepVolume(NaN, { delta: -5, fallback: 90 })).toBe(85);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,15 +16,18 @@ const clamp = (n: number) => Math.max(0, Math.min(100, Math.round(n)));
|
||||
* 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 normalizeVolume(raw: unknown, fallback: number = DEFAULT_VOLUME): number {
|
||||
export function normalizeVolume(raw: unknown, fallback: unknown = DEFAULT_VOLUME): number {
|
||||
if (typeof raw === "number" && Number.isFinite(raw)) return clamp(raw);
|
||||
if (typeof fallback === "number" && Number.isFinite(fallback)) return clamp(fallback);
|
||||
return DEFAULT_VOLUME;
|
||||
}
|
||||
|
||||
/** One volume step (wheel notch / arrow key) from `current`, falling back to `fallback` when the
|
||||
* player answered something unusable. */
|
||||
export function stepVolume(current: unknown, delta: number, fallback: number): number {
|
||||
const base = normalizeVolume(current, fallback);
|
||||
return clamp(base + delta);
|
||||
/** One volume step (wheel notch / arrow key) from `current`.
|
||||
*
|
||||
* `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 stepVolume(current: unknown, opts: { delta: number; fallback: unknown }): number {
|
||||
const base = normalizeVolume(current, opts.fallback);
|
||||
return clamp(base + opts.delta);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user