Skip to content

Commit 03e6977

Browse files
authored
Merge pull request #69 from stephenwilley/timing_fix
Fix timing drift in playback loop
2 parents f31992b + 128eef1 commit 03e6977

1 file changed

Lines changed: 28 additions & 5 deletions

File tree

backend/routers/replay.py

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import math
44
import os
55
import re
6+
import time
67

78
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
89
from services.storage import get_json
@@ -317,7 +318,18 @@ def prepare_frame(f: dict) -> dict:
317318
playing = False
318319
speed = 1.0
319320
frame_index = 0
320-
base_interval = 0.5
321+
322+
# Wall-clock anchor used to compute per-frame sleep durations.
323+
# Anchored at play/seek/speed-change so accumulated async overhead
324+
# never causes timing drift over long sessions.
325+
play_start_wall: float = 0.0
326+
play_start_session: float = 0.0
327+
328+
def reset_anchor():
329+
nonlocal play_start_wall, play_start_session
330+
if frame_index < len(frames):
331+
play_start_wall = time.monotonic()
332+
play_start_session = frames[frame_index]["timestamp"]
321333

322334
async def send_seek_frame(target_time: float):
323335
nonlocal frame_index
@@ -333,18 +345,21 @@ async def handle_command(cmd: str):
333345

334346
if cmd == "play":
335347
playing = True
348+
reset_anchor()
336349
elif cmd == "pause":
337350
playing = False
338351
elif cmd.startswith("speed:"):
339352
try:
340353
speed = float(cmd.split(":")[1])
341354
speed = max(0.25, min(50.0, speed))
355+
reset_anchor() # re-anchor at new speed
342356
except ValueError:
343357
pass
344358
elif cmd.startswith("seek:"):
345359
try:
346360
target_time = float(cmd.split(":")[1])
347361
await send_seek_frame(target_time)
362+
reset_anchor()
348363
except ValueError:
349364
pass
350365
elif cmd.startswith("seeklap:"):
@@ -356,12 +371,14 @@ async def handle_command(cmd: str):
356371
break
357372
if frame_index < len(frames):
358373
await websocket.send_json({"type": "frame", **prepare_frame(frames[frame_index])})
374+
reset_anchor()
359375
except ValueError:
360376
pass
361377
elif cmd == "reset":
362378
frame_index = 0
363379
playing = False
364380
await websocket.send_json({"type": "frame", **prepare_frame(frames[0])})
381+
reset_anchor()
365382

366383
async def check_command(timeout: float) -> bool:
367384
try:
@@ -381,11 +398,17 @@ async def check_command(timeout: float) -> bool:
381398
await websocket.send_json({"type": "finished"})
382399
continue
383400

384-
remaining = base_interval / speed
385-
while remaining > 0 and playing:
386-
chunk = min(remaining, 0.05)
401+
# Sleep until the next frame is due per wall clock.
402+
# sleep_remaining is recomputed from the actual clock each iteration
403+
# so any processing overhead is automatically absorbed.
404+
next_session_time = frames[frame_index]["timestamp"]
405+
target_wall = play_start_wall + (next_session_time - play_start_session) / speed
406+
sleep_remaining = target_wall - time.monotonic()
407+
408+
while sleep_remaining > 0 and playing:
409+
chunk = min(sleep_remaining, 0.05)
387410
await check_command(chunk)
388-
remaining -= chunk
411+
sleep_remaining = target_wall - time.monotonic()
389412
else:
390413
await check_command(1.0)
391414

0 commit comments

Comments
 (0)