From 8d31d98eaa76bb657c38cd58366243afc25db2d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 27 Oct 2026 00:31:34 +0000 Subject: [PATCH] Movies: use hls.js with a 2 minute buffer wherever the browser can (iPad), built-in player as fallback Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01UaC9HmtHMdRP8dAbN4wAKj --- public/video.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/public/video.js b/public/video.js index f1b2c61..fcb4608 100644 --- a/public/video.js +++ b/public/video.js @@ -444,10 +444,20 @@ 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' }); 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…'; - if (el.canPlayType('application/vnd.apple.mpegurl')) { el.src = url; } - 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); - 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; } } + // 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 + // starts as soon as the first piece arrives. The built-in player (which decides its own small buffer) is the fallback. + let Hls = null; try { Hls = await loadHls(); } catch {} if (!vp || vp.el !== el) 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(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) }