revert(player): drop the transform-scale HD hack (didn't beat YouTube's cap)

Confirmed on prod: rendering the iframe at 1920x1080 logical and CSS transform:
scale()-ing it down does NOT lift YouTube's quality cap — the embed stays ~360p in
the windowed player and manual HD still snaps back; only true fullscreen unlocks
1080p (which then persists for the session). YouTube caps by the on-screen size, not
the iframe's window.innerWidth, so the transform only shrank YouTube's native
controls for no benefit. Restore the plain 100%/100% mount.

Kept: scroll-anywhere volume (works), the native-menu-yield fix, max-w-6xl, and the
harmless vq=hd1080 hint.
This commit is contained in:
npeter83 2026-07-10 16:56:24 +02:00
parent 42f112d4df
commit 71e0a40481

View file

@ -1,4 +1,4 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { createPortal } from "react-dom";
import { useQuery, useQueryClient } from "@tanstack/react-query";
@ -20,15 +20,6 @@ import { useBackToClose } from "../lib/history";
// How close to the end (seconds) counts as "finished" → auto-mark watched.
const FINISH_MARGIN = 10;
// Fixed logical size the player iframe is rendered at, then CSS-scaled down to fit the (smaller)
// on-screen stage. YouTube caps an embed's max quality to the player's OWN inner viewport size, so
// a physically small player is stuck at ~360p even if you pick 1080p manually — it snaps back. A
// CSS `transform: scale()` shrinks the visual box WITHOUT changing the iframe's window.innerWidth,
// so YouTube keeps seeing a 1920×1080 viewport and allows 1080p while we display it small. (Actual
// quality is still bandwidth-gated by YouTube, but manual HD selection now sticks.)
const PLAYER_BASE_W = 1920;
const PLAYER_BASE_H = 1080;
// Persistent playback settings (stored in users.preferences). Auto-advance = what plays when a
// video ends; loop = whether it repeats the current video ("one"), wraps the list at its ends
// ("all"), or neither ("off"). Both apply to any queued player (feed or playlist).
@ -133,8 +124,6 @@ export default function PlayerModal({
// modal adjusts volume (not just the small player area).
const dialogRef = useRef<HTMLDivElement | null>(null);
const volTimerRef = useRef<number | undefined>(undefined);
// CSS scale that fits the 1920×1080 logical player onto the actual stage (see PLAYER_BASE_* above).
const [playerScale, setPlayerScale] = useState(0);
// Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
const [volumeUi, setVolumeUi] = useState<number | null>(null);
// When the user interacts with YouTube's own controls (gear/seek/CC), focus moves into the
@ -416,25 +405,6 @@ export default function PlayerModal({
return () => el.removeEventListener("wheel", onWheel);
}, []);
// Keep the 1920×1080 logical player scaled to exactly fit the on-screen stage (windowed AND
// fullscreen). Measured before paint so the player never flashes at the wrong size.
useLayoutEffect(() => {
const stage = stageRef.current;
if (!stage) return;
const update = () => {
const w = stage.clientWidth;
if (w > 0) setPlayerScale(w / PLAYER_BASE_W);
};
update();
const ro = new ResizeObserver(update);
ro.observe(stage);
document.addEventListener("fullscreenchange", update);
return () => {
ro.disconnect();
document.removeEventListener("fullscreenchange", update);
};
}, []);
// Yield the interaction overlay to YouTube's native UI. Clicking a native control (gear / seek
// bar / CC) moves focus into the player iframe — the only cross-origin signal we get. While the
// iframe holds focus we drop the overlay's pointer-events so the (arbitrarily tall) settings
@ -517,9 +487,11 @@ export default function PlayerModal({
origin: window.location.origin,
playsinline: 1,
// Best-effort HD hint. The IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops
// now (YouTube removed them), but the undocumented `vq` URL param is still honoured for many
// videos and is harmless otherwise. The real quality lever is the player's rendered size
// (see max-w-6xl below) — YouTube's ABR targets a resolution to match the pixel box.
// now (YouTube removed them), and the undocumented `vq` URL param is only occasionally
// honoured — kept because it's harmless. NOTE: YouTube hard-caps an embed's max quality by
// the player's on-screen size; a windowed embed is stuck ~360p and only true fullscreen
// lifts the cap (after which the higher pick persists for the session). We can't beat that
// programmatically — a CSS transform-scale trick was tried and does NOT fool the cap.
vq: "hd1080",
},
events: {
@ -609,15 +581,9 @@ export default function PlayerModal({
ref={stageRef}
className="player-stage relative aspect-video w-full bg-black rounded-t-2xl overflow-hidden"
>
{/* The player rendered at a fixed 1920×1080 logical size, then CSS-scaled down to fit the
stage this is what unlocks 1080p in the small windowed player (see PLAYER_BASE_*).
Hide the iframe entirely on error so YouTube's own error screen can't bleed through. */}
<div
className="absolute top-0 left-0 origin-top-left"
style={{ width: PLAYER_BASE_W, height: PLAYER_BASE_H, transform: `scale(${playerScale})` }}
>
{/* Hide the iframe entirely on error so YouTube's own error screen can't bleed
through our overlay. */}
<div ref={mountRef} className={`w-full h-full ${playerError != null ? "invisible" : ""}`} />
</div>
{/* Interaction layer over the CENTRE of the video: catches click (play/pause) and stops
the iframe stealing keyboard focus. It deliberately leaves the top AND bottom edges
uncovered so YouTube's native controls the top-right cluster (volume / CC / settings)