mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
fix(web-player): YouTube ENDED safety net for Shorts + flaky Android TV (#219)
YouTube Shorts never fire the ENDED state via the IFrame API, and some Android TV WebViews drop ENDED even for regular videos. The player advanced solely on onStateChange ENDED, so a missing event stalled the playlist indefinitely. Arm a duration-based fallback timer in onReady (getDuration + 3s slack) that calls nextItem() if ENDED never arrives. It is cleared on a real ENDED, on onError, when a newer player is created, and in teardownCurrentMedia so a stale timer can't force a spurious advance after rotation. Skipped when looping (single-item playlists) and when duration is 0 (live streams). The Tizen player is not affected: it embeds YouTube as a plain iframe and already advances multi-item playlists on a duration timer rather than the YT JS API, so it never waits for ENDED. Closes #215 Refs #184 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
11c2890433
commit
9d6c3c79b0
|
|
@ -513,6 +513,10 @@
|
|||
let ytApiCallbacks = [];
|
||||
let activeYtPlayer = null;
|
||||
let ytGeneration = 0;
|
||||
// #215: fallback advance timer for YouTube. Shorts (and some Android TV WebViews)
|
||||
// never fire the ENDED state, so onStateChange alone can stall the playlist. Armed
|
||||
// in onReady from the reported duration, cleared on ENDED/onError/teardown.
|
||||
let ytSafetyNet = null;
|
||||
|
||||
// AudioContext is created lazily on the first user gesture. Resuming it
|
||||
// is what convinces stricter browsers (Firefox) that the site is "user-
|
||||
|
|
@ -1964,6 +1968,7 @@
|
|||
const myGeneration = ++ytGeneration;
|
||||
|
||||
// Destroy old player without triggering side effects (callbacks check generation)
|
||||
if (ytSafetyNet) { clearTimeout(ytSafetyNet); ytSafetyNet = null; }
|
||||
if (activeYtPlayer) { try { activeYtPlayer.destroy(); } catch {} activeYtPlayer = null; }
|
||||
|
||||
// Vertical (Shorts) content is tagged st_aspect=vertical at ingest. Render it
|
||||
|
|
@ -2040,9 +2045,26 @@
|
|||
event.target.unMute();
|
||||
event.target.setVolume(100);
|
||||
}
|
||||
// #215 safety net: if ENDED never fires (Shorts, flaky Android TV
|
||||
// WebViews), advance from the reported duration + 3s slack. Cleared
|
||||
// on a real ENDED, on error, or on teardown. Skip when looping (a
|
||||
// single-item playlist intentionally loops forever).
|
||||
if (!shouldLoop) {
|
||||
let duration = 0;
|
||||
try { duration = event.target.getDuration(); } catch {}
|
||||
if (duration > 0) {
|
||||
clearTimeout(ytSafetyNet);
|
||||
ytSafetyNet = setTimeout(() => {
|
||||
if (myGeneration !== ytGeneration) return;
|
||||
console.log('YouTube safety net fired for:', item.filename);
|
||||
nextItem();
|
||||
}, (duration + 3) * 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (event) => {
|
||||
if (myGeneration !== ytGeneration) return;
|
||||
clearTimeout(ytSafetyNet); ytSafetyNet = null;
|
||||
console.error('YouTube error', event.data, 'for:', item.filename);
|
||||
if (playlist.length > 1) {
|
||||
console.log('Skipping unplayable YouTube video');
|
||||
|
|
@ -2056,6 +2078,7 @@
|
|||
// YT.PlayerState.ENDED = 0 — advance to next video
|
||||
// Ignore ENDED if video played for less than 3 seconds (spurious during init)
|
||||
if (event.data === 0 && !shouldLoop && (Date.now() - playStartTime) > 3000) {
|
||||
clearTimeout(ytSafetyNet); ytSafetyNet = null;
|
||||
console.log('YouTube video ended:', item.filename);
|
||||
nextItem();
|
||||
}
|
||||
|
|
@ -2085,6 +2108,9 @@
|
|||
// On a buffered widget reveal (keep set) the widget's advance/refresh timer was just
|
||||
// armed by renderContent and must survive; only a real teardown cancels it.
|
||||
if (!keep && advanceTimer) { clearTimeout(advanceTimer); advanceTimer = null; }
|
||||
// #215: a stale YouTube safety-net timer must never fire into a torn-down/rotated
|
||||
// playlist and force a spurious advance.
|
||||
if (ytSafetyNet) { clearTimeout(ytSafetyNet); ytSafetyNet = null; }
|
||||
// Cancel any in-flight buffered widget swap so its deferred reveal can't fire after
|
||||
// we've torn down (which would drop the incoming content). reveal() nulls this before
|
||||
// calling teardownCurrentMedia(iframe), so preserving `keep` is safe.
|
||||
|
|
|
|||
Loading…
Reference in a new issue