init
This commit is contained in:
@@ -0,0 +1,364 @@
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user