365 lines
9.9 KiB
JavaScript
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);
|
|
}
|
|
});
|