From 7c12d253675f71659b64ead28c4ebe0da27be69e Mon Sep 17 00:00:00 2001 From: Johnny322 Date: Tue, 24 Feb 2026 21:52:07 +0100 Subject: [PATCH] Remove delay between host and viewer --- src/App.vue | 65 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/src/App.vue b/src/App.vue index 787e04f..e814f61 100644 --- a/src/App.vue +++ b/src/App.vue @@ -316,6 +316,8 @@ type RealtimeState = { currentSelectorId: string | null lastAwardedTeamId: string | null isAnswerClip: boolean + playbackPosition: number + playbackCapturedAt: number } type RealtimeMessage = { @@ -377,7 +379,8 @@ export default { syncError: '', isApplyingRemote: false, queuedRemoteState: null as RealtimeState | null, - syncTimer: 0 + syncTimer: 0, + playbackSyncInterval: 0 } }, async mounted() { @@ -528,6 +531,7 @@ export default { this.gameIdInput = '' this.isHost = false this.syncError = '' + this.stopHostPlaybackSync() this.setGameInUrl('') }, async connectSession() { @@ -576,6 +580,7 @@ export default { }).catch(() => {}) }, buildRealtimeState(): RealtimeState { + const player = this.getPlayer() return { step: this.step, teams: this.teams.map((team) => ({ ...team })), @@ -585,7 +590,9 @@ export default { currentClipUrl: this.currentClipUrl, currentSelectorId: this.currentSelectorId, lastAwardedTeamId: this.lastAwardedTeamId, - isAnswerClip: this.isAnswerClip + isAnswerClip: this.isAnswerClip, + playbackPosition: player?.currentTime || 0, + playbackCapturedAt: Date.now() } }, publishState() { @@ -630,13 +637,27 @@ export default { this.isApplyingRemote = false } await nextTick() - this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl) + this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl, state) }, getCurrentTileStatus() { if (!this.currentTileKey) return 'available' return this.tiles[this.currentTileKey]?.status || 'available' }, - syncRemotePlayback(forceReload: boolean) { + setPlayerCurrentTime(player: HTMLAudioElement, targetTime: number) { + const seek = () => { + try { + player.currentTime = targetTime + } catch { + // ignore seek failures on unsupported ranges + } + } + if (player.readyState > 0) { + seek() + return + } + player.addEventListener('loadedmetadata', seek, { once: true }) + }, + syncRemotePlayback(forceReload: boolean, state: RealtimeState) { if (this.canControlGame) return const player = this.getPlayer() if (!player) return @@ -647,11 +668,23 @@ export default { return } + const elapsedSeconds = Math.max(0, (Date.now() - (state.playbackCapturedAt || Date.now())) / 1000) + let targetTime = state.playbackPosition || 0 + if (tileStatus === 'playing' || tileStatus === 'guessed') { + targetTime += elapsedSeconds + } + if (Number.isFinite(player.duration) && player.duration > 0) { + targetTime = Math.min(targetTime, Math.max(0, player.duration - 0.05)) + } + if (forceReload) { - player.currentTime = 0 player.load() } + if (Math.abs((player.currentTime || 0) - targetTime) > 0.2) { + this.setPlayerCurrentTime(player, targetTime) + } + if (tileStatus === 'paused') { player.pause() return @@ -707,6 +740,7 @@ export default { this.tentacleLevels = this.tentacleLevels.map(() => 0) }, teardownAudio() { + this.stopHostPlaybackSync() if (this.rafId) { cancelAnimationFrame(this.rafId) this.rafId = 0 @@ -785,9 +819,25 @@ export default { if (this.rafId) cancelAnimationFrame(this.rafId) this.rafId = requestAnimationFrame(tick) }, + startHostPlaybackSync() { + if (!this.isHost || !this.socketConnected || !this.socket) return + this.stopHostPlaybackSync() + this.playbackSyncInterval = window.setInterval(() => { + this.queueStateSync() + }, 400) + }, + stopHostPlaybackSync() { + if (!this.playbackSyncInterval) return + window.clearInterval(this.playbackSyncInterval) + this.playbackSyncInterval = 0 + }, handlePlayerPlay() { this.isPlaying = true this.startPulse() + if (this.isHost) { + this.startHostPlaybackSync() + this.queueStateSync() + } }, handlePlayerPause() { this.isPlaying = false @@ -796,6 +846,10 @@ export default { this.rafId = 0 } this.pulseLevel = 0 + if (this.isHost) { + this.stopHostPlaybackSync() + this.queueStateSync() + } }, togglePlayback() { const player = this.getPlayer() @@ -1017,6 +1071,7 @@ export default { this.teardownAudio() this.closeSocket() if (this.syncTimer) window.clearTimeout(this.syncTimer) + if (this.playbackSyncInterval) window.clearInterval(this.playbackSyncInterval) } }