better sync
This commit is contained in:
@@ -15,14 +15,25 @@
|
||||
let videoElement: HTMLVideoElement | undefined = $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(() => {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user