Files
operating-room-monitor-server/app/services/video/stream_worker.py
Kevin 8a4bad99d3 feat: 配置写死与 baked 模块,Alembic 建表,百度仅 BAIDU_*
- 新增 app/baked/algorithm|pipeline,非部署参数不再走 env;Settings 保留 DB/HTTP/RTSP/海康/百度/MinIO/Demo
- 移除 init_db_schema 与 reload 配置;main 仅 check_database;start*.sh 在 uvicorn 前执行 alembic upgrade head
- 依赖 psycopg[binary] 供 Alembic 同步 URL;alembic/env 注释与预发清单更新
- 撕段门控消费管线、各视频/语音/归档调用改为 baked
- 百度环境变量仅 BAIDU_APP_ID、BAIDU_API_KEY、BAIDU_SECRET_KEY 与 BAIDU_* 超时/ASR;人脸脚本与 baidu_speech 文案同步
- 全量单测与 .env.example 更新;.gitignore 忽略 refs/(本地权重/视频不入库)

Made-with: Cursor
2026-04-24 15:33:22 +08:00

120 lines
4.2 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.
"""单路 RTSP 拉流 worker负责打开、重连、读帧分发。
从 ``CameraSessionManager._camera_worker`` 抽出,保持同样的行为:
- 打开失败 → 退避 → 重试。
- 连续读帧失败达到阈值 → 释放连接 → 退避 → 重试。
- 读到可用帧后交给上游 ``frame_handler``,由其决定是否推理 / 跳帧。
不知道手术会话、推理结果或数据库。日志中出现 RTSP URL 时会脱敏 user:password。
"""
from __future__ import annotations
import asyncio
import re
from typing import Awaitable, Callable
from loguru import logger
from app.baked import pipeline as bp
from app.services.video.rtsp_capture import RtspCapture
FrameHandler = Callable[[object], Awaitable[None]]
_RTSP_CRED_RE = re.compile(r"(?P<scheme>rtsp://)(?P<userinfo>[^@/\s]+@)")
def redact_rtsp_url(url: str | None) -> str:
"""把 ``rtsp://user:pwd@host/...`` 脱敏为 ``rtsp://***@host/...``。"""
if not url:
return ""
return _RTSP_CRED_RE.sub(r"\g<scheme>***@", url)
class CameraStreamWorker:
"""以 async 循环封装单路 RTSP 的重连/读帧,交由 handler 处理帧。"""
def __init__(
self,
*,
surgery_id: str,
camera_id: str,
url: str,
) -> None:
self._surgery_id = surgery_id
self._camera_id = camera_id
self._url = url
async def run(
self,
*,
stream_ready: asyncio.Event,
stop_event: asyncio.Event,
frame_handler: FrameHandler,
) -> None:
cap: RtspCapture | None = None
consecutive_failures = 0
first_ready = True
safe_url = redact_rtsp_url(self._url)
try:
while not stop_event.is_set():
if cap is None:
try:
cap = RtspCapture(
self._url, open_timeout_sec=bp.VIDEO_OPEN_TIMEOUT_SEC
)
await asyncio.to_thread(cap.open)
consecutive_failures = 0
if first_ready:
stream_ready.set()
first_ready = False
logger.info(
"RTSP stream opened camera={} surgery={} url={}",
self._camera_id,
self._surgery_id,
safe_url,
)
except Exception as exc:
logger.warning(
"RTSP open failed camera={} surgery={} url={}: {}",
self._camera_id,
self._surgery_id,
safe_url,
exc,
)
if cap is not None:
await asyncio.to_thread(cap.release)
cap = None
await asyncio.sleep(bp.VIDEO_RECONNECT_BACKOFF_SECONDS)
continue
ok, frame = await asyncio.to_thread(cap.read)
if not ok or frame is None:
consecutive_failures += 1
if (
consecutive_failures
>= bp.VIDEO_READ_FAILURE_RECONNECT_THRESHOLD
):
logger.warning(
"RTSP reconnect camera={} surgery={} url={} after {} read failures",
self._camera_id,
self._surgery_id,
safe_url,
consecutive_failures,
)
await asyncio.to_thread(cap.release)
cap = None
consecutive_failures = 0
await asyncio.sleep(bp.VIDEO_RECONNECT_BACKOFF_SECONDS)
else:
await asyncio.sleep(0.05)
continue
consecutive_failures = 0
await frame_handler(frame)
finally:
if cap is not None:
await asyncio.to_thread(cap.release)