feat: 站点 JSON、语音终端 WebSocket 指派与客户端联调
- 用 OR_SITE_CONFIG_JSON_FILE 统一术间配置(video_rtsp_urls + voice_or_room_bindings) - VoiceTerminalHub:assignment、WS 推送与 HTTP 查询;开录/停录后 notify - 一键联调 orchestrate-and-start 与 /client/surgeries/start 共用指派逻辑,修复 demo 路径不发 WS - 语音桌面端:SIGINT 退出、shutdown 清理、仅 WS 指派、固定 pending 轮询间隔、界面仅保留录音时长 - 新增/调整契约与绑定测试,文档与示例配置同步 Made-with: Cursor
This commit is contained in:
@@ -33,7 +33,7 @@ python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
|
||||
--stream 'or-cam-02|./b.mp4|demo2'
|
||||
```
|
||||
|
||||
`--stream` 格式为 `CAMERA_ID|文件路径|RTSP_PATH`(竖线分隔,整条加引号),生成的 `VIDEO_RTSP_URLS_JSON` 会同时包含 `or-cam-01` 与 `or-cam-02`。
|
||||
`--stream` 格式为 `CAMERA_ID|文件路径|RTSP_PATH`(竖线分隔,整条加引号),脚本会在 stderr 打印含 `video_rtsp_urls` 与 `voice_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。
|
||||
|
||||
@@ -46,7 +46,7 @@ python3 scripts/demo_client/fake_rtsp_from_file.py --port 18554 \
|
||||
- 给该服务容器加 `--add-host=host.docker.internal:host-gateway`(Docker 20.10+),或
|
||||
- 直接把 URL 写成宿主在 **docker0/桥接网** 上可达的局域网 IP(如 `192.168.x.x`),保证从容器内 `curl`/`ffprobe` 能通
|
||||
|
||||
`docker-compose` 里可将 `VIDEO_RTSP_URLS_JSON` 写进 `environment:` 或 env 文件;**不要**在仅容器可解析的配置里写 `127.0.0.1` 去指宿主机上的 RTSP(`127.0.0.1` 在容器内是容器自己)。
|
||||
生产/容器环境请使用 **`OR_SITE_CONFIG_JSON_FILE`** 指向完整站点 JSON(含 `video_rtsp_urls` 与 `voice_or_room_bindings`)。**不要**在仅容器可解析的配置里写 `127.0.0.1` 去指宿主机上的 RTSP(`127.0.0.1` 在容器内是容器自己)。
|
||||
|
||||
若监控与假 RTSP **都在宿主机同一系统**里直接跑(非容器),则用 `rtsp://127.0.0.1:...` 即可;否则应使用上面「容器连宿主」的写法。
|
||||
|
||||
@@ -58,7 +58,7 @@ ffmpeg -re -i recording.mp4 -c:v libx264 -pix_fmt yuv420p -f rtsp -rtsp_transpor
|
||||
|
||||
(仍须先自行启动 MediaMTX 或等价 RTSP 服务端;上例为**播完即止**,若要循环请加 `-stream_loop -1`。)
|
||||
|
||||
Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放** 为路1/路2 指定文件,并配合下面 **一键开录** 上传,无需在页面里手抄 `python3` / `export` 命令。若必须完全手跑 `fake_rtsp_from_file.py`,请在上文命令示例与 `export VIDEO_RTSP_URLS_JSON=...` 方式自行在终端完成。
|
||||
Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放** 为路1/路2 指定文件,并配合下面 **一键开录** 上传。若必须完全手跑 `fake_rtsp_from_file.py`,请将其打印的站点 JSON 合并进 `OR_SITE_CONFIG_JSON_FILE`。
|
||||
|
||||
## 一键开录(不再手抄命令)
|
||||
|
||||
@@ -66,13 +66,13 @@ Demo 页面「调试:两路视频」中可用 **选择视频** / **拖放**
|
||||
|
||||
1. 落盘两路视频到临时目录
|
||||
2. 用 Docker 起 MediaMTX、两路 ffmpeg 推 RTSP(与 `fake_rtsp_from_file.py` 等效)
|
||||
3. 把 `{"or-cam-01":"rtsp://127.0.0.1:…","or-cam-02":"rtsp://127.0.0.1:…"}` 写入 `VIDEO_RTSP_URLS_JSON_FILE`(与开录/拉流同进程,固定本机回环;`DEMO_ORCHESTRATOR_RTSP_JSON_HOST` 仅影响你**手配**假流、给另一进程读 JSON 的用法)
|
||||
3. 把当前假流的 **video_rtsp_urls** 合并写入 `OR_SITE_CONFIG_JSON_FILE`(保留已有 `voice_or_room_bindings`;与开录/拉流同进程,固定本机回环)
|
||||
4. 调用与普通开录相同逻辑
|
||||
|
||||
**需同时满足**:
|
||||
|
||||
- `.env` 中 `DEMO_ORCHESTRATOR_ENABLED=true`(并重启 API)
|
||||
- 已设置 `VIDEO_RTSP_URLS_JSON_FILE` 指向**可写**的 JSON 文件;Docker 中请用 **bind-mount** 到容器内同一路径
|
||||
- 已设置 `OR_SITE_CONFIG_JSON_FILE` 指向**可写**的站点 JSON;Docker 中请用 **bind-mount** 到容器内同一路径
|
||||
- **运行 `main.py` 的进程**能执行本机 `docker` 与 `ffmpeg`(与手动跑 `fake_rtsp_from_file` 相同)。**仅将 API 放 Docker、且不挂载** ` /var/run/docker.sock` 时,容器内往往无法为你在宿主机起 MediaMTX,此时请继续用手动假流方式。
|
||||
|
||||
由于每次解析都会重新读取 `video_rtsp_url_map()`,覆盖 JSON 后**无需重启**主服务即可被下一次开录用到。
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The Operation Room server only opens RTSP URLs (OpenCV); there is no video-upload API.
|
||||
This script does NOT change the application backend: it runs ffmpeg + a small
|
||||
RTSP server (MediaMTX) so you can point VIDEO_RTSP_URLS_JSON to rtsp://.../yourpath.
|
||||
RTSP server (MediaMTX); put the printed ``video_rtsp_urls`` into ``OR_SITE_CONFIG_JSON_FILE``.
|
||||
|
||||
Requires:
|
||||
- ffmpeg in PATH
|
||||
@@ -210,14 +210,18 @@ def main() -> int:
|
||||
p = subprocess.Popen(publish_cmd) # noqa: S603
|
||||
procs.append(p)
|
||||
|
||||
j_compact = json.dumps(url_map, ensure_ascii=False, separators=(",", ":"))
|
||||
site_doc = {"video_rtsp_urls": url_map, "voice_or_room_bindings": []}
|
||||
print("---", file=sys.stderr)
|
||||
print("RTSP mapping (set on monitoring server):", file=sys.stderr)
|
||||
print("RTSP mapping (per camera):", file=sys.stderr)
|
||||
for k, u in url_map.items():
|
||||
print(f" {k}: {u}", file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
print("export (same machine as monitoring server, env snippet):", file=sys.stderr)
|
||||
print(f" export VIDEO_RTSP_URLS_JSON='{j_compact}'", file=sys.stderr)
|
||||
print(
|
||||
"OR site config (merge video_rtsp_urls into OR_SITE_CONFIG_JSON_FILE; "
|
||||
"add voice_or_room_bindings as needed):",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(json.dumps(site_doc, ensure_ascii=False, indent=2), file=sys.stderr)
|
||||
print("", file=sys.stderr)
|
||||
print("If the server runs in Docker on Mac/Win, use host.docker.internal, e.g.:", file=sys.stderr)
|
||||
for cam, u in url_map.items():
|
||||
|
||||
@@ -231,6 +231,12 @@
|
||||
<input id="surgery-id" type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" value="123456" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="small muted" style="margin-top:10px">
|
||||
<label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;max-width:52rem">
|
||||
<input type="checkbox" id="toggle-browser-voice-ui" style="margin-top:2px" />
|
||||
<span>显示<strong> §4.4 / §4.5</strong>(浏览器待确认与录音上传;默认关闭,主流程请用桌面语音客户端)</span>
|
||||
</label>
|
||||
</p>
|
||||
<div class="actions">
|
||||
<button id="btn-health" class="secondary">GET /health</button>
|
||||
<button type="button" class="secondary" id="btn-orch-status" title="检查一键联调接口是否已注册">GET 联调状态</button>
|
||||
@@ -241,7 +247,7 @@
|
||||
<section class="card">
|
||||
<h2>调试:多路视频 1–4 路(与一键联调 / 无真摄像头)</h2>
|
||||
<p class="callout-ok small">
|
||||
在下方选好各路视频、第 4.1 节勾选「一键联调」后点「开始手术」即可;服务端会起假 RTSP 并写 <code>VIDEO_RTSP_URLS_JSON_FILE</code>。无法使用一键时,请按 <code>scripts/demo_client/README.md</code> 在宿主机手跑
|
||||
在下方选好各路视频、第 4.1 节勾选「一键联调」后点「开始手术」即可;服务端会起假 RTSP 并合并写入 <code>OR_SITE_CONFIG_JSON_FILE</code> 的 <code>video_rtsp_urls</code>。无法使用一键时,请按 <code>scripts/demo_client/README.md</code> 在宿主机手跑
|
||||
<code>fake_rtsp_from_file.py</code> 并配置环境变量。
|
||||
</p>
|
||||
<div class="row" style="margin-top:8px; max-width:28rem">
|
||||
@@ -363,7 +369,7 @@
|
||||
<p class="small muted" style="margin:8px 0 0">
|
||||
<label style="display:inline-flex;align-items:flex-start;gap:8px;cursor:pointer;max-width:52rem">
|
||||
<input type="checkbox" id="orch-oneclick" style="margin-top:2px" />
|
||||
<span><strong>一键联调</strong>:点下面按钮时按「模拟路数」上传调试区为<strong>路 1…N</strong>选好的视频(1–4 路),由监控服务在<strong>能执行 docker+ffmpeg 的环境</strong>里自动起假 RTSP、写 <code>VIDEO_RTSP_URLS_JSON_FILE</code> 并开录(需 <code>DEMO_ORCHESTRATOR_ENABLED=true</code> 且该文件为可写挂载;详见 README)。不勾选时仍为普通 JSON 开录(需自行先起假流)。</span>
|
||||
<span><strong>一键联调</strong>:点下面按钮时按「模拟路数」上传调试区为<strong>路 1…N</strong>选好的视频(1–4 路),由监控服务在<strong>能执行 docker+ffmpeg 的环境</strong>里自动起假 RTSP、更新 <code>OR_SITE_CONFIG_JSON_FILE</code> 并开录(需 <code>DEMO_ORCHESTRATOR_ENABLED=true</code> 且该文件为可写挂载;详见 README)。不勾选时仍为普通开录(需自行先起假流并保证站点 JSON 中 RTSP 映射正确)。</span>
|
||||
</label>
|
||||
</p>
|
||||
<div class="actions">
|
||||
@@ -388,6 +394,7 @@
|
||||
<div id="result-render"></div>
|
||||
</section>
|
||||
|
||||
<div id="browser-voice-sections" hidden>
|
||||
<section class="card">
|
||||
<h2>§4.4 待确认耗材</h2>
|
||||
<div class="actions">
|
||||
@@ -427,6 +434,7 @@
|
||||
<a id="btn-download" class="small muted" href="#" download="voice.wav" style="display:none">下载 WAV(调试)</a>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside>
|
||||
@@ -483,6 +491,17 @@
|
||||
[...logEl.querySelectorAll(".log-item")].forEach(n => n.remove());
|
||||
};
|
||||
|
||||
const browserVoiceSections = $("browser-voice-sections");
|
||||
const toggleBrowserVoiceUi = $("toggle-browser-voice-ui");
|
||||
function syncBrowserVoiceUiVisibility() {
|
||||
if (!browserVoiceSections || !toggleBrowserVoiceUi) return;
|
||||
browserVoiceSections.hidden = !toggleBrowserVoiceUi.checked;
|
||||
}
|
||||
if (toggleBrowserVoiceUi) {
|
||||
toggleBrowserVoiceUi.addEventListener("change", syncBrowserVoiceUiVisibility);
|
||||
syncBrowserVoiceUiVisibility();
|
||||
}
|
||||
|
||||
async function apiJson(method, path, payload) {
|
||||
const url = baseUrl() + path;
|
||||
let res;
|
||||
@@ -523,8 +542,8 @@
|
||||
let hint = "";
|
||||
if (res.status === 404) {
|
||||
hint = "HTTP 404:本路径在服务端未注册。常见原因:1) 未设 DEMO_ORCHESTRATOR_ENABLED=true 并重启主进程,POST /internal/demo/orchestrate-and-start 未挂载;2)「服务端 Base URL」填错(须指向主 API 如 http://127.0.0.1:38080,不是本 demo 静态站 :38081)。可点「GET 联调状态」或打开浏览器控制台查看 [demo-client] 日志。";
|
||||
} else if (res.status === 400 && parsed && (parsed.detail || "").toString().indexOf("VIDEO_RTSP") >= 0) {
|
||||
hint = "需配置可写的 VIDEO_RTSP_URLS_JSON_FILE,且 Docker 下请 bind-mount 到容器内同路径。";
|
||||
} else if (res.status === 400 && parsed && (parsed.detail || "").toString().indexOf("OR_SITE_CONFIG") >= 0) {
|
||||
hint = "需配置可写的 OR_SITE_CONFIG_JSON_FILE(严格站点 JSON),且 Docker 下请 bind-mount 到容器内同路径。";
|
||||
} else if (res.status === 503) {
|
||||
hint = "合成假 RTSP 或开录失败,请见响应体与主服务终端 log(demo orchestrate-and-start / ffmpeg / docker)。";
|
||||
}
|
||||
@@ -640,12 +659,12 @@
|
||||
return;
|
||||
}
|
||||
const on = data.orchestrator_enabled === true;
|
||||
const fset = data.video_rtsp_urls_json_file_set === true;
|
||||
const fset = data.or_site_config_json_file_set === true;
|
||||
b.style.background = on && fset ? "rgba(34, 197, 94, 0.1)" : "rgba(245, 158, 11, 0.12)";
|
||||
b.style.color = "var(--text)";
|
||||
const fp = data.video_rtsp_urls_json_file || "(未设)";
|
||||
const fp = data.or_site_config_json_file || "(未设)";
|
||||
b.innerHTML = on
|
||||
? ("一键 <code>POST " + (data.orchestrate_path || "/internal/demo/orchestrate-and-start") + "</code>:" + (fset ? "已开放;RTSP 映射文件 " : "未设 ") + "<code>" + fp + "</code>")
|
||||
? ("一键 <code>POST " + (data.orchestrate_path || "/internal/demo/orchestrate-and-start") + "</code>:" + (fset ? "已开放;站点配置 " : "未设 ") + "<code>" + fp + "</code>")
|
||||
: ("一键开录 <strong>未注册</strong>:请在主服务 .env 设 <code>DEMO_ORCHESTRATOR_ENABLED=true</code> 并<strong>重启</strong>。当前 " + (data.orchestrate_path || "") + " 会 404。");
|
||||
} catch (e) {
|
||||
console.error("[demo-client] orchestrator-status failed", e);
|
||||
|
||||
Reference in New Issue
Block a user