Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24dd05db5d | ||
|
|
8d31d98eaa | ||
|
|
4d6cf9b961 | ||
|
|
dfe9880e47 | ||
|
|
822eb499e4 | ||
|
|
cc064d0ccf | ||
|
|
b204f968c6 |
No files matched your search
+1
-1
@@ -296,4 +296,4 @@ function openNow() {
|
|||||||
updateNow(); syncUI();
|
updateNow(); syncUI();
|
||||||
}
|
}
|
||||||
// Tell the page how tall the player is, so buttons that float at the bottom (Home, Ask) sit above it instead of covering it.
|
// Tell the page how tall the player is, so buttons that float at the bottom (Home, Ask) sit above it instead of covering it.
|
||||||
{ const pe = $('#player'); if (pe && window.ResizeObserver) new ResizeObserver(() => document.documentElement.style.setProperty('--playerh', (pe.hidden ? 0 : pe.offsetHeight) + 'px')).observe(pe); }
|
if (typeof ResizeObserver === 'function' && typeof document !== 'undefined') { const pe = document.getElementById('player'); if (pe) new ResizeObserver(() => document.documentElement.style.setProperty('--playerh', (pe.hidden ? 0 : pe.offsetHeight) + 'px')).observe(pe); }
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||||
const CACHE = 'hub-v86';
|
const CACHE = 'hub-v88';
|
||||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
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'];
|
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||||
|
|
||||||
|
|||||||
+28
-6
@@ -305,7 +305,14 @@ function videoBindMedia(el) {
|
|||||||
el.addEventListener('playing', () => { msg(''); const s = $('#vspin'); if (s) s.hidden = true; vdReport('playing'); });
|
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('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('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.'); });
|
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. */
|
/** 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(); }
|
} catch { fallback(); }
|
||||||
}
|
}
|
||||||
document.addEventListener('fullscreenchange', () => { const b = $('#vfs'); if (b) b.textContent = document.fullscreenElement ? 'Exit full screen' : 'Full screen'; });
|
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. */
|
/** Start (or restart) playing from `sec` with the current quality and tracks. */
|
||||||
async function videoStart(sec) {
|
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;
|
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;
|
||||||
@@ -432,12 +444,22 @@ async function videoStart(sec) {
|
|||||||
const o = q[2] || QUALITIES[0][2], p = new URLSearchParams({ path: '/library/metadata/' + vp.id, mediaIndex: 0, partIndex: 0, protocol: 'hls', fastSeek: 1, directPlay: 0, directStream: 1, subtitleSize: 100, audioBoost: 100, offset: Math.floor(sec), session: vp.session, maxVideoBitrate: o.kbps, 'X-Plex-Platform': 'Chrome' });
|
const o = q[2] || QUALITIES[0][2], p = new URLSearchParams({ path: '/library/metadata/' + vp.id, mediaIndex: 0, partIndex: 0, protocol: 'hls', fastSeek: 1, directPlay: 0, directStream: 1, subtitleSize: 100, audioBoost: 100, offset: Math.floor(sec), session: vp.session, maxVideoBitrate: o.kbps, 'X-Plex-Platform': 'Chrome' });
|
||||||
if (o.res) p.set('videoResolution', o.res); if (vp.audio) p.set('audioStreamID', vp.audio); if (vp.sub) { p.set('subtitleStreamID', vp.sub); p.set('subtitles', 'burn'); }
|
if (o.res) p.set('videoResolution', o.res); if (vp.audio) p.set('audioStreamID', vp.audio); if (vp.sub) { p.set('subtitleStreamID', vp.sub); p.set('subtitles', 'burn'); }
|
||||||
const url = '/plex/video/:/transcode/universal/start.m3u8?' + p; const t = $('#vmsg'); if (t) t.textContent = 'Preparing the stream…';
|
const url = '/plex/video/:/transcode/universal/start.m3u8?' + p; const t = $('#vmsg'); if (t) t.textContent = 'Preparing the stream…';
|
||||||
if (el.canPlayType('application/vnd.apple.mpegurl')) { el.src = url; }
|
// hls.js first wherever the browser can run it (iPad included): it lets us keep a long buffer, so a short drop in the connection does not stop the picture, and it
|
||||||
else { try { const Hls = await loadHls(); if (!vp || vp.el !== el) return; if (!Hls.isSupported()) throw new Error('This browser cannot play converted video'); const h = vp.hls = new Hls({ maxBufferLength: 40, enableWorker: true }); h.loadSource(url); h.attachMedia(el);
|
// starts as soon as the first piece arrives. The built-in player (which decides its own small buffer) is the fallback.
|
||||||
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.'; } } }); }
|
let Hls = null; try { Hls = await loadHls(); } catch {} if (!vp || vp.el !== el) return;
|
||||||
catch (e) { const tt = $('#vmsg'); if (tt) tt.textContent = e.message; return; } }
|
const native = !!el.canPlayType('application/vnd.apple.mpegurl');
|
||||||
|
if (Hls && Hls.isSupported() && !vp.noJs) {
|
||||||
|
const h = vp.hls = new Hls({ maxBufferLength: 120, maxMaxBufferLength: 600, maxBufferSize: 120 * 1e6, backBufferLength: 30, startFragPrefetch: true, testBandwidth: false, enableWorker: true, maxBufferHole: 0.5 });
|
||||||
|
h.loadSource(url); h.attachMedia(el);
|
||||||
|
h.on(Hls.Events.ERROR, (e, d) => { if (!d.fatal) return; const tt = $('#vmsg');
|
||||||
|
if (d.type === Hls.ErrorTypes.NETWORK_ERROR) { if (tt) tt.textContent = 'Connection problem. Retrying…'; h.startLoad(); }
|
||||||
|
else if (d.type === Hls.ErrorTypes.MEDIA_ERROR && Date.now() - (vp.recovered || 0) > 5000) { vp.recovered = Date.now(); h.recoverMediaError(); }
|
||||||
|
else if (native && vp && vp.el === el) { vp.noJs = true; videoStart(vp.base + el.currentTime); } // (the built-in player takes over)
|
||||||
|
else if (tt) tt.textContent = 'Could not play this. Try a lower quality in Quality & tracks.'; });
|
||||||
|
} else if (native) { el.src = url; }
|
||||||
|
else { const tt = $('#vmsg'); if (tt) tt.textContent = 'This browser cannot play converted video'; 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); };
|
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() {
|
function videoSettings() {
|
||||||
|
|||||||
+2
-2
@@ -37,8 +37,8 @@ const users = require('../users'), push = require('../push');
|
|||||||
|
|
||||||
// events: a local event starting in 10 minutes
|
// events: a local event starting in 10 minutes
|
||||||
const soon = new Date(Date.now() + 10 * 60e3), pad = n => String(n).padStart(2, '0');
|
const soon = new Date(Date.now() + 10 * 60e3), pad = n => String(n).padStart(2, '0');
|
||||||
const st = `${soon.getUTCFullYear()}-${pad(soon.getUTCMonth() + 1)}-${pad(soon.getUTCDate())}T${pad(soon.getUTCHours())}:${pad(soon.getUTCMinutes())}`;
|
const utc = d => d.toISOString().slice(0, 16), st = utc(soon), en = utc(new Date(soon.getTime() + 36e5)); // (the end is built from the real time, so an event just before midnight still ends after it starts)
|
||||||
fs.writeFileSync(path.join(d, 'events.json'), JSON.stringify([{ uid: 'e1', fields: { title: 'Dentist', location: 'Main St', notes: '', allDay: false, start: st, end: st.replace(/T(\d\d)/, (m, h) => 'T' + pad((+h + 1) % 24)), tz: 'UTC', repeat: 'none' }, created: 1, updated: 1 }]));
|
fs.writeFileSync(path.join(d, 'events.json'), JSON.stringify([{ uid: 'e1', fields: { title: 'Dentist', location: 'Main St', notes: '', allDay: false, start: st, end: en, tz: 'UTC', repeat: 'none' }, created: 1, updated: 1 }]));
|
||||||
writePush({ tasks: false }); sent.length = 0;
|
writePush({ tasks: false }); sent.length = 0;
|
||||||
await push.tick(new Date(Date.now() - 3 * 36e5), true);
|
await push.tick(new Date(Date.now() - 3 * 36e5), true);
|
||||||
ok('an event hours away sends nothing', sent.length === 0, sent);
|
ok('an event hours away sends nothing', sent.length === 0, sent);
|
||||||
|
|||||||
Reference in new issue
Block a user