|
|
<!doctype html>
|
|
|
<html lang="zh-CN">
|
|
|
<head>
|
|
|
<meta charset="utf-8" />
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
|
<title>SOP WebSocket 前端测试</title>
|
|
|
<style>
|
|
|
body {
|
|
|
margin: 0;
|
|
|
font-family: Arial, "Microsoft YaHei", sans-serif;
|
|
|
background: #f5f7fb;
|
|
|
color: #1f2937;
|
|
|
}
|
|
|
.container {
|
|
|
max-width: 1100px;
|
|
|
margin: 0 auto;
|
|
|
padding: 24px;
|
|
|
}
|
|
|
.card {
|
|
|
background: #fff;
|
|
|
border-radius: 12px;
|
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
|
|
padding: 20px;
|
|
|
margin-bottom: 16px;
|
|
|
}
|
|
|
h1 {
|
|
|
margin: 0 0 16px;
|
|
|
font-size: 24px;
|
|
|
}
|
|
|
label {
|
|
|
display: block;
|
|
|
margin: 10px 0 6px;
|
|
|
font-weight: 600;
|
|
|
}
|
|
|
input, button {
|
|
|
font-size: 14px;
|
|
|
}
|
|
|
input[type="text"], input[type="number"] {
|
|
|
width: 100%;
|
|
|
box-sizing: border-box;
|
|
|
border: 1px solid #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
padding: 10px;
|
|
|
}
|
|
|
input[type="file"] {
|
|
|
width: 100%;
|
|
|
padding: 10px 0;
|
|
|
}
|
|
|
.grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
gap: 12px;
|
|
|
}
|
|
|
.buttons {
|
|
|
display: flex;
|
|
|
gap: 10px;
|
|
|
margin-top: 16px;
|
|
|
flex-wrap: wrap;
|
|
|
}
|
|
|
button {
|
|
|
border: 0;
|
|
|
border-radius: 8px;
|
|
|
padding: 10px 16px;
|
|
|
cursor: pointer;
|
|
|
color: #fff;
|
|
|
background: #2563eb;
|
|
|
}
|
|
|
button:disabled {
|
|
|
cursor: not-allowed;
|
|
|
background: #9ca3af;
|
|
|
}
|
|
|
.danger {
|
|
|
background: #dc2626;
|
|
|
}
|
|
|
.status {
|
|
|
padding: 10px 12px;
|
|
|
border-radius: 8px;
|
|
|
background: #eef2ff;
|
|
|
color: #3730a3;
|
|
|
margin-bottom: 12px;
|
|
|
white-space: pre-wrap;
|
|
|
}
|
|
|
.progress-wrap {
|
|
|
height: 16px;
|
|
|
background: #e5e7eb;
|
|
|
border-radius: 999px;
|
|
|
overflow: hidden;
|
|
|
}
|
|
|
.progress-bar {
|
|
|
height: 100%;
|
|
|
width: 0%;
|
|
|
background: linear-gradient(90deg, #22c55e, #16a34a);
|
|
|
transition: width 0.2s;
|
|
|
}
|
|
|
.counts {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
|
gap: 8px;
|
|
|
}
|
|
|
.count-item {
|
|
|
padding: 10px;
|
|
|
border-radius: 8px;
|
|
|
background: #f3f4f6;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
}
|
|
|
.log {
|
|
|
height: 280px;
|
|
|
overflow: auto;
|
|
|
background: #111827;
|
|
|
color: #d1d5db;
|
|
|
border-radius: 8px;
|
|
|
padding: 12px;
|
|
|
font-family: Consolas, monospace;
|
|
|
font-size: 13px;
|
|
|
white-space: pre-wrap;
|
|
|
}
|
|
|
video {
|
|
|
width: 100%;
|
|
|
max-height: 520px;
|
|
|
background: #000;
|
|
|
border-radius: 8px;
|
|
|
}
|
|
|
a {
|
|
|
color: #2563eb;
|
|
|
word-break: break-all;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
<body>
|
|
|
<div class="container">
|
|
|
<div class="card">
|
|
|
<h1>SOP 视频检测 WebSocket 前端测试</h1>
|
|
|
<div id="status" class="status">未连接</div>
|
|
|
|
|
|
<label>WebSocket 地址</label>
|
|
|
<input id="wsUrl" type="text" value="ws://10.21.221.41:8000/ws/detect" />
|
|
|
|
|
|
<label>HTTP 服务地址,用于播放结果视频</label>
|
|
|
<input id="httpBase" type="text" value="http://10.21.221.41:8000" />
|
|
|
|
|
|
<label>选择要上传的视频</label>
|
|
|
<input id="videoFile" type="file" accept="video/*" />
|
|
|
|
|
|
<div class="grid">
|
|
|
<div>
|
|
|
<label>conf</label>
|
|
|
<input id="conf" type="number" value="0.5" min="0" max="1" step="0.01" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<label>iou</label>
|
|
|
<input id="iou" type="number" value="0.45" min="0" max="1" step="0.01" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<label>skip</label>
|
|
|
<input id="skip" type="number" value="5" min="1" step="1" />
|
|
|
</div>
|
|
|
<div>
|
|
|
<label>max_frames,0 表示完整视频</label>
|
|
|
<input id="maxFrames" type="number" value="0" min="0" step="1" />
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="buttons">
|
|
|
<button id="startBtn">上传并开始检测</button>
|
|
|
<button id="pauseBtn" disabled>暂停处理</button>
|
|
|
<button id="endBtn" class="danger" disabled>结束检测</button>
|
|
|
<button id="stopBtn" class="danger" disabled>关闭连接</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h2>实时进度</h2>
|
|
|
<div class="progress-wrap"><div id="progressBar" class="progress-bar"></div></div>
|
|
|
<p id="progressText">0%</p>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h2>实时计数</h2>
|
|
|
<div id="counts" class="counts">暂无计数</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h2>实时日志</h2>
|
|
|
<div id="log" class="log"></div>
|
|
|
</div>
|
|
|
|
|
|
<div class="card">
|
|
|
<h2>处理后视频</h2>
|
|
|
<video id="resultVideo" controls></video>
|
|
|
<p id="resultLinks"></p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
const statusEl = document.getElementById("status");
|
|
|
const logEl = document.getElementById("log");
|
|
|
const countsEl = document.getElementById("counts");
|
|
|
const progressBar = document.getElementById("progressBar");
|
|
|
const progressText = document.getElementById("progressText");
|
|
|
const resultVideo = document.getElementById("resultVideo");
|
|
|
const resultLinks = document.getElementById("resultLinks");
|
|
|
const startBtn = document.getElementById("startBtn");
|
|
|
const pauseBtn = document.getElementById("pauseBtn");
|
|
|
const endBtn = document.getElementById("endBtn");
|
|
|
const stopBtn = document.getElementById("stopBtn");
|
|
|
|
|
|
let ws = null;
|
|
|
let paused = false;
|
|
|
let ending = false;
|
|
|
|
|
|
function setStatus(text) {
|
|
|
statusEl.textContent = text;
|
|
|
}
|
|
|
|
|
|
function appendLog(text) {
|
|
|
logEl.textContent += text + "\n";
|
|
|
logEl.scrollTop = logEl.scrollHeight;
|
|
|
}
|
|
|
|
|
|
function renderCounts(counts) {
|
|
|
const entries = Object.entries(counts || {});
|
|
|
if (!entries.length) {
|
|
|
countsEl.textContent = "暂无计数";
|
|
|
return;
|
|
|
}
|
|
|
countsEl.innerHTML = entries.map(([name, value]) => (
|
|
|
`<div class="count-item"><span>${name}</span><strong>${value}</strong></div>`
|
|
|
)).join("");
|
|
|
}
|
|
|
|
|
|
function renderProgress(msg) {
|
|
|
const progress = Number(msg.progress || 0);
|
|
|
progressBar.style.width = `${Math.max(0, Math.min(100, progress))}%`;
|
|
|
progressText.textContent = `${progress.toFixed(1)}% | ${msg.frame || 0}/${msg.total_frames || 0}`;
|
|
|
}
|
|
|
|
|
|
async function sendFileInChunks(socket, file) {
|
|
|
const chunkSize = 1024 * 1024;
|
|
|
let offset = 0;
|
|
|
while (offset < file.size) {
|
|
|
const chunk = file.slice(offset, offset + chunkSize);
|
|
|
const buffer = await chunk.arrayBuffer();
|
|
|
socket.send(buffer);
|
|
|
offset += chunkSize;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
startBtn.onclick = async () => {
|
|
|
const file = document.getElementById("videoFile").files[0];
|
|
|
const wsUrl = document.getElementById("wsUrl").value.trim();
|
|
|
|
|
|
if (!file) {
|
|
|
alert("请先选择视频文件");
|
|
|
return;
|
|
|
}
|
|
|
if (!wsUrl.startsWith("ws://") && !wsUrl.startsWith("wss://")) {
|
|
|
alert("WebSocket 地址必须以 ws:// 或 wss:// 开头");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
logEl.textContent = "";
|
|
|
resultLinks.innerHTML = "";
|
|
|
resultVideo.removeAttribute("src");
|
|
|
resultVideo.load();
|
|
|
renderCounts({});
|
|
|
renderProgress({ progress: 0, frame: 0, total_frames: 0 });
|
|
|
|
|
|
startBtn.disabled = true;
|
|
|
pauseBtn.disabled = true;
|
|
|
pauseBtn.textContent = "暂停处理";
|
|
|
paused = false;
|
|
|
ending = false;
|
|
|
endBtn.disabled = true;
|
|
|
stopBtn.disabled = false;
|
|
|
setStatus("正在连接 WebSocket...");
|
|
|
|
|
|
ws = new WebSocket(wsUrl);
|
|
|
ws.binaryType = "arraybuffer";
|
|
|
|
|
|
ws.onopen = () => {
|
|
|
setStatus("已连接,正在打开算法开关...");
|
|
|
ws.send(JSON.stringify({ type: "switch", enabled: true }));
|
|
|
};
|
|
|
|
|
|
ws.onmessage = async (event) => {
|
|
|
const msg = JSON.parse(event.data);
|
|
|
|
|
|
if (msg.type === "ready") {
|
|
|
appendLog("服务端 ready");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "switch") {
|
|
|
appendLog("算法开关已开启");
|
|
|
ws.send(JSON.stringify({
|
|
|
type: "start",
|
|
|
filename: file.name,
|
|
|
options: {
|
|
|
conf: Number(document.getElementById("conf").value),
|
|
|
iou: Number(document.getElementById("iou").value),
|
|
|
skip: Number(document.getElementById("skip").value),
|
|
|
imgsz: 640,
|
|
|
max_frames: Number(document.getElementById("maxFrames").value),
|
|
|
save_video: true
|
|
|
}
|
|
|
}));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "upload_started") {
|
|
|
setStatus("正在上传视频...");
|
|
|
appendLog("开始上传视频");
|
|
|
await sendFileInChunks(ws, file);
|
|
|
ws.send(JSON.stringify({ type: "end" }));
|
|
|
appendLog("视频上传完成,等待服务端处理");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "processing_started") {
|
|
|
setStatus("服务端正在处理视频...");
|
|
|
appendLog("检测开始");
|
|
|
pauseBtn.disabled = false;
|
|
|
endBtn.disabled = false;
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "stopping") {
|
|
|
ending = true;
|
|
|
pauseBtn.disabled = true;
|
|
|
endBtn.disabled = true;
|
|
|
setStatus(msg.message || "正在结束检测...");
|
|
|
appendLog(msg.message || "已请求结束检测");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "cancelled") {
|
|
|
ending = true;
|
|
|
pauseBtn.disabled = true;
|
|
|
endBtn.disabled = true;
|
|
|
setStatus(msg.message || "检测已结束,正在生成当前结果...");
|
|
|
appendLog(msg.message || "检测已结束");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "pause") {
|
|
|
paused = Boolean(msg.paused);
|
|
|
pauseBtn.textContent = paused ? "恢复处理" : "暂停处理";
|
|
|
setStatus(paused ? "检测已暂停,服务仍在运行" : "服务端正在处理视频...");
|
|
|
appendLog(msg.message || (paused ? "检测已暂停" : "检测已恢复"));
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "progress") {
|
|
|
renderProgress(msg);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "counts") {
|
|
|
renderCounts(msg.counts);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "log") {
|
|
|
appendLog(msg.message || "");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "done") {
|
|
|
const httpBase = document.getElementById("httpBase").value.replace(/\/$/, "");
|
|
|
const videoUrl = httpBase + msg.output_video_url;
|
|
|
const reportUrl = httpBase + msg.report_url;
|
|
|
|
|
|
setStatus("检测完成");
|
|
|
pauseBtn.disabled = true;
|
|
|
endBtn.disabled = true;
|
|
|
renderCounts(msg.counts || {});
|
|
|
resultVideo.src = videoUrl;
|
|
|
resultLinks.innerHTML = `结果视频:<a href="${videoUrl}" target="_blank">${videoUrl}</a><br>` +
|
|
|
`检测报告:<a href="${reportUrl}" target="_blank">${reportUrl}</a>`;
|
|
|
appendLog("检测完成");
|
|
|
ws.close();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (msg.type === "error") {
|
|
|
setStatus("服务端错误:" + msg.message);
|
|
|
appendLog("错误:" + msg.message);
|
|
|
pauseBtn.disabled = true;
|
|
|
endBtn.disabled = true;
|
|
|
ws.close();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
ws.onerror = () => {
|
|
|
setStatus("WebSocket 连接失败,请检查服务地址、端口、防火墙和服务是否以 0.0.0.0 启动");
|
|
|
};
|
|
|
|
|
|
ws.onclose = () => {
|
|
|
startBtn.disabled = false;
|
|
|
pauseBtn.disabled = true;
|
|
|
pauseBtn.textContent = "暂停处理";
|
|
|
endBtn.disabled = true;
|
|
|
stopBtn.disabled = true;
|
|
|
};
|
|
|
};
|
|
|
|
|
|
pauseBtn.onclick = () => {
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
|
paused = !paused;
|
|
|
ws.send(JSON.stringify({ type: "pause", paused }));
|
|
|
pauseBtn.textContent = paused ? "恢复处理" : "暂停处理";
|
|
|
};
|
|
|
|
|
|
endBtn.onclick = () => {
|
|
|
if (!ws || ws.readyState !== WebSocket.OPEN || ending) return;
|
|
|
ending = true;
|
|
|
pauseBtn.disabled = true;
|
|
|
endBtn.disabled = true;
|
|
|
setStatus("已发送结束检测请求,等待服务端生成当前结果...");
|
|
|
appendLog("前端请求结束检测");
|
|
|
ws.send(JSON.stringify({ type: "stop" }));
|
|
|
};
|
|
|
|
|
|
stopBtn.onclick = () => {
|
|
|
if (ws) ws.close();
|
|
|
setStatus("连接已关闭");
|
|
|
};
|
|
|
</script>
|
|
|
</body>
|
|
|
</html>
|