Player: keep retrying after a lost connection, resume songs cut short, recover on tab return, never let a screen error block playback
build-container / build (push) Successful in 3m30s
build-container / build (push) Successful in 3m30s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8b77bacb7a
commit
fd5812c5c2
1 file changed
+17
-8
+17
-8
@@ -51,6 +51,7 @@ async function tcPump() {
|
||||
}
|
||||
function tcKick() { if (tc.ctl && !tcWant().includes(tc.cur)) tc.ctl.abort(); tcPump(); }
|
||||
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;
|
||||
pl.retries = 0; pl.buf = false;
|
||||
@@ -60,10 +61,12 @@ function loadCur(delay = 220) {
|
||||
clearTimeout(loadTimer); clearTimeout(stallTimer); pl.retries = 0; pl.scrobbled = false;
|
||||
const tok = ++loadTok; pl.buf = true;
|
||||
if (delay) pl.a.pause();
|
||||
setMeta(t); renderMini(); updateNow(); renderNpCard(); syncUI(); markPlaying(); saveState();
|
||||
if (lyr.open) { lyrShell(); lyrReload(); }
|
||||
if (typeof vizTrackChanged === 'function') vizTrackChanged();
|
||||
if (typeof mmCoverTheme === 'function') mmCoverTheme(t); if (typeof mmRadioTopUp === 'function') mmRadioTopUp(); if (typeof mfPaint === 'function') mfPaint();
|
||||
try { // (a bug in any screen's refresh must never stop the music from starting)
|
||||
setMeta(t); renderMini(); updateNow(); renderNpCard(); syncUI(); markPlaying(); saveState();
|
||||
if (lyr.open) { lyrShell(); lyrReload(); }
|
||||
if (typeof vizTrackChanged === 'function') vizTrackChanged();
|
||||
if (typeof mmCoverTheme === 'function') mmCoverTheme(t); if (typeof mmRadioTopUp === 'function') mmRadioTopUp(); if (typeof mfPaint === 'function') mfPaint();
|
||||
} catch (e) { console.error('player refresh', e); }
|
||||
const go = () => {
|
||||
if (tok !== loadTok) return;
|
||||
const hit = tc.m.get(t.src);
|
||||
@@ -84,7 +87,8 @@ function recover(now) {
|
||||
clearTimeout(stallTimer); const t = cur(), a = pl.a;
|
||||
if (!t || a.paused) return;
|
||||
stallTimer = setTimeout(() => {
|
||||
if (a.paused || (a.readyState >= 3 && !a.error)) return;
|
||||
const forced = pl.forceRecover; pl.forceRecover = false;
|
||||
if (a.paused || (!forced && a.readyState >= 3 && !a.error)) return;
|
||||
const hit = tc.m.get(t.src), pos = a.currentTime;
|
||||
const bad = a.error && (a.error.code === 3 || a.error.code === 4); // cannot decode this file (not a network problem)
|
||||
if (bad && !hit) {
|
||||
@@ -93,7 +97,9 @@ function recover(now) {
|
||||
}
|
||||
const want = hit && !pl.alt.has(t.key) ? hit.url : srcOf(t);
|
||||
if (!hit && !navigator.onLine) return; // 'online' will call us again
|
||||
if (!hit && !bad && pl.retries >= 5) { pl.buf = false; syncUI(); toast('Lost the connection. It will carry on when it is back.'); return; }
|
||||
if (!hit && !bad && pl.retries >= 5) { // keep trying every 15 s (the 'online' signal is not always sent, e.g. when only the server was unreachable)
|
||||
if (!pl.gaveUp) { pl.gaveUp = true; pl.buf = false; syncUI(); toast('Lost the connection. It will carry on when it is back.'); }
|
||||
stallTimer = setTimeout(() => { pl.retries = 0; recover(true); }, 15000); return; }
|
||||
if (!hit || a.src !== hit.url) pl.retries++;
|
||||
a.src = want;
|
||||
a.addEventListener('loadedmetadata', () => { try { a.currentTime = pos; } catch {} a.play().catch(() => {}); }, { once: true });
|
||||
@@ -162,7 +168,10 @@ function tlReport(state) {
|
||||
tlSend(state, tlKey, tlPos, tlDur);
|
||||
}
|
||||
function tlStop() { if (tlKey) tlSend('stopped', tlKey, tlPos, tlDur); tlKey = null; }
|
||||
pl.a.addEventListener('ended', () => advance(true));
|
||||
pl.a.addEventListener('ended', () => { // a dropped connection can look like the end of the song: if it stopped well short of the length, carry on from there instead of skipping
|
||||
const a = pl.a, t = cur(), d = t && t.dur ? t.dur / 1000 : 0;
|
||||
if (t && d > 20 && !/^radio:/.test(String(t.key)) && a.currentTime < d - 8 && pl.endRetry !== t.key + '|' + Math.floor(a.currentTime / 5)) { pl.endRetry = t.key + '|' + Math.floor(a.currentTime / 5); pl.forceRecover = true; pl.buf = true; syncUI(); recover(true); return; }
|
||||
advance(true); });
|
||||
let lastSave = 0, lastPos = 0;
|
||||
pl.a.addEventListener('timeupdate', () => {
|
||||
if (Date.now() - lastSave > 5000) { lastSave = Date.now(); saveState(); }
|
||||
@@ -174,7 +183,7 @@ pl.a.addEventListener('timeupdate', () => {
|
||||
});
|
||||
addEventListener('pagehide', saveState);
|
||||
['play', 'pause', 'timeupdate', 'loadedmetadata', 'durationchange'].forEach(e => pl.a.addEventListener(e, () => syncUI()));
|
||||
pl.a.addEventListener('playing', () => { tlReport('playing'); pl.buf = false; pl.retries = 0; clearTimeout(stallTimer); syncUI(); if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing'; });
|
||||
pl.a.addEventListener('playing', () => { tlReport('playing'); pl.buf = false; pl.retries = 0; pl.gaveUp = false; clearTimeout(stallTimer); syncUI(); if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'playing'; });
|
||||
pl.a.addEventListener('pause', () => { if (!pl.buf) tlReport('paused'); pl.buf = false; clearTimeout(stallTimer); syncUI(); if ('mediaSession' in navigator) navigator.mediaSession.playbackState = 'paused'; });
|
||||
pl.a.addEventListener('canplaythrough', () => tcKick());
|
||||
['waiting', 'stalled'].forEach(e => pl.a.addEventListener(e, () => { if (pl.a.paused) return; pl.buf = true; syncUI(); recover(); }));
|
||||
|
||||
Reference in new issue
Block a user