Update css/player-base.js

This commit is contained in:
ashley 2025-08-27 09:21:05 +02:00
parent 5ec27472a3
commit 3ee33ab166

View File

@ -23,656 +23,44 @@ var _yt_player = videojs;
document.addEventListener("DOMContentLoaded", () => {
const qs = new URLSearchParams(location.search);
const vidKey = qs.get("v") || "";
if (vidKey) { try { localStorage.removeItem(`progress-${vidKey}`); } catch {} }
document.addEventListener("DOMContentLoaded", () => {
// video.js 8 init - source can be seen in https://poketube.fun/static/vjs.min.js or the vjs.min.js file
const video = videojs('video', {
controls: true,
autoplay: false,
preload: 'auto'
});
// --- query + minimal state ---
const qs = new URLSearchParams(window.location.search);
const qua = qs.get("quality") || "";
const vidKey = qs.get('v') || '';
const PROG_KEY = vidKey ? `progress-${vidKey}` : null;
// persist last position (quietly)
let lastSaved = 0;
const SAVE_INTERVAL = 1500;
function saveProgress(t) {
if (!PROG_KEY) return;
if (t - lastSaved >= 1.0) {
lastSaved = t;
try { localStorage.setItem(PROG_KEY, String(t)); } catch {}
}
const player = videojs('video', {
controls: true,
autoplay: false,
preload: 'auto',
html5: {
vhs: {
// VHS handles MPEG-DASH from <source type="application/dash+xml">
overrideNative: true, // prefer VHS over any native MSE impl quirks
enableLowInitialPlaylist: true
}
}
// initialize progress if empty
try {
if (PROG_KEY && localStorage.getItem(PROG_KEY) == null) localStorage.setItem(PROG_KEY, "0");
} catch {}
});
// raw media elements
const videoEl = document.getElementById('video');
const audio = document.getElementById('aud');
// resolve initial sources robustly (works whether <audio src> or <source> children are used)
const pickAudioSrc = () => {
const s = audio?.getAttribute?.('src');
if (s) return s;
const child = audio?.querySelector?.('source');
if (child?.getAttribute?.('src')) return child.getAttribute('src');
if (audio?.currentSrc) return audio.currentSrc;
return null;
};
let audioSrc = pickAudioSrc();
const srcObj = video.src();
const initialVideoSrc = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].src) : srcObj;
const initialVideoType = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].type) : undefined;
// readiness + sync state
let audioReady = false, videoReady = false;
let syncInterval = null;
// thresholds / constants
const BIG_DRIFT = 0.5;
const MICRO_DRIFT = 0.05;
const SYNC_INTERVAL_MS = 240;
const SEEK_STICKY_EPS = 0.03;
// utility: clamping + safe currentTime
const clamp01 = v => Math.max(0, Math.min(1, Number(v)));
function canSafelySetCT(media) {
try {
// HAVE_METADATA (1) is enough to set currentTime without DOMExceptions for MP4/WebM
return typeof media.readyState === 'number' ? media.readyState >= 1 : true;
} catch { return false; }
}
function safeSetCT(media, t) {
try {
if (!isFinite(t) || t < 0) return;
if (canSafelySetCT(media)) media.currentTime = t;
} catch {}
}
// clear sync ticker
function clearSyncLoop() {
if (syncInterval) {
clearInterval(syncInterval);
syncInterval = null;
try { audio.playbackRate = 1; } catch {}
}
}
// drift-compensation loop for micro-sync
function startSyncLoop() {
clearSyncLoop();
syncInterval = setInterval(() => {
const vt = Number(video.currentTime());
const at = Number(audio.currentTime);
if (!isFinite(vt) || !isFinite(at)) return;
// save progress (throttled)
saveProgress(vt);
const delta = vt - at;
// large drift → snap
if (Math.abs(delta) > BIG_DRIFT) {
safeSetCT(audio, vt);
try { audio.playbackRate = 1; } catch {}
return;
}
// micro drift → gentle nudge by rate
if (Math.abs(delta) > MICRO_DRIFT) {
const targetRate = 1 + (delta * 0.12);
try { audio.playbackRate = Math.max(0.85, Math.min(1.15, targetRate)); } catch {}
} else {
try { audio.playbackRate = 1; } catch {}
}
}, SYNC_INTERVAL_MS);
}
// align start when both are ready
function tryStart() {
if (audioReady && videoReady) {
// resume from stored time once (quiet)
let resumeT = 0;
try {
if (PROG_KEY) resumeT = parseFloat(localStorage.getItem(PROG_KEY) || "0") || 0;
} catch {}
const vt0 = resumeT > 0 ? resumeT : Number(video.currentTime());
if (isFinite(vt0) && Math.abs(Number(audio.currentTime) - vt0) > 0.1) {
safeSetCT(audio, vt0);
try { video.currentTime(vt0); } catch {}
}
// autoplay policy: try normal, if blocked try muted, then unmute
const startBoth = () => {
const pv = video.play();
const pa = audio.play();
pv?.catch(()=>{}); pa?.catch(()=>{});
};
const attempt = () => {
startSyncLoop();
setupMediaSession();
startBoth();
};
attempt();
// If autoplay blocked: try muted bootstrap, then unmute after a moment
setTimeout(() => {
if (video.paused()) {
try { video.muted(true); } catch {}
try { audio.muted = true; } catch {}
startBoth();
setTimeout(() => {
try { video.muted(false); } catch {}
try { audio.muted = false; } catch {}
}, 800);
}
}, 200);
}
}
// generic one-shot retry helper for DOM media element
function attachRetry(elm, resolveSrc, markReady) {
const initial = resolveSrc?.();
// mark readiness on either loadedmetadata|loadeddata
const onLoaded = () => {
try { elm._didRetry = false; } catch {}
markReady();
tryStart();
};
elm.addEventListener('loadeddata', onLoaded, { once: true });
elm.addEventListener('loadedmetadata', onLoaded, { once: true });
// one quiet retry on error
elm.addEventListener('error', () => {
const retryURL = resolveSrc?.() || initial;
if (!elm._didRetry && retryURL) {
elm._didRetry = true;
try {
elm.removeAttribute('src');
[...elm.querySelectorAll('source')].forEach(n => n.remove());
elm.src = retryURL;
elm.load();
} catch {}
} else {
// swallow to avoid console spam/UI noise
}
}, { once: true });
}
// media session / hardware keys
function setupMediaSession() {
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.metadata = new MediaMetadata({
title: document.title || 'Video',
artist: '',
album: '',
artwork: []
});
} catch {}
navigator.mediaSession.setActionHandler('play', () => {
video.play()?.catch(()=>{}); audio.play()?.catch(()=>{});
});
navigator.mediaSession.setActionHandler('pause', () => {
video.pause(); audio.pause();
});
navigator.mediaSession.setActionHandler('seekbackward', ({ seekOffset }) => {
const skip = seekOffset || 10;
const to = Math.max(0, Number(video.currentTime()) - skip);
video.currentTime(to); safeSetCT(audio, to);
});
navigator.mediaSession.setActionHandler('seekforward', ({ seekOffset }) => {
const skip = seekOffset || 10;
const to = Number(video.currentTime()) + skip;
video.currentTime(to); safeSetCT(audio, to);
});
navigator.mediaSession.setActionHandler('seekto', ({ seekTime, fastSeek }) => {
if (!isFinite(seekTime)) return;
if (fastSeek && 'fastSeek' in audio) { try { audio.fastSeek(seekTime); } catch { safeSetCT(audio, seekTime); } }
else safeSetCT(audio, seekTime);
try { video.currentTime(seekTime); } catch {}
});
navigator.mediaSession.setActionHandler('stop', () => {
video.pause(); audio.pause();
try { video.currentTime(0); } catch {}
try { audio.currentTime = 0; } catch {}
clearSyncLoop();
});
}
}
// ** DESKTOP MEDIA-KEY FALLBACK **
document.addEventListener('keydown', e => {
switch (e.code) {
case 'AudioPlay':
case 'MediaPlayPause':
if (video.paused()) { video.play()?.catch(()=>{}); audio.play()?.catch(()=>{}); }
else { video.pause(); audio.pause(); }
break;
case 'AudioPause':
video.pause(); audio.pause();
break;
case 'AudioNext':
case 'MediaTrackNext': {
const tFwd = Number(video.currentTime()) + 10;
video.currentTime(tFwd); safeSetCT(audio, tFwd);
break;
}
case 'AudioPrevious':
case 'MediaTrackPrevious': {
const tBwd = Math.max(0, Number(video.currentTime()) - 10);
video.currentTime(tBwd); safeSetCT(audio, tBwd);
break;
}
}
});
// === PRIMARY SYNC/RETRY LOGIC (skips when qua=medium) ===
if (qua !== "medium") {
// attach retry & ready markers to the real elements
attachRetry(audio, pickAudioSrc, () => { audioReady = true; });
attachRetry(videoEl, () => {
// prefer current player src if any; fallback to initial
const s = video.src();
return Array.isArray(s) ? (s[0] && s[0].src) : (s || initialVideoSrc);
}, () => { videoReady = true; });
// keep audio volume mirrored to player volume both ways
video.on('volumechange', () => { try { audio.volume = clamp01(video.volume()); } catch {} });
audio.addEventListener('volumechange', () => { try { video.volume(clamp01(audio.volume)); } catch {} });
// rate sync (rare, but keep consistent)
video.on('ratechange', () => {
try { audio.playbackRate = video.playbackRate(); } catch {}
});
// Sync when playback starts
video.on('play', () => {
if (!syncInterval) startSyncLoop();
const vt = Number(video.currentTime());
if (Math.abs(vt - Number(audio.currentTime)) > 0.3) safeSetCT(audio, vt);
if (audioReady) audio.play()?.catch(()=>{});
});
video.on('pause', () => { audio.pause(); clearSyncLoop(); });
// pause audio when video is buffering :3
video.on('waiting', () => { audio.pause(); clearSyncLoop(); });
// resume audio when video resumes
video.on('playing', () => {
if (audioReady) audio.play()?.catch(()=>{});
if (!syncInterval) startSyncLoop();
});
// === refined seek: debounce + sticky lock ===
let seekingDebounce = null;
function seekSyncNow() {
const vt = Number(video.currentTime());
if (!isFinite(vt)) return;
if (Math.abs(vt - Number(audio.currentTime)) > SEEK_STICKY_EPS) safeSetCT(audio, vt);
}
video.on('seeking', () => {
audio.pause();
clearSyncLoop();
seekSyncNow();
if (seekingDebounce) clearTimeout(seekingDebounce);
});
video.on('seeked', () => {
seekSyncNow();
seekingDebounce = setTimeout(() => {
if (audioReady) audio.play()?.catch(()=>{});
if (!syncInterval) startSyncLoop();
}, 60); // tiny sticky to let demux stabilize
});
// Detects when video or audio finishes buffering; nudge sync
video.on('canplaythrough', seekSyncNow);
audio.addEventListener('canplaythrough', seekSyncNow);
// stop everything on media end
video.on('ended', () => { try { audio.pause(); } catch {}; clearSyncLoop(); });
audio.addEventListener('ended', () => { try { video.pause(); } catch {}; clearSyncLoop(); });
// pause when exiting full screen :3
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement) {
video.pause(); audio.pause(); clearSyncLoop();
}
});
// === PiP + Fullscreen sync niceties (quiet) ===
document.addEventListener('enterpictureinpicture', () => { if (audioReady) audio.play()?.catch(()=>{}); });
document.addEventListener('leavepictureinpicture', () => { /* no-op; keep state */ });
// === Wake Lock (if supported) to prevent sleep during playback (quiet, optional) ===
let wakeLock = null;
async function requestWakeLock() {
try { wakeLock = await navigator.wakeLock.request('screen'); } catch {}
}
function releaseWakeLock() { try { wakeLock?.release(); } catch {} wakeLock = null; }
video.on('playing', requestWakeLock);
video.on('pause', releaseWakeLock);
window.addEventListener('visibilitychange', () => {
// Reacquire on visibility change if still playing
if (!document.hidden && !video.paused()) requestWakeLock();
});
// === VIDEO.JS AUTO-RETRY (30s GRACE + ONLY WHEN REALLY BROKEN) ===
// We ignore tech errors for the first 30s after stream actually starts (play or loadeddata).
// After 30s, retries are invisible and only trigger if playback is genuinely stuck.
const BASE_STEPS_MS = [250, 400, 650, 900, 1200, 1600, 2000, 2600, 3400, 4400]; // a bit longer tail
const JITTER = 120;
let vjsRetryCount = 0;
let allowRetries = false; // becomes true only after grace window
let graceTimerStarted = false;
let graceTimerId = null;
// tiny watchdogs
let watch = { t: 0, at: 0, active: false };
const WATCH_GRACE_MS = 2200; // if no time advance while “playing” post-grace → retry
let readyWatch = { ok: 0, bad: 0 }; // readyState health tiebreaker
function currentVideoSrc() {
const s = video.src();
return Array.isArray(s) ? (s[0] && s[0].src) : s;
}
function currentVideoType() {
const s = video.src();
return Array.isArray(s) ? (s[0] && s[0].type) : undefined;
}
// treat as *healthy* if we clearly have playable media or are advancing time
function isPlaybackHealthy() {
try {
if (!video.paused() && Number(video.currentTime()) > 0) return true;
if (typeof video.readyState === 'function') {
if (video.readyState() >= 2 && isFinite(video.duration()) && video.duration() > 0) return true;
}
const el = videoEl;
if (el && typeof el.readyState === 'number' && el.readyState >= 2) return true;
} catch {}
return false;
}
// start 30s grace on first real start signal
function startGraceIfNeeded() {
if (graceTimerStarted) return;
graceTimerStarted = true;
graceTimerId = setTimeout(() => {
allowRetries = true;
if (!isPlaybackHealthy()) scheduleVideoRetry('post-30s-initial');
}, 30000);
}
video.one('loadeddata', startGraceIfNeeded);
video.one('play', startGraceIfNeeded);
// exponential backoff with jitter
function backoffDelay(i) {
const base = BASE_STEPS_MS[Math.min(i, BASE_STEPS_MS.length - 1)];
const jitter = Math.floor((Math.random() * 2 - 1) * JITTER);
return Math.max(120, base + jitter);
}
// only retry when truly broken, and only after grace
function scheduleVideoRetry(reason) {
if (!allowRetries) return;
if (isPlaybackHealthy()) { vjsRetryCount = 0; return; }
if ('onLine' in navigator && !navigator.onLine) {
const onlineOnce = () => {
window.removeEventListener('online', onlineOnce);
scheduleVideoRetry('back-online');
};
window.addEventListener('online', onlineOnce, { once: true });
return;
}
const delay = backoffDelay(vjsRetryCount++);
const keepTime = Number(video.currentTime());
// pause & clear sync while we refetch (quiet, no UI flicker)
try { video.pause(); } catch {}
try { audio.pause(); } catch {}
clearSyncLoop();
setTimeout(() => {
const srcUrl = currentVideoSrc() || initialVideoSrc;
const type = currentVideoType() || initialVideoType;
try {
if (type) video.src({ src: srcUrl, type });
else video.src(srcUrl);
} catch {}
try { videoEl.load && videoEl.load(); } catch {}
video.one('loadeddata', () => {
try {
if (isFinite(keepTime)) {
video.currentTime(keepTime);
safeSetCT(audio, keepTime);
}
} catch {}
video.play()?.catch(()=>{});
if (audioReady) audio.play()?.catch(()=>{});
if (!syncInterval) startSyncLoop();
});
}, delay);
}
// watchdog: only active after grace; if time does not advance while “playing”, do a fast retry
function startWatchdog() {
watch.active = true;
watch.t = Number(video.currentTime());
watch.at = performance.now();
}
function stopWatchdog() { watch.active = false; }
video.on('playing', () => { startWatchdog(); if (allowRetries) vjsRetryCount = 0; });
video.on('pause', () => { stopWatchdog(); });
video.on('waiting', () => { startWatchdog(); });
video.on('timeupdate', () => {
if (!allowRetries || !watch.active) return;
const ct = Number(video.currentTime());
if (ct !== watch.t) {
watch.t = ct;
watch.at = performance.now();
return;
}
if ((performance.now() - watch.at) > WATCH_GRACE_MS && !video.paused()) {
scheduleVideoRetry('watchdog');
stopWatchdog();
}
});
// readyState health sampling: if repeatedly bad post-grace, trigger retry
const READY_SAMPLE_MS = 500;
setInterval(() => {
if (!allowRetries) return;
try {
const rs = videoEl?.readyState || 0; // 0-4
if (rs >= 3) { readyWatch.ok++; readyWatch.bad = 0; }
else { readyWatch.bad++; }
if (readyWatch.bad >= 6 && !video.paused()) { // ~3s of poor readiness
readyWatch.bad = 0;
scheduleVideoRetry('readystate');
}
} catch {}
}, READY_SAMPLE_MS);
// error gating: ignore everything until grace ends; after that, only retry if truly broken
function browserThinksPlayable() {
try {
const type = currentVideoType() || initialVideoType;
if (type && videoEl && typeof videoEl.canPlayType === 'function') {
const res = videoEl.canPlayType(type);
return !!(res && res !== 'no');
}
} catch {}
return false;
}
function shouldRetryForError(err) {
if (!allowRetries) return false;
if (isPlaybackHealthy()) return false;
if (!err) return true;
// HTML5 codes: 1=aborted, 2=network, 3=decode, 4=src not supported (noisy)
if (err.code === 2 || err.code === 3) return true;
if (err.code === 4) {
if (videoReady || browserThinksPlayable()) return true;
return false; // real "not supported" → do not loop
}
const msg = String(err.message || '').toLowerCase();
if (
msg.includes('network error') ||
msg.includes('media download') ||
msg.includes('server or network failed') ||
msg.includes('demuxer') ||
msg.includes('decode') ||
msg.includes('pipe') ||
msg.includes('hls') ||
msg.includes('dash')
) return true;
return false;
}
// main error hook (gated by 30s)
video.on('error', () => {
const err = video.error && video.error();
if (shouldRetryForError(err)) scheduleVideoRetry('error');
});
// treat transient stalls/aborts as retryable, but only after grace and only if not healthy
['stalled','abort','suspend','emptied','loadeddata'].forEach(ev => {
video.on(ev, () => { if (allowRetries && !isPlaybackHealthy()) scheduleVideoRetry(ev); });
});
// if we truly can play, reset counters
function markHealthy() { vjsRetryCount = 0; }
video.on('canplay', markHealthy);
video.on('playing', markHealthy);
video.on('loadeddata', markHealthy);
// === AUDIO WATCHDOG (quiet, invisible) ===
let audioWatch = { t: 0, at: 0, playing: false };
const AUDIO_WATCH_MS = 2500;
const AUDIO_STEPS_MS = [200, 350, 500, 700, 900, 1200, 1600];
let audioRetryCount = 0;
function audioStartWatch() {
audioWatch.t = Number(audio.currentTime) || 0;
audioWatch.at = performance.now();
audioWatch.playing = true;
}
function audioStopWatch() { audioWatch.playing = false; }
const audioWatchTicker = setInterval(() => {
if (!allowRetries || !audioWatch.playing) return;
const at = Number(audio.currentTime) || 0;
if (at !== audioWatch.t) {
audioWatch.t = at;
audioWatch.at = performance.now();
return;
}
// not advancing while video is playing → consider retry
if (!video.paused() && (performance.now() - audioWatch.at) > AUDIO_WATCH_MS) {
const step = Math.min(audioRetryCount, AUDIO_STEPS_MS.length - 1);
const delay = AUDIO_STEPS_MS[step] + Math.floor(Math.random()*60);
audioRetryCount++;
const keep = Number(video.currentTime()) || at;
try { audio.pause(); } catch {}
try { clearSyncLoop(); } catch {}
setTimeout(() => {
audioSrc = pickAudioSrc() || audioSrc;
try {
audio.removeAttribute('src');
[...audio.querySelectorAll('source')].forEach(n => n.remove());
if (audioSrc) audio.src = audioSrc;
audio.load();
} catch {}
const relink = () => {
audio.removeEventListener('loadeddata', relink);
try {
if (isFinite(keep)) safeSetCT(audio, keep);
audio.play()?.catch(()=>{});
if (!syncInterval) startSyncLoop();
audioRetryCount = 0;
audioStartWatch();
} catch {}
};
audio.addEventListener('loadeddata', relink, { once: true });
}, delay);
}
}, 420);
// keep audio watchdog aligned with video state
video.on('playing', audioStartWatch);
video.on('pause', audioStopWatch);
video.on('waiting', audioStartWatch);
audio.addEventListener('playing', audioStartWatch);
audio.addEventListener('pause', audioStopWatch);
// === Offline/Online bridging ===
window.addEventListener('offline', () => { /* quiet; let watchdog pick it up */ });
window.addEventListener('online', () => { if (allowRetries && !isPlaybackHealthy()) scheduleVideoRetry('online'); });
// === Page visibility: reduce background churn (timers quiet but playback undisturbed) ===
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// stop aggressive sync loop to save cycles; resume on show
clearSyncLoop();
} else if (!syncInterval && !video.paused()) {
startSyncLoop();
}
});
// === Clean up on unload (avoid stray timers) ===
window.addEventListener('beforeunload', () => {
clearSyncLoop();
try { releaseWakeLock(); } catch {}
try { saveProgress(Number(video.currentTime()) || 0); } catch {}
try { clearTimeout(graceTimerId); } catch {}
});
// === Minimal source sanity: if duration NaN for too long post-grace, nudge reload ===
setInterval(() => {
if (!allowRetries) return;
try {
const dur = video.duration();
if (!isFinite(dur) || dur <= 0) {
// only if also not healthy; avoids churn on live streams
if (!isPlaybackHealthy()) scheduleVideoRetry('duration-nan');
}
} catch {}
}, 5000);
// optional: tiny robustness without UI changes (safe, quiet)
player.on('error', () => {
const err = player.error();
// ignore genuine "not supported"; only attempt a quiet refresh for net/decode stalls
if (!err || err.code === 2 || err.code === 3) {
const ct = player.currentTime() || 0;
const src = player.currentSource(); // {src, type}
try {
player.pause();
// re-set the same MPD silently
if (src && src.src) player.src(src);
player.one('loadeddata', () => {
try { if (isFinite(ct) && ct > 0) player.currentTime(ct); } catch {}
player.play().catch(()=>{});
});
} catch {}
}
});
});
// hai!! if ur asking why are they here - its for smth in the future!!!!!!