From acb0cd3161a180bae77a7675dee3b3b98e0a1552 Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Mon, 13 Apr 2026 02:14:30 -0700 Subject: [PATCH] better sync --- src/routes/+page.svelte | 87 ++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ccf81d5..d7e3353 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -15,14 +15,25 @@ let videoElement: HTMLVideoElement | undefined = $state(); let newUrl = $state(""); - let ignoreNextEvent = false; + let isSyncing = false; + let syncTimeout: ReturnType | undefined; + + function syncAction(fn: () => void) { + isSyncing = true; + clearTimeout(syncTimeout); + fn(); + syncTimeout = setTimeout(() => { + isSyncing = false; + }, 50); + } $effect(() => { if (!videoElement || !videoState) return; + const el = videoElement; const state = videoState; - if (videoElement.src !== state.url) { - videoElement.src = state.url; + if (el.src !== state.url) { + el.src = state.url; } // Account for server to client clock drift slightly, but MVP assumes relatively synced clocks. @@ -31,47 +42,66 @@ ? state.timePosition + Number(timeElapsedMicros) / 1_000_000 : state.timePosition; - const diff = Math.abs(videoElement.currentTime - expectedTime); + const diff = Math.abs(el.currentTime - expectedTime); - // Allow up to 2 seconds drift without snapping - if (diff > 2.0) { - ignoreNextEvent = true; - videoElement.currentTime = expectedTime; - } - - if (state.isPlaying && videoElement.paused) { - ignoreNextEvent = true; - videoElement.play().catch(console.error); - } else if (!state.isPlaying && !videoElement.paused) { - ignoreNextEvent = true; - videoElement.pause(); + if (!state.isPlaying) { + if (el.currentTime < expectedTime && diff <= 2 && !el.paused) { + // Behind pause point. Keep playing to catch up. (timeupdate will pause it) + } else { + // Ahead or reached pause point. Pause and rewind if needed. + syncAction(() => { + el.pause(); + el.currentTime = expectedTime; + }); + } + } else { + // Sync on play by unpausing slightly in future + if (el.paused) { + syncAction(() => { + el.currentTime = expectedTime; + el.play().catch(console.error); + }); + } else if (diff > 2.0) { + // Allow up to 2s drift while playing + syncAction(() => { + el.currentTime = expectedTime; + }); + } } }); function handlePlay() { - if (ignoreNextEvent) { - ignoreNextEvent = false; - return; - } + if (isSyncing) return; playReducer({ timePosition: videoElement?.currentTime ?? 0 }); } function handlePause() { - if (ignoreNextEvent) { - ignoreNextEvent = false; - return; - } + if (isSyncing) return; pauseReducer({ timePosition: videoElement?.currentTime ?? 0 }); } function handleSeeked() { - if (ignoreNextEvent) { - ignoreNextEvent = false; - return; - } + if (isSyncing) return; seekReducer({ timePosition: videoElement?.currentTime ?? 0 }); } + function handleTimeUpdate() { + if (!videoElement || !videoState) return; + const el = videoElement; + + if (!videoState.isPlaying) { + const expectedTime = videoState.timePosition; + if (!el.paused && el.currentTime >= expectedTime) { + syncAction(() => { + el.pause(); + if (el.currentTime - expectedTime > 0.01) { + el.currentTime = expectedTime; + } + }); + } + } + } + function handleSetUrl(e: SubmitEvent) { e.preventDefault(); if (!newUrl.trim() || !$conn.isActive) return; @@ -109,6 +139,7 @@ onplay={handlePlay} onpause={handlePause} onseeked={handleSeeked} + ontimeupdate={handleTimeUpdate} class="w-full max-w-2xl bg-black" > Your browser does not support the video tag.