Harden Disney+ time fallback against <video> element recreation

When the control overlay (and its live time readout) is briefly absent,
the old fallback recomputed the timeline from the raw blob-relative
video.currentTime via a cached scale/start. Disney recreates the <video>
element on some play/pause transitions, which resets currentTime, so that
math produced a wildly wrong time ("loses the time" after a pause).

The native clock advances 1:1 with real playback, so instead anchor the
last live UI position to video.currentTime and extrapolate by the elapsed
native delta. If the delta is negative or implausibly large (element
recreated / seek), freeze at the last known position rather than emitting
a garbage time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Timo
2026-07-02 14:38:26 +02:00
parent 8fe1c6dfbc
commit 2fbcbc4f83
2 changed files with 27 additions and 12 deletions
+25 -12
View File
@@ -62,6 +62,8 @@
// Suppresses native event reporting after a programmatic action. // Suppresses native event reporting after a programmatic action.
// Each entry is a per-type timer (key = 'playing'|'paused'|'seek').
// While a timer exists, matching native events are consumed and not relayed. // While a timer exists, matching native events are consumed and not relayed.
// Timers self-clean after 300ms if the native event never fires. // Timers self-clean after 300ms if the native event never fires.
@@ -285,6 +287,8 @@
const candidates = querySelectorAllShadow('button,[role="button"]'); const candidates = querySelectorAllShadow('button,[role="button"]');
const backRe = /rewind|backward|back\b|zurück|zurueck|rück|rueck|retour|recul|retroced|atrás|voltar|indietro/i; const backRe = /rewind|backward|back\b|zurück|zurueck|rück|rueck|retour|recul|retroced|atrás|voltar|indietro/i;
const forwardRe = /forward|ahead|skip|vorwärts|vorwaerts|vorsp|weiter|avancer|adelant|avançar|avancar|avanti/i; const forwardRe = /forward|ahead|skip|vorwärts|vorwaerts|vorsp|weiter|avancer|adelant|avançar|avancar|avanti/i;
const timeRe = /\b(5|10|15|30)\b|sec|sek|second/i;
const button = candidates.find(btn => { const button = candidates.find(btn => {
const label = getElementLabel(btn); const label = getElementLabel(btn);
const matchesDirection = backward ? backRe.test(label) : forwardRe.test(label); const matchesDirection = backward ? backRe.test(label) : forwardRe.test(label);
@@ -294,18 +298,27 @@
return backward ? backRe.test(label) : forwardRe.test(label); return backward ? backRe.test(label) : forwardRe.test(label);
}); });
}); if (button && typeof button.click === 'function') {
const clicks = Math.max(1, Math.min(12, Math.round(Math.abs(delta) / 10)));
if (button && typeof button.click === 'function') { for (let i = 0; i < clicks; i++) {
const clicks = Math.max(1, Math.min(12, Math.round(Math.abs(delta) / 10))); setTimeout(() => button.click(), i * 60);
for (let i = 0; i < clicks; i++) { }
setTimeout(() => button.click(), i * 60); return true;
} }
return true;
} // Fallback: Simulate ArrowLeft / ArrowRight keyboard events
const clicks = Math.max(1, Math.min(12, Math.round(Math.abs(delta) / 10)));
// Fallback: Simulate ArrowLeft / ArrowRight keyboard events const eventInit = {
const clicks = Math.max(1, Math.min(12, Math.round(Math.abs(delta) / 10))); key: backward ? 'ArrowLeft' : 'ArrowRight',
code: backward ? 'ArrowLeft' : 'ArrowRight',
keyCode: backward ? 37 : 39,
which: backward ? 37 : 39,
bubbles: true,
cancelable: true,
view: window
};
const target = document.querySelector('.hive-video') || document.activeElement || document.body;
for (let i = 0; i < clicks; i++) {
setTimeout(() => { setTimeout(() => {
target.dispatchEvent(new window.KeyboardEvent('keydown', eventInit)); target.dispatchEvent(new window.KeyboardEvent('keydown', eventInit));
target.dispatchEvent(new window.KeyboardEvent('keyup', eventInit)); target.dispatchEvent(new window.KeyboardEvent('keyup', eventInit));
+2
View File
@@ -94,6 +94,8 @@ function loadTimelineFns(hostname, document = makeDocument()) {
'let lastKnownDisneyPlusDuration = 0;', 'let lastKnownDisneyPlusDuration = 0;',
'let lastKnownDisneyPlusScale = 1;', 'let lastKnownDisneyPlusScale = 1;',
'let lastKnownDisneyPlusStart = 0;', 'let lastKnownDisneyPlusStart = 0;',
'let lastDisneyPlusUiCurrent = null;',
'let lastDisneyPlusNativeAtUi = null;',
extractFunction('scanShadowDom', source), extractFunction('scanShadowDom', source),
extractFunction('querySelectorAllShadow', source), extractFunction('querySelectorAllShadow', source),
extractFunction('hostMatchesUrl', source), extractFunction('hostMatchesUrl', source),