From c0c472f3efbe98e8fbeeaa4b52505309fb380af7 Mon Sep 17 00:00:00 2001 From: Jonathan Sykes Date: Wed, 14 Oct 2026 03:56:21 +0800 Subject: [PATCH] Stop asking for HEVC on devices without hardware decode and fall back when frames drop --- frontend/app.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/frontend/app.js b/frontend/app.js index cfaa6b3..8294e0a 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -105,8 +105,17 @@ function saveDataToStorage(jsonStr) { // is the browser's own claim; if a copy still fails to play here, the device // is remembered (NO_HEVC_KEY) and stops asking — it gets H.264 as before. const NO_HEVC_KEY = 'ytpNoHevc'; +// canPlayType only says the browser can decode HEVC — often in software, which +// stutters the picture while the audio stays fine. Ask MediaCapabilities whether +// it is hardware-accelerated (powerEfficient) and smooth; remember the answer. +const HEVC_HW_KEY = 'ytpHevcHw'; +try { + navigator.mediaCapabilities.decodingInfo({ type: 'file', video: { contentType: 'video/mp4; codecs="hvc1.1.6.L93.B0"', width: 1280, height: 720, bitrate: 1500000, framerate: 30 } }) + .then((i) => { try { localStorage.setItem(HEVC_HW_KEY, i.supported && i.smooth && i.powerEfficient ? '1' : '0'); } catch { /* storage blocked */ } }) + .catch(() => {}); +} catch { /* no MediaCapabilities */ } function hevcCapable() { - try { if (localStorage.getItem(NO_HEVC_KEY) === '1') return false; } catch { /* storage blocked */ } + try { if (localStorage.getItem(NO_HEVC_KEY) === '1' || localStorage.getItem(HEVC_HW_KEY) === '0') return false; } catch { /* storage blocked */ } try { return document.createElement('video').canPlayType('video/mp4; codecs="hvc1.1.6.L93.B0"') !== ''; } catch { return false; } @@ -2013,6 +2022,36 @@ document.addEventListener('play', () => { preload(m, { quiet: true }); }, true); +// Dropped-frame watchdog: an HEVC server copy that this device decodes in +// software drops frames while the audio plays on. Sample the first ~30 s of +// each play; if it is clearly struggling, stop asking this device for HEVC and +// restart the song on the compatible stream, from where it is. +{ + const dropWatch = { id: '', t: 0, tries: 0 }; + document.addEventListener('playing', (e) => { + const v = e.target; + if (!v || v.tagName !== 'VIDEO' || !current || !current.meta || !v.getVideoPlaybackQuality) return; + if (!(current.qualities || []).some((q) => q.codec === 'hevc') || !current.serverCached) return; + if (dropWatch.id === current.meta.id && dropWatch.tries >= 6) return; + if (dropWatch.id !== current.meta.id) { dropWatch.id = current.meta.id; dropWatch.tries = 0; } + const id = current.meta.id, at = v.getVideoPlaybackQuality(), t0 = Date.now(); + const check = setInterval(() => { + if (!current || !current.meta || current.meta.id !== id || v.paused || Date.now() - t0 > 30000) { clearInterval(check); return; } + dropWatch.tries++; + const q = v.getVideoPlaybackQuality(); + const total = q.totalVideoFrames - at.totalVideoFrames, dropped = q.droppedVideoFrames - at.droppedVideoFrames; + if (total >= 60 && dropped / total > 0.2) { + clearInterval(check); + try { localStorage.setItem(NO_HEVC_KEY, '1'); } catch { /* storage blocked */ } + toast('Video was choppy on this device — switching to the compatible format.', { duration: 4000 }); + const pos = v.currentTime || 0; + if (pos > 1 && data.resumePositions && rememberPosEnabled()) data.resumePositions[id] = Math.floor(pos); + Player.loadVideo(current.meta, { preferStream: true, nocache: true, resume: true, reveal: false }); + } + }, 3000); + }, true); +} + // ============================================================================ // Video editor — cut parts out of a video and save a custom offline copy