Merge improvement/player-drop-hd-experiment: drop HD flash, clean notes (v0.36.4)

This commit is contained in:
npeter83 2026-07-10 18:14:32 +02:00
commit f0bab315ad
3 changed files with 12 additions and 81 deletions

View file

@ -1 +1 @@
0.36.3 0.36.4

View file

@ -20,14 +20,6 @@ import { useBackToClose } from "../lib/history";
// How close to the end (seconds) counts as "finished" → auto-mark watched. // How close to the end (seconds) counts as "finished" → auto-mark watched.
const FINISH_MARGIN = 10; 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 // 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 // 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). // ("all"), or neither ("off"). Both apply to any queued player (feed or playlist).
@ -132,9 +124,6 @@ export default function PlayerModal({
// modal adjusts volume (not just the small player area). // modal adjusts volume (not just the small player area).
const dialogRef = useRef<HTMLDivElement | null>(null); const dialogRef = useRef<HTMLDivElement | null>(null);
const volTimerRef = useRef<number | undefined>(undefined); 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. // Volume level to flash in the on-player overlay (null = hidden). Auto-fades after a moment.
const [volumeUi, setVolumeUi] = useState<number | null>(null); const [volumeUi, setVolumeUi] = useState<number | null>(null);
// When the user interacts with YouTube's own controls (gear/seek/CC), focus moves into the // When the user interacts with YouTube's own controls (gear/seek/CC), focus moves into the
@ -157,32 +146,6 @@ export default function PlayerModal({
if (document.fullscreenElement) document.exitFullscreen?.(); if (document.fullscreenElement) document.exitFullscreen?.();
else el.requestFullscreen?.(); 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) => { const flashVolume = (level: number) => {
setVolumeUi(level); setVolumeUi(level);
window.clearTimeout(volTimerRef.current); window.clearTimeout(volTimerRef.current);
@ -525,24 +488,18 @@ export default function PlayerModal({
playsinline: 1, playsinline: 1,
// Best-effort HD hint. The IFrame API's setPlaybackQuality/suggestedQuality are hard no-ops // 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 // now (YouTube removed them), and the undocumented `vq` URL param is only occasionally
// honoured — kept because it's harmless. YouTube hard-caps an embed's max quality by the // honoured — kept because it's harmless. NOTE: YouTube hard-caps an embed's max quality by
// player's on-screen size; a windowed embed is stuck ~360p and only true fullscreen lifts // the player's on-screen size; a windowed embed is stuck ~360p and only true fullscreen
// the cap (then it persists for the session). A CSS transform-scale trick does NOT fool it; // lifts the cap (after which the higher pick persists for the session). We can't beat that
// we instead do a one-time fullscreen flash on open (see attemptHdUnlock) to lift the cap. // programmatically — a CSS transform-scale trick was tried and does NOT fool the cap.
vq: "hd1080", vq: "hd1080",
}, },
events: { events: {
// Keep keyboard focus on the modal, not the iframe, so shortcuts work right away, and try // Keep keyboard focus on the modal, not the iframe, so shortcuts work right away.
// the one-time HD-unlock flash while the modal-open click is still a live user activation. onReady: () => focusModal(),
onReady: () => {
focusModal();
attemptHdUnlock();
},
onStateChange: (e: any) => { onStateChange: (e: any) => {
// 1 === playing → sync the navigated-to video's title/author for display, and retry the // 1 === playing → sync the navigated-to video's title/author for display.
// HD-unlock (a second chance at the flash right as playback begins).
if (e?.data === 1) { if (e?.data === 1) {
attemptHdUnlock();
const p = playerRef.current; const p = playerRef.current;
const d = p && typeof p.getVideoData === "function" ? p.getVideoData() : null; const d = p && typeof p.getVideoData === "function" ? p.getVideoData() : null;
if (d) setLiveData({ title: d.title, author: d.author }); if (d) setLiveData({ title: d.title, author: d.author });
@ -556,15 +513,6 @@ export default function PlayerModal({
advanceOnEnd(); 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…). // The embed couldn't play this video (embedding disabled, removed, private…).
// Surface our own message + an "Open on YouTube" escape hatch. // Surface our own message + an "Open on YouTube" escape hatch.
onError: (e: any) => setPlayerError(typeof e?.data === "number" ? e.data : -1), onError: (e: any) => setPlayerError(typeof e?.data === "number" ? e.data : -1),
@ -578,12 +526,6 @@ export default function PlayerModal({
return () => { return () => {
cancelled = true; cancelled = true;
window.clearInterval(timer); 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. // Final flush, then refresh the feed + playlists so resume bars reflect this session.
Promise.resolve(persist()).finally(() => { Promise.resolve(persist()).finally(() => {
qc.invalidateQueries({ queryKey: ["feed"] }); qc.invalidateQueries({ queryKey: ["feed"] });
@ -651,7 +593,6 @@ export default function PlayerModal({
<div <div
onClick={() => { onClick={() => {
focusModal(); focusModal();
attemptHdUnlock(); // fallback: a real click is a fresh user activation for the flash
togglePlay(); togglePlay();
}} }}
title={t("player.shortcutsHint")} title={t("player.shortcutsHint")}

View file

@ -15,19 +15,10 @@ export interface ReleaseEntry {
export const RELEASE_NOTES: ReleaseEntry[] = [ export const RELEASE_NOTES: ReleaseEntry[] = [
{ {
version: "0.36.3", version: "0.36.4",
date: "2026-07-10", date: "2026-07-10",
summary: "Experimental: the player tries to unlock higher video quality automatically.", chores: [
features: [ "Internal in-app player cleanup.",
"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",
summary: "Player's YouTube controls back to normal size (a quality experiment was reverted).",
fixes: [
"Reverted an experiment that tried to raise video quality in the small (windowed) player: it only shrank YouTube's on-screen controls without actually improving quality, because YouTube caps an embedded player's quality by its on-screen size. Tip for the sharpest picture: switch the player to fullscreen once — YouTube then keeps the higher quality even after you return to the small player.",
], ],
}, },
{ {
@ -41,9 +32,8 @@ export const RELEASE_NOTES: ReleaseEntry[] = [
{ {
version: "0.36.0", version: "0.36.0",
date: "2026-07-10", date: "2026-07-10",
summary: "Sharper in-app playback and full access to YouTube's own player menu.", summary: "Full access to YouTube's own player menu in the in-app player.",
features: [ features: [
"The in-app player is now larger and asks YouTube for higher quality, so videos look sharper than the old low-resolution default. (For the maximum your connection allows, go fullscreen.)",
"You can now open and navigate YouTube's own player menu — Quality, Speed, Captions and More options — all the way down. When you use YouTube's built-in controls, the app's click-to-play and scroll-for-volume shortcuts briefly step aside (a small badge shows this), and they return the moment you move the pointer off the video.", "You can now open and navigate YouTube's own player menu — Quality, Speed, Captions and More options — all the way down. When you use YouTube's built-in controls, the app's click-to-play and scroll-for-volume shortcuts briefly step aside (a small badge shows this), and they return the moment you move the pointer off the video.",
], ],
}, },