Stop asking for HEVC on devices without hardware decode and fall back when frames drop

This commit is contained in:
Jonathan Sykes
2026-10-14 03:56:21 +08:00
parent f465cc86d0
commit c0c472f3ef

View File

@@ -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