diff --git a/public/sw.js b/public/sw.js index 375c2a7..1e476fc 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // App-shell cache only; Seafile/API traffic always goes to the network. -const CACHE = 'hub-v87'; +const CACHE = 'hub-v88'; const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png']; diff --git a/public/video.js b/public/video.js index fb57458..f1b2c61 100644 --- a/public/video.js +++ b/public/video.js @@ -305,7 +305,14 @@ function videoBindMedia(el) { el.addEventListener('playing', () => { msg(''); const s = $('#vspin'); if (s) s.hidden = true; vdReport('playing'); }); el.addEventListener('waiting', () => { const s = $('#vspin'); if (s) s.hidden = false; vp.waitSince = Date.now(); if (vp.mode === 'hls') setTimeout(() => { if (vp && vp.el === el && vp.waitSince && Date.now() - vp.waitSince > 9000 && el.readyState < 3) { msg('Taking a while… restarting from here'); videoStart(vp.base + el.currentTime); } }, 9500); }); el.addEventListener('canplay', () => { const s = $('#vspin'); if (s) s.hidden = true; if (vp) vp.waitSince = 0; }); - el.addEventListener('ended', () => videoEnded()); + el.addEventListener('ended', () => { // a converted stream can report "ended" when it has only caught up with what Plex has made so far: carry on from there instead of stopping + const at = vp ? vp.base + el.currentTime : 0, total = vp && vp.m && vp.m.duration ? vp.m.duration / 1000 : 0; + if (vp && vp.el === el && total && at < total - 45 && videoRescue('ended early', at)) return; videoEnded(); }); + if (!el._wd) el._wd = setInterval(() => { // nothing has moved for a while although it should be playing: start again from the same place + if (!vp || vp.el !== el) { clearInterval(el._wd); el._wd = 0; return; } + if (el.paused || el.ended || el.seeking || vp.opening || document.visibilityState !== 'visible') { vp.still = 0; vp.lastT = el.currentTime; return; } + if (Math.abs(el.currentTime - (vp.lastT || 0)) < 0.2) { if ((vp.still = (vp.still || 0) + 1) >= 3) { vp.still = 0; videoRescue('stalled', vp.base + el.currentTime); } } else vp.still = 0; + vp.lastT = el.currentTime; }, 4000); el.addEventListener('error', () => { if (vp && vp.el === el && vp.mode === 'direct') { msg('The browser cannot play this file directly. Converting it…'); vp.q = 'auto'; vp.forceHls = true; videoStart(vp.pos); } else msg('Could not play this. Try a lower quality in Quality & tracks.'); }); } /** Refresh whatever controls are on screen (the player page's, and the mini player's) from the video's state. */ @@ -417,6 +424,11 @@ function videoFullscreen() { } catch { fallback(); } } document.addEventListener('fullscreenchange', () => { const b = $('#vfs'); if (b) b.textContent = document.fullscreenElement ? 'Exit full screen' : 'Full screen'; }); +/** The picture stopped on its own: start again from where it was, at most 4 times in a row, and say so. */ +function videoRescue(why, at) { + if (!vp) return false; const now = Date.now(); if (now - (vp.rescueAt || 0) > 90000) vp.rescues = 0; if ((vp.rescues || 0) >= 4) return false; + vp.rescues = (vp.rescues || 0) + 1; vp.rescueAt = now; const e = $('#vmsg'); if (e) e.textContent = 'Connection hiccup. Picking up where it stopped…'; videoStart(at); return true; +} /** Start (or restart) playing from `sec` with the current quality and tracks. */ async function videoStart(sec) { if (!vp) return; const el = vp.el, { m, media, part } = vp; sec = Math.max(0, sec || 0); if (m.duration && sec * 1000 > m.duration - 15000) sec = 0; @@ -437,7 +449,7 @@ async function videoStart(sec) { h.on(Hls.Events.ERROR, (e, d) => { if (d.fatal) { if (d.type === Hls.ErrorTypes.NETWORK_ERROR) { const tt = $('#vmsg'); if (tt) tt.textContent = 'Connection problem. Retrying…'; h.startLoad(); } else if (d.type === Hls.ErrorTypes.MEDIA_ERROR) h.recoverMediaError(); else { const tt = $('#vmsg'); if (tt) tt.textContent = 'Could not play this. Try a lower quality in Quality & tracks.'; } } }); } catch (e) { const tt = $('#vmsg'); if (tt) tt.textContent = e.message; return; } } } - el.play().catch(() => {}); + el.play().catch(e => { const t = $('#vmsg'); if (t && e && e.name === 'NotAllowedError') t.textContent = 'Tap the play button to start'; }); // (the browser only lets a tap start the picture) } const vdReport = state => { if (!vp) return; const pos = (vp.base + (vp.el ? vp.el.currentTime : 0)) * 1000; tlSend(state, vp.id, pos, vp.m.duration || 0); }; function videoSettings() {