feat: frame work off the event loop, each frame encoded once

- Endpoints that wait for the camera or encode/compute (capture, rgb, depth,
  depth/color, pointcloud, pointcloud/roi, measure, snapshot, and the SDK
  calls discover/open/close) are plain `def`: FastAPI runs them in its
  thread pool, so one slow request no longer holds the others. Plain `def`
  rather than asyncio.to_thread in each route: it is FastAPI's own mechanism
  and keeps the route bodies unchanged.
- /ws and the NATS snapshot stay async and use asyncio.to_thread for the
  frame wait, the encoding and the recording write.
- EncodedFrame: the JPEG q85, 16-bit PNG, colormap JPEG q90, their base64
  and the depth stats of a frame are computed once, on first use, and
  shared: parallel requests, /ws and NATS on the same frame get the same
  bytes. The last 4 frames' encodings are kept.
- camera.cached_frame(): the whole cached frame (get_rgb/get_depth unchanged).

Contract unchanged: same endpoints, same JSON fields. frame_id still counts
responses (now under a lock); two parallel /api/capture served by one frame
have different frame_id and the same timestamp and images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
OliviaCandClaude Opus 5.5 committed 2026-09-29 13:03:28 +02:00
1 parent cc28127c9c
commit 1ad422062c
4 files changed
+125 -50

No files matched your search

+5 -3
View File
@@ -56,15 +56,17 @@ async def websocket_stream(ws: WebSocket):
try:
while True:
if streaming and _camera and _camera.is_open():
frame = _camera.capture(timeout_ms=500)
# Waiting for the frame, encoding and saving run in threads: the
# event loop keeps serving REST and the other sockets meanwhile.
frame = await asyncio.to_thread(_camera.capture, 500)
if frame:
data = _frame_to_dict(frame)
data = await asyncio.to_thread(_frame_to_dict, frame)
if predicting and _detector is not None:
data["detections"] = await asyncio.to_thread(_detector.detect, frame.rgb)
with timer.phase("ws_send"):
await ws.send_json(data)
if _saver and _saver.is_recording:
_saver.save_rec_frame(frame.rgb)
await asyncio.to_thread(_saver.save_rec_frame, frame.rgb)
await asyncio.sleep(0.033)
try: