Files
dsserver/public/static/js/roi.js
T
zimoyin 4c841b9dbf init
2026-04-02 17:42:00 +08:00

365 lines
9.9 KiB
JavaScript

// ROI配置页面脚本
let streams = [];
let currentStream = null;
let roiPoints = [];
let isDrawing = false;
let canvas = null;
let ctx = null;
let img = null;
let draggedPoint = null;
let scaleX = 1, scaleY = 1;
// 加载视频流列表
async function loadStreams() {
try {
const res = await fetch('/streams');
const data = await res.json();
if (data.code === 0) {
streams = data.data;
renderStreamList();
}
} catch (e) {
showToast('加载视频流失败', 'error');
}
}
// 渲染流列表
function renderStreamList() {
const list = document.getElementById('streamList');
if (streams.length === 0) {
list.innerHTML = '<div class="empty-state">暂无视频流</div>';
return;
}
list.innerHTML = streams.map(stream => `
<div class="stream-item" data-key="${stream.key}" onclick="selectStream('${stream.key}')">
<div class="stream-name">${stream.app} / ${stream.stream}</div>
<div class="stream-info">${stream.resolution} @ ${stream.fps}fps</div>
</div>
`).join('');
}
// 选择视频流
async function selectStream(streamKey) {
currentStream = streams.find(s => s.key === streamKey);
if (!currentStream) return;
// 更新UI
document.querySelectorAll('.stream-item').forEach(item => {
item.classList.toggle('active', item.dataset.key === streamKey);
});
document.getElementById('currentStream').textContent = currentStream.app + ' / ' + currentStream.stream;
// 加载视频和ROI
await loadVideo(currentStream);
await loadROI(streamKey);
}
// 加载视频
async function loadVideo(stream) {
const area = document.getElementById('videoArea');
area.innerHTML = `
<div class="video-wrapper" id="videoWrapper">
<img id="previewImg" alt="视频预览" style="min-width: 320px; min-height: 240px; background: #000;">
<canvas id="roiCanvas" class="roi-canvas"></canvas>
<div class="coord-display" id="coordDisplay">坐标: 0, 0</div>
</div>
`;
img = document.getElementById('previewImg');
canvas = document.getElementById('roiCanvas');
ctx = canvas.getContext('2d');
// 使用 ZLM 截图API获取帧
const snapUrl = `/proxy/zlm/snap?app=${stream.app}&stream=${stream.stream}`;
console.log('ROI页面截图URL:', snapUrl);
// 添加错误处理
img.onerror = function() {
console.error('ROI页面图片加载失败');
};
// 定期刷新帧
let lastTimestamp = 0;
const refreshFrame = () => {
const timestamp = Date.now();
if (timestamp - lastTimestamp < 100) return;
lastTimestamp = timestamp;
img.src = snapUrl + '&t=' + timestamp;
};
img.onload = () => {
console.log('ROI页面图片加载成功');
resizeCanvas();
};
refreshFrame();
const interval = setInterval(refreshFrame, 500); // 每500ms刷新
img.dataset.interval = interval;
// 绑定画布事件
bindCanvasEvents();
}
// 调整画布大小
function resizeCanvas() {
if (!img || !canvas) return;
const rect = img.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
scaleX = 640 / rect.width;
scaleY = 480 / rect.height;
drawROI();
}
// 加载ROI配置
async function loadROI(streamKey) {
try {
const res = await fetch('/getRoi?stream_key=' + streamKey);
const data = await res.json();
if (data.code === 0 && data.data) {
roiPoints = data.data.map(p => ({
x: p.x / scaleX,
y: p.y / scaleY
}));
drawROI();
}
} catch (e) {
console.error('加载ROI失败:', e);
}
}
// 绑定画布事件
function bindCanvasEvents() {
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('dblclick', onDoubleClick);
canvas.addEventListener('contextmenu', onRightClick);
// 坐标显示
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = Math.round((e.clientX - rect.left) * scaleX);
const y = Math.round((e.clientY - rect.top) * scaleY);
document.getElementById('coordDisplay').textContent = `坐标: ${x}, ${y}`;
});
}
// 鼠标按下
function onMouseDown(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检查是否点击了现有点
const pointIndex = findPointAt(x, y);
if (pointIndex !== -1) {
draggedPoint = pointIndex;
return;
}
// 添加新点(最多4个点)
if (isDrawing && roiPoints.length < 4) {
roiPoints.push({ x, y });
drawROI();
}
}
// 鼠标移动
function onMouseMove(e) {
if (draggedPoint !== null) {
const rect = canvas.getBoundingClientRect();
roiPoints[draggedPoint].x = e.clientX - rect.left;
roiPoints[draggedPoint].y = e.clientY - rect.top;
drawROI();
}
}
// 鼠标释放
function onMouseUp() {
draggedPoint = null;
}
// 双击删除点
function onDoubleClick(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const pointIndex = findPointAt(x, y);
if (pointIndex !== -1) {
roiPoints.splice(pointIndex, 1);
drawROI();
}
}
// 右键完成绘制
function onRightClick(e) {
e.preventDefault();
if (isDrawing && roiPoints.length >= 3) {
isDrawing = false;
canvas.classList.remove('drawing');
document.getElementById('btnDraw').disabled = false;
showToast('绘制完成,可以拖拽调整顶点');
}
}
// 查找点击的点
function findPointAt(x, y) {
const threshold = 10;
for (let i = 0; i < roiPoints.length; i++) {
const p = roiPoints[i];
const dist = Math.sqrt((p.x - x) ** 2 + (p.y - y) ** 2);
if (dist < threshold) return i;
}
return -1;
}
// 绘制ROI
function drawROI() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (roiPoints.length === 0) return;
// 绘制填充区域
ctx.fillStyle = 'rgba(233, 69, 96, 0.2)';
ctx.strokeStyle = '#e94560';
ctx.lineWidth = 2;
ctx.beginPath();
roiPoints.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
});
if (roiPoints.length >= 3 && !isDrawing) {
ctx.closePath();
}
ctx.fill();
ctx.stroke();
// 绘制顶点
roiPoints.forEach((p, i) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
ctx.fillStyle = '#e94560';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// 绘制序号
ctx.fillStyle = '#fff';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText((i + 1).toString(), p.x, p.y);
});
// 绘制连线提示
if (isDrawing && roiPoints.length > 0) {
ctx.setLineDash([5, 5]);
ctx.strokeStyle = '#888';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(roiPoints[roiPoints.length - 1].x, roiPoints[roiPoints.length - 1].y);
ctx.lineTo(roiPoints[0].x, roiPoints[0].y);
ctx.stroke();
ctx.setLineDash([]);
}
}
// 开始绘制
function startDrawing() {
if (!currentStream) {
showToast('请先选择视频流', 'error');
return;
}
isDrawing = true;
roiPoints = [];
canvas.classList.add('drawing');
document.getElementById('btnDraw').disabled = true;
drawROI();
showToast('点击画布添加顶点,最多4个点,右键完成');
}
// 清空ROI
function clearROI() {
roiPoints = [];
isDrawing = false;
canvas.classList.remove('drawing');
document.getElementById('btnDraw').disabled = false;
drawROI();
}
// 重置为默认ROI
function resetROI() {
if (!img) return;
const rect = img.getBoundingClientRect();
roiPoints = [
{ x: 0, y: 0 },
{ x: rect.width, y: 0 },
{ x: rect.width, y: rect.height },
{ x: 0, y: rect.height }
];
isDrawing = false;
canvas.classList.remove('drawing');
document.getElementById('btnDraw').disabled = false;
drawROI();
}
// 保存ROI
async function saveROI() {
if (!currentStream) {
showToast('请先选择视频流', 'error');
return;
}
if (roiPoints.length < 3) {
showToast('至少需要3个点形成检测区域', 'error');
return;
}
// 转换为原始坐标
const normalizedPoints = roiPoints.map(p => ({
x: Math.round(p.x * scaleX),
y: Math.round(p.y * scaleY)
}));
try {
const res = await fetch('/saveRoi', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'stream_key=' + encodeURIComponent(currentStream.key) +
'&roi=' + encodeURIComponent(JSON.stringify(normalizedPoints))
});
const data = await res.json();
if (data.code === 0) {
showToast('保存成功');
} else {
showToast(data.msg || '保存失败', 'error');
}
} catch (e) {
showToast('保存失败: ' + e.message, 'error');
}
}
// 显示提示
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = 'toast show' + (type === 'error' ? ' error' : '');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// 初始化
loadStreams();
// 窗口大小改变时重绘
window.addEventListener('resize', () => {
if (img) {
setTimeout(resizeCanvas, 100);
}
});