Merge improvement/player-hd-unlock-flash: experimental windowed-HD unlock (v0.36.3)

This commit is contained in:
npeter83 2026-07-10 17:30:50 +02:00
commit 5d3dce6214
3 changed files with 75 additions and 8 deletions

View file

@ -1 +1 @@
0.36.2
0.36.3

View file

@ -20,6 +20,14 @@ import { useBackToClose } from "../lib/history";
// How close to the end (seconds) counts as "finished" → auto-mark watched.
const FINISH_MARGIN = 10;
// One-time HD-unlock (see attemptHdUnlock). YouTube hard-caps a windowed embed to ~360p and only a
// real fullscreen lifts the cap; the lift then persists for the whole page session. So on the first
// video opened we briefly flash the player to fullscreen and back to coax YouTube past the cap once.
// Cap the flash at this many ms, but exit as soon as we see the quality actually rise.
const HD_UNLOCK_MAX_MS = 2500;
// Module-scoped so the flash happens only ONCE per page session (the unlock persists afterwards).
let hdUnlockDone = false;
// 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).
@ -124,6 +132,9 @@ 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);
// HD-unlock flash bookkeeping (see attemptHdUnlock / finishHdUnlock).
const unlockTimerRef = useRef<number | undefined>(undefined);
const unlockingRef = useRef(false);
// 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
@ -146,6 +157,32 @@ export default function PlayerModal({
if (document.fullscreenElement) document.exitFullscreen?.();
else el.requestFullscreen?.();
};
// End the HD-unlock flash: exit fullscreen (if we're still in it) and mark the session unlocked so
// it never flashes again. Called either when YouTube's quality is seen to rise or on the max timeout.
const finishHdUnlock = () => {
if (!unlockingRef.current) return;
unlockingRef.current = false;
window.clearTimeout(unlockTimerRef.current);
hdUnlockDone = true;
if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {});
};
// Coax YouTube past its windowed 360p cap: on the first opened video, briefly enter fullscreen so
// YouTube lifts the cap (which then persists for the session), then exit back to the small player.
// Needs live user activation (the modal-open click) — silently retried on the next click if it's
// already expired, and a no-op once the session is unlocked. Best-effort; can't be forced further.
const attemptHdUnlock = () => {
if (hdUnlockDone || unlockingRef.current) return;
const el = stageRef.current;
if (!el || !el.requestFullscreen || document.fullscreenElement) return;
el.requestFullscreen()
.then(() => {
unlockingRef.current = true;
unlockTimerRef.current = window.setTimeout(finishHdUnlock, HD_UNLOCK_MAX_MS);
})
.catch(() => {
/* no user activation yet / fullscreen blocked — leave hdUnlockDone false to retry on a click */
});
};
const flashVolume = (level: number) => {
setVolumeUi(level);
window.clearTimeout(volTimerRef.current);
@ -488,18 +525,24 @@ export default function PlayerModal({
playsinline: 1,
// Best-effort HD hint. The IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops
// 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.
// honoured — kept because it's harmless. 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 (then it persists for the session). A CSS transform-scale trick does NOT fool it;
// we instead do a one-time fullscreen flash on open (see attemptHdUnlock) to lift the cap.
vq: "hd1080",
},
events: {
// Keep keyboard focus on the modal, not the iframe, so shortcuts work right away.
onReady: () => focusModal(),
// Keep keyboard focus on the modal, not the iframe, so shortcuts work right away, and try
// the one-time HD-unlock flash while the modal-open click is still a live user activation.
onReady: () => {
focusModal();
attemptHdUnlock();
},
onStateChange: (e: any) => {
// 1 === playing → sync the navigated-to video's title/author for display.
// 1 === playing → sync the navigated-to video's title/author for display, and retry the
// HD-unlock (a second chance at the flash right as playback begins).
if (e?.data === 1) {
attemptHdUnlock();
const p = playerRef.current;
const d = p && typeof p.getVideoData === "function" ? p.getVideoData() : null;
if (d) setLiveData({ title: d.title, author: d.author });
@ -513,6 +556,15 @@ export default function PlayerModal({
advanceOnEnd();
}
},
// While the HD-unlock flash is running, exit fullscreen as soon as YouTube's quality is
// seen to rise (no need to hold the flash the full timeout). onPlaybackQualityChange is one
// of the few still-live quality signals; e.data is a label ("hd1080"/"hd720"/"large"/…).
onPlaybackQualityChange: (e: any) => {
const q = String(e?.data || "");
if (unlockingRef.current && (q === "hd1080" || q === "hd720" || q === "large" || q === "highres")) {
finishHdUnlock();
}
},
// The embed couldn't play this video (embedding disabled, removed, private…).
// Surface our own message + an "Open on YouTube" escape hatch.
onError: (e: any) => setPlayerError(typeof e?.data === "number" ? e.data : -1),
@ -526,6 +578,12 @@ export default function PlayerModal({
return () => {
cancelled = true;
window.clearInterval(timer);
// If the modal closes / advances mid-flash, don't leave the player stuck in fullscreen.
window.clearTimeout(unlockTimerRef.current);
if (unlockingRef.current) {
unlockingRef.current = false;
if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {});
}
// Final flush, then refresh the feed + playlists so resume bars reflect this session.
Promise.resolve(persist()).finally(() => {
qc.invalidateQueries({ queryKey: ["feed"] });
@ -593,6 +651,7 @@ export default function PlayerModal({
<div
onClick={() => {
focusModal();
attemptHdUnlock(); // fallback: a real click is a fresh user activation for the flash
togglePlay();
}}
title={t("player.shortcutsHint")}

View file

@ -14,6 +14,14 @@ export interface ReleaseEntry {
}
export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.36.3",
date: "2026-07-10",
summary: "Experimental: the player tries to unlock higher video quality automatically.",
features: [
"The in-app player now tries to lift YouTube's windowed quality limit for you. YouTube caps a small embedded player to a low resolution and only real fullscreen unlocks HD — so on the first video you open in a session, the player briefly flashes to fullscreen and back to trigger that unlock, which then sticks for the rest of the session. You may notice a short flash on that first video; later videos won't flash. (It's still bandwidth-dependent, and can be reverted if it doesn't help on your setup.)",
],
},
{
version: "0.36.2",
date: "2026-07-10",