make stuff work good

This commit is contained in:
ashley 2025-10-17 19:49:11 +02:00
parent 7a15b8c143
commit 0e97b07a3c

View File

@ -14,7 +14,6 @@ var versionclient = "youtube.player.web_20250917_22_RC00"
* Available under Apache License Version 2.0 * Available under Apache License Version 2.0
* <https://github.com/mozilla/vtt.js/blob/main/LICENSE> * <https://github.com/mozilla/vtt.js/blob/main/LICENSE>
*/ */
document.addEventListener("DOMContentLoaded", () => { 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 // 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', { const video = videojs('video', {
@ -104,6 +103,11 @@ document.addEventListener("DOMContentLoaded", () => {
let prevAudioMuted = false; let prevAudioMuted = false;
let pendingUnmute = false; let pendingUnmute = false;
// user intent + buffering flags
let userWantsPlay = false; // true when user pressed play (well start as soon as its valid)
let videoBuffering = false;
let audioBuffering = false;
// New state: track whether user explicitly muted/unmuted // New state: track whether user explicitly muted/unmuted
let userMutedVideo = false; let userMutedVideo = false;
let userMutedAudio = false; let userMutedAudio = false;
@ -174,6 +178,29 @@ document.addEventListener("DOMContentLoaded", () => {
} catch {} } catch {}
} }
// status / notice box (uses your loopedIndicator element)
const noticeBox = document.getElementById('loopedIndicator');
let statusHideTimer = null;
function showNotice(msg) {
if (!noticeBox) return;
try {
noticeBox.textContent = msg;
noticeBox.style.display = 'block';
noticeBox.style.width = 'fit-content';
if (statusHideTimer) { clearTimeout(statusHideTimer); statusHideTimer = null; }
} catch {}
}
function hideNotice(withDelay = 300) {
if (!noticeBox) return;
try {
if (statusHideTimer) clearTimeout(statusHideTimer);
statusHideTimer = setTimeout(() => {
noticeBox.style.display = 'none';
}, Math.max(0, withDelay));
} catch {}
}
// sync loop is cleared
function clearSyncLoop() { function clearSyncLoop() {
if (syncInterval) { if (syncInterval) {
clearInterval(syncInterval); clearInterval(syncInterval);
@ -182,6 +209,21 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// helpers to decide if we can start or continue playback
function readyForPlayback() {
const t = Number(video.currentTime());
return videoReady && audioReady && bothPlayableAt(t) && !videoBuffering && !audioBuffering;
}
function ensurePausedTogether() {
try { video.pause(); } catch {}
try { audio.pause(); } catch {}
clearSyncLoop();
vIsPlaying = false;
aIsPlaying = false;
}
// playback is kept in sync between both elements
function startSyncLoop() { function startSyncLoop() {
clearSyncLoop(); clearSyncLoop();
syncInterval = setInterval(() => { syncInterval = setInterval(() => {
@ -231,13 +273,16 @@ document.addEventListener("DOMContentLoaded", () => {
}, SYNC_INTERVAL_MS); }, SYNC_INTERVAL_MS);
} }
// we track playback state
const markVPlaying = () => { vIsPlaying = true; maybeUnmuteRestore(); }; const markVPlaying = () => { vIsPlaying = true; maybeUnmuteRestore(); };
const markAPlaying = () => { aIsPlaying = true; maybeUnmuteRestore(); }; const markAPlaying = () => { aIsPlaying = true; maybeUnmuteRestore(); };
const markVNotPlaying = () => { vIsPlaying = false; }; const markVNotPlaying = () => { vIsPlaying = false; };
const markANotPlaying = () => { aIsPlaying = false; }; const markANotPlaying = () => { aIsPlaying = false; };
// we know both are active
function bothActivelyPlaying() { return vIsPlaying && aIsPlaying; } function bothActivelyPlaying() { return vIsPlaying && aIsPlaying; }
// both get unmuted when ready
function maybeUnmuteRestore() { function maybeUnmuteRestore() {
if (!pendingUnmute) return; if (!pendingUnmute) return;
if (bothActivelyPlaying()) { if (bothActivelyPlaying()) {
@ -249,6 +294,7 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// helper: promise play with result
async function tryPlay(el) { async function tryPlay(el) {
try { try {
const p = el.play(); const p = el.play();
@ -257,9 +303,26 @@ document.addEventListener("DOMContentLoaded", () => {
} catch { return false; } } catch { return false; }
} }
// gate play attempts while loading/buffering
function blockIfNotReadyThenQueue() {
if (!readyForPlayback()) {
userWantsPlay = true; // remember intent
showNotice('Buffering…');
ensurePausedTogether();
return true; // blocked
}
hideNotice(200);
return false; // not blocked
}
// both play in sync
async function playTogether({ allowMutedRetry = true } = {}) { async function playTogether({ allowMutedRetry = true } = {}) {
if (syncing || restarting) return; if (syncing || restarting) return;
// dont let it play when audio is loading or video is loading
if (blockIfNotReadyThenQueue()) return;
syncing = true; syncing = true;
userWantsPlay = true; // explicit intent to be playing
try { try {
const t = Number(video.currentTime()); const t = Number(video.currentTime());
if (isFinite(t) && Math.abs(Number(audio.currentTime) - t) > 0.05) { if (isFinite(t) && Math.abs(Number(audio.currentTime) - t) > 0.05) {
@ -285,16 +348,16 @@ document.addEventListener("DOMContentLoaded", () => {
if (!vOk && aOk) { if (!vOk && aOk) {
try { try {
video.play().catch(() => { video.play().catch(() => {
showError('Video failed to start.'); showNotice('Video failed to start.');
setTimeout(() => { setTimeout(() => {
video.play().catch(() => showError('Video retry failed.')); video.play().catch(() => showNotice('Video retry failed.'));
}, 3000); }, 3000);
}); });
} catch {} } catch {}
} }
if (vOk && !aOk) { if (vOk && !aOk) {
try { try {
audio.play().catch(() => showError('Audio failed to start.')); audio.play().catch(() => showNotice('Audio failed to start.'));
} catch {} } catch {}
} }
@ -304,18 +367,25 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// both pause at once
function pauseTogether() { function pauseTogether() {
if (syncing) return; if (syncing) return;
syncing = true; syncing = true;
try { try {
userWantsPlay = false; // user paused explicitly
video.pause(); video.pause();
audio.pause(); audio.pause();
clearSyncLoop(); clearSyncLoop();
showNotice('Paused');
hideNotice(600);
} finally { } finally {
syncing = false; syncing = false;
} }
} }
// soooo i know what ur gonna say, its a looped indicator but ur using for errors, and what?
// like, why not i use the same element for both its not illegal...i think?
// search it up lol
const errorBox = document.getElementById('loopedIndicator'); const errorBox = document.getElementById('loopedIndicator');
function showError(msg) { function showError(msg) {
if (errorBox) { if (errorBox) {
@ -327,6 +397,7 @@ document.addEventListener("DOMContentLoaded", () => {
const clamp = v => Math.max(0, Math.min(1, Number(v))); const clamp = v => Math.max(0, Math.min(1, Number(v)));
// media session controls work, these are legit so anoying to work with
function setupMediaSession() { function setupMediaSession() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
try { try {
@ -361,6 +432,7 @@ document.addEventListener("DOMContentLoaded", () => {
} }
} }
// progress save/restore (safe bounds)
const PROGRESS_KEY = `progress-${vidKey}`; const PROGRESS_KEY = `progress-${vidKey}`;
function restoreProgress() { function restoreProgress() {
try { try {
@ -369,47 +441,68 @@ document.addEventListener("DOMContentLoaded", () => {
if (isFinite(saved) && saved > 3 && dur && saved < (dur - 10)) { if (isFinite(saved) && saved > 3 && dur && saved < (dur - 10)) {
video.currentTime(saved); video.currentTime(saved);
safeSetCT(audio, saved); safeSetCT(audio, saved);
firstSeekDone = true; firstSeekDone = true; // prevent immediate reseek jitter
} }
} catch {} } catch {}
} }
function saveProgressThrottled() { function saveProgressThrottled() {
// simple throttle by modulo of seconds
try { try {
const t = Math.floor(Number(video.currentTime()) || 0); const t = Math.floor(Number(video.currentTime()) || 0);
if (t % 2 === 0) localStorage.setItem(PROGRESS_KEY, String(t)); if (t % 2 === 0) localStorage.setItem(PROGRESS_KEY, String(t));
} catch {} } catch {}
} }
function wireResilience(el, label) { // network resilience (stall/waiting recovery)
function wireResilience(el, label, isVideo) {
try { try {
el.addEventListener('waiting', () => { el.addEventListener('waiting', () => {
// when video or audio goes into waiting, pause both to avoid audio ghosting // when video or audio goes into waiting, pause both to avoid audio ghosting
showError(`${label} waiting…`); showNotice('Buffering…');
try { audio.pause(); } catch {} if (isVideo) videoBuffering = true; else audioBuffering = true;
try { video.pause(); } catch {} ensurePausedTogether();
}); });
el.addEventListener('stalled', () => { el.addEventListener('stalled', () => {
showError(`${label} stalled`); showNotice(`${label} stalled`);
// also pause the other if (isVideo) videoBuffering = true; else audioBuffering = true;
try { audio.pause(); } catch {} ensurePausedTogether();
try { video.pause(); } catch {}
}); });
el.addEventListener('suspend', () => { el.addEventListener('suspend', () => {
// no-op /* no-op; advisory */
}); });
el.addEventListener('emptied', () => { el.addEventListener('emptied', () => {
showError(`${label} source emptied`); showNotice(`${label} source emptied`);
try { audio.pause(); } catch {} if (isVideo) videoBuffering = true; else audioBuffering = true;
try { video.pause(); } catch {} ensurePausedTogether();
}); });
el.addEventListener('error', () => { el.addEventListener('error', () => {
showError(`${label} error`); showNotice(`${label} error`);
try { audio.pause(); } catch {} if (isVideo) videoBuffering = true; else audioBuffering = true;
try { video.pause(); } catch {} ensurePausedTogether();
});
// clear buffering flags on canplay / playing
el.addEventListener('canplay', () => {
if (isVideo) videoBuffering = false; else audioBuffering = false;
if (readyForPlayback() && userWantsPlay) {
hideNotice(150);
playTogether({ allowMutedRetry: true });
}
});
el.addEventListener('playing', () => {
if (isVideo) videoBuffering = false; else audioBuffering = false;
if (readyForPlayback()) hideNotice(200);
});
el.addEventListener('canplaythrough', () => {
if (isVideo) videoBuffering = false; else audioBuffering = false;
if (readyForPlayback() && userWantsPlay) {
hideNotice(150);
playTogether({ allowMutedRetry: true });
}
}); });
} catch {} } catch {}
} }
// guards to ensure elements exist before heavy sync logic
const hasExternalAudio = !!audio && audio.tagName === 'AUDIO' && !!pickAudioSrc(); const hasExternalAudio = !!audio && audio.tagName === 'AUDIO' && !!pickAudioSrc();
if (qua !== "medium" && hasExternalAudio) { if (qua !== "medium" && hasExternalAudio) {
@ -428,13 +521,19 @@ document.addEventListener("DOMContentLoaded", () => {
const tryStart = () => { const tryStart = () => {
if (audioReady && videoReady && !restarting) { if (audioReady && videoReady && !restarting) {
// restore progress once we know duration/metadata
restoreProgress(); restoreProgress();
const t = Number(video.currentTime()); const t = Number(video.currentTime());
if (isFinite(t) && Math.abs(Number(audio.currentTime) - t) > 0.1) { if (isFinite(t) && Math.abs(Number(audio.currentTime) - t) > 0.1) {
safeSetCT(audio, t); safeSetCT(audio, t);
} }
if (bothPlayableAt(t)) playTogether({ allowMutedRetry: true }); if (bothPlayableAt(t)) {
else pauseTogether(); // dont auto-play unless user asked; just sync readiness
if (userWantsPlay) playTogether({ allowMutedRetry: true });
} else {
ensurePausedTogether();
}
setupMediaSession(); setupMediaSession();
} }
}; };
@ -442,45 +541,40 @@ document.addEventListener("DOMContentLoaded", () => {
attachRetry(audio, pickAudioSrc, () => { audioReady = true; }); attachRetry(audio, pickAudioSrc, () => { audioReady = true; });
attachRetry(videoEl, () => videoSrc, () => { videoReady = true; }); attachRetry(videoEl, () => videoSrc, () => { videoReady = true; });
// Listen for manual mute/unmute on video // todo: fiixxx mute stuff lol
video.on('volumechange', () => { video.on('volumechange', () => {
try { try {
const isMuted = video.muted(); const isMuted = video.muted();
// If user toggles mute, record intent // If user toggles mute, record intent
if (isMuted !== userMutedVideo) { userMutedVideo = !!isMuted;
userMutedVideo = isMuted;
}
if (!isMuted && !userMutedVideo) {
// sync audio mute
audio.muted = false;
}
if (audio) { if (audio) {
audio.muted = isMuted; audio.muted = isMuted;
} }
if (!video.muted()) { if (!isMuted) {
audio.volume = clamp(video.volume()); audio.volume = clamp(video.volume());
} }
} catch {} } catch {}
}); });
// Listen manual mute/unmute on audio (if exposed) // Listen manual mute/unmute on audio (if exposed somewhere)
audio.addEventListener('volumechange', () => { audio.addEventListener('volumechange', () => {
try { try {
const isMuted = audio.muted; userMutedAudio = !!audio.muted;
if (isMuted !== userMutedAudio) {
userMutedAudio = isMuted;
}
if (!userMutedAudio && !userMutedAudio) {
// sync video mute
video.muted(false);
}
} catch {} } catch {}
}); });
video.on('ratechange', () => { try { audio.playbackRate = video.playbackRate(); } catch {} }); video.on('ratechange', () => { try { audio.playbackRate = video.playbackRate(); } catch {} });
// capture user play intent; if not ready, queue and block actual play so one click is enough
video.on('play', () => { video.on('play', () => {
vIsPlaying = true; userWantsPlay = true;
if (!readyForPlayback()) {
showNotice('Buffering…');
ensurePausedTogether();
// will auto-resume on canplay/canplaythrough
return;
}
markVPlaying();
if (!aIsPlaying) playTogether(); if (!aIsPlaying) playTogether();
}); });
audio.addEventListener('play', () => { audio.addEventListener('play', () => {
@ -502,12 +596,16 @@ document.addEventListener("DOMContentLoaded", () => {
} }
}); });
// large seeks pause and resync
let wasPlayingBeforeSeek = false; let wasPlayingBeforeSeek = false;
let lastSeekTime = 0; let lastSeekTime = 0;
video.on('seeking', () => { video.on('seeking', () => {
if (restarting) return; if (restarting) return;
wasPlayingBeforeSeek = !video.paused(); wasPlayingBeforeSeek = !video.paused();
lastSeekTime = Number(video.currentTime()); lastSeekTime = Number(video.currentTime());
// while seeking, block start until canplay
showNotice('Buffering…');
ensurePausedTogether();
}); });
video.on('seeked', () => { video.on('seeked', () => {
@ -524,39 +622,53 @@ document.addEventListener("DOMContentLoaded", () => {
} }
if (seekDiff > threshold) { if (seekDiff > threshold) {
pauseTogether(); ensurePausedTogether();
safeSetCT(audio, newTime); safeSetCT(audio, newTime);
setTimeout(() => { setTimeout(() => {
if (wasPlayingBeforeSeek && bothPlayableAt(newTime)) { if (wasPlayingBeforeSeek) {
playTogether({ allowMutedRetry: true }); userWantsPlay = true;
if (readyForPlayback()) playTogether({ allowMutedRetry: true });
else showNotice('Buffering…');
} }
}, 180); }, 180);
} else { } else {
safeSetCT(audio, newTime); safeSetCT(audio, newTime);
if (wasPlayingBeforeSeek) {
userWantsPlay = true;
if (readyForPlayback()) playTogether({ allowMutedRetry: true });
else showNotice('Buffering…');
}
} }
}); });
// save progress periodically
try { try {
video.on('timeupdate', saveProgressThrottled); video.on('timeupdate', saveProgressThrottled);
} catch {} } catch {}
wireResilience(videoEl, 'Video'); // wire stall/err resilience
wireResilience(audio, 'Audio'); wireResilience(videoEl, 'Video', true);
wireResilience(audio, 'Audio', false);
// looping restarts properly
// doesnt work LOOOOOOOOL
// sooo... I guess, TODO: fix the looping??????
async function restartLoop() { async function restartLoop() {
if (restarting) return; if (restarting) return;
restarting = true; restarting = true;
try { try {
clearSyncLoop(); clearSyncLoop();
pauseTogether(); ensurePausedTogether();
const startAt = 0.001; const startAt = 0.001;
suppressEndedUntil = performance.now() + 800; suppressEndedUntil = performance.now() + 800;
video.currentTime(startAt); video.currentTime(startAt);
safeSetCT(audio, startAt); safeSetCT(audio, startAt);
userWantsPlay = true;
await playTogether(); await playTogether();
} finally { restarting = false; } } finally { restarting = false; }
} }
// okay, this actually, legit, not working idk why guuuh
video.on('ended', () => { video.on('ended', () => {
if (restarting) return; if (restarting) return;
if (performance.now() < suppressEndedUntil) return; if (performance.now() < suppressEndedUntil) return;
@ -570,17 +682,23 @@ document.addEventListener("DOMContentLoaded", () => {
else pauseTogether(); else pauseTogether();
}); });
// try to resume if media becomes playable again after waiting
videoEl.addEventListener('canplay', () => { videoEl.addEventListener('canplay', () => {
if (!video.paused() && !audio.paused()) return; videoBuffering = false;
const t = Number(video.currentTime()); if (readyForPlayback() && userWantsPlay) {
if (bothPlayableAt(t)) playTogether({ allowMutedRetry: true }); hideNotice(150);
playTogether({ allowMutedRetry: true });
}
}); });
audio.addEventListener('canplay', () => { audio.addEventListener('canplay', () => {
if (!video.paused() && !audio.paused()) return; audioBuffering = false;
const t = Number(video.currentTime()); if (readyForPlayback() && userWantsPlay) {
if (bothPlayableAt(t)) playTogether({ allowMutedRetry: true }); hideNotice(150);
playTogether({ allowMutedRetry: true });
}
}); });
// clean up on unload to avoid stray timers
try { try {
window.addEventListener('pagehide', () => { clearSyncLoop(); }); window.addEventListener('pagehide', () => { clearSyncLoop(); });
window.addEventListener('beforeunload', () => { window.addEventListener('beforeunload', () => {
@ -591,14 +709,28 @@ document.addEventListener("DOMContentLoaded", () => {
} catch {} } catch {}
} else { } else {
// fallback when medium quality (no external <audio>) or audio element missing: // fallback when medium quality (no external <audio>) or audio element missing:
// keep Media Session + progress for the single video element so UX is still solid
try { try {
video.on('timeupdate', () => { video.on('timeupdate', () => {
try { localStorage.setItem(`progress-${vidKey}`, String(Math.floor(Number(video.currentTime()) || 0))); } catch {} try { localStorage.setItem(`progress-${vidKey}`, String(Math.floor(Number(video.currentTime()) || 0))); } catch {}
}); });
} catch {} } catch {}
setupMediaSession(); setupMediaSession();
// also gate play here if the single stream is buffering
video.on('play', () => {
userWantsPlay = true;
if (!(videoReady && canPlayAt(videoEl, Number(video.currentTime())) && !videoBuffering)) {
showNotice('Buffering…');
video.pause();
}
});
videoEl.addEventListener('waiting', () => { videoBuffering = true; showNotice('Buffering…'); video.pause(); });
videoEl.addEventListener('canplay', () => { videoBuffering = false; if (userWantsPlay) { hideNotice(150); video.play().catch(()=>{}); }});
videoEl.addEventListener('playing', () => { videoBuffering = false; hideNotice(200); });
} }
}); });
// 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