Merge improvement/player-hd-unlock-flash: experimental windowed-HD unlock (v0.36.3)
This commit is contained in:
commit
5d3dce6214
3 changed files with 75 additions and 8 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.36.2
|
||||
0.36.3
|
||||
|
|
@ -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")}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue