Yolov/templates/flv2.html

1316 lines
43 KiB
HTML
Raw Normal View History

2025-11-26 13:55:04 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>低空数智实时目标检测系统</title>
<script src="{{ url_for('static', filename='socket.io.min.js') }}"></script>
<script src="{{ url_for('static', filename='chart.js') }}"></script>
<script src="{{ url_for('static', filename='flv.min.js') }}"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #1e1e2e;
color: #e0def4;
line-height: 1.6;
overflow-x: hidden;
}
.container {
max-width: 1800px;
margin: 0 auto;
padding: 20px;
}
header {
background: linear-gradient(135deg, #312b7c 0%, #1a1a1a 100%);
padding: 20px 30px;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
border: 1px solid #4c4ca7;
}
h1 {
font-size: 2.5rem;
background: linear-gradient(90deg, #eb6ea5, #c3a5ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
margin-bottom: 15px;
text-shadow: 0 0 10px rgba(146, 94, 255, 0.5);
}
.description {
text-align: center;
margin-bottom: 15px;
color: #a6adc8;
font-size: 1.1rem;
}
.config-container {
display: flex;
justify-content: center;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.config-box {
background: rgba(40, 44, 82, 0.7);
padding: 12px 25px;
border-radius: 8px;
font-size: 1.1rem;
display: flex;
align-items: center;
border: 1px solid #5a61c5;
min-width: 300px;
}
.config-label {
margin-right: 10px;
color: #c3a5ff;
}
.config-select {
background: rgba(30, 30, 46, 0.8);
color: #e0def4;
border: 1px solid #5a61c5;
border-radius: 5px;
padding: 8px 12px;
flex: 1;
font-size: 1rem;
}
.status-container {
display: flex;
justify-content: center;
align-items: center;
gap: 20px;
margin-bottom: 15px;
flex-wrap: wrap;
}
.status-box {
background: rgba(40, 44, 82, 0.7);
padding: 12px 25px;
border-radius: 8px;
font-size: 1.1rem;
display: flex;
align-items: center;
border: 1px solid #5a61c5;
}
.status-indicator {
width: 15px;
height: 15px;
border-radius: 50%;
margin-right: 10px;
}
.active {
background-color: #9beb72;
box-shadow: 0 0 10px rgba(155, 235, 114, 0.5);
}
.inactive {
background-color: #f96060;
}
.fps-value {
font-weight: bold;
color: #9beb72;
}
.frame-count {
font-weight: bold;
color: #c3a5ff;
}
.controls {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 20px;
}
.btn {
padding: 12px 30px;
font-size: 1.1rem;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
font-weight: 600;
background: linear-gradient(135deg, #6a5af9, #d66efd);
color: white;
box-shadow: 0 5px 15px rgba(106, 90, 249, 0.4);
}
.btn:disabled {
background: #444444;
color: #777777;
cursor: not-allowed;
box-shadow: none;
}
.btn:hover:not(:disabled) {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(106, 90, 249, 0.6);
}
.video-section {
display: flex;
gap: 30px;
margin-bottom: 30px;
}
.video-container {
flex: 1;
background: rgba(30, 30, 46, 0.8);
border-radius: 15px;
overflow: hidden;
position: relative;
border: 1px solid #4c4ca7;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.video-container h2 {
background: linear-gradient(90deg, #312b7c, #4c4ca7);
padding: 15px;
margin: 0;
font-size: 1.4rem;
text-align: center;
}
.video-content {
padding: 15px;
height: 500px;
display: flex;
align-items: center;
justify-content: center;
background-color: #0f0f1b;
}
.video-player {
width: 100%;
height: 100%;
background-color: #000;
border-radius: 8px;
}
.statistics {
background: rgba(30, 30, 46, 0.8);
border-radius: 15px;
padding: 25px;
margin-bottom: 30px;
border: 1px solid #4c4ca7;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.statistics h2 {
margin-bottom: 20px;
text-align: center;
font-size: 1.8rem;
color: #c3a5ff;
}
.chart-container {
height: 300px;
position: relative;
}
.detections {
background: rgba(30, 30, 46, 0.8);
border-radius: 15px;
padding: 25px;
border: 1px solid #4c4ca7;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.detections h2 {
margin-bottom: 20px;
text-align: center;
font-size: 1.8rem;
color: #c3a5ff;
}
#detection-list {
list-style: none;
max-height: 300px;
overflow-y: auto;
padding: 10px;
background: rgba(20, 20, 35, 0.5);
border-radius: 8px;
border: 1px solid #5a61c5;
}
#detection-list::-webkit-scrollbar {
width: 10px;
}
#detection-list::-webkit-scrollbar-track {
background: rgba(20, 20, 35, 0.5);
}
#detection-list::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #6a5af9, #d66efd);
border-radius: 5px;
}
.detection-item {
background: rgba(40, 44, 82, 0.7);
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
border: 1px solid #5a61c5;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.detection-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-weight: bold;
}
.detection-class {
color: #eb6ea5;
font-size: 1.2rem;
}
.detection-confidence {
color: #9beb72;
}
.detection-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.info-label {
color: #a6adc8;
}
.info-value {
color: #c3a5ff;
font-weight: 500;
}
/* 系统消息专用样式 */
.system-message .detection-header {
border-bottom: 1px solid #5a61c5;
padding-bottom: 8px;
}
.system-message .detection-title {
color: #42c8ff;
font-size: 1.2rem;
}
.system-message .detection-time {
color: #a6adc8;
font-size: 0.9rem;
}
.system-message .detection-content {
padding-top: 10px;
color: #e0def4;
line-height: 1.5;
}
footer {
text-align: center;
padding: 20px;
color: #a6adc8;
margin-top: 30px;
border-top: 1px solid #5a61c5;
}
.floating-buttons {
position: fixed;
bottom: 30px;
right: 30px;
display: flex;
gap: 15px;
}
.floating-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #6a5af9, #d66efd);
color: white;
border: none;
font-size: 1.5rem;
cursor: pointer;
box-shadow: 0 8px 20px rgba(106, 90, 249, 0.5);
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
}
.floating-btn:hover {
transform: translateY(-5px) rotate(10deg);
box-shadow: 0 12px 25px rgba(106, 90, 249, 0.7);
}
.stream-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 15px;
}
.stream-btn {
padding: 8px 20px;
background: rgba(76, 76, 167, 0.5);
border: 1px solid #5a61c5;
color: #e0def4;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.stream-btn:hover {
background: rgba(106, 90, 249, 0.7);
}
.stream-url {
display: flex;
align-items: center;
margin-top: 10px;
padding: 10px;
background: rgba(30, 30, 46, 0.5);
border-radius: 5px;
font-size: 0.9rem;
}
.stream-url input {
flex: 1;
background: transparent;
border: none;
color: #e0def4;
padding: 5px;
font-size: 0.9rem;
}
.stream-url button {
background: rgba(106, 90, 249, 0.5);
border: none;
color: white;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
margin-left: 5px;
}
.stream-url button:hover {
background: rgba(106, 90, 249, 0.8);
}
.url-label {
margin-right: 10px;
color: #a6adc8;
}
/* Toast通知系统 */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
max-width: 350px;
}
.toast {
background: rgba(40, 44, 82, 0.95);
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 15px;
border-left: 4px solid #6a5af9;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
display: flex;
align-items: flex-start;
animation: slideIn 0.3s, fadeOut 0.5s 4.5s;
transition: transform 0.3s, opacity 0.3s;
}
.toast-icon {
font-size: 1.5rem;
margin-right: 12px;
color: #6a5af9;
}
.toast-content {
flex: 1;
}
.toast-title {
font-weight: bold;
color: #c3a5ff;
margin-bottom: 5px;
font-size: 1.1rem;
}
.toast-message {
color: #e0def4;
line-height: 1.4;
}
.toast-close {
background: none;
border: none;
color: #a6adc8;
font-size: 1.2rem;
cursor: pointer;
margin-left: 10px;
padding: 0;
line-height: 1;
}
.toast-close:hover {
color: #eb6ea5;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@media (max-width: 1200px) {
.video-section {
flex-direction: column;
}
.video-container {
height: 450px;
}
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.video-content {
height: 350px;
}
.status-container {
flex-direction: column;
}
.controls {
flex-direction: column;
}
.btn {
width: 100%;
}
.stream-controls {
flex-direction: column;
}
.stream-url {
flex-direction: column;
align-items: flex-start;
}
.stream-url input {
width: 100%;
margin-top: 5px;
}
.stream-url button {
width: 100%;
margin-top: 5px;
margin-left: 0;
}
.config-container {
flex-direction: column;
}
.config-box {
width: 100%;
}
.toast-container {
left: 20px;
right: 20px;
max-width: none;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>低空数智实时目标检测系统</h1>
<p class="description">基于深度学习的目标检测系统,实时分析视频流并展示检测结果</p>
<div class="config-container">
<div class="config-box">
<span class="config-label">推流地址:</span>
<select id="stream-select" class="config-select">
<option value="rtmp://123.132.248.154:6009/live/14">测试车辆视频</option>
<option value="rtmp://box.wisestcity.com:1935/live/5">机场推流</option>
<option value="rtmp://box.wisestcity.com:1935/live/7">无人机推流</option>
<option value="rtmp://localhost:1935/live/14">测试火灾视频</option>
<option value="rtmp://box.wisestcity.com:1935/live/10">测试游泳视频</option>
</select>
</div>
<div class="config-box">
<span class="config-label">模型选择:</span>
<select id="model-select" class="config-select">
<option value="car.pt">车辆识别</option>
<option value="yolov8x.pt">YOLOv8 XLarge</option>
<option value="yolo11n.pt">YOLOv11 N</option>
<option value="yolo11x.pt">YOLOv11 X</option>
<option value="best.pt">火情识别</option>
<option value="20250901\\2025090109483695260039.pt">溺水识别</option>
</select>
</div>
</div>
<div class="status-container">
<div class="status-box">
<div class="status-indicator inactive" id="status-indicator"></div>
<span>状态: <span id="connection-status">未连接</span></span>
</div>
<div class="status-box">
<span>FPS: <span class="fps-value" id="fps-value">0</span></span>
</div>
<div class="status-box">
<span>总帧数: <span class="frame-count" id="frame-count">0</span></span>
</div>
<div class="status-box">
<span>检测目标数: <span class="frame-count" id="detections-count">0</span></span>
</div>
</div>
<div class="controls">
<button id="start-btn" class="btn">开始检测</button>
<button id="stop-btn" class="btn" disabled>停止检测</button>
</div>
</header>
<section class="video-section">
<div class="video-container">
<h2>原始视频流</h2>
<div class="video-content">
<video id="original-stream" class="video-player" controls></video>
</div>
<div class="stream-controls">
<button id="play-original" class="stream-btn">播放</button>
<button id="pause-original" class="stream-btn">暂停</button>
<button id="mute-original" class="stream-btn">静音</button>
</div>
<div class="stream-url">
<span class="url-label">推流地址:</span>
<input type="text" id="original-url" value="http://123.132.248.154:8800/flv/live/14.flv">
<button id="update-original">更新</button>
</div>
</div>
<div class="video-container">
<h2>检测后视频流 <span id="objects-count" style="font-size: 0.8rem; color: #eb6ea5;">(0个检测对象)</span>
</h2>
<div class="video-content">
<video id="annotated-stream" class="video-player" controls></video>
</div>
<div class="stream-controls">
<button id="play-annotated" class="stream-btn">播放</button>
<button id="pause-annotated" class="stream-btn">暂停</button>
<button id="mute-annotated" class="stream-btn">静音</button>
</div>
<div class="stream-url">
<span class="url-label">推流地址:</span>
<input type="text" id="annotated-url" value="http://123.132.248.154:8800/flv/live/11.flv">
<button id="update-annotated">更新</button>
</div>
</div>
</section>
<section class="statistics">
<h2>目标检测统计</h2>
<div class="chart-container">
<canvas id="detection-chart"></canvas>
</div>
</section>
<section class="detections">
<h2>实时检测结果</h2>
<ul id="detection-list">
<li class="detection-item system-message">
<div class="detection-header">
<span class="detection-title">系统信息</span>
<span class="detection-time">准备就绪</span>
</div>
<div class="detection-content">
点击"开始检测"按钮启动系统
</div>
</li>
</ul>
</section>
<footer>
<p>YOLOv8 目标检测系统 &copy; 2025 | 实时视频分析解决方案</p>
</footer>
<div class="floating-buttons">
<button class="floating-btn" id="snapshot-btn">📷</button>
<button class="floating-btn" id="toggle-btn"></button>
</div>
<!-- Toast通知容器 -->
<div class="toast-container" id="toast-container"></div>
</div>
<script>
// 全局变量
let socket;
let chart;
let detectionActive = false;
let originalPlayer = null;
let annotatedPlayer = null;
let currentStream = null;
let currentModel = "yolov8m";
// DOM元素
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
const statusIndicator = document.getElementById('status-indicator');
const connectionStatus = document.getElementById('connection-status');
const fpsValue = document.getElementById('fps-value');
const frameCount = document.getElementById('frame-count');
const detectionsCount = document.getElementById('detections-count');
const objectsCount = document.getElementById('objects-count');
const originalStream = document.getElementById('original-stream');
const annotatedStream = document.getElementById('annotated-stream');
const detectionList = document.getElementById('detection-list');
const snapshotBtn = document.getElementById('snapshot-btn');
const toggleBtn = document.getElementById('toggle-btn');
const originalUrl = document.getElementById('original-url');
const annotatedUrl = document.getElementById('annotated-url');
const streamSelect = document.getElementById('stream-select');
const modelSelect = document.getElementById('model-select');
const playOriginalBtn = document.getElementById('play-original');
const pauseOriginalBtn = document.getElementById('pause-original');
const muteOriginalBtn = document.getElementById('mute-original');
const playAnnotatedBtn = document.getElementById('play-annotated');
const pauseAnnotatedBtn = document.getElementById('pause-annotated');
const muteAnnotatedBtn = document.getElementById('mute-annotated');
const updateOriginalBtn = document.getElementById('update-original');
const updateAnnotatedBtn = document.getElementById('update-annotated');
const toastContainer = document.getElementById('toast-container');
// 初始化页面
document.addEventListener('DOMContentLoaded', function () {
// 初始化检测图表
initializeChart();
// 设置按钮事件
startBtn.addEventListener('click', startDetection);
stopBtn.addEventListener('click', stopDetection);
snapshotBtn.addEventListener('click', takeSnapshot);
toggleBtn.addEventListener('click', toggleFullscreen);
// 视频控制事件
playOriginalBtn.addEventListener('click', () => playStream('original'));
pauseOriginalBtn.addEventListener('click', () => pauseStream('original'));
muteOriginalBtn.addEventListener('click', () => muteStream('original'));
playAnnotatedBtn.addEventListener('click', () => playStream('annotated'));
pauseAnnotatedBtn.addEventListener('click', () => pauseStream('annotated'));
muteAnnotatedBtn.addEventListener('click', () => muteStream('annotated'));
updateOriginalBtn.addEventListener('click', () => updateStream('original'));
updateAnnotatedBtn.addEventListener('click', () => updateStream('annotated'));
// 监听选择变化
streamSelect.addEventListener('change', updateStreamSelection);
modelSelect.addEventListener('change', updateModelSelection);
// 显示系统提示
showSystemMessage('欢迎使用YOLOv8目标检测系统正在检查服务状态...');
// 检查服务状态并尝试恢复
checkServiceStatus();
});
// 初始化检测统计图表
function initializeChart() {
const ctx = document.getElementById('detection-chart').getContext('2d');
chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '检测目标数量',
data: [],
borderColor: '#eb6ea5',
backgroundColor: 'rgba(235, 110, 165, 0.2)',
tension: 0.4,
borderWidth: 3,
pointRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#a6adc8',
font: {
size: 14
}
}
},
tooltip: {
backgroundColor: 'rgba(30, 30, 46, 0.9)',
titleColor: '#c3a5ff',
bodyColor: '#e0def4',
borderColor: '#6a5af9',
borderWidth: 1
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: '#a6adc8'
},
grid: {
color: 'rgba(90, 95, 140, 0.2)'
}
},
x: {
ticks: {
color: '#a6adc8'
},
grid: {
color: 'rgba(90, 95, 140, 0.2)'
}
}
},
animation: {
duration: 300
}
}
});
}
// 更新检测统计图表
function updateChart(count, time_str) {
if (chart.data.datasets[0].data.length >= 20) {
chart.data.datasets[0].data.shift();
chart.data.labels.shift()
}
chart.data.datasets[0].data.push(count);
chart.data.labels.push(time_str);
chart.update();
}
// 初始化视频播放器
function initPlayer(type) {
const videoElement = type === 'original' ? originalStream : annotatedStream;
// let url = type === 'original' ? originalUrl.value : annotatedUrl.value;
// url = url.replace("rtmp","http").replace("1935","8081")
// url = url + ".flv"
url = originalUrl.value
if (flvjs.isSupported()) {
try {
if (type === 'original' && originalPlayer) {
originalPlayer.destroy();
originalPlayer = null;
}
if (type === 'annotated' && annotatedPlayer) {
annotatedPlayer.destroy();
annotatedPlayer = null;
}
const player = flvjs.createPlayer({
type: 'flv',
url: url,
isLive: true
});
player.attachMediaElement(videoElement);
player.load();
player.play().catch(e => {
console.log(`${type}播放器自动播放失败:`, e);
showSystemMessage(`${type}播放器自动播放失败: ${e.message}`);
});
if (type === 'original') {
originalPlayer = player;
} else {
annotatedPlayer = player;
}
return player;
} catch (error) {
console.error(`${type}播放器创建失败:`, error);
showSystemMessage(`${type}播放器创建失败: ${error.message}`);
return null;
}
} else {
showSystemMessage('当前浏览器不支持FLV播放');
return null;
}
}
// 播放视频流
function playStream(type) {
const videoElement = type === 'original' ? originalStream : annotatedStream;
const player = type === 'original' ? originalPlayer : annotatedPlayer;
if (player) {
try {
player.play();
} catch (e) {
console.log('播放失败:', e);
showSystemMessage('播放失败: ' + e.message);
}
} else {
initPlayer(type);
}
}
// 暂停视频流
function pauseStream(type) {
const videoElement = type === 'original' ? originalStream : annotatedStream;
if (videoElement && !videoElement.paused) {
videoElement.pause();
}
}
// 静音视频流
function muteStream(type) {
const videoElement = type === 'original' ? originalStream : annotatedStream;
if (videoElement) {
videoElement.muted = !videoElement.muted;
}
}
// 更新视频流地址
function updateStream(type) {
// let url = type === 'original' ? originalUrl.value : annotatedUrl.value;
let url = annotatedUrl.value;
if (url) {
initPlayer(type);
}
}
// 显示系统消息(修复版)
function showSystemMessage(message) {
// 创建系统消息元素
const item = document.createElement('li');
item.className = 'detection-item system-message';
const now = new Date();
const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
item.innerHTML = `
<div class="detection-header">
<span class="detection-title">系统信息</span>
<span class="detection-time">${timeString}</span>
</div>
<div class="detection-content">
${message}
</div>
`;
// 添加到列表顶部
detectionList.insertBefore(item, detectionList.firstChild);
// 限制消息数量
if (detectionList.children.length > 20) {
detectionList.removeChild(detectionList.lastChild);
}
// 自动滚动到最新消息
detectionList.scrollTop = 0;
// 同时显示Toast通知
showToast('系统通知', message);
}
// 显示Toast通知
function showToast(title, message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.innerHTML = `
<div class="toast-icon"></div>
<div class="toast-content">
<div class="toast-title">${title}</div>
<div class="toast-message">${message}</div>
</div>
<button class="toast-close">×</button>
`;
toastContainer.appendChild(toast);
// 添加关闭事件
const closeBtn = toast.querySelector('.toast-close');
closeBtn.addEventListener('click', () => {
toast.style.transform = 'translateX(100%)';
toast.style.opacity = '0';
setTimeout(() => {
toast.remove();
}, 300);
});
// 自动移除
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
toast.remove();
}, 500);
}, 5000);
}
// 更新检测结果列表
function updateDetectionList(detections) {
// 移除所有系统消息
const systemMessages = detectionList.querySelectorAll('.system-message');
systemMessages.forEach(msg => msg.remove());
// 清空检测结果
const detectionItems = detectionList.querySelectorAll('.detection-item:not(.system-message)');
detectionItems.forEach(item => item.remove());
if (detections.length === 0) {
const item = document.createElement('li');
item.className = 'detection-item';
item.innerHTML = `
<div class="detection-header">
<span class="detection-class">未检测到目标</span>
<span class="detection-confidence">${new Date().toLocaleTimeString()}</span>
</div>
`;
detectionList.appendChild(item);
return;
}
// 按置信度排序
const sortedDetections = [...detections].sort((a, b) => b.confidence - a.confidence);
// 创建检测项
sortedDetections.forEach(det => {
const item = document.createElement('li');
item.className = 'detection-item';
const box = det.box.map(num => Math.round(num));
item.innerHTML = `
<div class="detection-header">
<span class="detection-class">${det.class_name}</span>
<span class="detection-confidence">置信度: ${det.confidence.toFixed(2)}</span>
</div>
<div class="detection-info">
<div class="info-label">位置:</div>
<div class="info-value">[${box.join(', ')}]</div>
<div class="info-label">类别ID:</div>
<div class="info-value">${det.class_id}</div>
<div class="info-label">检测时间:</div>
<div class="info-value">${new Date().toLocaleTimeString()}</div>
</div>
`;
detectionList.appendChild(item);
});
}
// 检查服务状态
function checkServiceStatus() {
fetch('/status')
.then(response => response.json())
.then(data => {
if (data.active) {
// 服务正在运行恢复UI状态
restoreUIState(data);
showSystemMessage('检测服务正在运行中,正在恢复连接...');
// 自动连接WebSocket
connectWebSocket();
// 尝试初始化视频播放器
setTimeout(() => {
initPlayer('original');
initPlayer('annotated');
}, 1000);
} else {
showSystemMessage('检测服务当前未运行,点击"开始检测"按钮启动系统');
}
})
.catch(error => {
console.error('获取服务状态失败:', error);
showSystemMessage('无法获取服务状态');
});
}
// 恢复UI状态
function restoreUIState(status) {
startBtn.disabled = true;
stopBtn.disabled = false;
connectionStatus.textContent = '已连接';
connectionStatus.style.color = '#9beb72';
statusIndicator.className = 'status-indicator active';
// 更新统计信息
fpsValue.textContent = status.fps.toFixed(1);
frameCount.textContent = status.frame_count;
detectionsCount.textContent = status.detections_count;
objectsCount.textContent = `(${status.detections_count}个检测对象)`;
// 更新图表
updateChart(status.detections_count);
detectionActive = true;
}
// 连接WebSocket
function connectWebSocket() {
// 初始化Socket连接
socket = io("http://192.168.10.131:9025");
// 连接事件
socket.on('connect', () => {
connectionStatus.textContent = '已连接';
connectionStatus.style.color = '#9beb72';
statusIndicator.className = 'status-indicator active';
showSystemMessage('已成功连接到实时检测服务');
});
socket.on('disconnect', () => {
connectionStatus.textContent = '已断开';
connectionStatus.style.color = '#f96060';
statusIndicator.className = 'status-indicator inactive';
resetUI();
showSystemMessage('服务器连接已断开');
});
// 接收检测结果
socket.on('detection_results', (data) => {
console.log('data----------------', data)
// 更新统计信息
fpsValue.textContent = data.fps.toFixed(1);
frameCount.textContent = data.frame_count;
detectionsCount.textContent = data.detections.length;
objectsCount.textContent = `(${data.detections.length}个检测对象)`;
// 更新图表
updateChart(data.detections.length, data.time_str);
// 更新检测结果列表
updateDetectionList(data.detections);
});
// 错误处理
socket.on('error', (error) => {
connectionStatus.textContent = '错误';
connectionStatus.style.color = '#f96060';
statusIndicator.className = 'status-indicator inactive';
resetUI();
showSystemMessage('连接错误: ' + error.message);
});
}
// 开始检测
function startDetection() {
if (detectionActive) {
showSystemMessage('检测已在运行中');
return;
}
// 获取当前选择的推流地址和模型
const selectedStream = streamSelect.value;
const selectedModel = modelSelect.value;
// 更新UI状态
startBtn.disabled = true;
stopBtn.disabled = false;
connectionStatus.textContent = '连接中...';
connectionStatus.style.color = '#f9c862';
statusIndicator.className = 'status-indicator';
showSystemMessage('正在连接到检测服务器...');
// 向服务端发送开始检测的命令
fetch('/start_detection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
rtmp_url: selectedStream,
model_name: selectedModel
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
detectionActive = true;
showSystemMessage('目标检测已启动,正在接收实时视频数据...');
// 连接WebSocket
connectWebSocket();
// 初始化视频播放器
initPlayer('original');
initPlayer('annotated');
} else {
showSystemMessage('启动检测失败: ' + data.message);
resetUI();
}
})
.catch(error => {
showSystemMessage('启动检测请求失败: ' + error.message);
resetUI();
});
}
// 停止检测
function stopDetection() {
if (!detectionActive) {
showSystemMessage('检测未运行');
return;
}
fetch('/stop_detection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
detectionActive = false;
showSystemMessage('目标检测已停止');
resetUI();
// 断开Socket连接
if (socket) {
socket.disconnect();
}
// 停止视频播放
pauseStream('original');
pauseStream('annotated');
// 销毁播放器
if (originalPlayer) {
originalPlayer.destroy();
originalPlayer = null;
}
if (annotatedPlayer) {
annotatedPlayer.destroy();
annotatedPlayer = null;
}
} else {
showSystemMessage('停止检测失败: ' + data.message);
}
})
.catch(error => {
showSystemMessage('停止检测请求失败: ' + error.message);
});
}
// 重置UI状态
function resetUI() {
startBtn.disabled = false;
stopBtn.disabled = true;
connectionStatus.textContent = '未连接';
connectionStatus.style.color = '#f96060';
statusIndicator.className = 'status-indicator inactive';
}
// 截图功能
function takeSnapshot() {
if (!annotatedStream || !annotatedStream.videoWidth) {
showSystemMessage('当前无检测画面可保存');
return;
}
// 创建canvas捕获当前帧
const canvas = document.createElement('canvas');
canvas.width = annotatedStream.videoWidth;
canvas.height = annotatedStream.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(annotatedStream, 0, 0, canvas.width, canvas.height);
// 创建下载链接
const link = document.createElement('a');
link.download = `检测截图_${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
showSystemMessage('检测截图已保存');
}
// 全屏切换
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
toggleBtn.textContent = '⬜';
} else {
document.exitFullscreen();
toggleBtn.textContent = '⬛';
}
}
// 更新推流地址选择
function updateStreamSelection() {
const selectedStream = streamSelect.value;
originalUrl.value = selectedStream;
showSystemMessage(`已选择推流地址: ${selectedStream}`);
}
// 更新模型选择
function updateModelSelection() {
const selectedModel = modelSelect.value;
showSystemMessage(`已选择模型: ${modelSelect.options[modelSelect.selectedIndex].text}`);
}
</script>
</body>
</html>