Files
RealSense-D435i-G1/src/backend/api/websocket.py
T
OliviaCandClaude Opus 5.5 1ad422062c 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>
2026-09-29 13:03:28 +02:00

99 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import logging
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from src.backend.api.routes import _frame_to_dict
from src.backend.services.image_saver import ImageSaver
from src.backend.services.phase_timing import timer
try:
from src.backend.services.detector import Detector
except Exception:
Detector = None # onnxruntime is optional; YOLO features stay disabled
logger = logging.getLogger(__name__)
ws_router = APIRouter()
_camera = None
_saver: ImageSaver | None = None
_detector = None # Detector | None when onnxruntime is installed
def set_camera(camera):
global _camera
_camera = camera
def set_image_saver(saver: ImageSaver) -> None:
global _saver
_saver = saver
def set_detector(detector) -> None:
global _detector
_detector = detector
@ws_router.websocket("/ws")
async def websocket_stream(ws: WebSocket):
"""WebSocket stream of RGB+depth frames.
Client → server text messages:
'start' — begin streaming
'stop' — pause streaming
'close' — disconnect
'predict_on' — enable YOLO inference per frame (adds 'detections' to JSON)
'predict_off' — disable YOLO inference
'conf:0.55' — set YOLO confidence threshold (0.0–1.0)
"""
await ws.accept()
logger.info("WebSocket client connected")
streaming = False
predicting = False
try:
while True:
if streaming and _camera and _camera.is_open():
# 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 = 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:
await asyncio.to_thread(_saver.save_rec_frame, frame.rgb)
await asyncio.sleep(0.033)
try:
msg = await asyncio.wait_for(ws.receive_text(), timeout=0.01)
if msg == "start":
streaming = True
elif msg == "stop":
streaming = False
elif msg == "close":
break
elif msg == "predict_on":
predicting = _detector is not None
if not predicting:
logger.warning("predict_on requested but detector not loaded")
elif msg == "predict_off":
predicting = False
elif msg.startswith("conf:") and _detector is not None:
try:
_detector.set_conf(float(msg.split(":", 1)[1]))
except ValueError:
logger.warning(f"Invalid conf value: {msg!r}")
except asyncio.TimeoutError:
pass
if not streaming:
await asyncio.sleep(0.1)
except WebSocketDisconnect:
logger.info("WebSocket client disconnected")