Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d31d98eaa | ||
|
|
4d6cf9b961 | ||
|
|
dfe9880e47 | ||
|
|
822eb499e4 | ||
|
|
cc064d0ccf | ||
|
|
86cc8dd449 |
No files matched your search
@@ -295,3 +295,5 @@ function openNow() {
|
|||||||
el.querySelector('#npp').onclick = prev; el.querySelector('#npn').onclick = next; el.querySelector('#npt').onclick = toggle;
|
el.querySelector('#npp').onclick = prev; el.querySelector('#npn').onclick = next; el.querySelector('#npt').onclick = toggle;
|
||||||
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.
|
||||||
|
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); }
|
||||||
+8
-3
@@ -1064,8 +1064,8 @@ body.vizopen { overflow: hidden; }
|
|||||||
.crtick { position: absolute; top: 0; width: 2px; height: 100%; background: var(--text); opacity: .55; } .crearn { display: flex; align-items: center; gap: 10px; margin: 6px 0 0 52px; padding: 8px 10px; border-radius: 12px; background: var(--panel2); } .crearn .btn { min-height: 34px; padding: 0 12px; }
|
.crtick { position: absolute; top: 0; width: 2px; height: 100%; background: var(--text); opacity: .55; } .crearn { display: flex; align-items: center; gap: 10px; margin: 6px 0 0 52px; padding: 8px 10px; border-radius: 12px; background: var(--panel2); } .crearn .btn { min-height: 34px; padding: 0 12px; }
|
||||||
.crdet { margin: 6px 0 0 52px; } .crdet summary { cursor: pointer; color: var(--muted); font-size: 14px; } .crwk { padding: 8px 0; border-bottom: 1px solid var(--line); } .crwk:last-child { border-bottom: 0; }
|
.crdet { margin: 6px 0 0 52px; } .crdet summary { cursor: pointer; color: var(--muted); font-size: 14px; } .crwk { padding: 8px 0; border-bottom: 1px solid var(--line); } .crwk:last-child { border-bottom: 0; }
|
||||||
|
|
||||||
#kidreq { position: fixed; left: 12px; bottom: calc(env(safe-area-inset-bottom, 0px) + 74px); z-index: 55; min-height: 40px; padding: 0 16px; border-radius: 22px; border: 1px solid var(--line); background: var(--panel); color: var(--text); font: inherit; font-weight: 600; box-shadow: 0 4px 14px rgba(0,0,0,.3); cursor: pointer; }
|
#kidreq { position: fixed; left: 12px; bottom: calc(env(safe-area-inset-bottom, 0px) + 74px + var(--playerh, 0px)); z-index: 55; min-height: 40px; padding: 0 16px; border-radius: 22px; border: 1px solid var(--line); background: var(--panel); color: var(--text); font: inherit; font-weight: 600; box-shadow: 0 4px 14px rgba(0,0,0,.3); cursor: pointer; }
|
||||||
@media (min-width: 900px) { #kidreq { bottom: 16px; left: 236px; } }
|
@media (min-width: 900px) { #kidreq { bottom: calc(16px + var(--playerh, 0px)); left: 236px; } }
|
||||||
|
|
||||||
/* simple mode: no menus, big buttons */
|
/* simple mode: no menus, big buttons */
|
||||||
body.simple #nav, body.simple #navbar, body.simple #subnav, body.simple #chatfab, body.simple .fab { display: none !important; }
|
body.simple #nav, body.simple #navbar, body.simple #subnav, body.simple #chatfab, body.simple .fab { display: none !important; }
|
||||||
@@ -1073,4 +1073,9 @@ body.simple #shell { flex-direction: column; }
|
|||||||
.simplewrap { max-width: 920px; } .simplewrap h1 { font-size: 34px; margin: 12px 0 18px; }
|
.simplewrap { max-width: 920px; } .simplewrap h1 { font-size: 34px; margin: 12px 0 18px; }
|
||||||
.simplegrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 16px; }
|
.simplegrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 16px; }
|
||||||
.simpletile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: 170px; border-radius: 28px; color: #fff; text-decoration: none; font-size: 24px; font-weight: 700; box-shadow: 0 6px 18px rgba(0,0,0,.3); } .simpletile svg { width: 64px; height: 64px; }
|
.simpletile { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; min-height: 170px; border-radius: 28px; color: #fff; text-decoration: none; font-size: 24px; font-weight: 700; box-shadow: 0 6px 18px rgba(0,0,0,.3); } .simpletile svg { width: 64px; height: 64px; }
|
||||||
#simplehome { position: fixed; left: 14px; bottom: calc(env(safe-area-inset-bottom, 0px) + 14px); z-index: 60; display: flex; align-items: center; gap: 10px; padding: 14px 24px; border-radius: 32px; background: var(--accent); color: var(--accent-t, #fff); font-size: 22px; font-weight: 700; text-decoration: none; box-shadow: 0 6px 18px rgba(0,0,0,.4); } #simplehome[hidden] { display: none; } #simplehome svg { width: 30px; height: 30px; }
|
#simplehome { position: fixed; left: 14px; bottom: calc(env(safe-area-inset-bottom, 0px) + 14px + var(--playerh, 0px)); z-index: 60; display: flex; align-items: center; gap: 10px; padding: 14px 24px; border-radius: 32px; background: var(--accent); color: var(--accent-t, #fff); font-size: 22px; font-weight: 700; text-decoration: none; box-shadow: 0 6px 18px rgba(0,0,0,.4); } #simplehome[hidden] { display: none; } #simplehome svg { width: 30px; height: 30px; }
|
||||||
|
|
||||||
|
/* the buttons that float at the bottom sit above the player, not on top of it (--playerh is its height, set in player.js) */
|
||||||
|
body.simple #view { padding-bottom: 110px; }
|
||||||
|
/* while a sheet, the lyrics or the visualizer covers the screen, the floating Home and Ask buttons step aside so they never sit on its controls */
|
||||||
|
body:has(#sheet) #simplehome, body:has(#sheet) #kidreq, body:has(#lyr) #simplehome, body:has(#lyr) #kidreq, body.vizopen #simplehome, body.vizopen #kidreq { display: none !important; }
|
||||||
+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() {
|
||||||
|
|||||||
Reference in new issue
Block a user