From 8fc3dfb370ea9fd0ec442af4306d8d03d762bacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sun, 10 May 2026 13:40:45 -0700 Subject: [PATCH] fix(runtime): skip drift corrections on playing video elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seeking a playing video resets the browser's decoder pipeline, causing a ~150ms freeze while it re-buffers. During that freeze the monotonic clock advances, drift grows, and strict sync fires another seek — creating a perpetual stutter loop (176 seek events / 8s observed on the apple-presentation composition). Skip strict and force drift corrections for playing video elements; only hard sync (>0.5s catastrophic drift) warrants the decoder-reset cost. Audio elements are unaffected and retain the full correction tiers. Also propagate the asset-loading overlay state to the timeline so controls are disabled during "Preparing preview assets", matching the existing behavior for the initial composition loading overlay. --- packages/core/src/runtime/media.ts | 18 ++++++++++++++++-- .../studio/src/player/components/Player.tsx | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/core/src/runtime/media.ts b/packages/core/src/runtime/media.ts index 11d7706593..5d76dbc947 100644 --- a/packages/core/src/runtime/media.ts +++ b/packages/core/src/runtime/media.ts @@ -198,13 +198,26 @@ export function syncRuntimeMedia(params: { const offsetJumped = !firstTickOfClip && Math.abs(offset - prevOffset!) > 0.5; const catastrophicDrift = drift > 3; const hardSync = drift > 0.5 && (firstTickOfClip || offsetJumped || catastrophicDrift); + // Playing video elements use the browser's native decoder pipeline for + // timing. Seeking a playing video resets the decoder, causing a ~150ms + // freeze while it re-buffers — during which the monotonic clock advances, + // creating a perpetual seek→freeze→drift→seek stutter loop. Skip strict + // and force sync for playing videos; only hard sync (>0.5s) warrants + // the decoder-reset cost. + const isPlayingVideo = el.tagName === "VIDEO" && !el.paused; // Only apply strict sync when offset has stabilized (not growing). // During initial buffering, offset grows ~16ms/tick as the timeline // advances while media stays at 0. Accumulated drift from pause/play // toggling shows up as a stable, non-zero offset (delta near 0). const offsetStabilized = prevOffset !== undefined && Math.abs(offset - prevOffset) < 0.004; let strictSync = false; - if (!hardSync && !firstTickOfClip && offsetStabilized && drift > STRICT_DRIFT_THRESHOLD) { + if ( + !isPlayingVideo && + !hardSync && + !firstTickOfClip && + offsetStabilized && + drift > STRICT_DRIFT_THRESHOLD + ) { const samples = (strictDriftSamples.get(el) ?? 0) + 1; strictDriftSamples.set(el, samples); if (samples >= STRICT_REQUIRED_SAMPLES) { @@ -214,7 +227,8 @@ export function syncRuntimeMedia(params: { } else if (drift <= STRICT_DRIFT_THRESHOLD) { strictDriftSamples.set(el, 0); } - if (hardSync || strictSync || (params.forceSync && drift > 0.02)) { + const forceSync = !isPlayingVideo && params.forceSync && drift > 0.02; + if (hardSync || strictSync || forceSync) { try { el.currentTime = relTime; } catch (err) { diff --git a/packages/studio/src/player/components/Player.tsx b/packages/studio/src/player/components/Player.tsx index f47c044162..3f1a6f6612 100644 --- a/packages/studio/src/player/components/Player.tsx +++ b/packages/studio/src/player/components/Player.tsx @@ -273,8 +273,8 @@ export const Player = forwardRef( assetOverlayVisible && !shaderTransitionLoading && !showCompositionOverlay; useEffect(() => { - onCompositionLoadingChange?.(showCompositionOverlay); - }, [onCompositionLoadingChange, showCompositionOverlay]); + onCompositionLoadingChange?.(showCompositionOverlay || showAssetOverlay); + }, [onCompositionLoadingChange, showCompositionOverlay, showAssetOverlay]); return (