test something

This commit is contained in:
ashley 2025-10-05 14:46:02 +02:00
parent 418bd19804
commit 1d4bf3748b

View File

@ -3,6 +3,7 @@ var _yt_player = videojs;
var versionclient = "youtube.player.web_20250917_22_RC00" var versionclient = "youtube.player.web_20250917_22_RC00"
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const player = videojs("video", { const player = videojs("video", {
controls: true, controls: true,
@ -11,13 +12,13 @@ document.addEventListener("DOMContentLoaded", () => {
errorDisplay: false, errorDisplay: false,
}); });
// quick params + lightweight progress seed // quick params + tiny progress seed
const qs = new URLSearchParams(location.search); const qs = new URLSearchParams(location.search);
const qua = qs.get("quality") || ""; const qua = qs.get("quality") || "";
const vidKey = qs.get("v"); const vidKey = qs.get("v");
try { if (vidKey) localStorage.setItem(`progress-${vidKey}`, 0); } catch {} try { if (vidKey) localStorage.setItem(`progress-${vidKey}`, 0); } catch {}
// raw media nodes // raw nodes
const videoEl = document.getElementById("video"); const videoEl = document.getElementById("video");
const audioEl = document.getElementById("aud"); const audioEl = document.getElementById("aud");
@ -33,7 +34,7 @@ document.addEventListener("DOMContentLoaded", () => {
audioEl.preload = "auto"; audioEl.preload = "auto";
} catch {} } catch {}
// initial source probes // source sniffers
const pickAudioSrc = () => { const pickAudioSrc = () => {
const s = audioEl?.getAttribute?.("src"); const s = audioEl?.getAttribute?.("src");
if (s) return s; if (s) return s;
@ -45,36 +46,37 @@ document.addEventListener("DOMContentLoaded", () => {
let audioSrc = pickAudioSrc(); let audioSrc = pickAudioSrc();
const srcObj = player.src(); const srcObj = player.src();
const initialVideoSrc = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].src) : srcObj; const initialVideoSrc = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].src) : srcObj;
const initialVideoType = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].type) : undefined; const initialVideoType = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].type) : undefined;
// small state bag // small state
let audioReady = false, videoReady = false; let audioReady = false, videoReady = false;
let mediaSessionReady = false; let mediaSessionReady = false;
// sync knobs // sync knobs
const BIG_DRIFT = 0.45; // snap threshold (s) const BIG_DRIFT = 0.45; // snap if beyond this (s)
const MICRO_DRIFT = 0.035; // subtle rate nudge (s) const MICRO_DRIFT = 0.035; // tiny nudge (s)
const EPS = 0.12; // buffer epsilon (s) const EPS = 0.12; // buffer epsilon (s)
// guards // guards + timers
let volGuard = false; let volGuard = false;
let syncGuard = false; // block re-entrant sync let syncGuard = false;
let couplingGuard = false; // block play/pause echo let couplingGuard = false;
let autosyncTimer = null; let autosyncTimer = null;
const AUTOSYNC_MS = 400; const AUTOSYNC_MS = 400;
// seeking flags // buffering/loader awareness
let bufferingHold = false; // we decided to pause both because it's buffering
let resumeAfterBuffer = false; // restore play once its ready again
// seek flow flags
let audioPauseForSeek = false; let audioPauseForSeek = false;
let playerSeekWasPlaying = false; let playerSeekWasPlaying = false;
// buffer-aware resume flag // loop tracking
let resumeAfterBuffer = false;
// loop detection
let lastVideoTime = 0; let lastVideoTime = 0;
// helpers // basic helpers
function timeInBuffered(media, t) { function timeInBuffered(media, t) {
try { try {
const br = media.buffered; const br = media.buffered;
@ -88,9 +90,9 @@ document.addEventListener("DOMContentLoaded", () => {
} }
function canPlayAt(media, t) { function canPlayAt(media, t) {
try { try {
const rs = Number(media.readyState || 0); const rs = Number(media.readyState || 0); // 0..4
if (!isFinite(t)) return false; if (!isFinite(t)) return false;
if (rs >= 3) return true; // HAVE_FUTURE_DATA if (rs >= 3) return true; // HAVE_FUTURE_DATA
return timeInBuffered(media, t); return timeInBuffered(media, t);
} catch { return false; } } catch { return false; }
} }
@ -99,22 +101,16 @@ document.addEventListener("DOMContentLoaded", () => {
const clamp01 = v => Math.max(0, Math.min(1, Number(v))); const clamp01 = v => Math.max(0, Math.min(1, Number(v)));
function isLooping() { try { return !!player.loop?.() || !!videoEl.loop; } catch { return !!videoEl.loop; } } function isLooping() { try { return !!player.loop?.() || !!videoEl.loop; } catch { return !!videoEl.loop; } }
function isSpinnerVisible() { // check if the “loading bar/spinner” is up (real-world test: UI class + readystate + buffer)
try { function isBufferingUI() {
if (player.hasClass("vjs-waiting")) return true;
const sp = player.el().querySelector(".vjs-loading-spinner");
if (!sp) return false;
const st = window.getComputedStyle(sp);
const hidden = st.display === "none" || st.visibility === "hidden" || Number(st.opacity) === 0;
return !hidden;
} catch { return false; }
}
function isBufferingNow() {
try { try {
const el = player.el();
const uiWait = player.hasClass("vjs-waiting") || player.hasClass("vjs-seeking");
const rs = Number(videoEl.readyState || 0);
const t = Number(player.currentTime()); const t = Number(player.currentTime());
const lowReady = (videoEl.readyState || 0) < 3 || (audioEl.readyState || 0) < 3; const atEdge = !timeInBuffered(videoEl, isFinite(t) ? t + 0.05 : 0);
return isSpinnerVisible() || lowReady || !bothPlayableAt(t); return !!(uiWait || rs < 3 || atEdge);
} catch { return true; } } catch { return false; }
} }
// keep mute/volume mirrored either way // keep mute/volume mirrored either way
@ -144,18 +140,11 @@ document.addEventListener("DOMContentLoaded", () => {
audioEl.addEventListener("volumechange", mirrorFromAudioVolumeMute); audioEl.addEventListener("volumechange", mirrorFromAudioVolumeMute);
player.ready(() => mirrorFromPlayerVolumeMute()); player.ready(() => mirrorFromPlayerVolumeMute());
// one-button control helpers (buffer-aware) // one-button control
function tryResumeAfterBuffer() {
if (!resumeAfterBuffer) return;
if (!isBufferingNow()) {
resumeAfterBuffer = false;
playBoth();
}
}
function playBoth() { function playBoth() {
if (couplingGuard) return; if (couplingGuard) return;
if (isBufferingNow()) { // if loading bar/spinner is up, do NOT play anything if (isBufferingUI()) { // dont try to play while loader is up
resumeAfterBuffer = true; // remember to resume later resumeAfterBuffer = true;
return; return;
} }
couplingGuard = true; couplingGuard = true;
@ -171,12 +160,16 @@ document.addEventListener("DOMContentLoaded", () => {
audioEl.pause(); audioEl.pause();
stopAutosync(); stopAutosync();
couplingGuard = false; couplingGuard = false;
resumeAfterBuffer = false; // cancel pending resume when user paused
} }
// video is the boss: when it plays/pauses, audio mirrors (buffer-aware) // master behavior: video dictates play/pause; audio mirrors
player.on("play", () => { player.on("play", () => {
if (isBufferingNow()) { resumeAfterBuffer = true; pauseBoth(); return; } if (isBufferingUI()) { // if UI shows loader, cancel play and wait
bufferingHold = true;
resumeAfterBuffer = true;
pauseBoth();
return;
}
if (audioEl.paused) audioEl.play()?.catch(()=>{}); if (audioEl.paused) audioEl.play()?.catch(()=>{});
startAutosync(); startAutosync();
try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "playing"; } catch {} try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "playing"; } catch {}
@ -187,7 +180,7 @@ document.addEventListener("DOMContentLoaded", () => {
try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "paused"; } catch {} try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "paused"; } catch {}
}); });
// if the platform tries to play/pause audio alone, route through video // dont let the hidden audio go rogue
audioEl.addEventListener("play", () => { if (player.paused()) playBoth(); }); audioEl.addEventListener("play", () => { if (player.paused()) playBoth(); });
audioEl.addEventListener("pause", () => { audioEl.addEventListener("pause", () => {
if (audioPauseForSeek) return; if (audioPauseForSeek) return;
@ -213,6 +206,7 @@ document.addEventListener("DOMContentLoaded", () => {
let pos = Number(player.currentTime()); let pos = Number(player.currentTime());
if (!isFinite(pos) || pos < 0) pos = 0; if (!isFinite(pos) || pos < 0) pos = 0;
// avoid flicker; allow jump-back only on loop wrap
if (pos + 0.2 < lastMSPos && isLooping()) lastMSPos = 0; if (pos + 0.2 < lastMSPos && isLooping()) lastMSPos = 0;
else { else {
if (pos < lastMSPos && (lastMSPos - pos) < 0.2) pos = lastMSPos; if (pos < lastMSPos && (lastMSPos - pos) < 0.2) pos = lastMSPos;
@ -248,6 +242,7 @@ document.addEventListener("DOMContentLoaded", () => {
updateMSPositionState(false); updateMSPositionState(false);
}; };
// when OS seekbar moves: pause audio first (to avoid ping-pong), mirror time, then optionally resume
function msSeekTo(targetTime) { function msSeekTo(targetTime) {
const wasPlaying = !player.paused(); const wasPlaying = !player.paused();
audioPauseForSeek = true; audioPauseForSeek = true;
@ -262,7 +257,8 @@ document.addEventListener("DOMContentLoaded", () => {
audioPauseForSeek = false; audioPauseForSeek = false;
} }
tagState(); tagState();
if (wasPlaying) playBoth(); if (wasPlaying && !isBufferingUI()) playBoth();
else if (wasPlaying) resumeAfterBuffer = true;
} }
navigator.mediaSession.setActionHandler("play", () => { playBoth(); tagState(); }); navigator.mediaSession.setActionHandler("play", () => { playBoth(); tagState(); });
@ -298,7 +294,7 @@ document.addEventListener("DOMContentLoaded", () => {
mediaSessionReady = true; mediaSessionReady = true;
} }
// hardware media keys (fallback) // media keys fallback
document.addEventListener("keydown", e => { document.addEventListener("keydown", e => {
switch (e.code) { switch (e.code) {
case "AudioPlay": case "AudioPlay":
@ -323,19 +319,18 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
// single-shot sync step used by events + autosyncer // one sync tick (events + autosyncer use this)
function doSyncOnce() { function doSyncOnce() {
if (syncGuard) return; if (syncGuard) return;
if (isBufferingUI()) { // loader up → hold steady, no cute tricks
try { audioEl.playbackRate = 1; } catch {}
return;
}
const vt = Number(player.currentTime()); const vt = Number(player.currentTime());
const at = Number(audioEl.currentTime); const at = Number(audioEl.currentTime);
if (!isFinite(vt) || !isFinite(at)) return; if (!isFinite(vt) || !isFinite(at)) return;
// if spinner/loading bar is up or readiness is low, make sure nothing plays
if (isBufferingNow()) {
try { audioEl.pause(); } catch {}
return;
}
// handle loop wrap cleanly // handle loop wrap cleanly
if (vt + 0.02 < lastVideoTime && isLooping()) { if (vt + 0.02 < lastVideoTime && isLooping()) {
safeSetCT(audioEl, vt); safeSetCT(audioEl, vt);
@ -363,17 +358,25 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// event-driven sync while foregrounded // foreground sync
player.on("timeupdate", doSyncOnce); player.on("timeupdate", doSyncOnce);
// cheap autosync in case timeupdate slows (background/lockscreen) // background/lockscreen helper
function startAutosync() { if (!autosyncTimer) autosyncTimer = setInterval(doSyncOnce, AUTOSYNC_MS); } function startAutosync() {
function stopAutosync() { if (autosyncTimer) { clearInterval(autosyncTimer); autosyncTimer = null; } } if (autosyncTimer) return;
autosyncTimer = setInterval(doSyncOnce, AUTOSYNC_MS);
}
function stopAutosync() {
if (!autosyncTimer) return;
clearInterval(autosyncTimer);
autosyncTimer = null;
}
// when user drags the player's own seekbar: pause audio instantly, then restore // when user drags the player's seekbar: pause audio immediately (no flapping), mirror time, then resume if needed
player.on("seeking", () => { player.on("seeking", () => {
syncGuard = true; syncGuard = true;
playerSeekWasPlaying = !player.paused(); playerSeekWasPlaying = !player.paused();
resumeAfterBuffer = playerSeekWasPlaying; // if we were playing, well resume after seek/buffer
audioPauseForSeek = true; audioPauseForSeek = true;
try { audioEl.pause(); } catch {} try { audioEl.pause(); } catch {}
safeSetCT(audioEl, Number(player.currentTime())); safeSetCT(audioEl, Number(player.currentTime()));
@ -383,11 +386,14 @@ document.addEventListener("DOMContentLoaded", () => {
player.on("seeked", () => { player.on("seeked", () => {
safeSetCT(audioEl, Number(player.currentTime())); safeSetCT(audioEl, Number(player.currentTime()));
syncGuard = false; syncGuard = false;
if (playerSeekWasPlaying && !isBufferingNow() && bothPlayableAt(Number(player.currentTime()))) playBoth(); if (!isBufferingUI() && resumeAfterBuffer && bothPlayableAt(Number(player.currentTime()))) {
playBoth();
resumeAfterBuffer = false;
}
updateMSPositionState(false); updateMSPositionState(false);
}); });
// if some platform UI seeks the audio directly, mirror time but don't let it desync // if some UI seeks the audio directly, mirror it to video (still respecting loader)
audioEl.addEventListener("seeking", () => { audioEl.addEventListener("seeking", () => {
const at = Number(audioEl.currentTime) || 0; const at = Number(audioEl.currentTime) || 0;
syncGuard = true; syncGuard = true;
@ -403,23 +409,37 @@ document.addEventListener("DOMContentLoaded", () => {
updateMSPositionState(false); updateMSPositionState(false);
}); });
// buffering handling; if loading bar/spinner is up, keep both fully paused // buffering hooks — if the loader shows, *nothing* plays
function onBufferStart() { function enterBuffering(reason) {
const wasPlaying = !player.paused(); bufferingHold = true;
resumeAfterBuffer = wasPlaying || resumeAfterBuffer; if (!player.paused() || !audioEl.paused) {
pauseBoth(); resumeAfterBuffer = true;
pauseBoth();
}
} }
function onBufferEnd() { function exitBuffering() {
tryResumeAfterBuffer(); bufferingHold = false;
if (resumeAfterBuffer && !isBufferingUI() && bothPlayableAt(Number(player.currentTime()))) {
playBoth();
}
resumeAfterBuffer = false;
} }
player.on("waiting", onBufferStart);
player.on("stalled", onBufferStart); player.on("waiting", () => enterBuffering("waiting"));
player.on("loadstart", onBufferStart); player.on("stalled", () => enterBuffering("stalled"));
player.on("canplay", onBufferEnd); player.on("suspend", () => enterBuffering("suspend"));
player.on("canplaythrough", onBufferEnd); player.on("seeking", () => enterBuffering("seeking"));
player.on("playing", onBufferEnd); player.on("canplay", () => exitBuffering());
audioEl.addEventListener("canplay", onBufferEnd); player.on("playing", () => exitBuffering());
audioEl.addEventListener("canplaythrough", onBufferEnd); player.on("canplaythrough", () => exitBuffering());
player.on("progress", () => { if (!isBufferingUI()) exitBuffering(); });
// just to be safe, also react to class flips (loading spinner on/off)
const spinnerObserver = new MutationObserver(() => {
if (isBufferingUI()) enterBuffering("ui");
else exitBuffering();
});
try { spinnerObserver.observe(player.el(), { attributes: true, attributeFilter: ["class"] }); } catch {}
// simple error surfacing // simple error surfacing
const errorBox = document.getElementById("loopedIndicator"); const errorBox = document.getElementById("loopedIndicator");
@ -437,12 +457,12 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
// loop/end: never stop on loop // loop/end behavior — never stop on loop
player.on("ended", () => { player.on("ended", () => {
if (isLooping()) { if (isLooping()) {
safeSetCT(audioEl, 0); safeSetCT(audioEl, 0);
player.currentTime(0); player.currentTime(0);
playBoth(); if (!isBufferingUI()) playBoth(); else resumeAfterBuffer = true;
lastMSPos = 0; lastMSPos = 0;
updateMSPositionState(false); updateMSPositionState(false);
} else { } else {
@ -455,7 +475,7 @@ document.addEventListener("DOMContentLoaded", () => {
safeSetCT(audioEl, 0); safeSetCT(audioEl, 0);
if (player.paused()) { if (player.paused()) {
player.currentTime(0); player.currentTime(0);
playBoth(); if (!isBufferingUI()) playBoth(); else resumeAfterBuffer = true;
} }
lastMSPos = 0; lastMSPos = 0;
updateMSPositionState(false); updateMSPositionState(false);
@ -469,7 +489,7 @@ document.addEventListener("DOMContentLoaded", () => {
if (!document.fullscreenElement) pauseBoth(); if (!document.fullscreenElement) pauseBoth();
}); });
// tiny, quiet retry on load error (helps <source> vs src cases) // tiny quiet retry on load error
function attachReady(elm, resolveSrc, markReady) { function attachReady(elm, resolveSrc, markReady) {
const onLoaded = () => { try { markReady(); } catch {} tryStart(); }; const onLoaded = () => { try { markReady(); } catch {} tryStart(); };
elm.addEventListener("loadeddata", onLoaded, { once: true }); elm.addEventListener("loadeddata", onLoaded, { once: true });
@ -492,9 +512,9 @@ document.addEventListener("DOMContentLoaded", () => {
if (audioReady && videoReady) { if (audioReady && videoReady) {
const t = Number(player.currentTime()); const t = Number(player.currentTime());
if (isFinite(t) && Math.abs(Number(audioEl.currentTime) - t) > 0.1) safeSetCT(audioEl, t); if (isFinite(t) && Math.abs(Number(audioEl.currentTime) - t) > 0.1) safeSetCT(audioEl, t);
if (!isBufferingNow() && bothPlayableAt(t)) playBoth();
else { audioEl.pause(); player.pause(); }
setupMediaSession(); setupMediaSession();
if (!isBufferingUI() && bothPlayableAt(t)) playBoth();
else { audioEl.pause(); player.pause(); }
} }
} }
@ -512,7 +532,8 @@ document.addEventListener("DOMContentLoaded", () => {
const tryKick = () => { const tryKick = () => {
if (audioReady && videoReady && player.paused()) { if (audioReady && videoReady && player.paused()) {
const t = Number(player.currentTime()); const t = Number(player.currentTime());
if (!isBufferingNow() && bothPlayableAt(t)) playBoth(); if (!isBufferingUI() && bothPlayableAt(t)) playBoth();
else resumeAfterBuffer = true;
} }
}; };
player.el().addEventListener("click", tryKick, { once: true }); player.el().addEventListener("click", tryKick, { once: true });
@ -520,6 +541,7 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
// https://codeberg.org/ashley/poke/src/branch/main/src/libpoketube/libpoketube-youtubei-objects.json // https://codeberg.org/ashley/poke/src/branch/main/src/libpoketube/libpoketube-youtubei-objects.json