diff --git a/css/player-base-new.js b/css/player-base-new.js index 78c2875c..dbfb2fba 100644 --- a/css/player-base-new.js +++ b/css/player-base-new.js @@ -2,8 +2,7 @@ var _yt_player = videojs; var versionclient = "youtube.player.web_20250917_22_RC00" - - +// video.js boot document.addEventListener("DOMContentLoaded", () => { const player = videojs("video", { controls: true, @@ -12,17 +11,17 @@ document.addEventListener("DOMContentLoaded", () => { errorDisplay: false, }); - // quick params + tiny progress seed + // tiny params stash const qs = new URLSearchParams(location.search); const qua = qs.get("quality") || ""; const vidKey = qs.get("v"); try { if (vidKey) localStorage.setItem(`progress-${vidKey}`, 0); } catch {} - // raw nodes + // raw tags const videoEl = document.getElementById("video"); const audioEl = document.getElementById("aud"); - // keep the hidden audio truly hidden/quiet + // keep the hidden audio really hidden try { audioEl.controls = false; audioEl.setAttribute("aria-hidden", "true"); @@ -34,7 +33,7 @@ document.addEventListener("DOMContentLoaded", () => { audioEl.preload = "auto"; } catch {} - // source sniffers + // poke sources const pickAudioSrc = () => { const s = audioEl?.getAttribute?.("src"); if (s) return s; @@ -46,17 +45,17 @@ document.addEventListener("DOMContentLoaded", () => { let audioSrc = pickAudioSrc(); 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; - // small state + // a little state let audioReady = false, videoReady = false; let mediaSessionReady = false; // sync knobs - const BIG_DRIFT = 0.45; // snap if beyond this (s) - const MICRO_DRIFT = 0.035; // tiny nudge (s) - const EPS = 0.12; // buffer epsilon (s) + const BIG_DRIFT = 0.45; + const MICRO_DRIFT = 0.035; + const EPS = 0.12; // guards + timers let volGuard = false; @@ -65,18 +64,14 @@ document.addEventListener("DOMContentLoaded", () => { let autosyncTimer = null; const AUTOSYNC_MS = 400; - // buffering/loader awareness - let bufferingHold = false; // we decided to pause both because it's buffering - let resumeAfterBuffer = false; // restore play once it’s ready again - - // seek flow flags + // seek flags let audioPauseForSeek = false; let playerSeekWasPlaying = false; - // loop tracking + // last video time (for loop wrap) let lastVideoTime = 0; - // basic helpers + // helper: can we play at time t? function timeInBuffered(media, t) { try { const br = media.buffered; @@ -90,9 +85,9 @@ document.addEventListener("DOMContentLoaded", () => { } function canPlayAt(media, t) { try { - const rs = Number(media.readyState || 0); // 0..4 + const rs = Number(media.readyState || 0); if (!isFinite(t)) return false; - if (rs >= 3) return true; // HAVE_FUTURE_DATA + if (rs >= 3) return true; return timeInBuffered(media, t); } catch { return false; } } @@ -101,19 +96,21 @@ document.addEventListener("DOMContentLoaded", () => { const clamp01 = v => Math.max(0, Math.min(1, Number(v))); function isLooping() { try { return !!player.loop?.() || !!videoEl.loop; } catch { return !!videoEl.loop; } } - // check if the “loading bar/spinner” is up (real-world test: UI class + readystate + buffer) - function isBufferingUI() { + // helper: is the vjs spinner actually up? + function spinnerUp() { 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 atEdge = !timeInBuffered(videoEl, isFinite(t) ? t + 0.05 : 0); - return !!(uiWait || rs < 3 || atEdge); + const root = player.el(); + if (!root) return false; + const hasWaiting = player.hasClass?.("vjs-waiting") || player.hasClass?.("vjs-seeking") || player.hasClass?.("vjs-processing"); + const sp = root.querySelector(".vjs-loading-spinner"); + if (!sp) return !!hasWaiting; + const cs = window.getComputedStyle(sp); + const visible = cs.visibility !== "hidden" && cs.display !== "none" && Number(cs.opacity || "1") > 0; + return !!hasWaiting || visible; } catch { return false; } } - // keep mute/volume mirrored either way + // mute/volume mirror either way function mirrorFromPlayerVolumeMute() { if (volGuard) return; volGuard = true; try { @@ -140,13 +137,9 @@ document.addEventListener("DOMContentLoaded", () => { audioEl.addEventListener("volumechange", mirrorFromAudioVolumeMute); player.ready(() => mirrorFromPlayerVolumeMute()); - // one-button control + // single control helpers function playBoth() { if (couplingGuard) return; - if (isBufferingUI()) { // don’t try to play while loader is up - resumeAfterBuffer = true; - return; - } couplingGuard = true; player.play()?.catch(()=>{}); audioEl.play()?.catch(()=>{}); @@ -162,14 +155,14 @@ document.addEventListener("DOMContentLoaded", () => { couplingGuard = false; } - // master behavior: video dictates play/pause; audio mirrors + // buffering-aware gate: if spinner is up, neither should be playing + function enforceBufferingGate() { + if (spinnerUp()) pauseBoth(); + } + + // video is the boss player.on("play", () => { - if (isBufferingUI()) { // if UI shows loader, cancel play and wait - bufferingHold = true; - resumeAfterBuffer = true; - pauseBoth(); - return; - } + if (spinnerUp()) { pauseBoth(); return; } if (audioEl.paused) audioEl.play()?.catch(()=>{}); startAutosync(); try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "playing"; } catch {} @@ -180,14 +173,17 @@ document.addEventListener("DOMContentLoaded", () => { try { if ("mediaSession" in navigator) navigator.mediaSession.playbackState = "paused"; } catch {} }); - // don’t let the hidden audio go rogue - audioEl.addEventListener("play", () => { if (player.paused()) playBoth(); }); + // platform trying to mess with audio alone? keep them glued + audioEl.addEventListener("play", () => { + if (spinnerUp()) { audioEl.pause(); return; } + if (player.paused()) playBoth(); + }); audioEl.addEventListener("pause", () => { if (audioPauseForSeek) return; if (!player.paused()) pauseBoth(); }); - // media-session timeline (keeps OS seekbar calm) + // OS timeline sync (calm, monotonic) let lastMSPos = 0, lastMSAt = 0; const MS_THROTTLE_MS = 250; function getDuration() { @@ -195,25 +191,21 @@ document.addEventListener("DOMContentLoaded", () => { if (!isFinite(d) || d <= 0) d = Number(player.duration()); if (!isFinite(d) || d <= 0) d = Number(audioEl.duration); return isFinite(d) && d > 0 ? d : null; - } + } function updateMSPositionState(throttle = true) { if (!("mediaSession" in navigator)) return; const now = performance.now(); if (throttle && (now - lastMSAt) < MS_THROTTLE_MS) return; const dur = getDuration(); if (!dur) return; - let pos = Number(player.currentTime()); if (!isFinite(pos) || pos < 0) pos = 0; - - // avoid flicker; allow jump-back only on loop wrap if (pos + 0.2 < lastMSPos && isLooping()) lastMSPos = 0; else { if (pos < lastMSPos && (lastMSPos - pos) < 0.2) pos = lastMSPos; lastMSPos = pos; } lastMSAt = now; - try { if ("setPositionState" in navigator.mediaSession) { navigator.mediaSession.setPositionState({ @@ -242,7 +234,6 @@ document.addEventListener("DOMContentLoaded", () => { updateMSPositionState(false); }; - // when OS seekbar moves: pause audio first (to avoid ping-pong), mirror time, then optionally resume function msSeekTo(targetTime) { const wasPlaying = !player.paused(); audioPauseForSeek = true; @@ -257,11 +248,11 @@ document.addEventListener("DOMContentLoaded", () => { audioPauseForSeek = false; } tagState(); - if (wasPlaying && !isBufferingUI()) playBoth(); - else if (wasPlaying) resumeAfterBuffer = true; + if (spinnerUp()) return; // if still buffering, stay paused + if (wasPlaying) playBoth(); } - navigator.mediaSession.setActionHandler("play", () => { playBoth(); tagState(); }); + navigator.mediaSession.setActionHandler("play", () => { if (!spinnerUp()) playBoth(); tagState(); }); navigator.mediaSession.setActionHandler("pause", () => { pauseBoth(); tagState(); }); navigator.mediaSession.setActionHandler("stop", () => { pauseBoth(); @@ -294,11 +285,12 @@ document.addEventListener("DOMContentLoaded", () => { mediaSessionReady = true; } - // media keys fallback + // hardware keys document.addEventListener("keydown", e => { switch (e.code) { case "AudioPlay": case "MediaPlayPause": + if (spinnerUp()) { pauseBoth(); break; } if (player.paused()) playBoth(); else pauseBoth(); break; case "AudioPause": @@ -308,30 +300,27 @@ document.addEventListener("DOMContentLoaded", () => { case "MediaTrackNext": { const t = Number(player.currentTime()) + 10; player.currentTime(t); safeSetCT(audioEl, t); + enforceBufferingGate(); break; } case "AudioPrevious": case "MediaTrackPrevious": { const t = Math.max(0, Number(player.currentTime()) - 10); player.currentTime(t); safeSetCT(audioEl, t); + enforceBufferingGate(); break; } } }); - // one sync tick (events + autosyncer use this) + // single sync step function doSyncOnce() { if (syncGuard) return; - if (isBufferingUI()) { // loader up → hold steady, no cute tricks - try { audioEl.playbackRate = 1; } catch {} - return; - } - + if (spinnerUp()) { pauseBoth(); return; } const vt = Number(player.currentTime()); const at = Number(audioEl.currentTime); if (!isFinite(vt) || !isFinite(at)) return; - // handle loop wrap cleanly if (vt + 0.02 < lastVideoTime && isLooping()) { safeSetCT(audioEl, vt); if (player.paused()) playBoth(); @@ -345,11 +334,7 @@ document.addEventListener("DOMContentLoaded", () => { if (!bothPlayableAt(vt)) return; const delta = vt - at; - if (Math.abs(delta) > BIG_DRIFT) { - safeSetCT(audioEl, vt); - try { audioEl.playbackRate = 1; } catch {} - return; - } + if (Math.abs(delta) > BIG_DRIFT) { safeSetCT(audioEl, vt); try { audioEl.playbackRate = 1; } catch {} return; } if (Math.abs(delta) > MICRO_DRIFT) { const target = 1 + (delta * 0.12); try { audioEl.playbackRate = Math.max(0.97, Math.min(1.03, target)); } catch {} @@ -358,13 +343,16 @@ document.addEventListener("DOMContentLoaded", () => { } } - // foreground sync + // main sync player.on("timeupdate", doSyncOnce); - // background/lockscreen helper + // cheap autosync + also our buffering gate keeper function startAutosync() { if (autosyncTimer) return; - autosyncTimer = setInterval(doSyncOnce, AUTOSYNC_MS); + autosyncTimer = setInterval(() => { + enforceBufferingGate(); + doSyncOnce(); + }, AUTOSYNC_MS); } function stopAutosync() { if (!autosyncTimer) return; @@ -372,34 +360,32 @@ document.addEventListener("DOMContentLoaded", () => { autosyncTimer = null; } - // when user drags the player's seekbar: pause audio immediately (no flapping), mirror time, then resume if needed + // when user drags the player's seekbar: kill audio first, line up, then resume if we were playing player.on("seeking", () => { syncGuard = true; playerSeekWasPlaying = !player.paused(); - resumeAfterBuffer = playerSeekWasPlaying; // if we were playing, we’ll resume after seek/buffer audioPauseForSeek = true; try { audioEl.pause(); } catch {} safeSetCT(audioEl, Number(player.currentTime())); audioPauseForSeek = false; updateMSPositionState(false); + enforceBufferingGate(); }); player.on("seeked", () => { safeSetCT(audioEl, Number(player.currentTime())); syncGuard = false; - if (!isBufferingUI() && resumeAfterBuffer && bothPlayableAt(Number(player.currentTime()))) { - playBoth(); - resumeAfterBuffer = false; - } + if (!spinnerUp() && playerSeekWasPlaying && bothPlayableAt(Number(player.currentTime()))) playBoth(); updateMSPositionState(false); }); - // if some UI seeks the audio directly, mirror it to video (still respecting loader) + // if some UI seeks audio directly, mirror to video, but obey the spinner gate audioEl.addEventListener("seeking", () => { const at = Number(audioEl.currentTime) || 0; syncGuard = true; player.currentTime(at); syncGuard = false; updateMSPositionState(false); + enforceBufferingGate(); }); audioEl.addEventListener("seeked", () => { const at = Number(audioEl.currentTime) || 0; @@ -409,37 +395,13 @@ document.addEventListener("DOMContentLoaded", () => { updateMSPositionState(false); }); - // buffering hooks — if the loader shows, *nothing* plays - function enterBuffering(reason) { - bufferingHold = true; - if (!player.paused() || !audioEl.paused) { - resumeAfterBuffer = true; - pauseBoth(); - } - } - function exitBuffering() { - bufferingHold = false; - if (resumeAfterBuffer && !isBufferingUI() && bothPlayableAt(Number(player.currentTime()))) { - playBoth(); - } - resumeAfterBuffer = false; - } - - player.on("waiting", () => enterBuffering("waiting")); - player.on("stalled", () => enterBuffering("stalled")); - player.on("suspend", () => enterBuffering("suspend")); - player.on("seeking", () => enterBuffering("seeking")); - player.on("canplay", () => exitBuffering()); - player.on("playing", () => exitBuffering()); - 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(); + // buffering hooks: rely on real spinner, not guesses + player.on("waiting", enforceBufferingGate); + player.on("progress", enforceBufferingGate); + player.on("playing", () => { + if (!spinnerUp() && !player.paused()) audioEl.play()?.catch(()=>{}); + startAutosync(); }); - try { spinnerObserver.observe(player.el(), { attributes: true, attributeFilter: ["class"] }); } catch {} // simple error surfacing const errorBox = document.getElementById("loopedIndicator"); @@ -457,12 +419,12 @@ document.addEventListener("DOMContentLoaded", () => { } }); - // loop/end behavior — never stop on loop + // never stop on loop player.on("ended", () => { if (isLooping()) { safeSetCT(audioEl, 0); player.currentTime(0); - if (!isBufferingUI()) playBoth(); else resumeAfterBuffer = true; + if (!spinnerUp()) playBoth(); lastMSPos = 0; updateMSPositionState(false); } else { @@ -475,7 +437,7 @@ document.addEventListener("DOMContentLoaded", () => { safeSetCT(audioEl, 0); if (player.paused()) { player.currentTime(0); - if (!isBufferingUI()) playBoth(); else resumeAfterBuffer = true; + if (!spinnerUp()) playBoth(); } lastMSPos = 0; updateMSPositionState(false); @@ -484,12 +446,12 @@ document.addEventListener("DOMContentLoaded", () => { } }); - // leaving fullscreen? pause cleanly + // leaving fullscreen? chill everything document.addEventListener("fullscreenchange", () => { if (!document.fullscreenElement) pauseBoth(); }); - // tiny quiet retry on load error + // tiny quiet ready/retry helper function attachReady(elm, resolveSrc, markReady) { const onLoaded = () => { try { markReady(); } catch {} tryStart(); }; elm.addEventListener("loadeddata", onLoaded, { once: true }); @@ -507,18 +469,18 @@ document.addEventListener("DOMContentLoaded", () => { }, { once: true }); } - // spin-up when both sides are ready + // bring up both only when ready and not buffering function tryStart() { if (audioReady && videoReady) { const t = Number(player.currentTime()); if (isFinite(t) && Math.abs(Number(audioEl.currentTime) - t) > 0.1) safeSetCT(audioEl, t); - setupMediaSession(); - if (!isBufferingUI() && bothPlayableAt(t)) playBoth(); + if (!spinnerUp() && bothPlayableAt(t)) playBoth(); else { audioEl.pause(); player.pause(); } + setupMediaSession(); } } - // wire readiness unless medium quality shortcut + // readiness wires if (qua !== "medium") { attachReady(audioEl, pickAudioSrc, () => { audioReady = true; }); attachReady(videoEl, () => { @@ -527,13 +489,12 @@ document.addEventListener("DOMContentLoaded", () => { }, () => { videoReady = true; }); } - // one-time click to satisfy autoplay policies + // one-tap gesture nudge player.ready(() => { const tryKick = () => { if (audioReady && videoReady && player.paused()) { const t = Number(player.currentTime()); - if (!isBufferingUI() && bothPlayableAt(t)) playBoth(); - else resumeAfterBuffer = true; + if (!spinnerUp() && bothPlayableAt(t)) playBoth(); } }; player.el().addEventListener("click", tryKick, { once: true });