diff --git a/public/player.js b/public/player.js index 01a46c7..20f0d99 100644 --- a/public/player.js +++ b/public/player.js @@ -58,11 +58,25 @@ addEventListener('online', () => { pl.retries = 0; tcKick(); recover(true); }); addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && cur() && !pl.a.paused && (pl.a.readyState < 3 || pl.a.error)) { pl.retries = 0; recover(true); } }); // (back from sleep or another app) let loadTok = 0, loadTimer = null, stallTimer = null; +// Watchdog: a song that sits on "Buffering…" without moving is not left there. After 10 s it is reloaded, after 20 s it is switched to the copy Plex converts to MP3, +// and after 30 s it is skipped. (This does not rely on the browser sending a "stalled" or "error" event, which a hung request may never do.) +const wd = { tries: 0, at: 0, pos: -1, key: '' }; +if (typeof setInterval === 'function') setInterval(() => { + const t = cur(), a = pl.a; + if (!t || !pl.buf || /^radio:/.test(String(t.key)) || !navigator.onLine) { wd.at = Date.now(); wd.pos = -1; return; } + if (wd.key !== t.key) { wd.key = t.key; wd.tries = 0; wd.at = Date.now(); } + if (a.currentTime !== wd.pos) { wd.pos = a.currentTime; wd.at = Date.now(); return; } + if (Date.now() - wd.at < 10000 || loadTimer && a.paused && Date.now() - wd.at < 12000) return; + wd.at = Date.now(); wd.tries++; + if (wd.tries === 1) { pl.forceRecover = true; pl.recovering = true; recover(true); } + else if (wd.tries === 2) { pl.alt.add(t.key); loadCur(0); } + else { wd.tries = 0; toast('"' + t.title + '" would not load. Skipping.'); advance(true); } +}, 2000); pl.retries = 0; pl.buf = false; // Show the new track at once, but wait a moment before fetching it so quickly tapping Next several times only loads the last one. function loadCur(delay = 220) { const t = cur(); if (!t) return; - clearTimeout(loadTimer); clearTimeout(stallTimer); pl.retries = 0; pl.scrobbled = false; pl.recovering = false; + clearTimeout(loadTimer); clearTimeout(stallTimer); pl.retries = 0; pl.scrobbled = false; pl.recovering = false; wd.tries = 0; wd.at = Date.now(); const tok = ++loadTok; pl.buf = true; if (delay) pl.a.pause(); try { // (a bug in any screen's refresh must never stop the music from starting) @@ -76,7 +90,7 @@ function loadCur(delay = 220) { const hit = tc.m.get(t.src); pl.a.src = hit && !pl.alt.has(t.key) ? hit.url : srcOf(t); if (typeof actLog === 'function') actLog(/podcast|audiobook|\/ab\/|radio/.test(String(t.src || '')) ? 'listen' : 'play', (t.artist ? t.artist + ' - ' : '') + (t.title || '')); - pl.a.play().catch(e => { if (e && e.name === 'NotAllowedError') { pl.buf = false; syncUI(); toast('Tap play to start'); } }); + pl.a.play().catch(e => { if (tok !== loadTok) return; if (e && e.name === 'NotAllowedError') { pl.buf = false; syncUI(); toast('Tap play to start'); } else if (!(e && e.name === 'AbortError')) { pl.forceRecover = true; pl.recovering = true; recover(true); } }); tcKick(); const n = pl.q[pl.i + 1]; if (n) new Image().src = px.art(n.thumb, 600); };