iPhone: stop rate-slewing the video for sync; add playback telemetry beacon

This commit is contained in:
Jonathan Sykes
2026-10-14 08:31:38 +08:00
parent 4fa33e9810
commit c242641b2d
3 changed files with 45 additions and 1 deletions

View File

@@ -2835,6 +2835,14 @@ const Player = {
if (this.master.seeking) return;
const drift = this.secondary.currentTime - this.master.currentTime;
const abs = Math.abs(drift);
// iPhone: audio/video currentTime jitter alone exceeds the soft threshold,
// so rate-slewing the visible video every tick causes the stutter. Only
// snap on a real, sustained desync.
if (IS_IOS_DEVICE) {
if (abs >= 0.5 && Date.now() - (this._lastSnapAt || 0) > 5000) { this.snapVideoTo(this.master, this.secondary); this._resyncSpeed(); }
else if (abs < 0.5) this._resyncSpeed();
return;
}
if (abs >= this.HARD_SYNC_THRESHOLD && Date.now() - (this._lastSnapAt || 0) < this.SNAP_COOLDOWN_MS) {
// Just snapped: ease in instead of snapping again.
const base = parseFloat(els.speed.value) || 1;
@@ -3024,6 +3032,38 @@ function showSpinner(on) {
els.spinner.classList.toggle('hidden', !on);
}
const IS_IOS_DEVICE = /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
// Playback telemetry: 1 sample/s for 60 s after playback starts or the app
// returns to the foreground, sent once to /api/diag (read via container logs).
const Diag = (() => {
let timer = null, samples = [], events = [], t0 = 0, tag = '';
const ev = (n) => { if (timer) events.push([Math.round(performance.now() - t0), n]); };
const q = (V) => { try { const x = V.getVideoPlaybackQuality(); return [x.droppedVideoFrames, x.totalVideoFrames]; } catch { return [-1, -1]; } };
function flush() {
if (!timer) return;
clearInterval(timer); timer = null;
const V = els.video;
const body = JSON.stringify({ tag, ua: navigator.userAgent, standalone: matchMedia('(display-mode: standalone)').matches,
id: current && current.meta && current.meta.id, mode: Player.mode, src: (V.currentSrc || '').replace(/^https?:\/\/[^/]+/, '').slice(0, 80),
size: [V.videoWidth, V.videoHeight], hevc: hevcCapable(), samples, events });
try { navigator.sendBeacon('/api/diag', new Blob([body], { type: 'application/json' })); } catch {}
}
function start(why) {
flush(); samples = []; events = []; tag = why; t0 = performance.now();
timer = setInterval(() => {
const V = els.video, m = Player.master, s = Player.secondary;
samples.push([samples.length, Player.mode, m === V ? 'v' : 'a', +(m ? m.currentTime : 0).toFixed(2),
s ? +(s.currentTime - m.currentTime).toFixed(3) : null, m ? m.playbackRate : 0, q(V), V.readyState, V.paused ? 1 : 0, document.visibilityState === 'visible' ? 1 : 0]);
if (samples.length >= 60) flush();
}, 1000);
}
for (const n of ['waiting', 'stalled', 'seeking', 'seeked', 'ratechange', 'error']) els.video.addEventListener(n, () => ev('v:' + n), true);
els.video.addEventListener('playing', () => { if (!timer) start('playing'); }, true);
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && Player._wantsPlaying) start('return'); else flush(); });
return { start, flush };
})();
function chooseQuality() {
const qs = current.qualities || [];
if (!qs.length) return null;