better sync

This commit is contained in:
2026-04-13 02:14:30 -07:00
parent 8aa84ea446
commit acb0cd3161

View File

@@ -15,14 +15,25 @@
let videoElement: HTMLVideoElement | undefined = $state(); let videoElement: HTMLVideoElement | undefined = $state();
let newUrl = $state(""); let newUrl = $state("");
let ignoreNextEvent = false; let isSyncing = false;
let syncTimeout: ReturnType<typeof setTimeout> | undefined;
function syncAction(fn: () => void) {
isSyncing = true;
clearTimeout(syncTimeout);
fn();
syncTimeout = setTimeout(() => {
isSyncing = false;
}, 50);
}
$effect(() => { $effect(() => {
if (!videoElement || !videoState) return; if (!videoElement || !videoState) return;
const el = videoElement;
const state = videoState; const state = videoState;
if (videoElement.src !== state.url) { if (el.src !== state.url) {
videoElement.src = state.url; el.src = state.url;
} }
// Account for server to client clock drift slightly, but MVP assumes relatively synced clocks. // 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 + Number(timeElapsedMicros) / 1_000_000
: state.timePosition; : 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 (!state.isPlaying) {
if (diff > 2.0) { if (el.currentTime < expectedTime && diff <= 2 && !el.paused) {
ignoreNextEvent = true; // Behind pause point. Keep playing to catch up. (timeupdate will pause it)
videoElement.currentTime = expectedTime; } else {
} // Ahead or reached pause point. Pause and rewind if needed.
syncAction(() => {
if (state.isPlaying && videoElement.paused) { el.pause();
ignoreNextEvent = true; el.currentTime = expectedTime;
videoElement.play().catch(console.error); });
} else if (!state.isPlaying && !videoElement.paused) { }
ignoreNextEvent = true; } else {
videoElement.pause(); // 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() { function handlePlay() {
if (ignoreNextEvent) { if (isSyncing) return;
ignoreNextEvent = false;
return;
}
playReducer({ timePosition: videoElement?.currentTime ?? 0 }); playReducer({ timePosition: videoElement?.currentTime ?? 0 });
} }
function handlePause() { function handlePause() {
if (ignoreNextEvent) { if (isSyncing) return;
ignoreNextEvent = false;
return;
}
pauseReducer({ timePosition: videoElement?.currentTime ?? 0 }); pauseReducer({ timePosition: videoElement?.currentTime ?? 0 });
} }
function handleSeeked() { function handleSeeked() {
if (ignoreNextEvent) { if (isSyncing) return;
ignoreNextEvent = false;
return;
}
seekReducer({ timePosition: videoElement?.currentTime ?? 0 }); 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) { function handleSetUrl(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
if (!newUrl.trim() || !$conn.isActive) return; if (!newUrl.trim() || !$conn.isActive) return;
@@ -109,6 +139,7 @@
onplay={handlePlay} onplay={handlePlay}
onpause={handlePause} onpause={handlePause}
onseeked={handleSeeked} onseeked={handleSeeked}
ontimeupdate={handleTimeUpdate}
class="w-full max-w-2xl bg-black" class="w-full max-w-2xl bg-black"
> >
Your browser does not support the video tag. Your browser does not support the video tag.