33import math
44import os
55import re
6+ import time
67
78from fastapi import APIRouter , WebSocket , WebSocketDisconnect , Query
89from 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