From 71e0a40481d0152a245a583496b00a4e343ef56c Mon Sep 17 00:00:00 2001 From: npeter83 Date: Fri, 10 Jul 2026 16:56:24 +0200 Subject: [PATCH] revert(player): drop the transform-scale HD hack (didn't beat YouTube's cap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- frontend/src/components/PlayerModal.tsx | 52 +++++-------------------- 1 file changed, 9 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 75dd8dd..88568a5 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -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(null); const volTimerRef = useRef(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(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. */} -
-
-
+ {/* Hide the iframe entirely on error so YouTube's own error screen can't bleed + through our overlay. */} +
{/* 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)