289 lines
10 KiB
JavaScript
289 lines
10 KiB
JavaScript
// 视频实时监控页面脚本
|
|
let streams = [];
|
|
let detectionStatus = {};
|
|
let processedFrames = {}; // 存储处理后的帧
|
|
|
|
// 加载视频流列表
|
|
async function loadStreams() {
|
|
try {
|
|
const res = await fetch('/streams');
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
streams = data.data;
|
|
document.getElementById('cameraCount').textContent = streams.length;
|
|
renderVideos();
|
|
}
|
|
} catch (e) {
|
|
addLog('加载视频流失败: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
// 加载检测状态
|
|
async function loadStatus() {
|
|
try {
|
|
const res = await fetch('/status');
|
|
const data = await res.json();
|
|
if (data.code === 0) {
|
|
document.getElementById('checkInterval').textContent = data.data.check_interval;
|
|
}
|
|
} catch (e) {
|
|
console.error('加载状态失败:', e);
|
|
}
|
|
}
|
|
|
|
// 渲染视频卡片
|
|
function renderVideos() {
|
|
const container = document.getElementById('videoContainer');
|
|
if (streams.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">暂无视频流</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = streams.map(stream => `
|
|
<div class="video-card" data-stream="${stream.key}">
|
|
<div class="video-header">
|
|
<span class="video-title">${stream.app} / ${stream.stream}</span>
|
|
<div class="video-status">
|
|
<span class="detection-badge idle" id="badge-${stream.key}">无人</span>
|
|
<span style="color: #888;">${stream.resolution} @ ${stream.fps}fps</span>
|
|
<div class="process-toggle">
|
|
<button class="toggle-btn active" id="btn-raw-${stream.key}" onclick="toggleProcess('${stream.key}', 'raw')">原始</button>
|
|
<button class="toggle-btn" id="btn-processed-${stream.key}" onclick="toggleProcess('${stream.key}', 'processed')">处理后</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="video-container">
|
|
<img id="frame-raw-${stream.key}" class="video-frame" alt="原始视频">
|
|
<img id="frame-processed-${stream.key}" class="video-frame hidden" alt="处理后视频">
|
|
<canvas id="roi-${stream.key}" class="roi-overlay"></canvas>
|
|
</div>
|
|
<div class="video-info">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<span class="info-label">检测状态</span>
|
|
<span class="info-value" id="detect-${stream.key}">正常</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">最后检测</span>
|
|
<span class="info-value" id="last-${stream.key}">-</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">码率</span>
|
|
<span class="info-value">${formatBytes(stream.bytesSpeed)}/s</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<span class="info-label">协议</span>
|
|
<span class="info-value">JPEG帧</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// 初始化所有视频流
|
|
streams.forEach(stream => initVideo(stream));
|
|
}
|
|
|
|
// 切换原始/处理后画面
|
|
function toggleProcess(streamKey, type) {
|
|
const rawFrame = document.getElementById(`frame-raw-${streamKey}`);
|
|
const processedFrame = document.getElementById(`frame-processed-${streamKey}`);
|
|
const rawBtn = document.getElementById(`btn-raw-${streamKey}`);
|
|
const processedBtn = document.getElementById(`btn-processed-${streamKey}`);
|
|
|
|
const stream = streams.find(s => s.key === streamKey);
|
|
if (!stream) return;
|
|
|
|
if (type === 'raw') {
|
|
rawFrame.classList.remove('hidden');
|
|
processedFrame.classList.add('hidden');
|
|
rawBtn.classList.add('active');
|
|
processedBtn.classList.remove('active');
|
|
// 立即刷新原始帧
|
|
rawFrame.src = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}&t=${Date.now()}`;
|
|
} else {
|
|
rawFrame.classList.add('hidden');
|
|
processedFrame.classList.remove('hidden');
|
|
rawBtn.classList.remove('active');
|
|
processedBtn.classList.add('active');
|
|
// 立即刷新处理后帧
|
|
processedFrame.src = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}&t=${Date.now()}&processed=1`;
|
|
}
|
|
}
|
|
|
|
// 初始化单个视频
|
|
function initVideo(stream) {
|
|
const rawFrame = document.getElementById(`frame-raw-${stream.key}`);
|
|
const processedFrame = document.getElementById(`frame-processed-${stream.key}`);
|
|
const canvas = document.getElementById(`roi-${stream.key}`);
|
|
|
|
// 加载并绘制ROI区域
|
|
loadAndDrawROI(stream.key, canvas);
|
|
|
|
// 开始拉取视频帧
|
|
startFramePolling(stream.key, rawFrame, processedFrame);
|
|
}
|
|
|
|
// 轮询获取视频帧
|
|
function startFramePolling(streamKey, rawFrame, processedFrame) {
|
|
// 使用 ZLM 的截图API获取帧
|
|
const stream = streams.find(s => s.key === streamKey);
|
|
if (!stream) {
|
|
console.error('未找到流:', streamKey);
|
|
return;
|
|
}
|
|
|
|
// 构建截图URL (使用 ZLM 的 getSnap 接口)
|
|
const snapUrl = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}`;
|
|
|
|
console.log('开始轮询截图:', streamKey, 'URL:', snapUrl);
|
|
|
|
// 添加图片加载错误处理
|
|
rawFrame.onerror = function() {
|
|
console.error('原始帧加载失败:', streamKey);
|
|
};
|
|
processedFrame.onerror = function() {
|
|
console.error('处理后帧加载失败:', streamKey);
|
|
};
|
|
|
|
rawFrame.onload = function() {
|
|
console.log('原始帧加载成功:', streamKey);
|
|
};
|
|
|
|
// 定期刷新帧
|
|
let lastTimestamp = 0;
|
|
const refreshFrame = () => {
|
|
try {
|
|
// 添加时间戳防止缓存
|
|
const timestamp = Date.now();
|
|
if (timestamp - lastTimestamp < 100) {
|
|
return; // 防止过于频繁的刷新
|
|
}
|
|
lastTimestamp = timestamp;
|
|
|
|
// 根据当前显示状态刷新对应的帧
|
|
if (!rawFrame.classList.contains('hidden')) {
|
|
rawFrame.src = `${snapUrl}&t=${timestamp}`;
|
|
}
|
|
if (!processedFrame.classList.contains('hidden')) {
|
|
processedFrame.src = `${snapUrl}&t=${timestamp}&processed=1`;
|
|
}
|
|
} catch (e) {
|
|
console.error('获取帧失败:', e);
|
|
}
|
|
};
|
|
|
|
// 先加载一次
|
|
refreshFrame();
|
|
|
|
// 每500ms刷新一次 (2fps) - 降低频率避免过载
|
|
const interval = setInterval(refreshFrame, 500);
|
|
|
|
// 保存interval以便清理
|
|
rawFrame.dataset.interval = interval;
|
|
}
|
|
|
|
// 加载并绘制ROI
|
|
async function loadAndDrawROI(streamKey, canvas) {
|
|
try {
|
|
const res = await fetch('/getRoi?stream_key=' + streamKey);
|
|
const data = await res.json();
|
|
if (data.code === 0 && data.data) {
|
|
drawROI(canvas, data.data);
|
|
}
|
|
} catch (e) {
|
|
console.error('加载ROI失败:', e);
|
|
}
|
|
}
|
|
|
|
// 绘制ROI区域
|
|
function drawROI(canvas, points) {
|
|
const ctx = canvas.getContext('2d');
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = rect.width;
|
|
canvas.height = rect.height;
|
|
|
|
// 坐标转换(假设原始坐标是640x480)
|
|
const scaleX = canvas.width / 640;
|
|
const scaleY = canvas.height / 480;
|
|
|
|
ctx.strokeStyle = '#e94560';
|
|
ctx.lineWidth = 2;
|
|
ctx.fillStyle = 'rgba(233, 69, 96, 0.2)';
|
|
|
|
ctx.beginPath();
|
|
points.forEach((p, i) => {
|
|
const x = p.x * scaleX;
|
|
const y = p.y * scaleY;
|
|
if (i === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
});
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
|
|
// 格式化字节
|
|
function formatBytes(bytes) {
|
|
if (!bytes || bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
// 添加日志
|
|
function addLog(message, type = 'info') {
|
|
const logContent = document.getElementById('logContent');
|
|
const time = new Date().toLocaleTimeString();
|
|
const entry = document.createElement('div');
|
|
entry.className = 'log-entry';
|
|
entry.innerHTML = `
|
|
<span class="log-time">${time}</span>
|
|
<span class="log-${type}">${message}</span>
|
|
`;
|
|
logContent.insertBefore(entry, logContent.firstChild);
|
|
if (logContent.children.length > 50) {
|
|
logContent.removeChild(logContent.lastChild);
|
|
}
|
|
}
|
|
|
|
// 模拟检测状态更新(实际应通过WebSocket)
|
|
function simulateDetection() {
|
|
streams.forEach(stream => {
|
|
const isDetected = Math.random() > 0.9;
|
|
const badge = document.getElementById('badge-' + stream.key);
|
|
const detectStatus = document.getElementById('detect-' + stream.key);
|
|
const lastCheck = document.getElementById('last-' + stream.key);
|
|
|
|
if (badge && detectStatus && lastCheck) {
|
|
if (isDetected) {
|
|
badge.className = 'detection-badge detected';
|
|
badge.textContent = '有人';
|
|
detectStatus.className = 'info-value detected';
|
|
detectStatus.textContent = '检测到人员';
|
|
} else {
|
|
badge.className = 'detection-badge idle';
|
|
badge.textContent = '无人';
|
|
detectStatus.className = 'info-value';
|
|
detectStatus.textContent = '正常';
|
|
}
|
|
lastCheck.textContent = new Date().toLocaleTimeString();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 初始化
|
|
loadStreams();
|
|
loadStatus();
|
|
setInterval(loadStreams, 30000); // 30秒刷新流列表
|
|
setInterval(simulateDetection, 1000); // 1秒更新检测状态
|
|
|
|
// 窗口大小改变时重绘ROI
|
|
window.addEventListener('resize', () => {
|
|
streams.forEach(stream => {
|
|
const canvas = document.getElementById('roi-' + stream.key);
|
|
if (canvas) loadAndDrawROI(stream.key, canvas);
|
|
});
|
|
});
|