fix stuff + add stuff
This commit is contained in:
parent
0cad0b3ff0
commit
1d07ca4bd1
@ -2,6 +2,7 @@
|
|||||||
var _yt_player = videojs;
|
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", () => {
|
||||||
// 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', {
|
||||||
@ -23,12 +24,22 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const audioEl = document.getElementById('aud');
|
const audioEl = document.getElementById('aud');
|
||||||
let volGuard = false;
|
let volGuard = false;
|
||||||
|
|
||||||
// FIX: ensure inline playback hint for iOS/Safari
|
// FIX: inline playback hint for iOS/Safari
|
||||||
try { videoEl.setAttribute('playsinline', ''); videoEl.setAttribute('webkit-playsinline', ''); } catch {}
|
try { videoEl.setAttribute('playsinline', ''); videoEl.setAttribute('webkit-playsinline', ''); } catch {}
|
||||||
|
|
||||||
// global anti-ping-pong guard
|
// --- GLOBAL STATE GUARDS ----------------------------------------------------
|
||||||
let syncing = false; // prevents normal ping-pong
|
let syncing = false; // prevents normal ping-pong
|
||||||
let restarting = false; // prevents loop-end ping-pong
|
let restarting = false; // prevents loop-end ping-pong
|
||||||
|
let seekingInProgress = false; // true between seeking/seeked
|
||||||
|
let resumeAfterSeek = false; // whether to auto-resume after seek
|
||||||
|
let didFirstSeek = false; // special handling for the first seek only
|
||||||
|
let vIsPlaying = false; // becomes true on 'playing'
|
||||||
|
let aIsPlaying = false; // becomes true on 'playing'
|
||||||
|
|
||||||
|
// Mute memory for muted-retry on autoplay policy
|
||||||
|
let prevVideoMuted = false;
|
||||||
|
let prevAudioMuted = false;
|
||||||
|
let pendingUnmute = false;
|
||||||
|
|
||||||
// FIX: explicit loop-state variables
|
// FIX: explicit loop-state variables
|
||||||
let desiredLoop =
|
let desiredLoop =
|
||||||
@ -40,26 +51,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// FIX: tracks the short window *during* a loop restart
|
// FIX: tracks the short window *during* a loop restart
|
||||||
let suppressEndedUntil = 0;
|
let suppressEndedUntil = 0;
|
||||||
|
|
||||||
// FIX: co-play tracking flags (true only when each element fires 'playing')
|
|
||||||
let vIsPlaying = false;
|
|
||||||
let aIsPlaying = false;
|
|
||||||
|
|
||||||
// remember mute states for temporary autoplay retries
|
|
||||||
let prevVideoMuted = false;
|
|
||||||
let prevAudioMuted = false;
|
|
||||||
let pendingUnmute = false;
|
|
||||||
|
|
||||||
// FIX: seeking coordination (prevents first-load seek ping-pong)
|
|
||||||
let seekingInProgress = false;
|
|
||||||
let resumeAfterSeek = false;
|
|
||||||
|
|
||||||
// FIX: state arbiter watchdog (forces both to share same paused/playing state)
|
// FIX: state arbiter watchdog (forces both to share same paused/playing state)
|
||||||
let arbiterTimer = null;
|
let arbiterTimer = null;
|
||||||
const ARBITER_MS = 150;
|
const ARBITER_MS = 150;
|
||||||
|
|
||||||
|
// NEW: suppression window so arbiter ignores the fragile post-seek moment
|
||||||
|
let arbiterSuppressUntil = 0;
|
||||||
|
const now = () => performance.now();
|
||||||
|
const arbiterSuppressed = () => now() < arbiterSuppressUntil;
|
||||||
|
|
||||||
function startArbiter() {
|
function startArbiter() {
|
||||||
if (arbiterTimer) clearInterval(arbiterTimer);
|
if (arbiterTimer) clearInterval(arbiterTimer);
|
||||||
arbiterTimer = setInterval(() => {
|
arbiterTimer = setInterval(() => {
|
||||||
if (syncing || restarting || seekingInProgress) return;
|
if (syncing || restarting || seekingInProgress || arbiterSuppressed()) return;
|
||||||
|
|
||||||
// treat "playing" strictly; ended counts as paused
|
// treat "playing" strictly; ended counts as paused
|
||||||
const vPlaying = !video.paused() && !video.ended();
|
const vPlaying = !video.paused() && !video.ended();
|
||||||
@ -95,7 +99,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
loopObserver.observe(videoEl, { attributes: true, attributeFilter: ['loop'] });
|
loopObserver.observe(videoEl, { attributes: true, attributeFilter: ['loop'] });
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// resolve initial sources robustly (works whether <audio src> or <source> children are used)
|
// --- SOURCE RESOLUTION ------------------------------------------------------
|
||||||
const pickAudioSrc = () => {
|
const pickAudioSrc = () => {
|
||||||
const s = audio?.getAttribute?.('src');
|
const s = audio?.getAttribute?.('src');
|
||||||
if (s) return s;
|
if (s) return s;
|
||||||
@ -110,7 +114,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const videoSrc = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].src) : srcObj;
|
const videoSrc = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].src) : srcObj;
|
||||||
const videoType = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].type) : undefined;
|
const videoType = Array.isArray(srcObj) ? (srcObj[0] && srcObj[0].type) : undefined;
|
||||||
|
|
||||||
// readiness + sync state
|
// --- READINESS + SYNC STATE -------------------------------------------------
|
||||||
let audioReady = false, videoReady = false;
|
let audioReady = false, videoReady = false;
|
||||||
let syncInterval = null;
|
let syncInterval = null;
|
||||||
|
|
||||||
@ -136,7 +140,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const rs = Number(media.readyState || 0);
|
const rs = Number(media.readyState || 0);
|
||||||
if (!isFinite(t)) return false;
|
if (!isFinite(t)) return false;
|
||||||
// HAVE_FUTURE_DATA (3) or better means imminent playback
|
// HAVE_FUTURE_DATA (3) or better means imminent playback
|
||||||
if (rs >= 3) return true; // FIX: rely on readyState for can-play gate
|
if (rs >= 3) return true; // rely on readyState for can-play gate
|
||||||
return timeInBuffered(media, t);
|
return timeInBuffered(media, t);
|
||||||
} catch { return false; }
|
} catch { return false; }
|
||||||
}
|
}
|
||||||
@ -201,9 +205,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIX: unified play/pause coordinators (no ping-pong)
|
// --- UNIFIED PLAY/PAUSE -----------------------------------------------------
|
||||||
async function playTogether({ allowMutedRetry = true } = {}) {
|
async function playTogether({ allowMutedRetry = true } = {}) {
|
||||||
if (syncing || restarting || seekingInProgress) return; // FIX: don't start while seeking
|
if (syncing || restarting || seekingInProgress) return; // don't start while seeking
|
||||||
syncing = true;
|
syncing = true;
|
||||||
try {
|
try {
|
||||||
// align clocks first
|
// align clocks first
|
||||||
@ -229,6 +233,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!syncInterval) startSyncLoop();
|
if (!syncInterval) startSyncLoop();
|
||||||
|
|
||||||
|
// NEW: after resuming, suppress arbiter briefly to avoid post-seek races
|
||||||
|
arbiterSuppressUntil = now() + 600;
|
||||||
} finally {
|
} finally {
|
||||||
syncing = false;
|
syncing = false;
|
||||||
}
|
}
|
||||||
@ -246,6 +253,17 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureCoPlaySoon() {
|
||||||
|
// Used when only one side fired 'play' (user pressed spacebar, etc.)
|
||||||
|
const t = Number(video.currentTime());
|
||||||
|
if (!bothPlayableAt(t)) {
|
||||||
|
pauseTogether();
|
||||||
|
// wait a bit for decoders/buffers to be ready, then resume both
|
||||||
|
await waitUntilPlayable(t, 1200);
|
||||||
|
}
|
||||||
|
await playTogether({ allowMutedRetry: true });
|
||||||
|
}
|
||||||
|
|
||||||
function tryStart() {
|
function tryStart() {
|
||||||
if (audioReady && videoReady && !restarting) {
|
if (audioReady && videoReady && !restarting) {
|
||||||
const t = Number(video.currentTime());
|
const t = Number(video.currentTime());
|
||||||
@ -337,59 +355,67 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIX: one-time "unlock" to enable later programmatic plays on Safari/iOS
|
// One-time "unlock" to enable later programmatic plays on Safari/iOS
|
||||||
let mediaUnlocked = false;
|
let mediaUnlocked = false;
|
||||||
const unlock = () => {
|
const unlock = () => {
|
||||||
if (mediaUnlocked) return;
|
if (mediaUnlocked) return;
|
||||||
mediaUnlocked = true;
|
mediaUnlocked = true;
|
||||||
// Try a quick muted play/pause to grant future play permission
|
// Quick muted play/pause grants future play permission
|
||||||
try { audio.muted = true; audio.play().then(() => { audio.pause(); }).catch(()=>{}); } catch {}
|
try { audio.muted = true; audio.play().then(() => { audio.pause(); }).catch(()=>{}); } catch {}
|
||||||
try { const was = !!video.muted(); video.muted(true); video.play().then(()=>{ video.pause(); video.muted(was); }).catch(()=>{}); } catch {}
|
try {
|
||||||
|
const was = !!video.muted();
|
||||||
|
video.muted(true);
|
||||||
|
video.play().then(()=>{ video.pause(); video.muted(was); }).catch(()=>{});
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
window.addEventListener('click', unlock, { once: true, capture: true });
|
window.addEventListener('click', unlock, { once: true, capture: true });
|
||||||
window.addEventListener('keydown', unlock, { once: true, capture: true });
|
window.addEventListener('keydown', unlock, { once: true, capture: true });
|
||||||
|
|
||||||
if (qua !== "medium") {
|
if (qua !== "medium") {
|
||||||
|
// mark ready when metadata/data is loaded
|
||||||
attachRetry(audio, pickAudioSrc, () => { audioReady = true; });
|
attachRetry(audio, pickAudioSrc, () => { audioReady = true; });
|
||||||
attachRetry(videoEl, () => {
|
attachRetry(videoEl, () => {
|
||||||
const s = video.src();
|
const s = video.src();
|
||||||
return Array.isArray(s) ? (s[0] && s[0].src) : (s || videoSrc);
|
return Array.isArray(s) ? (s[0] && s[0].src) : (s || videoSrc);
|
||||||
}, () => { videoReady = true; });
|
}, () => { videoReady = true; });
|
||||||
|
|
||||||
|
// mirror volume + mute to hidden audio
|
||||||
const clamp = v => Math.max(0, Math.min(1, Number(v)));
|
const clamp = v => Math.max(0, Math.min(1, Number(v)));
|
||||||
video.on('volumechange', () => {
|
video.on('volumechange', () => {
|
||||||
try { audio.volume = clamp(video.volume()); audio.muted = video.muted(); } catch {}
|
try { audio.volume = clamp(video.volume()); audio.muted = video.muted(); } catch {}
|
||||||
});
|
});
|
||||||
// remove audio->video volume mirroring to avoid feedback loops
|
|
||||||
// audio.addEventListener('volumechange', () => { ... });
|
|
||||||
|
|
||||||
video.on('ratechange', () => { try { audio.playbackRate = video.playbackRate(); } catch {} });
|
video.on('ratechange', () => { try { audio.playbackRate = video.playbackRate(); } catch {} });
|
||||||
|
|
||||||
// sync-safe event bridging using the coordinators (no ping-pong)
|
// --- soft event bridging (no tug-of-war) ---------------------------------
|
||||||
|
// If user hits play on the video while audio hasn't started yet, we ensure co-play soon.
|
||||||
video.on('play', () => {
|
video.on('play', () => {
|
||||||
if (seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
vIsPlaying = true;
|
vIsPlaying = true;
|
||||||
if (!aIsPlaying) playTogether({ allowMutedRetry: true });
|
// delay arbiter to avoid pausing just because audio isn't "playing" yet
|
||||||
|
arbiterSuppressUntil = now() + 600;
|
||||||
|
ensureCoPlaySoon();
|
||||||
});
|
});
|
||||||
|
// We do NOT mirror audio->video here to avoid double-trigger loops.
|
||||||
audio.addEventListener('play', () => {
|
audio.addEventListener('play', () => {
|
||||||
if (seekingInProgress) return; // FIX
|
if (seekingInProgress || restarting) return;
|
||||||
aIsPlaying = true;
|
aIsPlaying = true;
|
||||||
if (!vIsPlaying) playTogether({ allowMutedRetry: true });
|
arbiterSuppressUntil = now() + 600;
|
||||||
});
|
});
|
||||||
|
|
||||||
video.on('pause', () => {
|
video.on('pause', () => {
|
||||||
if (restarting || seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
vIsPlaying = false;
|
vIsPlaying = false;
|
||||||
pauseTogether();
|
pauseTogether();
|
||||||
});
|
});
|
||||||
audio.addEventListener('pause', () => {
|
audio.addEventListener('pause', () => {
|
||||||
if (restarting || seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
aIsPlaying = false;
|
aIsPlaying = false;
|
||||||
pauseTogether();
|
pauseTogether();
|
||||||
});
|
});
|
||||||
|
|
||||||
video.on('waiting', () => {
|
video.on('waiting', () => {
|
||||||
if (restarting || seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
vIsPlaying = false;
|
vIsPlaying = false;
|
||||||
try { audio.pause(); } catch{};
|
try { audio.pause(); } catch{};
|
||||||
clearSyncLoop();
|
clearSyncLoop();
|
||||||
@ -399,6 +425,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
video.on('playing', markVPlaying);
|
video.on('playing', markVPlaying);
|
||||||
audio.addEventListener('playing', markAPlaying);
|
audio.addEventListener('playing', markAPlaying);
|
||||||
|
|
||||||
|
// basic error surface
|
||||||
const errorBox = document.getElementById('loopedIndicator');
|
const errorBox = document.getElementById('loopedIndicator');
|
||||||
video.on('error', () => {
|
video.on('error', () => {
|
||||||
const mediaError = video.error();
|
const mediaError = video.error();
|
||||||
@ -414,7 +441,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIX: tiny helper to wait until both are actually playable near a time
|
// Tiny helper to wait until both are actually playable near a time
|
||||||
const waitUntilPlayable = (t, timeoutMs = 800) => new Promise(resolve => {
|
const waitUntilPlayable = (t, timeoutMs = 800) => new Promise(resolve => {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
@ -424,59 +451,62 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
tick();
|
tick();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- FIRST-SEEK PING-PONG FIX --------------------------------------------
|
||||||
|
// On 'seeking' we PAUSE BOTH, align clocks immediately, and suppress arbiter.
|
||||||
|
let wasPlayingBeforeSeek = false;
|
||||||
|
|
||||||
video.on('seeking', () => {
|
video.on('seeking', () => {
|
||||||
if (restarting) return;
|
if (restarting) return;
|
||||||
seekingInProgress = true;
|
seekingInProgress = true;
|
||||||
// We decide whether to resume playback *before* the browser auto-pauses for the seek.
|
wasPlayingBeforeSeek = !video.paused();
|
||||||
resumeAfterSeek = !video.paused();
|
resumeAfterSeek = wasPlayingBeforeSeek;
|
||||||
// Explicitly pause audio; video is paused by the browser.
|
|
||||||
try { audio.pause(); } catch {}
|
// Hard stop both to avoid any side beginning alone
|
||||||
|
pauseTogether();
|
||||||
clearSyncLoop();
|
clearSyncLoop();
|
||||||
// Reset co-play flags as we enter an intermediate state.
|
|
||||||
vIsPlaying = false; aIsPlaying = false;
|
// Align clocks to the new target (best-effort; videoEl.currentTime may still settle)
|
||||||
// Sync time early to handle various event timings across browsers.
|
|
||||||
const vt = Number(video.currentTime());
|
const vt = Number(video.currentTime());
|
||||||
if (Math.abs(vt - Number(audio.currentTime)) > 0.1) safeSetCT(audio, vt);
|
if (isFinite(vt) && Math.abs(vt - Number(audio.currentTime)) > 0.05) safeSetCT(audio, vt);
|
||||||
|
|
||||||
|
vIsPlaying = false; aIsPlaying = false;
|
||||||
|
|
||||||
|
// Suppress arbiter longer for the *first* seek on some mobile browsers
|
||||||
|
arbiterSuppressUntil = now() + (didFirstSeek ? 900 : 1300);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The 'seeked' handler is now async and uses try/finally to be more robust.
|
|
||||||
video.on('seeked', async () => {
|
video.on('seeked', async () => {
|
||||||
if (restarting) return;
|
if (restarting) return;
|
||||||
|
|
||||||
// A try/finally block ensures we always exit the 'seeking' state, preventing the player
|
|
||||||
// from getting stuck if an error occurs during the post-seek play/pause action.
|
|
||||||
try {
|
|
||||||
const vt = Number(video.currentTime());
|
const vt = Number(video.currentTime());
|
||||||
// Always re-sync on 'seeked' as this is the most accurate time after the jump.
|
if (Math.abs(vt - Number(audio.currentTime)) > 0.05) safeSetCT(audio, vt);
|
||||||
if (Math.abs(vt - Number(audio.currentTime)) > 0.05) {
|
|
||||||
safeSetCT(audio, vt);
|
// Wait until BOTH are playable at the new time before resuming;
|
||||||
}
|
// this avoids one side starting first and getting paused by the arbiter.
|
||||||
|
await waitUntilPlayable(vt, 1500);
|
||||||
|
|
||||||
if (resumeAfterSeek) {
|
if (resumeAfterSeek) {
|
||||||
// Wait for both media elements to report they have enough data to play.
|
await playTogether({ allowMutedRetry: true });
|
||||||
await waitUntilPlayable(vt, 1000);
|
|
||||||
// Crucially, we now 'await' the play action. This prevents the 'finally'
|
|
||||||
// block from running and clearing 'seekingInProgress' too early, which was the
|
|
||||||
// cause of the original ping-pong bug.
|
|
||||||
await playTogether({ allowMutedRetry: false });
|
|
||||||
} else {
|
} else {
|
||||||
// If the video was paused before seeking, ensure both elements are paused after.
|
|
||||||
pauseTogether();
|
pauseTogether();
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
// This code now runs *after* playTogether() has finished.
|
// Give decoders a grace window before the arbiter resumes judging
|
||||||
|
arbiterSuppressUntil = now() + 700;
|
||||||
|
|
||||||
seekingInProgress = false;
|
seekingInProgress = false;
|
||||||
resumeAfterSeek = false;
|
resumeAfterSeek = false;
|
||||||
}
|
if (!didFirstSeek) didFirstSeek = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
video.on('canplaythrough', () => {
|
video.on('canplaythrough', () => {
|
||||||
if (restarting || seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
const vt = Number(video.currentTime());
|
const vt = Number(video.currentTime());
|
||||||
if (Math.abs(vt - Number(audio.currentTime)) > 0.1) safeSetCT(audio, vt);
|
if (Math.abs(vt - Number(audio.currentTime)) > 0.1) safeSetCT(audio, vt);
|
||||||
});
|
});
|
||||||
audio.addEventListener('canplaythrough', () => {
|
audio.addEventListener('canplaythrough', () => {
|
||||||
if (restarting || seekingInProgress) return; // FIX
|
if (restarting || seekingInProgress) return;
|
||||||
const vt = Number(video.currentTime());
|
const vt = Number(video.currentTime());
|
||||||
if (Math.abs(vt - Number(audio.currentTime)) > 0.1) safeSetCT(audio, vt);
|
if (Math.abs(vt - Number(audio.currentTime)) > 0.1) safeSetCT(audio, vt);
|
||||||
});
|
});
|
||||||
@ -498,6 +528,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
await waitUntilPlayable(startAt, 1000);
|
await waitUntilPlayable(startAt, 1000);
|
||||||
await playTogether({ allowMutedRetry: true });
|
await playTogether({ allowMutedRetry: true });
|
||||||
|
|
||||||
|
// brief suppression as we re-enter playback
|
||||||
|
arbiterSuppressUntil = now() + 600;
|
||||||
} finally {
|
} finally {
|
||||||
restarting = false;
|
restarting = false;
|
||||||
}
|
}
|
||||||
@ -522,11 +555,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIX: start the state-arbiter watchdog
|
// Start the state-arbiter watchdog
|
||||||
startArbiter();
|
startArbiter();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user