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

+4
View File
@@ -466,6 +466,10 @@ class RealSenseD435:
return last
return self.capture()
def cached_frame(self) -> Optional[CameraFrame]:
"""Like get_rgb()/get_depth(), but the whole frame (so its encodings can be shared)."""
return self._cached_or_capture()
def get_rgb(self) -> Optional[np.ndarray]:
f = self._cached_or_capture()
return f.rgb if f else None