Yolov/templates/flv2.html

1316 lines
43 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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.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>