Yolov/templates/flv2.html

814 lines
26 KiB
HTML
Raw Blame History

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.0">
<title>视频流播放器</title>
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2a3a, #0d1825);
color: white;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
background: linear-gradient(90deg, #3498db, #9b59b6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
font-size: 1.2rem;
opacity: 0.8;
}
.player-container {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
@media (max-width: 1024px) {
.player-container {
grid-template-columns: 1fr;
}
}
.video-section {
background: rgba(0, 0, 0, 0.5);
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.video-wrapper {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 Aspect Ratio */
background: #000;
}
#videoElement {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.video-controls {
padding: 15px;
background: rgba(255, 255, 255, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.control-btn {
background: #3498db;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 1rem;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.control-btn:hover {
background: #2980b9;
transform: translateY(-2px);
}
.control-btn:disabled {
background: #7f8c8d;
cursor: not-allowed;
transform: none;
}
.video-info {
font-size: 0.9rem;
opacity: 0.8;
}
.info-panel {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 20px;
}
.info-card {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 15px;
border-left: 4px solid #3498db;
}
.info-card h3 {
font-size: 1.2rem;
margin-bottom: 10px;
color: #3498db;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 5px 0;
border-bottom: 1px dashed rgba(255, 255, 255, 0.1);
}
.info-label {
opacity: 0.7;
}
.info-value {
font-weight: 600;
}
.detection-list {
max-height: 300px;
overflow-y: auto;
}
.detection-item {
background: rgba(52, 152, 219, 0.1);
border-radius: 5px;
padding: 10px;
margin-bottom: 8px;
border-left: 3px solid #3498db;
}
.detection-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
}
.detection-class {
font-weight: 600;
color: #3498db;
}
.detection-confidence {
background: #3498db;
color: white;
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8rem;
}
.detection-box {
font-size: 0.85rem;
opacity: 0.8;
}
.stream-controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.input-group label {
font-size: 0.9rem;
opacity: 0.8;
}
.input-group input {
padding: 12px 15px;
border: none;
border-radius: 5px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 1rem;
}
.input-group input:focus {
outline: none;
background: rgba(255, 255, 255, 0.2);
}
.empty-state {
text-align: center;
padding: 40px 20px;
opacity: 0.5;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 15px;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #3498db;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #2980b9;
}
/* 加载动画 */
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e74c3c;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #27ae60;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.task-selector {
display: flex;
gap: 10px;
margin-bottom: 20px;
overflow-x: auto;
padding: 10px 0;
}
.task-tab {
background: rgba(255, 255, 255, 0.1);
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
white-space: nowrap;
transition: all 0.3s ease;
border-bottom: 3px solid transparent;
}
.task-tab.active {
background: rgba(52, 152, 219, 0.2);
border-bottom-color: #3498db;
}
.task-tab:hover {
background: rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1><i class="fas fa-video"></i> 实时视频流检测播放器</h1>
<p class="subtitle">实时查看视频流检测结果和统计数据</p>
</header>
<div class="task-selector" id="taskTabs">
<!-- 任务选项卡将动态生成 -->
</div>
<div class="player-container">
<div class="video-section">
<div class="video-wrapper">
<video id="videoElement" controls autoplay muted playsinline>
您的浏览器不支持 video 标签。
</video>
</div>
<div class="video-controls">
<button class="control-btn" id="playBtn">
<i class="fas fa-play"></i> 播放
</button>
<div class="status-indicator">
<div class="status-dot" id="streamStatus"></div>
<span id="statusText">等待连接...</span>
</div>
<div class="video-info">
分辨率: <span id="videoResolution">-</span> |
延迟: <span id="videoLatency">-</span>ms
</div>
</div>
</div>
<div class="info-panel">
<div class="info-card">
<h3><i class="fas fa-info-circle"></i> 流信息</h3>
<div class="info-row">
<span class="info-label">当前任务:</span>
<span class="info-value" id="currentTask">未选择</span>
</div>
<div class="info-row">
<span class="info-label">流地址:</span>
<span class="info-value" id="streamUrl">-</span>
</div>
<div class="info-row">
<span class="info-label">FPS:</span>
<span class="info-value" id="streamFps">-</span>
</div>
<div class="info-row">
<span class="info-label">总帧数:</span>
<span class="info-value" id="totalFrames">0</span>
</div>
</div>
<div class="info-card">
<h3><i class="fas fa-chart-bar"></i> 检测统计</h3>
<div class="info-row">
<span class="info-label">检测总数:</span>
<span class="info-value" id="totalDetections">0</span>
</div>
<div class="info-row">
<span class="info-label">当前FPS:</span>
<span class="info-value" id="currentFps">0</span>
</div>
<div class="info-row">
<span class="info-label">平均置信度:</span>
<span class="info-value" id="avgConfidence">0%</span>
</div>
<div class="info-row">
<span class="info-label">最后更新时间:</span>
<span class="info-value" id="lastUpdate">-</span>
</div>
</div>
<div class="info-card detection-list">
<h3><i class="fas fa-search"></i> 实时检测结果</h3>
<div id="detectionsContainer">
<div class="empty-state">
<i class="fas fa-binoculars"></i>
<p>等待检测结果...</p>
</div>
</div>
</div>
</div>
</div>
<div class="stream-controls">
<div class="input-group">
<label for="customStreamUrl"><i class="fas fa-link"></i> 自定义流地址</label>
<input type="text" id="customStreamUrl"
placeholder="例如: rtmp://localhost:1935/live/13">
</div>
<div class="input-group">
<label for="streamType"><i class="fas fa-cog"></i> 流类型</label>
<input type="text" id="streamType" value="flv" disabled>
</div>
<div style="display: flex; align-items: flex-end;">
<button class="control-btn" id="loadCustomStream">
<i class="fas fa-sync-alt"></i> 加载自定义流
</button>
</div>
</div>
</div>
<script src="https://cdn.socket.io/4.6.0/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
<script>
// 全局变量
let flvPlayer = null;
let socket = null;
let currentTaskId = null;
let tasks = {};
let detectionStats = {
totalDetections: 0,
confidenceSum: 0,
lastUpdate: null,
frameCount: 0,
lastFrameTime: Date.now(),
currentFps: 0
};
// DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initWebSocket();
initEventListeners();
loadTasks();
// 默认加载第一个任务
setTimeout(() => {
if (Object.keys(tasks).length > 0) {
const firstTaskId = Object.keys(tasks)[0];
selectTask(firstTaskId);
}
}, 1000);
});
// 初始化WebSocket连接
function initWebSocket() {
socket = io();
socket.on('connect', function() {
updateStreamStatus(true, 'WebSocket已连接');
console.log('WebSocket连接成功');
});
socket.on('disconnect', function() {
updateStreamStatus(false, 'WebSocket已断开');
console.log('WebSocket连接断开');
});
socket.on('connect_error', function(error) {
updateStreamStatus(false, '连接错误');
console.error('WebSocket连接错误:', error);
});
socket.on('detection_results', function(data) {
handleDetectionResults(data);
});
}
// 初始化事件监听器
function initEventListeners() {
// 播放/暂停按钮
const playBtn = document.getElementById('playBtn');
const videoElement = document.getElementById('videoElement');
playBtn.addEventListener('click', function() {
if (videoElement.paused) {
videoElement.play();
playBtn.innerHTML = '<i class="fas fa-pause"></i> 暂停';
} else {
videoElement.pause();
playBtn.innerHTML = '<i class="fas fa-play"></i> 播放';
}
});
// 自定义流加载
document.getElementById('loadCustomStream').addEventListener('click', function() {
const customUrl = document.getElementById('customStreamUrl').value;
if (customUrl) {
loadFlvStream(customUrl);
}
});
// 视频元素事件
videoElement.addEventListener('loadedmetadata', function() {
updateVideoInfo();
});
videoElement.addEventListener('playing', function() {
updateStreamStatus(true, '正在播放');
});
videoElement.addEventListener('pause', function() {
updateStreamStatus(false, '已暂停');
});
videoElement.addEventListener('error', function() {
updateStreamStatus(false, '播放错误');
console.error('视频播放错误:', videoElement.error);
});
}
// 加载任务列表
async function loadTasks() {
try {
const response = await fetch('/api/tasks');
const result = await response.json();
if (result.status === 'success') {
tasks = {};
const taskTabs = document.getElementById('taskTabs');
taskTabs.innerHTML = '';
// 过滤出运行中的任务
const runningTasks = result.data.tasks.filter(task =>
task.status === 'running' || task.status === 'starting'
);
if (runningTasks.length === 0) {
taskTabs.innerHTML = '<div class="empty-state">暂无运行中的任务</div>';
return;
}
// 创建任务选项卡
runningTasks.forEach(task => {
tasks[task.task_id] = task;
const tab = document.createElement('div');
tab.className = 'task-tab';
tab.textContent = task.config.taskname || task.task_id.substring(0, 8);
tab.dataset.taskId = task.task_id;
tab.addEventListener('click', function() {
selectTask(task.task_id);
});
taskTabs.appendChild(tab);
});
// 激活第一个选项卡
if (runningTasks.length > 0 && !currentTaskId) {
const firstTaskId = runningTasks[0].task_id;
selectTask(firstTaskId);
}
}
} catch (error) {
console.error('加载任务失败:', error);
}
}
// 选择任务
function selectTask(taskId) {
// 更新选项卡状态
document.querySelectorAll('.task-tab').forEach(tab => {
tab.classList.remove('active');
if (tab.dataset.taskId === taskId) {
tab.classList.add('active');
}
});
// 更新当前任务
currentTaskId = taskId;
const task = tasks[taskId];
if (task) {
// 更新任务信息
document.getElementById('currentTask').textContent =
task.config.taskname || taskId.substring(0, 8);
// 获取推流地址(如果启用)
const pushUrl = task.config.push?.url;
if (pushUrl && task.config.push?.enable_push) {
// 加载推流
loadFlvStream(pushUrl);
} else {
// 如果没有推流地址,显示提示
updateStreamStatus(false, '该任务未启用推流');
document.getElementById('videoElement').src = '';
}
// 更新流信息
document.getElementById('streamUrl').textContent =
pushUrl || '未启用推流';
document.getElementById('streamFps').textContent =
task.stats.avg_fps || '-';
document.getElementById('totalFrames').textContent =
task.stats.total_frames || 0;
}
}
// 加载FLV流
function loadFlvStream(url) {
// 停止现有播放器
if (flvPlayer) {
flvPlayer.destroy();
flvPlayer = null;
}
const videoElement = document.getElementById('videoElement');
// 检查浏览器支持
if (!flvjs.isSupported()) {
alert('您的浏览器不支持FLV播放请使用Chrome、Firefox或Edge浏览器');
return;
}
console.log("url----------》",url)
// 创建FLV播放器
flvPlayer = flvjs.createPlayer({
type: 'flv',
url: url,
isLive: true,
hasAudio: false
}, {
enableWorker: true,
lazyLoadMaxDuration: 3 * 60,
seekType: 'range',
reuseRedirectedURL: true
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
// 播放器事件监听
flvPlayer.on(flvjs.Events.ERROR, function(errorType, errorDetail, errorInfo) {
console.error('FLV播放器错误:', errorType, errorDetail, errorInfo);
updateStreamStatus(false, '流加载失败');
});
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, function() {
console.log('FLV流加载完成');
updateStreamStatus(true, '流加载完成');
});
flvPlayer.on(flvjs.Events.METADATA_ARRIVED, function(metadata) {
console.log('流元数据:', metadata);
updateVideoInfo();
});
// 开始播放
flvPlayer.play().catch(error => {
console.error('播放失败:', error);
updateStreamStatus(false, '播放失败');
});
updateStreamStatus(true, '正在连接...');
}
// 更新视频信息
function updateVideoInfo() {
const videoElement = document.getElementById('videoElement');
const resolution = `${videoElement.videoWidth}x${videoElement.videoHeight}`;
document.getElementById('videoResolution').textContent = resolution;
// 计算延迟(简单估算)
if (videoElement.buffered.length > 0) {
const latency = (videoElement.buffered.end(0) - videoElement.currentTime) * 1000;
document.getElementById('videoLatency').textContent = Math.round(latency);
}
}
// 更新流状态
function updateStreamStatus(connected, message) {
const statusDot = document.getElementById('streamStatus');
const statusText = document.getElementById('statusText');
if (connected) {
statusDot.className = 'status-dot connected';
statusText.textContent = message;
} else {
statusDot.className = 'status-dot';
statusText.textContent = message;
}
}
// 处理检测结果
function handleDetectionResults(data) {
// 只显示当前任务的检测结果
if (data.task_id !== currentTaskId) {
return;
}
// 更新统计信息
updateDetectionStats(data);
// 显示检测结果
displayDetections(data);
// 更新最后更新时间
document.getElementById('lastUpdate').textContent =
new Date().toLocaleTimeString('zh-CN');
}
// 更新检测统计
function updateDetectionStats(data) {
// FPS计算
detectionStats.frameCount++;
const now = Date.now();
const timeDiff = now - detectionStats.lastFrameTime;
if (timeDiff >= 1000) { // 每秒更新一次FPS
detectionStats.currentFps = Math.round(
(detectionStats.frameCount * 1000) / timeDiff
);
detectionStats.frameCount = 0;
detectionStats.lastFrameTime = now;
document.getElementById('currentFps').textContent = detectionStats.currentFps;
}
// 检测统计
if (data.detections && data.detections.length > 0) {
detectionStats.totalDetections += data.detections.length;
const totalConfidence = data.detections.reduce((sum, det) =>
sum + det.confidence, 0
);
detectionStats.confidenceSum += totalConfidence;
const avgConfidence = (detectionStats.confidenceSum / detectionStats.totalDetections) * 100;
document.getElementById('totalDetections').textContent = detectionStats.totalDetections;
document.getElementById('avgConfidence').textContent = avgConfidence.toFixed(1) + '%';
}
}
// 显示检测结果
function displayDetections(data) {
const container = document.getElementById('detectionsContainer');
// 如果当前显示空状态,清空容器
if (container.querySelector('.empty-state')) {
container.innerHTML = '';
}
// 限制显示数量最多显示10个
while (container.children.length >= 10) {
container.removeChild(container.firstChild);
}
if (data.detections && data.detections.length > 0) {
data.detections.forEach(detection => {
const detectionItem = document.createElement('div');
detectionItem.className = 'detection-item';
detectionItem.innerHTML = `
<div class="detection-header">
<span class="detection-class">${detection.class_name}</span>
<span class="detection-confidence">${(detection.confidence * 100).toFixed(1)}%</span>
</div>
<div class="detection-box">
位置: [${detection.box.map(c => c.toFixed(1)).join(', ')}]
</div>
`;
// 添加到顶部(最新结果显示在最上面)
container.insertBefore(detectionItem, container.firstChild);
});
} else {
// 如果没有检测结果,显示空状态(如果容器为空)
if (container.children.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-binoculars"></i>
<p>等待检测结果...</p>
</div>
`;
}
}
}
// 页面卸载时清理
window.addEventListener('beforeunload', function() {
if (flvPlayer) {
flvPlayer.destroy();
flvPlayer = null;
}
if (socket) {
socket.disconnect();
}
});
// 定时刷新任务列表每30秒
setInterval(loadTasks, 30000);
</script>
</body>
</html>