Files
operating-room-monitor-server/scripts/demo_client
2026-05-21 15:48:03 +08:00
..
2026-05-21 15:48:03 +08:00
2026-05-21 15:48:03 +08:00
2026-05-21 15:48:03 +08:00
2026-05-21 15:48:03 +08:00

Demo Client

一个浏览器里的单页 demo用于手动触发 app/api.py 里部分 /client/* 接口:开始/结束手术、查询结果等。语音待确认、TTS 与麦克风录音请使用 voice-confirmation-web/(独立静态页)或其它专用客户端,或通过 Swagger/curl 直接调 API。

结构

scripts/demo_client/
  server.py                 # 基于 stdlib 的静态服务器;额外暴露 /labels.json
  index.html                # 单文件页面(原生 JS零构建依赖
  fake_rtsp_from_file.py    # 无真摄像头时:把本地视频按文件时长推一次到 RTSPffmpeg + Docker MediaMTX

调试:无真实摄像头,用录好的视频模拟 RTSP

监控服务只从 RTSP URL 拉流cv2.VideoCapture没有「上传视频文件」的 HTTP 接口;在不改 Python 后端的前提下,只能让「摄像头地址」指向一个**真实可连的 RTSP 源。

推荐做法:在本机把视频文件用 ffmpeg 推到本机上的 RTSP 服务(脚本用 Docker 启动 MediaMTX),得到 rtsp://127.0.0.1:<端口>/<路径>,再通过环境变量告诉后端(只改配置,不改仓库里的后端代码

单路(一个文件、一个 camera_id,兼容旧命令):

# 依赖ffmpeg、Docker首次会拉取 bluenviron/mediamtx
cd /path/to/operation-room-monitor-server
python3 scripts/demo_client/fake_rtsp_from_file.py /path/to/recording.mp4 --port 18554 --path demo

两路(两路不同视频、两个 camera_id一个 MediaMTX、两路 ffmpeg每路用不同的 RTSP_PATH

python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
  --stream 'or-cam-01|./a.mp4|demo1' \
  --stream 'or-cam-02|./b.mp4|demo2'

--stream 格式为 CAMERA_ID|文件路径|RTSP_PATH(竖线分隔,整条加引号),脚本会在 stderr 打印含 video_rtsp_urlsvoice_or_room_bindings: []站点 JSON 片段,可合并进 OR_SITE_CONFIG_JSON_FILE

另一终端启动监控服务前 source 或手动 export 上述变量,使 POST /client/surgeries/start 里使用的 camera_ids(如 or-cam-01,or-cam-02)能解析到对应 URL。Demo 页里「将 camera_id 填到开始手术」可一键同步两路 id。

监控在 Docker、假 RTSP 在宿主机(推荐联调拓扑)

常见安排是:假摄像头脚本fake_rtsp_from_file.py + ffmpeg + MediaMTX宿主机终端里跑,推流地址是 rtsp://127.0.0.1:<端口>/...监控 API 服务Docker 容器里跑,容器里的进程要访问宿主机上的 RTSP应使用

  • macOS / Windows Docker Desktoprtsp://host.docker.internal:<端口>/<路径>
  • Linuxhost.docker.internal 可能未预置,可任选其一:
    • 给该服务容器加 --add-host=host.docker.internal:host-gatewayDocker 20.10+),或
    • 直接把 URL 写成宿主在 docker0/桥接网 上可达的局域网 IP192.168.x.x),保证从容器内 curl/ffprobe 能通

生产/容器环境请使用 **OR_SITE_CONFIG_JSON_FILE** 指向完整站点 JSONvideo_rtsp_urlsvoice_or_room_bindings)。不要在仅容器可解析的配置里写 127.0.0.1 去指宿主机上的 RTSP127.0.0.1 在容器内是容器自己)。

若监控与假 RTSP 都在宿主机同一系统里直接跑(非容器),则用 rtsp://127.0.0.1:... 即可;否则应使用上面「容器连宿主」的写法。

发布失败时,可尝试把输入转码后再推流(示例,需自行调整):

ffmpeg -re -i recording.mp4 -c:v libx264 -pix_fmt yuv420p -f rtsp -rtsp_transport tcp rtsp://127.0.0.1:18554/demo

(仍须先自行启动 MediaMTX 或等价 RTSP 服务端;上例为播完即止,若要循环请加 -stream_loop -1。)

Demo 页面「调试:两路视频」中可用 选择视频 / 拖放 为路1/路2 指定文件,并配合下面 一键开录 上传。若必须完全手跑 fake_rtsp_from_file.py,请将其打印的站点 JSON 合并进 OR_SITE_CONFIG_JSON_FILE

一键开录(不再手抄命令)

在 §4.1 勾选 「一键联调」 后,在「调试」里为路1/路2各选一段视频,再点 开始手术,浏览器会把两路视频 multipart 上传到监控 APIPOST /internal/demo/orchestrate-and-start),由服务进程依次:

  1. 落盘两路视频到临时目录
  2. 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSPfake_rtsp_from_file.py 等效)
  3. 把当前假流的 video_rtsp_urls 合并写入 OR_SITE_CONFIG_JSON_FILE(保留已有 voice_or_room_bindings;与开录/拉流同进程,固定本机回环)
  4. 调用与普通开录相同逻辑

需同时满足

  • .envDEMO_ORCHESTRATOR_ENABLED=true(并重启 API
  • 已设置 OR_SITE_CONFIG_JSON_FILE 指向可写的站点 JSONDocker 中请用 bind-mount 到容器内同一路径
  • 运行 main.py 的进程能执行本机 dockerffmpeg(与手动跑 fake_rtsp_from_file 相同)。仅将 API 放 Docker、且不挂载 /var/run/docker.sock 时,容器内往往无法为你在宿主机起 MediaMTX此时请继续用手动假流方式。

由于每次解析都会重新读取 video_rtsp_url_map(),覆盖 JSON 后无需重启主服务即可被下一次开录用到。

运行方式

# 1) 启动宿主机 API默认自动使用已激活的非 base conda 环境,否则使用 uv
./deploy.sh

# 2) 启动 demo 客户端静态服务(默认 0.0.0.0:38081局域网可访问
./start_demo_client.sh
# 或直接:
python scripts/demo_client/server.py
# 浏览器热重载(需 dev 依赖;会生成本目录 labels.json 供静态托管):
uv run --group dev python scripts/demo_client/server.py --live
# 或指定端口:
python scripts/demo_client/server.py -p 9000 --host 0.0.0.0

# 3) 浏览器访问:
open http://127.0.0.1:38081/

页面顶部的「服务端 Base URL」默认指向当前页面同主机的 :38080。局域网访问 Demo 页时,通常会自动变成 http://<宿主机局域网IP>:38080;如果后端部署在其他主机/端口,直接改这里即可。

与「语音确认」页一致的热重载

  • API:默认在宿主机 conda/uv 环境运行;修改后端代码后重启 ./deploy.sh 进程即可。
  • 本 Demo--livelivereload监视本目录与 app/resources/consumable_classifier_labels.yaml)。
  • voice-confirmation-web/:部署载体与 start_http.* 说明见 voice-confirmation-web/README.md;仓库根的 start_voice_confirmation_web.sh|.bat 为封装入口(--plain / --single / livereload

页面包含什么

  • GET /health 连通性检查
  • §4.1 POST /client/surgeries/start — 含 surgery_id 校验、camera_ids 多值输入、candidate_consumables 标签编辑器(初始值从 /labels.json 载入,可增删)
  • §4.2 POST /client/surgeries/end
  • §4.3 GET /client/surgeries/{id}/result — 以表格渲染 detailssummary
  • 调试:无摄像头 — 多路视频选择与 camera_id;一键联调见上文;手跑假流见 fake_rtsp_from_file.py 与本文「调试:无真实摄像头」

GET /client/surgeries/{id}/pending-confirmationPOST .../resolve 仍由后端提供,本 Demo 页不包含相应 UI无待确认拉取、无 TTS、无麦克风录音

右侧「响应日志」按时间倒序展示每次请求的 method/url/status/body便于联调截图。

关于 /labels.json

server.py 在进程启动时读 app/resources/consumable_classifier_labels.yamlnames 映射并返回 {"labels": [...]}。优先用 PyYAML(主项目依赖已间接引入),缺失时回退到手写的最小 YAML 解析器(仅兼容该文件的已知形状)。

关闭 CORS生产环境

独立部署的 Demo 页和语音确认页跨域访问 API 时,需要 DEMO_CORS_ENABLED=true。正式部署建议在 .env 里把 DEMO_CORS_ORIGINS 收窄为具体来源,例如 http://my-host:38081,https://or-demo.example.com