feat(demo): 模拟客户端与一键联调支持 1–4 路视频

- demo_orch: orchestrate-and-start 支持 video1 必填、video2–4 可选,扩展 camera/rtsp 参数
- demo_client/index.html: 路数选择、路 3/4 表单项、一键与 camera_ids 同步按路数

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-24 15:53:32 +08:00
parent 8a4bad99d3
commit 8a6bfe9100
2 changed files with 203 additions and 65 deletions

View File

@@ -239,14 +239,25 @@
</section>
<section class="card">
<h2>调试:路视频(与一键联调 / 无真摄像头)</h2>
<h2>调试:路视频 14 路(与一键联调 / 无真摄像头)</h2>
<p class="callout-ok small">
<strong>路1 / 路2</strong>选好视频、§4.1 勾选「一键联调」后点「开始手术」即可;服务端会起假 RTSP 并写 <code>VIDEO_RTSP_URLS_JSON_FILE</code>。无法使用一键时,请按 <code>scripts/demo_client/README.md</code> 在宿主机手跑
下方选好各路视频、4.1 勾选「一键联调」后点「开始手术」即可;服务端会起假 RTSP 并写 <code>VIDEO_RTSP_URLS_JSON_FILE</code>。无法使用一键时,请按 <code>scripts/demo_client/README.md</code> 在宿主机手跑
<code>fake_rtsp_from_file.py</code> 并配置环境变量。
</p>
<h3>两路视频(为 §4.1 一键选文件;两路 <code>RTSP_PATH</code> / <code>camera_id</code> 须与 API 配置一致,如 <code>demo1</code> / <code>demo2</code></h3>
<div class="row" style="margin-top:10px; align-items:stretch; grid-template-columns:1fr 1fr">
<div class="debug-stream" id="debug-stream-1" style="border:1px solid var(--border); border-radius:8px; padding:10px">
<div class="row" style="margin-top:8px; max-width:28rem">
<div>
<label>模拟路数14</label>
<select id="debug-stream-count">
<option value="1">1 路</option>
<option value="2" selected>2 路</option>
<option value="3">3 路</option>
<option value="4">4 路</option>
</select>
</div>
</div>
<h3>各路视频(为第 4.1 节一键选文件;每路 <code>RTSP_PATH</code> 须不同,<code>camera_id</code> 须与开录时一致)</h3>
<div id="debug-streams-grid" class="row" style="margin-top:10px; align-items:stretch; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))">
<div class="debug-stream" id="debug-stream-1" data-stream-slot="1" style="border:1px solid var(--border); border-radius:8px; padding:10px">
<h3 style="margin:0 0 8px; color:var(--accent)">路 1</h3>
<label>视频(一键上传优先;可选手填本地路径作备注)</label>
<input id="debug-vpath-1" type="text" placeholder="/path/a.mp4 或 ./a.mp4" />
@@ -257,7 +268,7 @@
</div>
<div class="row" style="margin-top:8px">
<div>
<label>RTSP 路径名 <code>RTSP_PATH</code>URL 最后一段,两路须不同,如 <code>demo1</code></label>
<label>RTSP 路径名 <code>RTSP_PATH</code></label>
<input id="debug-rpath-1" type="text" value="demo1" />
</div>
<div>
@@ -266,7 +277,7 @@
</div>
</div>
</div>
<div class="debug-stream" id="debug-stream-2" style="border:1px solid var(--border); border-radius:8px; padding:10px">
<div class="debug-stream" id="debug-stream-2" data-stream-slot="2" style="border:1px solid var(--border); border-radius:8px; padding:10px">
<h3 style="margin:0 0 8px; color:var(--accent)">路 2</h3>
<label>视频(一键上传优先;可选手填本地路径作备注)</label>
<input id="debug-vpath-2" type="text" placeholder="/path/b.mp4 或 ./b.mp4" />
@@ -286,12 +297,52 @@
</div>
</div>
</div>
<div class="debug-stream" id="debug-stream-3" data-stream-slot="3" style="border:1px solid var(--border); border-radius:8px; padding:10px; display:none">
<h3 style="margin:0 0 8px; color:var(--accent)">路 3</h3>
<label>视频(一键上传优先;可选手填本地路径作备注)</label>
<input id="debug-vpath-3" type="text" placeholder="/path/c.mp4 或 ./c.mp4" />
<div class="actions" style="margin-top:6px; align-items:center">
<input type="file" id="debug-vfile-3" accept="video/*" hidden />
<button type="button" class="secondary" id="btn-dbg-pick-3">选择…</button>
<span id="debug-hint-3" class="small muted"></span>
</div>
<div class="row" style="margin-top:8px">
<div>
<label>RTSP 路径名 <code>RTSP_PATH</code></label>
<input id="debug-rpath-3" type="text" value="demo3" />
</div>
<div>
<label>camera_id</label>
<input id="debug-cam-3" type="text" value="or-cam-03" />
</div>
</div>
</div>
<div class="debug-stream" id="debug-stream-4" data-stream-slot="4" style="border:1px solid var(--border); border-radius:8px; padding:10px; display:none">
<h3 style="margin:0 0 8px; color:var(--accent)">路 4</h3>
<label>视频(一键上传优先;可选手填本地路径作备注)</label>
<input id="debug-vpath-4" type="text" placeholder="/path/d.mp4 或 ./d.mp4" />
<div class="actions" style="margin-top:6px; align-items:center">
<input type="file" id="debug-vfile-4" accept="video/*" hidden />
<button type="button" class="secondary" id="btn-dbg-pick-4">选择…</button>
<span id="debug-hint-4" class="small muted"></span>
</div>
<div class="row" style="margin-top:8px">
<div>
<label>RTSP 路径名 <code>RTSP_PATH</code></label>
<input id="debug-rpath-4" type="text" value="demo4" />
</div>
<div>
<label>camera_id</label>
<input id="debug-cam-4" type="text" value="or-cam-04" />
</div>
</div>
</div>
</div>
<p id="debug-file-note" class="muted small" style="margin:8px 0 0">
一键联调会<strong>直接上传</strong>你在此为路1/路2选择的文件。选文件时会把框内填成 <code>./文件名</code>,仅作展示;真正上传以文件选择器为准,无需在框里改路径。
一键联调会<strong>直接上传</strong>你在此为路选择的文件。选文件时会把框内填成 <code>./文件名</code>,仅作展示;真正上传以文件选择器为准,无需在框里改路径。
</p>
<div class="actions" style="margin-top:8px">
<button type="button" class="secondary" id="btn-debug-apply-cams" title="把路 camera_id 写进 §4.1 的 camera_ids">将 camera_id 填到开始手术</button>
<button type="button" class="secondary" id="btn-debug-apply-cams" title="把当前已启用的各路 camera_id 写进开录的 camera_ids">将 camera_id 填到开始手术</button>
</div>
</section>
@@ -312,7 +363,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/路2</strong>选好的两个视频,由监控服务在<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>选好的视频14 路),由监控服务在<strong>能执行 docker+ffmpeg 的环境</strong>里自动起假 RTSP、写 <code>VIDEO_RTSP_URLS_JSON_FILE</code> 并开录(需 <code>DEMO_ORCHESTRATOR_ENABLED=true</code> 且该文件为可写挂载;详见 README。不勾选时仍为普通 JSON 开录(需自行先起假流)。</span>
</label>
</p>
<div class="actions">
@@ -621,20 +672,35 @@
const sid = ensureSurgeryId();
if (!sid) return;
if ($("orch-oneclick") && $("orch-oneclick").checked) {
const f1 = $("debug-vfile-1").files[0];
const f2 = $("debug-vfile-2").files[0];
if (!f1 || !f2) {
alert("请先在上方「调试」里为 路1 / 路2 各「选择…」一个视频文件。");
const n = getDebugStreamCount();
const files = [];
for (let i = 1; i <= n; i++) {
const f = $("debug-vfile-" + i).files && $("debug-vfile-" + i).files[0];
files.push(f);
}
const missing = files.findIndex((x) => !x);
if (missing >= 0) {
alert("请先在上方「调试」里为 路 " + (missing + 1) + " 「选择…」一个视频文件(当前为 " + n + " 路)。");
return;
}
const fd = new FormData();
fd.append("video1", f1, f1.name);
fd.append("video2", f2, f2.name);
fd.append("surgery_id", sid);
fd.append("camera_1", ($("debug-cam-1").value || "or-cam-01").trim() || "or-cam-01");
fd.append("camera_2", ($("debug-cam-2").value || "or-cam-02").trim() || "or-cam-02");
fd.append("rtsp_path_1", ($("debug-rpath-1").value || "demo1").trim() || "demo1");
fd.append("rtsp_path_2", ($("debug-rpath-2").value || "demo2").trim() || "demo2");
fd.append("video1", files[0], files[0].name);
if (n >= 2) fd.append("video2", files[1], files[1].name);
if (n >= 3) fd.append("video3", files[2], files[2].name);
if (n >= 4) fd.append("video4", files[3], files[3].name);
const defCams = ["or-cam-01", "or-cam-02", "or-cam-03", "or-cam-04"];
const defRp = ["demo1", "demo2", "demo3", "demo4"];
for (let i = 1; i <= 4; i++) {
fd.append(
"camera_" + i,
($("debug-cam-" + i).value || defCams[i - 1]).trim() || defCams[i - 1],
);
fd.append(
"rtsp_path_" + i,
($("debug-rpath-" + i).value || defRp[i - 1]).trim() || defRp[i - 1],
);
}
fd.append("candidate_consumables_json", JSON.stringify([...tags]));
const { res, body } = await apiMultipart("/internal/demo/orchestrate-and-start", fd);
if (!res.ok) {
@@ -1194,27 +1260,56 @@
};
// ============================================================
// Debug: two streams for one-click upload (路1/路2)
// Debug: 14 streams for one-click upload
// ============================================================
$("btn-dbg-pick-1").onclick = () => $("debug-vfile-1").click();
$("debug-vfile-1").addEventListener("change", (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
$("debug-vpath-1").value = "./" + f.name;
$("debug-hint-1").textContent = "已选: " + f.name;
});
$("btn-dbg-pick-2").onclick = () => $("debug-vfile-2").click();
$("debug-vfile-2").addEventListener("change", (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
$("debug-vpath-2").value = "./" + f.name;
$("debug-hint-2").textContent = "已选: " + f.name;
});
function getDebugStreamCount() {
const sel = $("debug-stream-count");
const v = sel ? parseInt(sel.value, 10) : 2;
if (v === 1 || v === 2 || v === 3 || v === 4) return v;
return 2;
}
function applyDebugStreamVisibility() {
const n = getDebugStreamCount();
for (let i = 1; i <= 4; i++) {
const el = $("debug-stream-" + i);
if (!el) continue;
el.style.display = i <= n ? "block" : "none";
}
}
if ($("debug-stream-count")) {
$("debug-stream-count").addEventListener("change", () => {
applyDebugStreamVisibility();
});
applyDebugStreamVisibility();
}
for (let i = 1; i <= 4; i++) {
const pick = $("btn-dbg-pick-" + i);
const vfile = $("debug-vfile-" + i);
const vpath = $("debug-vpath-" + i);
const hint = $("debug-hint-" + i);
if (pick && vfile) {
pick.onclick = () => vfile.click();
vfile.addEventListener("change", (e) => {
const f = e.target.files && e.target.files[0];
if (!f) return;
vpath.value = "./" + f.name;
hint.textContent = "已选: " + f.name;
});
}
}
$("btn-debug-apply-cams").onclick = () => {
const a = ($("debug-cam-1").value || "or-cam-01").trim() || "or-cam-01";
const b = ($("debug-cam-2").value || "or-cam-02").trim() || "or-cam-02";
$("camera-ids").value = a + "," + b;
const defCams = ["or-cam-01", "or-cam-02", "or-cam-03", "or-cam-04"];
const n = getDebugStreamCount();
const parts = [];
for (let i = 1; i <= n; i++) {
const a = ($("debug-cam-" + i).value || defCams[i - 1]).trim() || defCams[i - 1];
parts.push(a);
}
$("camera-ids").value = parts.join(",");
};
(function setupDebugVideoDrop() {
@@ -1243,8 +1338,9 @@
$(hintId).textContent = "已选: " + f.name + "(拖放)";
});
}
bindStreamCard($("debug-stream-1"), "debug-vpath-1", "debug-hint-1");
bindStreamCard($("debug-stream-2"), "debug-vpath-2", "debug-hint-2");
for (let i = 1; i <= 4; i++) {
bindStreamCard($("debug-stream-" + i), "debug-vpath-" + i, "debug-hint-" + i);
}
})();
// ============================================================