better sync
This commit is contained in:
@@ -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(() => {
|
||||||
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isPlaying && videoElement.paused) {
|
|
||||||
ignoreNextEvent = true;
|
|
||||||
videoElement.play().catch(console.error);
|
|
||||||
} else if (!state.isPlaying && !videoElement.paused) {
|
|
||||||
ignoreNextEvent = true;
|
|
||||||
videoElement.pause();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user