You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sopSystem/test_websocket_frontend.html

369 lines
9.9 KiB
HTML

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!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_frames0 表示完整视频</label>
<input id="maxFrames" type="number" value="0" min="0" step="1" />
</div>
</div>
<div class="buttons">
<button id="startBtn">上传并开始检测</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 stopBtn = document.getElementById("stopBtn");
let ws = null;
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;
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("检测开始");
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("检测完成");
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);
ws.close();
}
};
ws.onerror = () => {
setStatus("WebSocket 连接失败,请检查服务地址、端口、防火墙和服务是否以 0.0.0.0 启动");
};
ws.onclose = () => {
startBtn.disabled = false;
stopBtn.disabled = true;
};
};
stopBtn.onclick = () => {
if (ws) ws.close();
setStatus("连接已关闭");
};
</script>
</body>
</html>