Initial commit of secondary development sample code

This commit is contained in:
2026-01-19 10:39:22 +08:00
commit c2697affd9
66 changed files with 17277 additions and 0 deletions

42
README.md Normal file
View File

@ -0,0 +1,42 @@
# 二次开发示例代码
工程总共包含几个模块,快速接收设备告警、深度对接设备接口 和 心跳保活。
## 快速接收设备告警
### http-server-demo 以HTTP协议接收设备告警最常用
参考具体文件下的源代码demo即可。
### tcp-server-demo 以TCP协议接收设备告警常见PLC控制等
参考具体文件下的源代码demo即可。
### custom-api-demo 自定义API部分用户的平台接口规则限定了可用这个
参考具体文件下的源代码demo即可。
## 深度对接设备接口
可将代码下载到Windows环境用谷歌浏览器直接打开.html文件运行内含相关功能的源代码用户可根据我司提供的代码修改做二次开发。本工程的代码可直接双击运行除此之外恕不提供其它的售后支持见谅。
### 实时画面播放&实时检测框
live.html是实时画面播放和实时检测结果可视化的demo将文件中的IP地址替换成KS设备的IP地址即可
![](深度对接设备接口/assets/live.png)
### 摄像头配置&算法绑定
source.html是视频流管理demo将文件中的IP地址替换成KS设备的IP地址即可
![](深度对接设备接口/assets/source.png)
### 底库分组&人脸底库
facelib.html是底库分组管理和人脸底库增删改查的demo将文件中的IP地址替换成KS设备的IP地址即可
![](深度对接设备接口/assets/facelib.png)
### 查看设备信息
device.html是设备信息demo将文件中的IP地址替换成KS设备的IP地址即可
![](深度对接设备接口/assets/device.png)
## 心跳保活
分别提供了python和java两种语言的服务端对接KS设备心跳的demo代码。
## 接收大模型复审告警
启动main文件在接收端创建一个server在设备端【大模型】-【复审任务】-【结果推送】处填上http://IP:port/vlreview ,即可接收设备的大模型的复审告警。
## 上位机实时画面
1. 示例代码下载到本地windows即可
2. 修改index.html中的serverIp、accessKey、accessSecret的三个字段。 三个修改该项在下图位置处修改。
![](上位机实时画面/assets/index.png)
![](上位机实时画面/assets/accesskey.png)
3. 双击index.html打开即可使用示例代码。

View File

@ -0,0 +1,862 @@
const ZQL_multivideo = {
setVideoEl: () => {
let videoContainer = document.querySelector("#video-container");
if (ZQL_playingSource.videoNum == 1) {
videoContainer.className = "one-video";
videoContainer.innerHTML = `
<div class="video-box">
<div class="tips" id="tip0">
<div class="icon-dot"></div>
<div class="deviceoffline">
<i class="z-icon-jiankonglixian" style="font-size: 40rem"></i>
<span>离线</span>
</div>
</div>
<div class="title-container" id="video-title0"></div>
<video ref="video" muted id="video0" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas0"></canvas>
</div>
`
} else {
videoContainer.className = "four-video";
videoContainer.innerHTML = `
<div class="video-box">
<div class="tips" id="tip0">
</div>
<div class="title-container" id="video-title0"></div>
<video ref="video" muted id="video0" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas0"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip1">
</div>
<div class="title-container" id="video-title1"></div>
<video ref="video" muted id="video1" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas1"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip2">
</div>
<div class="title-container" id="video-title2"></div>
<video ref="video" muted id="video2" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas2"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip3">
</div>
<div class="title-container" id="video-title3"></div>
<video ref="video" muted id="video3" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas3"></canvas>
</div>
`
}
},
liveLoading: (index) => {
let tipel = document.querySelector("#tip" + index);
tipel.innerHTML = `<div class="icon-dot"></div>`
},
liveOffline: (index) => {
let tipel = document.querySelector("#tip" + index);
tipel.innerHTML = `
<div class="deviceoffline">
<i class="z-icon-jiankonglixian" style="font-size: 40rem"></i>
<span>离线</span>
</div>
`
},
liveStopLoading: (index) => {
let tipel = document.querySelector("#tip" + index);
if (tipel) {
tipel.innerHTML = ``
}
},
setAlgList(index) {
let el = document.querySelector(`#video-title${index}`);
let algList = ZQL_sources[ZQL_playingSource[index]].alg;
let algEl = '<ul>';
for (let alg in algList) {
let name = algList[alg].reserved_args.ch_name;
algEl = algEl + `<li alg="${alg}" index="${index}">${name}</li>`
}
algEl = algEl + '</ul>'
el.innerHTML = `
<div class="camera">${ZQL_sources[ZQL_playingSource[index]].desc}</div>
<div class="alg">
<div class="algname">算法: ${ZQL_playingSource[index].alg ? ZQL_sources[ZQL_playingSource[index]].alg[alg].reserved_args.ch_name : ''}</div>
${algEl}
</div>
<div id="close${index}">关闭</div>
`;
el.querySelectorAll('li').forEach(item => {
item.addEventListener('click', (e) => {
let index = e.currentTarget.getAttribute("index");
let alg = e.currentTarget.getAttribute("alg")
ZQL_videosInfos[index].alg = alg;
let videlel = document.querySelector(`#video-title${index}`);
videlel.querySelector(".algname").innerHTML = '算法:' + ZQL_sources[ZQL_playingSource[index]].alg[alg].reserved_args.ch_name
})
})
document.querySelector(`#close${index}`).addEventListener('click', () => {
ZQL_multivideo.clearAlgList(index);
ZQL_multivideo.liveStopLoading(index);
ZQL_multivideo.destoryVideoByIndex(index);
ZQL_playingSource[index] = null;
ZQL_videosInfos[index] = null;
})
},
clearAlgList(index) {
let el = document.querySelector(`#video-title${index}`);
if (el) {
el.innerHTML = ""
}
},
detectSrs() {
if (ZQLGLOBAL.detectSrsTimer) {
clearInterval(ZQLGLOBAL.detectSrsTimer)
}
ZQLGLOBAL.detectSrsTimer = setInterval(() => {
ZQL_apis.detectStream().then((res) => {
if (document.visibilityState == "visible" && res.code == 0) {
for (let i = 0; i < ZQL_playingSource.videonum; i++) {
let deviceId_cameraId = ZQL_playingSource[i];
if (
deviceId_cameraId &&
ZQL_videosInfos[i] &&
ZQL_videosInfos[i].status != "离线"
) {
let deviceId = ZQL_sources[deviceId_cameraId].deviceId;
let cameraId = ZQL_sources[deviceId_cameraId].sourceId;
let streamInfo = res.streams.find(
(item) => item.url.indexOf(`${deviceId}/${cameraId}`) > 0
);
if (
!streamInfo ||
(streamInfo && streamInfo.publish.active == false)
) {
ZQL_multivideo.destoryVideoByIndex(i);
ZQL_multivideo.subscribeLive(deviceId_cameraId, i);
}
}
}
}
});
}, 60000)
},
handleRefresh(index) {
if (!ZQL_videosInfos[index]) {
return;
}
if (ZQL_videosInfos[index].status == "离线") {
ZQL_multivideo.destoryVideoByIndex(index);
ZQL_multivideo.subscribeLive(ZQL_playingSource[index], index);
} else {
if (!ZQL_videosInfos[index].stream) {
return;
}
let video = document.getElementById("video" + index);
video && (video.srcObject = null);
if (ZQL_videosInfos[index] && ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
ZQL_videosInfos[index].replayTimer = null;
}
ZQL_videosInfos[index] &&
ZQL_videosInfos[index].srsrtc &&
ZQL_videosInfos[index].srsrtc.destroy();
ZQL_videosInfos[index].srsrtc = null;
ZQL_videosInfos[index].status = "";
ZQL_multivideo.playVideo(ZQL_playingSource[index], index);
}
},
subscribeLive(cameraId, index) {
ZQL_multivideo.getCameraSize(cameraId, index);
ZQL_multivideo.liveLoading(index);
ZQL_apis
.subscribeLive(
ZQL_sources[cameraId].deviceId,
ZQL_sources[cameraId].sourceId
)
.then((data) => {
let stream = data.data;
if (data && stream) {
ZQL_videosInfos[index].stream = stream;
ZQL_multivideo.playVideo(cameraId, index);
} else {
if (ZQL_playingSource[index] == cameraId) {
ZQL_multivideo.liveOffline(index);
// ZQL_videosInfos[index].status = "离线";
// ZQL_videosInfos[index].loading = false;
// this.reSubcribe(cameraId, index);
}
}
})
.catch((err) => {
if (
ZQL_playingSource[index] == cameraId &&
ZQL_videosInfos[index]
) {
ZQL_multivideo.liveOffline(index);
// ZQL_videosInfos[index].status = "离线";
// ZQL_videosInfos[index].loading = false;
// this.reSubcribe(cameraId, index);
}
});
},
playVideo(cameraId, index) {
if (ZQL_videosInfos[index].srsrtc) {
return;
}
ZQL_videosInfos[index].loading = true;
let video = document.getElementById("video" + index);
let stream = ZQL_videosInfos[index].stream;
var srsrtc;
if (stream.indexOf("webrtc") >= 0) {
let videosrc =
"webrtc://" + ZQLGLOBAL.serverIp + "/live" + stream.split("/live")[1];
srsrtc = new JSWebrtc.Player(videosrc, {
video: video,
autoplay: true,
onPlay: (obj) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].loading = false;
ZQL_videosInfos[index].playerState = "success";
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
}
ZQL_videosInfos[index].refreshTime =
parseInt((Math.random() * 5 + 5) * 1000) * 60;
ZQL_videosInfos[index].refreshTimeInterval = setInterval(() => {
ZQL_multivideo.handleRefresh(index);
}, ZQL_videosInfos[index].refreshTime);
},
});
} else if (stream.indexOf(".flv") >= 0) {
let videosrc = `http://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.srs_http_server}/live${
stream.split("/live")[1]
}`;
srsrtc = mpegts.createPlayer(
{
type: "flv",
url: videosrc,
isLive: true,
},
{ enableWorker: true }
);
srsrtc.attachMediaElement(video);
srsrtc.load();
srsrtc
.play()
.then((res) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].playerState = "success";
ZQL_videosInfos[index].loading = false;
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
}
ZQL_videosInfos[index].refreshTime =
parseInt((Math.random() * 5 + 5) * 1000) * 60;
ZQL_videosInfos[index].refreshTimeInterval = setInterval(() => {
ZQL_multivideo.handleRefresh(index);
}, ZQL_videosInfos[index].refreshTime);
})
.catch((err) => { });
if (ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
}
ZQL_videosInfos[index].replayTimer = setTimeout(() => {
ZQL_multivideo.replayflv(srsrtc, cameraId, index);
}, 3000);
} else {
video.src = "staticdata/" + stream.split("/home/linaro/ks/")[1];
}
ZQL_videosInfos[index].srsrtc = srsrtc;
},
replayflv(srsrtc, cameraId, index) {
if (!ZQL_videosInfos[index]) {
return;
}
if (ZQL_videosInfos[index].playerState == "success") {
return;
} else {
srsrtc.unload();
srsrtc.load();
srsrtc
.play()
.then((res) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].playerState = "success";
ZQL_videosInfos[index].loading = false;
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
}
ZQL_videosInfos[index].refreshTime =
parseInt((Math.random() * 5 + 5) * 1000) * 60;
ZQL_videosInfos[index].refreshTimeInterval = setInterval(() => {
ZQL_multivideo.handleRefresh(index);
}, ZQL_videosInfos[index].refreshTime);
})
.catch((err) => {
// this.destoryVideoByIndex(index);
// this.subscribeLive(cameraId, index);
});
if (ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
}
ZQL_videosInfos[index].replayTimer = setTimeout(() => {
ZQL_multivideo.replayflv(srsrtc, cameraId, index);
}, 3000);
}
},
reSubcribe(cameraId, index) {
if (ZQL_videosInfos[index].subscribeTimeout) {
clearTimeout(ZQL_videosInfos[index].subscribeTimeout);
ZQL_videosInfos[index].subscribeTimeout = null;
}
ZQL_multivideo.videosInfos[index].subscribeTimeout = setTimeout(() => {
ZQL_multivideo.subscribeLive(cameraId, index);
}, 1000);
},
getCameraSize(id, index) {
ZQL_multivideo.setOrisize(
ZQL_sources[id].draw_size[0],
ZQL_sources[id].draw_size[1],
index, id
);
},
setOrisize(width, height, index, id) {
let container = document.querySelector(".video-box");
if (!container) {
return;
}
if (!ZQL_videosInfos[index]) {
let alg = null;
if (sessionStorage.getItem("curalgs")) {
let cameraId = ZQL_playingSource[index];
let curalgs = JSON.parse(sessionStorage.getItem("curalgs"));
alg = curalgs[cameraId]
? JSON.parse(JSON.stringify(curalgs[cameraId]))
: null;
}
ZQL_videosInfos[index] = {
id: id,
loading: true,
openWs: true,
alg: alg,
algListShow: false,
subscribeTimeout: null,
refreshTimeInterval: null, // 定时刷新定时器
refreshTime: null, // 定时刷新时间
replayTimer: null,
playerState: "pending",
detectInterval: null,
quanping: false,
srsrtc: null,
stream: "",
status: "",
};
}
if (ZQL_videosInfos[index]) {
let oriWidth = width;
let oriHeight = height;
ZQL_videosInfos[index].oriWidth = oriWidth;
ZQL_videosInfos[index].oriHeight = oriHeight;
if (
oriWidth / container.offsetWidth >
oriHeight / container.offsetHeight
) {
ZQL_videosInfos[index].actualHeight = container.offsetWidth / (oriWidth / oriHeight)
ZQL_videosInfos[index].actualWidth = container.offsetWidth;
} else {
ZQL_videosInfos[index].actualHeight = container.offsetHeight
ZQL_videosInfos[index].actualWidth = container.offsetHeight * (oriWidth / oriHeight)
}
// videoWidth = ZQL_videosInfos[index].actualWidth;
ZQL_multivideo.setPosition(index);
}
},
setPosition(index) {
let container = document.querySelector(".video-box");
let video = document.querySelector("#video" + index);
let canvas = document.getElementById("canvas" + index);
let width = ZQL_videosInfos[index].actualWidth, height = ZQL_videosInfos[index].actualHeight;
video.style.position = "absolute";
video.style.width = width + "px";
video.style.height = height + "px";
canvas.width = width;
canvas.height = height;
if (width / container.offsetWidth < height / container.offsetHeight) {
let left = (container.offsetWidth - width) / 2;
video.style.left = Math.floor(left) + "px";
video.style.top = 0 + "px";
canvas.style.left = Math.floor(left) + "px";
canvas.style.top = "0px";
} else {
let top = (container.offsetHeight - height) / 2;
video.style.top = Math.floor(top) + "px";
video.style.left = 0 + "px";
canvas.style.top = Math.floor(top) + "px";
canvas.style.left = "0px";
}
},
setAlarms: (data, index) => {
ZQL_multivideo.clearCanvas(index);
if (ZQL_videosInfos[index] && !ZQL_videosInfos[index].canvas) {
ZQL_videosInfos[index].canvas = document.getElementById("canvas" + index)
}
if (
!ZQL_videosInfos[index] ||
!ZQL_videosInfos[index].actualWidth ||
!ZQL_videosInfos[index].actualHeight ||
!ZQL_videosInfos[index].oriWidth ||
!ZQL_videosInfos[index].oriHeight
) {
return;
}
// let bbox = data.result.data.bbox;
let bbox = data.bbox;
if (Object.values(bbox.polygons).length > 0) {
Object.values(bbox.polygons).forEach((item) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
// let color = item.color;
let points = item.polygon.map((point) => {
return [
Math.round(
(point[0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
Math.round(
(point[1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
];
});
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "transparent";
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
ZQL_multivideo.drawPolygons(points, context);
ZQL_multivideo.drawPolygonInfo(context, Object.values(bbox.polygons), index);
});
}
if (bbox.rectangles.length > 0) {
bbox.rectangles.forEach((item, i) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
let coordinates = {
x: Math.round(
(item.xyxy[0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y: Math.round(
(item.xyxy[1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
x1: Math.round(
(item.xyxy[2] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y1: Math.round(
(item.xyxy[3] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
};
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "rgb(" + color.join(",") + ")";
context.fillText(item.label || "", coordinates.x, coordinates.y - 10);
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
// context.strokeRect(
// coordinates.x,
// coordinates.y,
// coordinates.x1 - coordinates.x,
// coordinates.y1 - coordinates.y
// );
let lines = [];
let lineWidth = (coordinates.x1 - coordinates.x) / 4;
let lineHeight = (coordinates.y1 - coordinates.y) / 4;
lines[0] = {
x: coordinates.x,
y: coordinates.y,
x1: coordinates.x + lineWidth,
y1: coordinates.y,
};
lines[1] = {
x: coordinates.x,
y: coordinates.y,
x1: coordinates.x,
y1: coordinates.y + lineHeight,
};
lines[2] = {
x: coordinates.x1,
y: coordinates.y,
x1: coordinates.x1 - lineWidth,
y1: coordinates.y,
};
lines[3] = {
x: coordinates.x1,
y: coordinates.y,
x1: coordinates.x1,
y1: coordinates.y + lineHeight,
};
lines[4] = {
x: coordinates.x,
y: coordinates.y1,
x1: coordinates.x + lineWidth,
y1: coordinates.y1,
};
lines[5] = {
x: coordinates.x,
y: coordinates.y1,
x1: coordinates.x,
y1: coordinates.y1 - lineHeight,
};
lines[6] = {
x: coordinates.x1,
y: coordinates.y1,
x1: coordinates.x1 - lineWidth,
y1: coordinates.y1,
};
lines[7] = {
x: coordinates.x1,
y: coordinates.y1,
x1: coordinates.x1,
y1: coordinates.y1 - lineHeight,
};
lines.forEach((item) => {
ZQL_multivideo.drawLine(context, item);
});
});
}
if (Object.values(bbox.lines).length > 0) {
Object.values(bbox.lines).forEach((item, i) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
let coordinates = {
x: Math.round(
(item.line[0][0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y: Math.round(
(item.line[0][1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
x1: Math.round(
(item.line[1][0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y1: Math.round(
(item.line[1][1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
};
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "rgb(" + color.join(",") + ")";
if (item.ext.direction) {
context.fillText(item.name, (coordinates.x + coordinates.x1) / 2, (coordinates.y + coordinates.y1) / 2 + 20);
}
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
ZQL_multivideo.drawLine(context, coordinates);
ZQL_multivideo.drawCountingInfo(context, Object.values(bbox.lines));
});
}
},
drawPolygons(points, context) {
context.beginPath();
context.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
context.lineTo(points[i][0], points[i][1]);
}
context.closePath();
context.fill();
context.stroke();
},
drawLine(ctx, line) {
ctx.beginPath();
ctx.moveTo(line.x, line.y);
ctx.lineTo(line.x1, line.y1);
ctx.stroke();
},
drawCountingInfo(context, lines) {
lines.forEach((item, index) => {
context.fillStyle = "rgb(255,0,0)";
if (item.ext.direction.length == 2) {
context.fillText(`[${item.name}] ${item.ext.action.count}: ${item.ext.result.count}`, 0, 20 * index + 20);
} else {
context.fillText(`[${item.name}] ${item.ext.action.increase}: ${item.ext.result.increase},${item.ext.action.decrease}: ${item.ext.result.decrease},${item.ext.action.delta}: ${item.ext.result.delta}`, 0, 20 * index + 20);
}
});
},
drawPolygonInfo(context, polygons, videoindex) {
polygons.forEach((item, index) => {
context.fillStyle =
"rgb(" +
JSON.parse(JSON.stringify(item.color)).reverse().join(",") +
")";
let leftPoint = item.polygon[0];
for (let i = 1; i < item.polygon.length; i++) {
if (item.polygon[i][0] < leftPoint[0]) {
leftPoint = item.polygon[i];
}
}
context.fillText(
`${item.name}`,
(leftPoint[0] * ZQL_videosInfos[videoindex].actualWidth) /
ZQL_videosInfos[videoindex].oriWidth,
(leftPoint[1] * ZQL_videosInfos[videoindex].actualHeight) /
ZQL_videosInfos[videoindex].oriHeight + 20
);
if (item.ext.result) {
context.fillStyle = "rgb(255,0,0)";
context.fillText(`${item.name}: ${item.ext.result}`, 0, 20 * index + 20);
}
});
},
destroyVideo(videonum) {
for (let i = 0; i < videonum; i++) {
ZQL_multivideo.destoryVideoByIndex(i);
}
},
destoryVideoByIndex(index) {
ZQL_multivideo.clearCanvas(index);
if (ZQL_videosInfos[index]) {
if (
ZQL_videosInfos[index] &&
ZQL_videosInfos[index].subscribeTimeout
) {
clearTimeout(ZQL_videosInfos[index].subscribeTimeout);
ZQL_videosInfos[index].subscribeTimeout = null;
}
if (ZQL_videosInfos[index] && ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
ZQL_videosInfos[index].replayTimer = null;
}
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
ZQL_videosInfos[index].refreshTimeInterval = null;
}
let video = document.getElementById("video" + index);
video && (video.srcObject = null);
ZQL_videosInfos[index].srsrtc &&
ZQL_videosInfos[index].srsrtc.destroy();
ZQL_multivideo.clearCanvas(index);
ZQL_videosInfos[index] = null;
}
},
clearCanvas(index) {
let canvas = document.getElementById("canvas" + index);
if (canvas && canvas.getContext("2d")) {
canvas
.getContext("2d")
.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
}
},
connectMqtt() {
let mqttclient = mqtt.connect(`ws://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.websocket}/mqtt`);
mqttclient.subscribe(
ZQLGLOBAL.resultTopic,
{ qos: 0 },
(error) => {
if (error) {
console.log("subscribe error:", error);
return;
}
}
);
mqttclient.on("message", (topic, payload) => {
let msg = JSON.parse(payload.toString());
if (msg.msg_type == "result") {
let id = msg.data.device.id + "_" + msg.data.source.id;
for (let i = 0; i < 4; i++) {
if (ZQL_videosInfos[i]) {
let alg =
ZQL_videosInfos[i].alg && ZQL_videosInfos[i].alg.algname;
if (
id == ZQL_playingSource[i] &&
msg.data.alg.name == ZQL_videosInfos[i].alg
) {
ZQL_multivideo.setAlarms(msg.data.reserved_data, i);
if (
ZQL_videosInfos[i] &&
ZQL_videosInfos[i].canvasTimeout
) {
clearTimeout(ZQL_videosInfos[i].canvasTimeout);
}
ZQL_videosInfos[i].canvasTimeout = setTimeout(() => {
ZQL_multivideo.clearCanvas(i);
}, 1000);
break;
}
}
}
}
});
}
}
const ZQL_apis = {
getDevices: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.getDevices}`,
// header: { Authorization: `Bearer ${ZQLGLOBAL.token}`},
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", `Bearer ${ZQLGLOBAL.token}`);
},
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getSources: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.getSources}`,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", `Bearer ${ZQLGLOBAL.token}`);
},
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
subscribeLive: (device_id, source_id) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.subscribe}?device_id=${device_id}&source_id=${source_id}`,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", `Bearer ${ZQLGLOBAL.token}`);
},
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
sysArgs: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.sysArgs}`,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", `Bearer ${ZQLGLOBAL.token}`);
},
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
detectStream: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.srs_http_api}/api/v1/streams?start=0&count=10000`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
detectVideo: (device_id, stream) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.detect}?device_id=${device_id}&stream=${stream}&draw_size=1280`,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", `Bearer ${ZQLGLOBAL.token}`);
},
success: function (res) {
if (res.error == 0) {
resolve({ status: 1 })
} else {
resolve({ status: 0 })
}
},
error: function (err) {
reject(err)
}
});
})
},
gettoken: () => {
var ak = ZQLGLOBAL.accessKey;
var sk = ZQLGLOBAL.accessSecret;
var timestamp = parseInt(new Date().getTime() / 1000);
var nonce = ZQL_apis.generateRandomString(10);
let signature = ZQL_apis.generateSignature(ak,sk,timestamp, nonce)
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.getToken}?signature=${signature}&ak=${ak}&timestamp=${timestamp}&nonce=${nonce}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
generateSignature: (ak,sk,timestamp, nonce) => {
var message = `${ak}:${timestamp}:${nonce}`;
var hash = CryptoJS.HmacSHA256(message, sk);
var signature = CryptoJS.enc.Hex.stringify(hash);
return signature
},
generateRandomString(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -0,0 +1,137 @@
.main-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: space-between;
padding: 10px;
box-sizing: border-box;
}
#video-container {
width: calc(100% - 310px);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.right-container {
width: 300px;
height: 100%;
overflow: auto;
}
#video-container .video-box {
background-color: #000;
position: relative;
}
#video-container .video-box .tips {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 1;
}
#video-container .video-box .tips .deviceoffline{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
#video-container .video-box canvas {
position: absolute;
}
#video-container.one-video .video-box {
width: 100%;
height: 100%;
}
#video-container.four-video .video-box {
width: calc(50% - 2px);
height: calc(50% - 2px);
}
#video-container.four-video .video-box:nth-child(1) {
margin-bottom: 4px;
}
#video-container.four-video .video-box:nth-child(1) {
margin-bottom: 4px;
}
.video-box .title-container {
position: absolute;
top: 0;
width: 100%;
height: 40px;
background-color: rgb(131 186 233 / 45%);
color: #fff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
box-sizing: border-box;
z-index: 1;
}
.video-box .title-container .alg {
width: 300px;
position: relative;
}
.video-box .title-container ul {
display: none;
background-color: rgb(131 186 233 / 45%);
}
.video-box .title-container:hover ul {
display: block;
position: absolute;
top: 30px;
left: 40px;
max-height: 300px;
overflow: auto;
}
.video-box .title-container:hover ul li {
width: 100%;
height: 32px;
box-sizing: border-box;
padding: 5px;
cursor: pointer;
}
/* 加载中 */
.icon-dot {
position: relative;
display: block;
border-radius: 50%;
background-color: #39f;
width: 40px;
height: 40px;
animation: ani-spin-bounce 1s 0s ease-in-out infinite;
}
@keyframes ani-spin-bounce {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
opacity: 0;
}
}
.layui-tree-main{
position: relative;
}

View File

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<title>webrtc</title>
<meta charset="utf-8">
<link href="js/layui/css/layui.css" rel="stylesheet">
<link href="index.css" rel="stylesheet">
<script>
let ZQLGLOBAL = {
token:'',
accessKey:'672439f1c3806207f0540f2d', // 修改成你自己的
accessSecret:'62960c23-fe35-4449-8780-719ab3c7b9de',// 修改成你自己的
serverIp: '192.168.1.88' ,// 修改成你自己的
getDevices: `:9192/ks/proxy/device`,
getSources: `:9192/ks/proxy/source`,
subscribe: `:9192/stream/live/subscribe`,
detect: `:9192/stream/attr`,
sysArgs: `:9192/ks/proxy/system/args`,
getToken:':9192/ks/proxy/user/token',
resultTopic: 'ks/proxy/result/+',
srs_server: 1935,
srs_http_api: 1985,
srs_http_server: 8080,
websocket: 8083,
detectSrsTimer:null
}
let ZQL_videosInfos = { 0: null, 1: null, 2: null, 3: null };
let ZQL_playingSource = {
0: null, 1: null, 2: null, 3: null,
videoNum: 4,
curposition: -1
}
let ZQL_sources = {};
</script>
<script type="text/javascript" src="js/crypto-js.js"></script>
<script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="js/jswebrtc.min.js"></script>
<script type="text/javascript" src="js/mpegts.js"></script>
<script type="text/javascript" src="js/mqtt.min.js"></script>
<script type="text/javascript" src="js/layui/layui.js"></script>
<script type="text/javascript" src="ZQL_common.js"></script>
</head>
<body>
<div class="main-container">
<div id="video-container" class="one-video">
<div class="video-box">
<div class="title-container" id="video-title1"></div>
<video ref="video" muted id="video1" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas1"></canvas>
</div>
</div>
<div class="right-container">
<!-- <div class="btn-container">
<i class="z-icon-onevideo" id="icon-oneviveo"></i>
<i class="z-icon-fourvideo" id="icon-fourviveo"></i>
</div> -->
<div id="ZQL_source_tree"></div>
</div>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</html>

View File

@ -0,0 +1,118 @@
// 设置文档结构
ZQL_multivideo.setVideoEl();
// 设置事件监听
// document.querySelector("#icon-oneviveo").addEventListener('click', () => {
// playingSource.videoNum = 1;
// setVideoEl();
// })
// document.querySelector("#icon-fourviveo").addEventListener('click', () => {
// playingSource.videoNum = 4;
// ZQL_multivideo.setVideoEl();
// })
ZQL_apis.gettoken().then(res => {
if(res.error_code == 0){
ZQLGLOBAL.token = res.data;
init();
}
})
// 1. 获取设备列表,摄像头列表,组成树形结构
function init() {
Promise.all([ZQL_apis.getDevices(), ZQL_apis.getSources()]).then(res => {
let devices = res[0].data;
for (let deviceId in res[1].data) {
for (let sourceId in res[1].data[deviceId]) {
res[1].data[deviceId][sourceId].sourceId = sourceId
res[1].data[deviceId][sourceId].deviceId = deviceId
}
}
for (let i = 0; i < devices.length; i++) {
devices[i].deviceId = devices[i].id;
devices[i].title = devices[i].name;
devices[i].type = 'device';
if (res[1].data[devices[i].id]) {
devices[i].children = Object.values(res[1].data[devices[i].id]).map(item => {
item.id = devices[i].deviceId + '_' + item.sourceId;
item.title = item.desc;
item.type = 'source';
item.checked = false;
ZQL_sources[devices[i].deviceId + '_' + item.sourceId] = item
return item
})
} else {
// 没有摄像头的设备不显示
devices.splice(i, 1)
i = i - 1
}
}
layui.use(function () {
var tree = layui.tree;
var layer = layui.layer;
tree.render({
elem: '#ZQL_source_tree',
data: devices,
// showCheckbox:true,
onlyIconControl: true, // 是否仅允许节点左侧图标控制展开收缩
click: function (obj) {
if (obj.data.sourceId) {
let key = obj.data.deviceId + '_' + obj.data.sourceId;
if (ZQL_sources[key].checked == false) {
ZQL_sources[key].checked = true
if (ZQL_playingSource.videoNum == 1) {
ZQL_playingSource[0] = key;
ZQL_multivideo.subscribeLive(key, 0);
ZQL_multivideo.setAlgList(0);
} else {
for (let i = 0; i < 4; i++) {
if (!ZQL_playingSource[i]) {
ZQL_playingSource[i] = key;
ZQL_multivideo.subscribeLive(key, i);
ZQL_multivideo.setAlgList(i)
break;
}
}
}
} else {
ZQL_sources[key].checked = false
if (ZQL_playingSource.videoNum == 1) {
ZQL_playingSource[0] = null;
ZQL_multivideo.destoryVideoByIndex(0);
ZQL_multivideo.clearAlgList(0);
ZQL_multivideo.liveStopLoading(0);
} else {
for (let i = 0; i < 4; i++) {
if (ZQL_playingSource[i] == key) {
ZQL_playingSource[i] = null;
ZQL_multivideo.destoryVideoByIndex(i);
ZQL_multivideo.clearAlgList(i)
ZQL_multivideo.liveStopLoading(i);
}
}
}
}
}
}
});
});
}).catch(err => {
console.log(err)
})
// 获取系统参数检测srs、连接mqtt
ZQL_apis.sysArgs().then(res => {
if (res.error_code == 0) {
let map = res.data.map;
ZQLGLOBAL = Object.assign(ZQLGLOBAL, map)
}
ZQL_multivideo.detectSrs();
ZQL_multivideo.connectMqtt()
}).catch(err => { })
}
//遗留
// 1.刷新
// 2.保存已选择的摄像头,
// 3.切换一分屏四分屏

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

12
上位机实时画面/js/mqtt.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,59 @@
package com.github.paicoding.forum.web.front.test;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* UDP 服务器
* 本类使用单线程的线程池实现一个简单的UDP服务器用于接收客户端发送的心跳信息。
* 实现方法多种多样,需要根据具体的业务场景进行选择。
*/
@Slf4j
@Component
public class UdpServer {
private static final int PORT = 10002;
private volatile boolean isRunning = true; // 标志变量
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@PostConstruct
public void startServer() {
executor.submit(this::listenUdpPackets);
}
@PreDestroy
public void stopServer() {
isRunning = false;
executor.shutdown();
log.info("UDP服务器已停止");
}
private void listenUdpPackets() {
try (DatagramSocket socket = new DatagramSocket(PORT)) {
log.info("UDP Server started on port {}", PORT);
byte[] buffer = new byte[102400];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while (isRunning) {
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength()).trim();
log.info("❤️ Heartbeat from: {} ", packet.getAddress());
log.info("📦 Data received: {}", message);
}
} catch (Exception e) {
log.error("UDP Server error: {}", e.getMessage(), e);
}
}
}

38
心跳保活/server.py Normal file
View File

@ -0,0 +1,38 @@
import json
import socket
import traceback
class UdpServer:
def __init__(self):
self.host = '0.0.0.0'
self.port = 10002
self.socket_server = self.__init()
def __init(self):
socket_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
socket_server.bind((self.host, self.port))
except:
print(traceback.format_exc())
finally:
return socket_server
def recv(self, buff_size=102400):
while True:
try:
data, addr = self.socket_server.recvfrom(buff_size)
data = json.loads(data.decode('utf-8'))
print('Received message: {}, from: {}'.format(data, addr))
except:
print(traceback.format_exc())
def main():
udp_server = UdpServer()
udp_server.recv()
return True
if '__main__' == __name__:
main()

View File

@ -0,0 +1,171 @@
## 一、功能概述
`Api` 类是一个用于处理检测结果、告警信息和告警视频的工具类。
[api_demo.py](api_demo.py) 为原始默认实现。
[api_demo_tcp.py](api_demo_tcp.py)实现最简单的tcp通讯如果需要自动重连队列等业务代码请自行实现。
此类名、方法名等框架是固定的,不可修改。你可以通过实现回调方法的具体逻辑(如 `send_result_callback``send_alert_callback``send_alert_video_callback`)以及配置类的属性(如 `ignore_result``ignore_alert` 等)来实现具体功能。
## 二、类的属性
`Api` 类**必须**包含以下主要属性,用于控制其行为:
| 属性名称 | 默认值 | 描述 |
| :------------------- | :----- | :----------------------------------------------------------- |
| `ignore_result` | `True` | 是否发送检测结果。`True` 表示不发送,`False` 表示发送。 |
| `ignore_alert` | `True` | 是否发送告警信息。`True` 表示不发送,`False` 表示发送。 |
| `draw_image` | `True` | 是否在告警图片上绘制告警信息。`True` 表示绘制,`False` 表示不绘制。 |
| `ignore_alert_video` | `True` | 是否发送告警视频。`True` 表示不发送,`False` 表示发送。 |
## 三、类的方法
`Api` 类**必须**包含以下三个方法,用于发送检测结果、告警信息和告警视频。可根据需求实现具体的回调方法。
### 1. 检测结果
- **方法**send_result_callback(self, result):
- **功能**:发送检测结果。
- **参数**
- `result`:检测结果的内容。具体格式和内容如下:
- **示例**
```json
{
"hit": false, //是否命中
"time": 1742458167.288579, //告警时间戳
"device": {
"id": "设备id",
"name": "设备名称",
"desc": "设备描述"
},
"source": {
"id": "数据源id",
"ipv4": "ip地址",
"desc": "数据源描述"
},
"alg": {
"name": "算法名称英文",
"ch_name": "算法名称中文",
"type": "general"
},
"reserved_data": {
"bbox": {
"rectangles": [
{
"xyxy": [668,562,790,656], //左上角、右下角坐标
"color": [0,0,255], //BGR颜色
"label": "未佩戴安全帽", //标签
"conf": 0.91, //置信度
"ext": {} //扩展字段
}
],
"polygons": {}, //多边形对象
"lines": {} //线段对象
},
"custom": {}
},
"hazard_level": "", //危险等级
}
```
### 2. 告警信息
- **方法**send_alert_callback(self, alert)
- **功能**:发送告警信息。
- **参数**
- `alert`:告警信息的内容。具体格式和内容如下:
- **示例**
```json
{
"id": "67dbcd3c5dc58a7aaa019e41", //告警id
"alert_time": 1742458171.808598, //告警时间戳
"device": {
"id": "设备id",
"name": "设备名称",
"desc": "设备描述"
},
"source": {
"id": "数据源id",
"ipv4": "ip地址",
"desc": "数据源描述"
},
"alg": {
"name": "算法名称英文",
"ch_name": "算法名称中文",
"type": "general"
},
"hazard_level": "", //危险等级
"image": "img_base64", //base64编码的图片数据
"reserved_data": {
"bbox": {
"rectangles": [
{
"xyxy": [668,560,790,656], //左上角、右下角坐标
"color": [0,0,255], //BGR颜色
"label": "未佩戴安全帽", //标签
"conf": 0.91, //置信度
"ext": {} //扩展字段
}
],
"polygons": {},//多边形对象
"lines": {} //线段对象
},
"custom": {}
}
}
```
### 3. 告警视频
- **方法**send_alert_video_callback(self, alert_video):
- **功能**:发送告警视频。
- **参数**
- `alert_video`:告警视频的内容。具体格式和内容如下:
- **示例**
```json
{
"id": "67dbcd3c5dc58a7aaa019e41", //告警id
"alert_time": 1742458171.808598, //告警时间戳
"device": {
"id": "设备id",
"name": "设备名称",
"desc": "设备描述"
},
"source": {
"id": "数据源id",
"ipv4": "ip地址",
"desc": "数据源描述"
},
"alg": {
"name": "算法名称英文",
"ch_name": "算法名称中文",
"type": "general"
},
"hazard_level": "", //危险等级
"video": "video_base64" //base64编码的视频数据
}
```
## 四、注意事项
1. **属性配置**:在调用发送方法之前,确保已经正确配置了类的属性,以启用或禁用所需的功能。
2. **方法实现**:默认情况下,`send_result_callback`、`send_alert_callback` 和 `send_alert_video_callback` 方法是空方法。在实际使用中,需要根据具体需求实现这些方法的逻辑,例如将数据发送到服务器。

View File

@ -0,0 +1,40 @@
class Api:
def __init__(self):
"""
Attributes:
self.ignore_result: 为True时不发送检测结果为False则发送检测结果
self.ignore_alert: 为True时不发送告警信息为False则发送告警信息
self.draw_image: 为True时告警图片会画上告警信息为False则不画
self.ignore_alert_video: 为True时不发送告警视频为False则发送
"""
self.ignore_result = True
self.ignore_alert = True
self.draw_image = True
self.ignore_alert_video = True
def send_result_callback(self, result):
"""
发送检测结果回调函数
Args:
result: 检测结果数据
Returns:
"""
pass
def send_alert_callback(self, alert):
"""
发送告警信息回调函数
Args:
alert: 告警数据
Returns:
"""
pass
def send_alert_video_callback(self, alert_video):
"""
发送告警视频回调函数
Args:
alert_video: 告警视频数据
Returns:
"""
pass

View File

@ -0,0 +1,68 @@
import json
import socket
# 导入日志
from logger import LOGGER
class Api:
"""
API类实现最简单的tcp通讯如果需要自动重连队列等业务代码请自行实现
"""
def __init__(self):
"""
Attributes:
self.ignore_result: 为True时不发送检测结果为False则发送检测结果
self.ignore_alert: 为True时不发送告警信息为False则发送告警信息
self.draw_image: 为True时告警图片会画上告警信息为False则不画
self.ignore_alert_video: 为True时不发送告警视频为False则发送
"""
self.ignore_result = True
self.ignore_alert = False
self.draw_image = True
self.ignore_alert_video = True
def send_result_callback(self, result):
"""
发送检测结果回调函数
Args:
result: 检测结果数据
Returns:
"""
pass
def send_alert_callback(self, alert):
"""
发送告警信息回调函数
Args:
alert: 告警数据
Returns:
"""
try:
LOGGER.info('发送TCP告警')
# 创建TCP socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 连接到目标服务器(这里用示例地址和端口)
s.connect(('192.168.0.4', 10001))
# 为了方便查看日志去掉base64编码图片
alert.pop('image')
json_data = json.dumps(alert, ensure_ascii=False)
data = json_data.encode('utf-8')
# 发送完整数据
s.sendall(data)
LOGGER.info(f'告警已发送至TCP服务器: {data}')
except Exception as e:
LOGGER.error(f'发送TCP告警失败: {str(e)}')
def send_alert_video_callback(self, alert_video):
"""
发送告警视频回调函数
Args:
alert_video: 告警视频数据
Returns:
"""
pass

View File

@ -0,0 +1,83 @@
# HTTP告警推送
> **http-server-demo** 分为三个文件夹。
>
> 1. **headers** http请求头的demo代码
> 2. **http-server** http服务端接收告警推送(无token版本)
> 3. **http-server-token** http服务端接收告警推送(有token版本)
## headers
如果需要将盒子产生的告警推送到您自建平台,并且你的平台需要验证**token**,则需要用到该文件夹下的`headers_demo.py`文件。
你可自行修改`headers_demo.py`文件,并将此文件上传到盒子平台的【数据推送】-【告警】-【HTTP】-【配置token】。
`headers_demo.py`文件说明:
- 类名必须为`Headers`,继承`BaseHeaders`类。`BaseHeaders`类通过`api.http`导入
```python
from api.http import Headers as BaseHeaders
```
- 定义三个实例变量:`get_headers_url`、`timeout`、`interval`。
`get_headers_url`:指定获取`token`的**URL**地址。
`timeout`:指定获取`token`的超时时间(单位:秒)。
`interval`:定时刷新`headers`的时间间隔(单位:秒)。
```python
class Headers(BaseHeaders):
def __init__(self):
self.get_headers_url = None
self.timeout = 5
interval = 60 * 10
super().__init__(interval)
```
- 必须实现`_generate_headers`方法。返回请求头字典`headers`。返回示例:
```python
{'authorization': 'Bearer abcdefghijklmnopqrstuvwxyz'}
```
- 完整实例如下:
```python
import requests
from api.http import Headers as BaseHeaders
from logger import LOGGER
class Headers(BaseHeaders):
def __init__(self):
self.get_headers_url = None
self.timeout = 5
interval = 60 * 10
super().__init__(interval)
def _generate_headers(self):
try:
headers = {
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkXXX'
}
return headers
except:
LOGGER.exception('_generate_headers')
return None
```
## http-server
该文件夹为**http**服务端代码,提供**python**和**java**代码。它主要用于接收盒子的**http**告警推送。如果您需要验证盒子的**http**推送功能是否正常,可使用此文件夹进行测试。
运行该文件夹下的代码,即可开启一个**http**服务端。在盒子平台的【数据推送】-【告警】-【HTTP】中启用推送管理并填写**http**服务端地址,即可开启推送功能。
## http-server-token
此文件夹同**http-server**文件夹,只是增加了**token**验证功能。
你首先需把其下的`headers_demo1.py`或`headers_demo2.py`文件上传到盒子平台的【数据推送】-【告警】-【HTTP】-【配置TOKEN】。
`headers_demo1.py`通过调用url接口获取token。(盒子必须可以ping通该url文件中的`get_headers_url`变量为**http**服务端URL)
`headers_demo2.py`固定token。

View File

@ -0,0 +1,46 @@
import requests
from api.http import Headers as BaseHeaders
from logger import LOGGER
class Headers(BaseHeaders):
def __init__(self):
"""
初始化Headers类
- `self.get_headers_url`: 获取token的URL地址根据实际环境修改。
- `self.timeout`: 请求超时时间设置为5秒。
- `interval`: 定时刷新headers的时间间隔设置为10分钟60秒 * 10
"""
self.get_headers_url = None
self.timeout = 5
interval = 60 * 10
super().__init__(interval)
def _generate_headers(self):
"""
生成请求头的方法_generate_headers方法名不允许修改
通过向指定的URL发送GET请求获取token并将token添加到请求头中
:return: 请求头字典
"""
try:
# 定义请求参数
params = {
'arg1': 'xxx',
'arg2': 'xxx'
}
if self.get_headers_url is not None:
# 发送GET请求获取token
resp = requests.get(self.get_headers_url, params=params, timeout=self.timeout)
if resp.status_code == 200:
token = resp.text
headers = {
'authorization': 'Bearer {}'.format(token)
}
return headers
else:
LOGGER.error('Get headers failed')
return None
except:
LOGGER.exception('_generate_headers')
return None

View File

@ -0,0 +1,47 @@
import requests
from api.http import Headers as BaseHeaders
from logger import LOGGER
class Headers(BaseHeaders):
def __init__(self):
"""
初始化Headers类
- `self.get_headers_url`: 获取token的URL地址根据实际环境修改。
- `self.timeout`: 请求超时时间设置为5秒。
- `interval`: 定时刷新headers的时间间隔设置为10分钟60秒 * 10
"""
self.get_headers_url = 'http://192.168.1.75:10000/token'
self.timeout = 5
interval = 60 * 10
super().__init__(interval)
def _generate_headers(self):
"""
生成请求头的方法_generate_headers方法名不允许修改
通过向指定的URL发送GET请求获取token并将token添加到请求头中
:return: 请求头字典
"""
try:
# 定义请求参数
params = {
'arg1': 'xxx',
'arg2': 'xxx'
}
if self.get_headers_url is not None:
# 发送GET请求获取token
resp = requests.get(self.get_headers_url, params=params, timeout=self.timeout)
LOGGER.info('Get headers resp {}'.format(resp))
if resp.status_code == 200:
token = resp.text
headers = {
'authorization': 'Bearer {}'.format(token)
}
return headers
else:
LOGGER.error('Get headers failed')
return None
except:
LOGGER.exception('_generate_headers')
return None

View File

@ -0,0 +1,35 @@
import requests
from api.http import Headers as BaseHeaders
from logger import LOGGER
class Headers(BaseHeaders):
def __init__(self):
"""
初始化Headers类
- `self.get_headers_url`: 获取token的URL地址根据实际环境修改。
- `self.timeout`: 请求超时时间设置为5秒。
- `interval`: 定时刷新headers的时间间隔设置为10分钟60秒 * 10
"""
self.get_headers_url = None
self.timeout = 5
interval = 60 * 10
super().__init__(interval)
def _generate_headers(self):
"""
生成请求头的方法_generate_headers方法名不允许修改
固定http请求头authorization以及值可自定义
:return: 请求头字典
"""
try:
headers = {
'authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkXXX'
}
return headers
except:
LOGGER.exception('_generate_headers')
return None

View File

@ -0,0 +1,45 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(path = "")
public class AlertController {
/**
* 目标平台接收告警及告警图片
* @param alertMsg
* @param authorization
*/
@PostMapping(path = "/alert")
public void getAlertMsg(@RequestBody AlertMsg alertMsg, @RequestHeader("Authorization") String authorization) {
log.info("authorization{}", authorization);
log.info("示例接收告警及告警图片:{}", alertMsg);
}
/**
* 目标平台接收告警及告警视频
*
* @param alertVideo
*/
@PostMapping(path = "/video")
public void getAlertVideo(@RequestBody AlertVideo alertVideo,@RequestHeader("Authorization") String authorization) {
log.info("authorization{}", authorization);
log.info("示例接收告警及告警视频:{}", alertVideo);
}
@GetMapping(path = "/token")
public String token() {
return RandomUtil.randomString(16);
}
}

View File

@ -0,0 +1,20 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class AlertMsg {
private String id;
@JsonProperty("alert_time")
private Double alertTime;
private Object device;
private Object source;
private Object alg;
private String image;
@JsonProperty("reserved_data")
private Object reservedData;
@JsonProperty("hazard_leve")
private String hazardLeve;
}

View File

@ -0,0 +1,18 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class AlertVideo {
private String id;
@JsonProperty("alert_time")
private Double alertTime;
private Object device;
private Object source;
private Object alg;
private String video;
@JsonProperty("hazard_leve")
private String hazardLeve;
}

View File

@ -0,0 +1,23 @@
import os
import sys
from flask import Flask
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
sys.path.append(CURRENT_PATH)
url_prefix = '/'
from app import alert
def create_app():
# 初始化Flask对象
app_ = Flask(__name__)
# 注册蓝图
app_.register_blueprint(alert.bp)
return app_
app = create_app()

View File

@ -0,0 +1,31 @@
import base64
import hashlib
import json
import secrets
import time
from flask import Blueprint, request
from app import url_prefix
bp = Blueprint('alert', __name__, url_prefix=url_prefix)
@bp.route('alert', methods=['POST'])
def post_alert():
# 获取token
auth_header = request.headers.get('authorization')
print(f"Authorization Header: {auth_header}")
data = json.loads(request.get_data().decode('utf-8'))
image = data.pop('image')
print(data)
with open('image.jpg', 'wb') as f:
f.write(base64.b64decode(image.encode('utf-8')))
return data
@bp.route('/token', methods=['GET'])
def gen_token():
print(request.args)
token = secrets.token_hex(32)
return token

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -0,0 +1,4 @@
from app import app
if '__main__' == __name__:
app.run(host='0.0.0.0', port=10000, debug=False)

View File

@ -0,0 +1,37 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping(path = "")
public class AlertController {
/**
* 目标平台接收告警及告警图片
*
* @param alertMsg
*/
@PostMapping(path = "/alert")
public void getAlertMsg(@RequestBody AlertMsg alertMsg) {
log.info("示例接收告警及告警图片:{}", alertMsg);
}
/**
* 目标平台接收告警及告警视频
*
* @param alertVideo
*/
@PostMapping(path = "/video")
public void getAlertVideo(@RequestBody AlertVideo alertVideo) {
log.info("示例接收告警及告警视频:{}", alertVideo);
}
}

View File

@ -0,0 +1,21 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class AlertMsg {
private String id;
@JsonProperty("alert_time")
private Double alertTime;
private Object device;
private Object source;
private Object alg;
private String image;
@JsonProperty("reserved_data")
private Object reservedData;
@JsonProperty("hazard_leve")
private String hazardLeve;
}

View File

@ -0,0 +1,19 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class AlertVideo {
private String id;
@JsonProperty("alert_time")
private Double alertTime;
private Object device;
private Object source;
private Object alg;
private String video;
@JsonProperty("hazard_leve")
private String hazardLeve;
}

View File

@ -0,0 +1,23 @@
import os
import sys
from flask import Flask
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
sys.path.append(CURRENT_PATH)
url_prefix = '/'
from app import alert
def create_app():
# 初始化Flask对象
app_ = Flask(__name__)
# 注册蓝图
app_.register_blueprint(alert.bp)
return app_
app = create_app()

View File

@ -0,0 +1,28 @@
import base64
import json
from flask import Blueprint, request
from app import url_prefix
bp = Blueprint('alert', __name__, url_prefix=url_prefix)
@bp.route('alert', methods=['POST'])
def post_alert():
data = json.loads(request.get_data().decode('utf-8'))
image = data.pop('image')
print(data)
with open('image.jpg', 'wb') as f:
f.write(base64.b64decode(image.encode('utf-8')))
return data
@bp.route('alert/video', methods=['POST'])
def post_alert_video():
data = json.loads(request.get_data().decode('utf-8'))
video = data.pop('video')
print(data)
with open('video.mp4', 'wb') as f:
f.write(base64.b64decode(video.encode('utf-8')))
return data

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -0,0 +1,4 @@
from app import app
if '__main__' == __name__:
app.run(host='0.0.0.0', port=10000, debug=False)

View File

@ -0,0 +1,107 @@
import json
import socket
import struct
import threading
import time
import traceback
class SocketServer:
def __init__(self):
self.server_host = '0.0.0.0'
self.server_port = 10001
self.socket_server = self.__listen()
self.conns = {}
self.__accept()
@staticmethod
def __set_reuse_addr(socket_obj):
"""
断开连接之后立马释放本地端口
Args:
socket_obj: socket对象
Returns: True or False
"""
socket_obj.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return True
def __listen(self):
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.__set_reuse_addr(socket_server)
# 绑定IP/端口
socket_server.bind((self.server_host, self.server_port))
# 最多同时处理5个连接请求
socket_server.listen(5)
except:
print(traceback.format_exc())
finally:
return socket_server
def __disconnect(self, addr):
print('Disconnected, client={}'.format(addr))
self.__close(self.conns[addr])
self.conns.pop(addr)
return True
def __accept(self):
def accept():
while True:
try:
conn, addr = self.socket_server.accept()
print('Connection established, client={}'.format(addr))
self.__set_reuse_addr(conn)
self.conns[addr] = conn
threading.Thread(target=self.__recv, args=(addr, conn), daemon=True).start()
except:
print(traceback.format_exc())
threading.Thread(target=accept, daemon=True).start()
return True
def __recv(self, addr, client_socket, buff_size=1024):
while True:
try:
data_length = client_socket.recv(4)
# 读取data_length
if data_length:
data_length = struct.unpack('i', data_length)[0]
print('Recv from: {}, data_length: {}'.format(addr, data_length))
# 读取data
if data_length <= buff_size:
data = client_socket.recv(data_length)
else:
buff_size_ = buff_size
# 已接收到的size
total_recv_size = 0
data = b''
while total_recv_size < data_length:
recv_data = client_socket.recv(buff_size_)
data += recv_data
total_recv_size += len(recv_data)
left_size = data_length - total_recv_size
if left_size < buff_size:
buff_size_ = left_size
data = json.loads(data.decode('utf-8'))
print('Recv from: {}, data: {}'.format(addr, data))
else:
break
except:
print(traceback.format_exc())
break
self.__disconnect(addr)
return False
@staticmethod
def __close(socket_obj):
try:
socket_obj.close()
except:
print(traceback.format_exc())
return True
if '__main__' == __name__:
tcp_server = SocketServer()
while True:
time.sleep(3)

View File

@ -0,0 +1,23 @@
import os
import sys
from flask import Flask
CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
sys.path.append(CURRENT_PATH)
url_prefix = '/'
from app import vlreview
def create_app():
# 初始化Flask对象
app_ = Flask(__name__)
# 注册蓝图
app_.register_blueprint(vlreview.bp)
return app_
app = create_app()

View File

@ -0,0 +1,15 @@
import base64
import json
from flask import Blueprint, request
from app import url_prefix
bp = Blueprint('vlreview', __name__, url_prefix=url_prefix)
@bp.route('vlreview', methods=['POST'])
def post_alert():
data = json.loads(request.get_data().decode('utf-8'))
print(data)
return data

View File

@ -0,0 +1,4 @@
from app import app
if '__main__' == __name__:
app.run(host='0.0.0.0', port=10010, debug=False)

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View File

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备信息</title>
<link rel="stylesheet" href="https://unpkg.com/layui@2.8.18/dist/css/layui.css">
<link rel="stylesheet" href="public/style.css">
<script>
let ZQLGLOBAL = {
serverIp: '192.168.1.169',// 修改成您的盒子aiboxd的IP地址
device: `:9091/ks/device`, // 视频源增删改查
};
</script>
</head>
<body>
<div class="layui-container" style="padding: 20px;">
<div class="layui-card">
<div class="layui-card-header">
<h2>设备信息</h2>
</div>
<div class="layui-card-body">
<form class="layui-form" action="" lay-filter="deviceForm">
<div class="layui-form-item">
<label class="layui-form-label">设备ID</label>
<div class="layui-input-block">
<input type="text" name="device_id" disabled lay-verify="required" placeholder="请输入" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">名称</label>
<div class="layui-input-block">
<input type="text" name="device_name" lay-verify="required" placeholder="请输入" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">设备描述</label>
<div class="layui-input-block">
<input type="text" name="device_desc" lay-verify="required" placeholder="请输入" autocomplete="off"
class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="saveData">保存</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- 引入Layui JS -->
<script src="https://unpkg.com/layui@2.8.18/dist/layui.js"></script>
<script src="public/jquery-1.10.2.min.js"></script>
<script src="device.js"></script>
</body>
</html>

View File

@ -0,0 +1,67 @@
class deviceInfo {
constructor() {
this.info = {};
this.init();
}
init() {
deviceApis.getDeviceInfo().then(res => {
console.log(res)
this.info = res.data;
this.initForm()
})
}
initForm() {
layui.use(() => {
var form = layui.form;
form.val('deviceForm', this.info);
form.on('submit(saveData)', (formData) => {
deviceApis.editDeviceInfo(Object.assign(this.info, formData.field)).then(res => {
console.log(res)
})
return false;
});
})
}
}
// 等待Layui加载完成后初始化
layui.use(['layer', 'form'], () => {
// 初始化表格管理器
window.deviceInfo = new deviceInfo();
});
const deviceApis = {
getDeviceInfo: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.device}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
editDeviceInfo: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "PUT",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.device}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
}

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人脸底库管理</title>
<link rel="stylesheet" href="https://unpkg.com/layui@2.8.18/dist/css/layui.css">
<link rel="stylesheet" href="public/style.css">
<style>
#addGroup {
position: absolute;
top: 10px;
left: 100px;
}
#group-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
#group-container li {
padding: 5px 10px;
border: 1px solid #d2d2d2;
border-radius: 4px;
}
#group-container li.active {
background-color: #00e4ff;
border-color: #00e4ff;
}
.layui-table-cell{
height: fit-content;
}
</style>
<script>
let ZQLGLOBAL = {
serverIp: '192.168.1.169',// 修改成您的盒子aiboxd的IP地址
face: `:9091/ks/face`, // 人脸增删改查
image: `:9091/ks/face/image`, // 图片
group: `:9091/ks/group`, // 底库分组
};
</script>
</head>
<body>
<div class="layui-container" style="padding: 20px;">
<div class="layui-card">
<div class="layui-card-header">
<h2>人脸底库管理</h2>
</div>
<div style="padding: 10px; position:relative;">
<h3 style="margin-bottom: 10px;">人脸分组</h3>
<ul id="group-container">
</ul>
<button class="layui-btn layui-btn-normal layui-btn-sm " id="addGroup">
<i class="layui-icon layui-icon-add-1"></i> 添加分组
</button>
</div>
<div class="layui-card-body">
<!-- 搜索和操作区域 -->
<div style="margin-bottom: 20px; display: flex;justify-content: space-between;">
<button class="layui-btn layui-btn-primary" id="searchBtn">
<i class="layui-icon layui-icon-search"></i> 查询
</button>
<button class="layui-btn layui-btn-normal" id="addBtn">
<i class="layui-icon layui-icon-add-1"></i> 添加数据
</button>
</div>
<!-- 表格区域 -->
<table id="dataTable" lay-filter="dataTable"></table>
<script type="text/html" id="image">
<img style="width:100px" src="{{= d.image }}"/>
</script>
</div>
</div>
</div>
<!-- 表格操作按钮模板 -->
<script type="text/html" id="operationBar">
<a class="layui-btn layui-btn-xs" lay-event="edit">
<i class="layui-icon layui-icon-edit"></i>
</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">
<i class="layui-icon layui-icon-delete"></i>
</a>
</script>
<!-- 引入Layui JS -->
<script src="https://unpkg.com/layui@2.8.18/dist/layui.js"></script>
<script src="public/jquery-1.10.2.min.js"></script>
<script src="facelib.js"></script>
</body>
</html>

View File

@ -0,0 +1,560 @@
class FaceLibManager {
constructor() {
this.page = 1;
this.size = 10;
this.group_id = '';
this.groupData = [];
this.data = [];
this.isEditing = false;
this.currentEditId = null;
this.table = null;
this.init();
}
async init() {
this.initLayui();
this.bindEvents();
await this.getGroupData();
this.getTableData();
}
// 初始化Layui
initLayui() {
layui.use(['table', 'layer', 'form'], () => {
const table = layui.table;
const layer = layui.layer;
const form = layui.form;
// 初始化表格
this.table = table.render({
elem: '#dataTable',
data: this.data,
cols: [[
// { field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'external_id', title: '外部ID', width: 150 },
{ field: 'name', title: '姓名', width: 150 },
{ field: 'image', title: '人脸图片', width: 150, templet: '#image' },
{ field: 'age', width: 100, title: '年龄', },
{ field: 'desc', title: '简介' },
{ field: 'update_time', width: 100, title: '更新时间', },
{ title: '操作', width: 200, toolbar: '#operationBar', fixed: 'right' }
]],
page: true,
limit: 10,
limits: [10, 20, 50],
height: 'full-220',
text: {
none: '暂无数据'
}
});
// 监听工具条事件
table.on('tool(dataTable)', (obj) => {
const data = obj.data;
if (obj.event === 'edit') {
this.editFaceModal(data, 'edit');
} else if (obj.event === 'del') {
this.deleteData(data.id);
}
});
});
}
getGroupData() {
return new Promise((resolve, reject) => {
faceLibApis.getGrpup().then(res => {
this.groupData = res.data.map(item => {
item.ext = JSON.parse(item.ext);
item.quality = item.ext.quality;
return item
});
if (this.groupData.length > 0) {
this.group_id = this.groupData[0].id;
resolve()
}
let el = document.querySelector("#group-container");
let innerHTML = ''
for (let i = 0; i < this.groupData.length; i++) {
if (i == 0) {
innerHTML += `<li class="group active" id="${this.groupData[i].id}">
${this.groupData[i].name}
<i class="layui-icon layui-icon-edit" editId="${this.groupData[i].id}"></i>
<i class="layui-icon layui-icon-delete" delId="${this.groupData[i].id}"></i>
</li>`;
} else {
innerHTML += `<li class="group" id="${this.groupData[i].id}">
${this.groupData[i].name}
<i class="layui-icon layui-icon-edit" editId="${this.groupData[i].id}"></i>
<i class="layui-icon layui-icon-delete" delId="${this.groupData[i].id}"></i>
</li>`;
}
}
el.innerHTML = innerHTML;
let editBtns = document.querySelectorAll("#group-container .layui-icon-edit");
editBtns.forEach(editBtn => {
editBtn.addEventListener('click', (e) => {
let group_id = e.target.getAttribute("editId");
this.editGroupModal(this.groupData.find(item => item.id == group_id), "edit")
})
})
let delBtns = document.querySelectorAll("#group-container .layui-icon-delete");
delBtns.forEach(delBtn => {
delBtn.addEventListener('click', (e) => {
let group_id = e.target.getAttribute("delId");
faceLibApis.delGroup({ ids: [group_id] }).then(res => {
this.getGroupData();
this.showNotification('数据更新成功!');
})
})
})
})
})
}
getTableData() {
faceLibApis.getFaceList({
page: this.page,
size: this.size,
group_id: this.group_id
}).then(res => {
this.data = res.data.data.map(item => {
// item.image = ZQLGLOBAL.serverIp + ':9092/staticdata' + item.image;
item.image = `http://${ZQLGLOBAL.serverIp}:9092/staticdata${item.image}`
return item
});
if (this.table) {
layui.use('table', () => {
layui.table.reload('dataTable', {
data: this.data
});
});
}
})
}
// 绑定事件
bindEvents() {
// 添加按钮事件
document.getElementById('addGroup').addEventListener('click', () => {
this.editGroupModal({
name: '',
quality: 0.35,
}, 'add');
});
document.getElementById('addBtn').addEventListener('click', () => {
this.editFaceModal({
name: '',
age: '',
sex: '',
desc: '',
external_id: '',
feature: '',
}, 'add');
});
// 搜索功能
document.getElementById('searchBtn').addEventListener('click', () => {
this.getTableData();
});
}
// 编辑分组
editGroupModal(data, type) {
layui.use(['layer', 'form'], () => {
const layer = layui.layer;
const form = layui.form;
layer.open({
type: 1,
title: type == 'add' ? '添加分组' : '编辑分组',
area: ['40vw', '40vh'],
content: `
<form class="layui-form" lay-filter="dataForm" style="padding: 20px;">
<input type="hidden" name="id" value="${data.id}">
<div class="layui-form-item">
<label class="layui-form-label">分组名称</label>
<div class="layui-input-block">
<input type="text" name="name" value="${data.name}" required lay-verify="required" placeholder="请输入描述" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">人脸质量</label>
<div class="layui-input-block">
<input type="text" name="quality" value="${data.quality}" required lay-verify="required" placeholder="请输入描述" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="saveData">保存</button>
</div>
</div>
</form>
`,
success: () => {
form.render();
// 监听表单提交
form.on('submit(saveData)', (formData) => {
if (type == 'add') {
faceLibApis.addGroup({
alg: 'face',
ext: { quality: formData.field.quality },
name: formData.field.name
}).then(res => {
this.getGroupData();
// 关闭模态框
layui.use('layer', () => {
layui.layer.closeAll();
});
this.showNotification('数据更新成功!');
})
} else {
faceLibApis.editGroup({
id: data.id,
alg: 'face',
ext: { quality: formData.field.quality },
name: formData.field.name
}).then(res => {
this.getGroupData();
// 关闭模态框
layui.use('layer', () => {
layui.layer.closeAll();
});
this.showNotification('数据更新成功!');
})
}
return false;
});
}
});
});
}
// 打开模态框
editFaceModal(data, type) {
layui.use(['layer', 'form'], () => {
const layer = layui.layer;
const form = layui.form;
layer.open({
type: 1,
title: type == 'add' ? '添加数据' : '编辑数据',
area: ['80vw', '80vh'],
content: `
<form class="layui-form" lay-filter="dataForm" style="padding: 20px;">
<input type="hidden" name="id" value="${data.id}">
<div class="layui-form-item">
<label class="layui-form-label">图片</label>
<div class="layui-input-block">
<input type="file" id="faceImage" placeholder="选择人脸" style="cursor: pointer;">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">姓名</label>
<div class="layui-input-block">
<input type="text" name="name" value="${data.name}" required lay-verify="required" placeholder="请输入姓名" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">外部ID</label>
<div class="layui-input-block">
<input type="text" name="external_id" value="${data.external_id}" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">性别</label>
<div class="layui-input-block">
<input type="text" name="sex" value="${data.sex}" placeholder="请输入性别" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">年龄</label>
<div class="layui-input-block">
<input type="number" name="age" value="${data.age}" placeholder="请输入年龄" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">简介</label>
<div class="layui-input-block">
<input type="text" name="desc" value="${data.desc}" placeholder="请输入简介" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="saveData">保存</button>
</div>
</div>
</form>
`,
success: () => {
form.render();
document.getElementById('faceImage').addEventListener('change', (event) => {
const files = event.target.files;
data.file = files[0]
});
// 监听表单提交
form.on('submit(saveData)', (formData) => {
console.log(formData)
this.handleFormSubmit(formData.field, data);
return false;
});
}
});
});
}
// 提交:添加/编辑
handleFormSubmit(formData, data) {
// 验证数据
if (!this.validateData(formData)) {
return;
}
let params = {
group_id: this.group_id,
name: formData.name,
age: formData.age,
sex: formData.sex,
desc: formData.desc,
external_id: formData.external_id
}
if (!data.id) {
faceLibApis.addFace(params).then(res => {
if (data.file) {
let formdata = new FormData();
formdata.append('id', res.data);
formdata.append('image', data.file)
faceLibApis.addImage(formdata).then(res => {
this.facecb('add')
})
} else {
this.facecb('add')
}
})
} else {
params.id = data.id;
faceLibApis.editFace(params).then(res => {
if (data.file) {
let formdata = new FormData();
formdata.append('id', params.id);
formdata.append('image', data.file)
faceLibApis.addImage(formdata).then(res => {
this.facecb('edit')
})
} else {
this.facecb('edit')
}
})
}
}
facecb(type) {
// 关闭模态框
layui.use('layer', () => {
layui.layer.closeAll();
});
this.showNotification(type == 'add' ? '数据添加成功!' : '数据更新成功!');
setTimeout(() => {
this.getTableData();
}, 1500)
}
// 验证数据
validateData(data) {
if (!data.name) {
this.showNotification('请输入姓名', 'error');
return false;
}
return true;
}
// 删除数据
deleteData(id) {
layui.use('layer', () => {
layui.layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, (index) => {
faceLibApis.delSource({ id: id }).then(res => {
this.getTableData();
this.showNotification('数据删除成功!');
layui.layer.close(index);
})
});
});
}
// 显示通知
showNotification(message, type = 'success') {
layui.use('layer', () => {
layui.layer.msg(message, {
icon: type === 'success' ? 1 : 2,
time: 2000
});
});
}
}
// 等待Layui加载完成后初始化
layui.use(['table', 'layer', 'form'], () => {
// 初始化人脸列表
window.faceLibManager = new FaceLibManager();
});
const faceLibApis = {
getFaceList: (params) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.face}?page=${params.page}&size=${params.size}&group_id=${params.group_id}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
addFace: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.face}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
editFace: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "PUT",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.face}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
delFace: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "DELETE",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.face}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
addImage: (formdata) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "PUT",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.image}`,
data: formdata, // 使用FormData对象
processData: false, // 告诉jQuery不要处理发送的数据
contentType: false,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getGrpup: () => {
let type = 'face';
// 获取人脸分组
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.group}?alg=${type}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
addGroup: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.group}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
editGroup: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "PUT",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.group}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
delGroup: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "DELETE",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.group}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
}

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="layui-container" style="padding: 20px;">
<div class="layui-card">
<div class="layui-card-header">
<h2>索引</h2>
</div>
<h3>
<a href="./live.html" target="_blank" rel="noopener noreferrer">实时画面:live.html</a>
</h3>
<h3>
<a href="./source.html" target="_blank" rel="noopener noreferrer">视频流管理:source.html</a>
</h3>
<h3>
<a href="./facelib.html" target="_blank" rel="noopener noreferrer">人脸底库管理:facelib.html</a>
</h3>
<h3>
<a href="./device.html" target="_blank" rel="noopener noreferrer">设备信息:device.html</a>
</h3>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,137 @@
.main-container {
width: 100vw;
height: 100vh;
display: flex;
justify-content: space-between;
padding: 10px;
box-sizing: border-box;
}
#video-container {
width: calc(100% - 310px);
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.right-container {
width: 300px;
height: 100%;
overflow: auto;
}
#video-container .video-box {
background-color: #000;
position: relative;
}
#video-container .video-box .tips {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
z-index: 1;
}
#video-container .video-box .tips .deviceoffline{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
#video-container .video-box canvas {
position: absolute;
}
#video-container.one-video .video-box {
width: 100%;
height: 100%;
}
#video-container.four-video .video-box {
width: calc(50% - 2px);
height: calc(50% - 2px);
}
#video-container.four-video .video-box:nth-child(1) {
margin-bottom: 4px;
}
#video-container.four-video .video-box:nth-child(1) {
margin-bottom: 4px;
}
.video-box .title-container {
position: absolute;
top: 0;
width: 100%;
height: 40px;
background-color: rgb(73 162 238 / 45%);
color: #00faff;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10px;
box-sizing: border-box;
z-index: 1;
}
.video-box .title-container .alg {
width: 300px;
position: relative;
}
.video-box .title-container ul {
display: none;
background-color: rgb(131 186 233 / 45%);
}
.video-box .title-container:hover ul {
display: block;
position: absolute;
top: 30px;
left: 40px;
max-height: 300px;
overflow: auto;
}
.video-box .title-container:hover ul li {
width: 100%;
height: 32px;
box-sizing: border-box;
padding: 5px;
cursor: pointer;
}
/* 加载中 */
.icon-dot {
position: relative;
display: block;
border-radius: 50%;
background-color: #39f;
width: 40px;
height: 40px;
animation: ani-spin-bounce 1s 0s ease-in-out infinite;
}
@keyframes ani-spin-bounce {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
opacity: 0;
}
}
.layui-tree-main{
position: relative;
}

View File

@ -0,0 +1,66 @@
<!DOCTYPE html>
<html>
<head>
<title>实时画面叠加实时检测结果</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://unpkg.com/layui@2.8.18/dist/css/layui.css">
<link href="live.css" rel="stylesheet">
<script>
let ZQLGLOBAL = {
serverIp: '192.168.1.169' ,// 修改成您的盒子aiboxd的IP地址
getDevices: `:9091/ks/device`,
getSources: `:9091/ks/source`,
subscribe: `:9089/ks/stream/live/subscribe`,
detect: `:9089/ks/stream/attr`,
tunnel:`:9091/ks/system/tunnel`,
sysArgs: `:9091/ks/system/args`,
resultTopic: 'ks/sink_local_result', //实时检测结果
streamCodeTopic: 'ks/stream',// 视频流状态码,状态码改变后应重新播放
srs_server: 1935,
srs_http_api: 1985,
srs_http_server: 8080,
websocket: 8083,
}
let ZQL_videosInfos = { 0: null, 1: null, 2: null, 3: null };
let ZQL_playingSource = {
0: null, 1: null, 2: null, 3: null,
videoNum: 4,
curposition: -1
}
let ZQL_sources = {};
</script>
<script type="text/javascript" src="public/crypto-js.js"></script>
<script type="text/javascript" src="public/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="public/jswebrtc.min.js"></script>
<script type="text/javascript" src="public/mpegts.js"></script>
<script type="text/javascript" src="public/mqtt.min.js"></script>
<script src="https://unpkg.com/layui@2.8.18/dist/layui.js"></script>
</head>
<body>
<div class="main-container">
<div id="video-container" class="one-video">
<div class="video-box">
<div class="title-container" id="video-title1"></div>
<video ref="video" muted id="video1" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas1"></canvas>
</div>
</div>
<div class="right-container">
<!-- <div class="btn-container">
<i class="z-icon-onevideo" id="icon-oneviveo"></i>
<i class="z-icon-fourvideo" id="icon-fourviveo"></i>
</div> -->
<div id="ZQL_source_tree"></div>
</div>
</div>
<script type="text/javascript" src="live.js"></script>
<script>
init();
</script>
</body>
</html>

View File

@ -0,0 +1,920 @@
const ZQL_multivideo = {
setVideoEl: () => {
let videoContainer = document.querySelector("#video-container");
if (ZQL_playingSource.videoNum == 1) {
videoContainer.className = "one-video";
videoContainer.innerHTML = `
<div class="video-box">
<div class="tips" id="tip0">
<div class="icon-dot"></div>
<div class="deviceoffline">
<i class="z-icon-jiankonglixian" style="font-size: 40rem"></i>
<span>离线</span>
</div>
</div>
<div class="title-container" id="video-title0"></div>
<video ref="video" muted id="video0" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas0"></canvas>
</div>
`
} else {
videoContainer.className = "four-video";
videoContainer.innerHTML = `
<div class="video-box">
<div class="tips" id="tip0">
</div>
<div class="title-container" id="video-title0"></div>
<video ref="video" muted id="video0" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas0"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip1">
</div>
<div class="title-container" id="video-title1"></div>
<video ref="video" muted id="video1" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas1"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip2">
</div>
<div class="title-container" id="video-title2"></div>
<video ref="video" muted id="video2" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas2"></canvas>
</div>
<div class="video-box">
<div class="tips" id="tip3">
</div>
<div class="title-container" id="video-title3"></div>
<video ref="video" muted id="video3" class="video-js" autoplay="autoplay" preload="auto"></video>
<canvas class="canvas-shuju" id="canvas3"></canvas>
</div>
`
}
},
liveLoading: (index) => {
let tipel = document.querySelector("#tip" + index);
tipel.innerHTML = `<div class="icon-dot"></div>`
},
liveOffline: (index) => {
let tipel = document.querySelector("#tip" + index);
tipel.innerHTML = `
<div class="deviceoffline">
<i class="z-icon-jiankonglixian" style="font-size: 40rem"></i>
<span>离线</span>
</div>
`
},
liveStopLoading: (index) => {
let tipel = document.querySelector("#tip" + index);
if (tipel) {
tipel.innerHTML = ``
}
},
setAlgList(index) {
let el = document.querySelector(`#video-title${index}`);
let algList = ZQL_sources[ZQL_playingSource[index]].alg;
let algEl = '<ul>';
for (let alg in algList) {
let name = algList[alg].reserved_args.ch_name;
algEl = algEl + `<li alg="${alg}" index="${index}">${name}</li>`
}
algEl = algEl + '</ul>'
el.innerHTML = `
<div class="camera">${ZQL_sources[ZQL_playingSource[index]].desc}</div>
<div class="alg">
<div class="algname">算法: ${ZQL_playingSource[index].alg ? ZQL_sources[ZQL_playingSource[index]].alg[alg].reserved_args.ch_name : ''}</div>
${algEl}
</div>
<div id="close${index}">关闭</div>
`;
el.querySelectorAll('li').forEach(item => {
item.addEventListener('click', (e) => {
let index = e.currentTarget.getAttribute("index");
let alg = e.currentTarget.getAttribute("alg")
ZQL_videosInfos[index].alg = alg;
let videlel = document.querySelector(`#video-title${index}`);
videlel.querySelector(".algname").innerHTML = '算法:' + ZQL_sources[ZQL_playingSource[index]].alg[alg].reserved_args.ch_name
})
})
document.querySelector(`#close${index}`).addEventListener('click', () => {
ZQL_multivideo.clearAlgList(index);
ZQL_multivideo.liveStopLoading(index);
ZQL_multivideo.destoryVideoByIndex(index);
ZQL_playingSource[index] = null;
ZQL_videosInfos[index] = null;
})
},
clearAlgList(index) {
let el = document.querySelector(`#video-title${index}`);
if (el) {
el.innerHTML = ""
}
},
handleRefresh(index) {
if (!ZQL_videosInfos[index]) {
return;
}
if (ZQL_videosInfos[index].status == "离线") {
ZQL_multivideo.destoryVideoByIndex(index);
ZQL_multivideo.subscribeLive(ZQL_playingSource[index], index);
} else {
if (!ZQL_videosInfos[index].stream) {
return;
}
let video = document.getElementById("video" + index);
video && (video.srcObject = null);
if (ZQL_videosInfos[index] && ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
ZQL_videosInfos[index].replayTimer = null;
}
ZQL_videosInfos[index] &&
ZQL_videosInfos[index].srsrtc &&
ZQL_videosInfos[index].srsrtc.destroy();
ZQL_videosInfos[index].srsrtc = null;
ZQL_videosInfos[index].status = "";
ZQL_multivideo.playVideo(ZQL_playingSource[index], index);
}
},
subscribeLive(cameraId, index) {
ZQL_multivideo.getCameraSize(cameraId, index);
ZQL_multivideo.liveLoading(index);
ZQL_apis
.subscribeLive(
// ZQL_sources[cameraId].deviceId,
// ZQL_sources[cameraId].sourceId
cameraId
)
.then((data) => {
let stream = data.data;
if (data && stream) {
ZQL_videosInfos[index].stream = stream;
ZQL_multivideo.playVideo(cameraId, index);
} else {
if (ZQL_playingSource[index] == cameraId) {
ZQL_multivideo.liveOffline(index);
// ZQL_videosInfos[index].status = "离线";
// ZQL_videosInfos[index].loading = false;
// this.reSubcribe(cameraId, index);
}
}
})
.catch((err) => {
if (
ZQL_playingSource[index] == cameraId &&
ZQL_videosInfos[index]
) {
ZQL_multivideo.liveOffline(index);
// ZQL_videosInfos[index].status = "离线";
// ZQL_videosInfos[index].loading = false;
// this.reSubcribe(cameraId, index);
}
});
},
playVideo(cameraId, index) {
if (ZQL_videosInfos[index].srsrtc) {
return;
}
ZQL_videosInfos[index].loading = true;
let video = document.getElementById("video" + index);
let stream = ZQL_videosInfos[index].stream;
var srsrtc;
if (stream.indexOf("webrtc") >= 0) {
let src =
"webrtc://" + ZQLGLOBAL.serverIp + "/live" + stream.split("/live")[1];
srsrtc = new JSWebrtc.Player(src, {
video: video,
autoplay: true,
onPlay: (obj) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].loading = false;
ZQL_videosInfos[index].playerState = "success";
},
});
} else if (stream.indexOf(".flv") >= 0) {
let src = `http://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.srs_http_server}/live${stream.split("/live")[1]
}`;
srsrtc = mpegts.createPlayer(
{
type: "flv",
url: src,
isLive: true,
},
{ enableWorker: true }
);
srsrtc.attachMediaElement(video);
srsrtc.load();
ZQL_videosInfos[index].playerState = "";
srsrtc
.play()
.then((res) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].playerState = "success";
ZQL_videosInfos[index].loading = false;
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
}
ZQL_videosInfos[index].refreshTime =
parseInt((Math.random() * 5 + 5) * 1000) * 60;
ZQL_videosInfos[index].refreshTimeInterval = setInterval(() => {
handleRefresh(index);
}, ZQL_videosInfos[index].refreshTime);
})
.catch((err) => { });
if (ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
}
ZQL_videosInfos[index].replayTimer = setTimeout(() => {
ZQL_multivideo.replayflv(srsrtc, cameraId, index);
}, 3000);
}
ZQL_videosInfos[index].srsrtc = srsrtc;
},
replayflv(srsrtc, cameraId, index) {
if (!ZQL_videosInfos[index]) {
return;
}
if (ZQL_videosInfos[index].playerState == "success") {
return;
} else {
srsrtc.unload();
srsrtc.load();
srsrtc
.play()
.then((res) => {
ZQL_multivideo.liveStopLoading(index);
ZQL_videosInfos[index].playerState = "success";
ZQL_videosInfos[index].loading = false;
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
}
ZQL_videosInfos[index].refreshTime =
parseInt((Math.random() * 5 + 5) * 1000) * 60;
ZQL_videosInfos[index].refreshTimeInterval = setInterval(() => {
ZQL_multivideo.handleRefresh(index);
}, ZQL_videosInfos[index].refreshTime);
})
.catch((err) => {
// this.destoryVideoByIndex(index);
// this.subscribeLive(cameraId, index);
});
if (ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
}
ZQL_videosInfos[index].replayTimer = setTimeout(() => {
ZQL_multivideo.replayflv(srsrtc, cameraId, index);
}, 3000);
}
},
reSubcribe(cameraId, index) {
if (ZQL_videosInfos[index].subscribeTimeout) {
clearTimeout(ZQL_videosInfos[index].subscribeTimeout);
ZQL_videosInfos[index].subscribeTimeout = null;
}
ZQL_multivideo.videosInfos[index].subscribeTimeout = setTimeout(() => {
ZQL_multivideo.subscribeLive(cameraId, index);
}, 1000);
},
getCameraSize(id, index) {
ZQL_multivideo.setOrisize(
ZQL_sources[id].draw_size[0],
ZQL_sources[id].draw_size[1],
index, id
);
},
setOrisize(width, height, index, id) {
let container = document.querySelector(".video-box");
if (!container) {
return;
}
if (!ZQL_videosInfos[index]) {
let alg = null;
if (sessionStorage.getItem("curalgs")) {
let cameraId = ZQL_playingSource[index];
let curalgs = JSON.parse(sessionStorage.getItem("curalgs"));
alg = curalgs[cameraId]
? JSON.parse(JSON.stringify(curalgs[cameraId]))
: null;
}
ZQL_videosInfos[index] = {
id: id,
loading: true,
openWs: true,
alg: alg,
algListShow: false,
subscribeTimeout: null,
refreshTimeInterval: null, // 定时刷新定时器
refreshTime: null, // 定时刷新时间
replayTimer: null,
playerState: "pending",
detectInterval: null,
quanping: false,
srsrtc: null,
stream: "",
status: "",
stream_code: "",
};
}
if (ZQL_videosInfos[index]) {
let oriWidth = width;
let oriHeight = height;
ZQL_videosInfos[index].oriWidth = oriWidth;
ZQL_videosInfos[index].oriHeight = oriHeight;
if (
oriWidth / container.offsetWidth >
oriHeight / container.offsetHeight
) {
ZQL_videosInfos[index].actualHeight = container.offsetWidth / (oriWidth / oriHeight)
ZQL_videosInfos[index].actualWidth = container.offsetWidth;
} else {
ZQL_videosInfos[index].actualHeight = container.offsetHeight
ZQL_videosInfos[index].actualWidth = container.offsetHeight * (oriWidth / oriHeight)
}
// videoWidth = ZQL_videosInfos[index].actualWidth;
ZQL_multivideo.setPosition(index);
}
},
setPosition(index) {
let container = document.querySelector(".video-box");
let video = document.querySelector("#video" + index);
let canvas = document.getElementById("canvas" + index);
let width = ZQL_videosInfos[index].actualWidth, height = ZQL_videosInfos[index].actualHeight;
video.style.position = "absolute";
video.style.width = width + "px";
video.style.height = height + "px";
canvas.width = width;
canvas.height = height;
if (width / container.offsetWidth < height / container.offsetHeight) {
let left = (container.offsetWidth - width) / 2;
video.style.left = Math.floor(left) + "px";
video.style.top = 0 + "px";
canvas.style.left = Math.floor(left) + "px";
canvas.style.top = "0px";
} else {
let top = (container.offsetHeight - height) / 2;
video.style.top = Math.floor(top) + "px";
video.style.left = 0 + "px";
canvas.style.top = Math.floor(top) + "px";
canvas.style.left = "0px";
}
},
setAlarms: (data, index) => {
ZQL_multivideo.clearCanvas(index);
if (ZQL_videosInfos[index] && !ZQL_videosInfos[index].canvas) {
ZQL_videosInfos[index].canvas = document.getElementById("canvas" + index)
}
if (
!ZQL_videosInfos[index] ||
!ZQL_videosInfos[index].actualWidth ||
!ZQL_videosInfos[index].actualHeight ||
!ZQL_videosInfos[index].oriWidth ||
!ZQL_videosInfos[index].oriHeight
) {
return;
}
// let bbox = data.result.data.bbox;
let bbox = data.bbox;
if (Object.values(bbox.polygons).length > 0) {
Object.values(bbox.polygons).forEach((item) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
// let color = item.color;
let points = item.polygon.map((point) => {
return [
Math.round(
(point[0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
Math.round(
(point[1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
];
});
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "transparent";
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
ZQL_multivideo.drawPolygons(points, context);
ZQL_multivideo.drawPolygonInfo(context, Object.values(bbox.polygons), index);
});
}
if (bbox.rectangles.length > 0) {
bbox.rectangles.forEach((item, i) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
let coordinates = {
x: Math.round(
(item.xyxy[0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y: Math.round(
(item.xyxy[1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
x1: Math.round(
(item.xyxy[2] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y1: Math.round(
(item.xyxy[3] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
};
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "rgb(" + color.join(",") + ")";
context.fillText(item.label || "", coordinates.x, coordinates.y - 10);
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
// context.strokeRect(
// coordinates.x,
// coordinates.y,
// coordinates.x1 - coordinates.x,
// coordinates.y1 - coordinates.y
// );
let lines = [];
let lineWidth = (coordinates.x1 - coordinates.x) / 4;
let lineHeight = (coordinates.y1 - coordinates.y) / 4;
lines[0] = {
x: coordinates.x,
y: coordinates.y,
x1: coordinates.x + lineWidth,
y1: coordinates.y,
};
lines[1] = {
x: coordinates.x,
y: coordinates.y,
x1: coordinates.x,
y1: coordinates.y + lineHeight,
};
lines[2] = {
x: coordinates.x1,
y: coordinates.y,
x1: coordinates.x1 - lineWidth,
y1: coordinates.y,
};
lines[3] = {
x: coordinates.x1,
y: coordinates.y,
x1: coordinates.x1,
y1: coordinates.y + lineHeight,
};
lines[4] = {
x: coordinates.x,
y: coordinates.y1,
x1: coordinates.x + lineWidth,
y1: coordinates.y1,
};
lines[5] = {
x: coordinates.x,
y: coordinates.y1,
x1: coordinates.x,
y1: coordinates.y1 - lineHeight,
};
lines[6] = {
x: coordinates.x1,
y: coordinates.y1,
x1: coordinates.x1 - lineWidth,
y1: coordinates.y1,
};
lines[7] = {
x: coordinates.x1,
y: coordinates.y1,
x1: coordinates.x1,
y1: coordinates.y1 - lineHeight,
};
lines.forEach((item) => {
ZQL_multivideo.drawLine(context, item);
});
});
}
if (Object.values(bbox.lines).length > 0) {
Object.values(bbox.lines).forEach((item, i) => {
let color = JSON.parse(JSON.stringify(item.color)).reverse();
let coordinates = {
x: Math.round(
(item.line[0][0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y: Math.round(
(item.line[0][1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
x1: Math.round(
(item.line[1][0] * ZQL_videosInfos[index].actualWidth) /
ZQL_videosInfos[index].oriWidth
),
y1: Math.round(
(item.line[1][1] * ZQL_videosInfos[index].actualHeight) /
ZQL_videosInfos[index].oriHeight
),
};
let context = ZQL_videosInfos[index].canvas.getContext("2d");
context.font = "20px Arial bolder";
context.fillStyle = "rgb(" + color.join(",") + ")";
if (item.ext.direction) {
context.fillText(item.name, (coordinates.x + coordinates.x1) / 2, (coordinates.y + coordinates.y1) / 2 + 20);
}
context.strokeStyle = "rgb(" + color.join(",") + ")";
context.lineWidth = 2;
ZQL_multivideo.drawLine(context, coordinates);
ZQL_multivideo.drawCountingInfo(context, Object.values(bbox.lines));
});
}
},
drawPolygons(points, context) {
context.beginPath();
context.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
context.lineTo(points[i][0], points[i][1]);
}
context.closePath();
context.fill();
context.stroke();
},
drawLine(ctx, line) {
ctx.beginPath();
ctx.moveTo(line.x, line.y);
ctx.lineTo(line.x1, line.y1);
ctx.stroke();
},
drawCountingInfo(context, lines) {
lines.forEach((item, index) => {
context.fillStyle = "rgb(255,0,0)";
if (item.ext.direction.length == 2) {
context.fillText(`[${item.name}] ${item.ext.action.count}: ${item.ext.result.count}`, 0, 20 * index + 20);
} else {
context.fillText(`[${item.name}] ${item.ext.action.increase}: ${item.ext.result.increase},${item.ext.action.decrease}: ${item.ext.result.decrease},${item.ext.action.delta}: ${item.ext.result.delta}`, 0, 20 * index + 20);
}
});
},
drawPolygonInfo(context, polygons, videoindex) {
polygons.forEach((item, index) => {
context.fillStyle =
"rgb(" +
JSON.parse(JSON.stringify(item.color)).reverse().join(",") +
")";
let leftPoint = item.polygon[0];
for (let i = 1; i < item.polygon.length; i++) {
if (item.polygon[i][0] < leftPoint[0]) {
leftPoint = item.polygon[i];
}
}
context.fillText(
`${item.name}`,
(leftPoint[0] * ZQL_videosInfos[videoindex].actualWidth) /
ZQL_videosInfos[videoindex].oriWidth,
(leftPoint[1] * ZQL_videosInfos[videoindex].actualHeight) /
ZQL_videosInfos[videoindex].oriHeight + 20
);
if (item.ext.result) {
context.fillStyle = "rgb(255,0,0)";
context.fillText(`${item.name}: ${item.ext.result}`, 0, 20 * index + 20);
}
});
},
destroyVideo(videonum) {
for (let i = 0; i < videonum; i++) {
ZQL_multivideo.destoryVideoByIndex(i);
}
},
destoryVideoByIndex(index) {
ZQL_multivideo.clearCanvas(index);
if (ZQL_videosInfos[index]) {
if (
ZQL_videosInfos[index] &&
ZQL_videosInfos[index].subscribeTimeout
) {
clearTimeout(ZQL_videosInfos[index].subscribeTimeout);
ZQL_videosInfos[index].subscribeTimeout = null;
}
if (ZQL_videosInfos[index] && ZQL_videosInfos[index].replayTimer) {
clearTimeout(ZQL_videosInfos[index].replayTimer);
ZQL_videosInfos[index].replayTimer = null;
}
if (ZQL_videosInfos[index].refreshTimeInterval) {
clearInterval(ZQL_videosInfos[index].refreshTimeInterval);
ZQL_videosInfos[index].refreshTimeInterval = null;
}
let video = document.getElementById("video" + index);
video && (video.srcObject = null);
ZQL_videosInfos[index].srsrtc &&
ZQL_videosInfos[index].srsrtc.destroy();
ZQL_multivideo.clearCanvas(index);
ZQL_videosInfos[index] = null;
}
},
clearCanvas(index) {
let canvas = document.getElementById("canvas" + index);
if (canvas && canvas.getContext("2d")) {
canvas
.getContext("2d")
.clearRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
}
},
connectMqtt() {
let mqttclient = mqtt.connect(`ws://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.websocket}/mqtt`);
mqttclient.subscribe(
ZQLGLOBAL.resultTopic,
{ qos: 0 },
(error) => {
if (error) {
console.log("subscribe error:", error);
return;
}
}
);
mqttclient.subscribe(
ZQLGLOBAL.streamCodeTopic,
{ qos: 0 },
(error) => {
if (error) {
console.log("subscribe error:", error);
return;
}
}
);
mqttclient.on("message", (topic, payload) => {
let msg = JSON.parse(payload.toString());
if (msg.msg_type == "result") {
let id = msg.data.source.id;
for (let i = 0; i < 4; i++) {
if (ZQL_videosInfos[i]) {
let alg =
ZQL_videosInfos[i].alg && ZQL_videosInfos[i].alg.algname;
if (
id == ZQL_playingSource[i] &&
msg.data.alg.name == ZQL_videosInfos[i].alg
) {
ZQL_multivideo.setAlarms(msg.data.reserved_data, i);
if (
ZQL_videosInfos[i] &&
ZQL_videosInfos[i].canvasTimeout
) {
clearTimeout(ZQL_videosInfos[i].canvasTimeout);
}
ZQL_videosInfos[i].canvasTimeout = setTimeout(() => {
ZQL_multivideo.clearCanvas(i);
}, 1000);
break;
}
}
}
}
if (msg.msg_type == "stream_code") {
let cameraId = msg.data.source_id;
for (let i = 0; i < ZQL_playingSource.videoNum; i++) {
if (cameraId == ZQL_playingSource[i] && ZQL_videosInfos[i]) {
if (!ZQL_videosInfos[i].stream_code) {
ZQL_videosInfos[i].stream_code = msg.data.stream_code;
} else if (
msg.data.stream_code != ZQL_videosInfos[i].stream_code
) {
ZQL_videosInfos[i].stream_code = msg.data.stream_code;
ZQL_multivideo.handleRefresh(i);
}
break;
}
}
}
});
}
}
const ZQL_apis = {
getSources: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.getSources}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
subscribeLive: (source_id) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.subscribe}?source_id=${source_id}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
sysArgs: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.tunnel}`,
success: function (tunnel) {
if (tunnel.data.enable == false) {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.sysArgs}`,
success: function (res) {
resolve(res.data.map.local)
},
error: function (err) {
reject(err)
}
});
} else {
resolve({
"srs_server": tunnel.data.srs_server,
"srs_http_api": tunnel.data.srs_http_api,
"srs_http_server": tunnel.data.srs_http_server,
"websocket": tunnel.data.websocket
})
}
},
error: function (err) {
reject(err)
}
});
})
},
detectStream: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}:${ZQLGLOBAL.srs_http_api}/api/v1/streams?start=0&count=10000`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
detectVideo: (device_id, stream) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.detect}?device_id=${device_id}&stream=${stream}&draw_size=1280`,
success: function (res) {
if (res.error == 0) {
resolve({ status: 1 })
} else {
resolve({ status: 0 })
}
},
error: function (err) {
reject(err)
}
});
})
},
gettoken: () => {
var ak = ZQLGLOBAL.accessKey;
var sk = ZQLGLOBAL.accessSecret;
var timestamp = parseInt(new Date().getTime() / 1000);
var nonce = ZQL_apis.generateRandomString(10);
let signature = ZQL_apis.generateSignature(ak, sk, timestamp, nonce)
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.getToken}?signature=${signature}&ak=${ak}&timestamp=${timestamp}&nonce=${nonce}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
generateSignature: (ak, sk, timestamp, nonce) => {
var message = `${ak}:${timestamp}:${nonce}`;
var hash = CryptoJS.HmacSHA256(message, sk);
var signature = CryptoJS.enc.Hex.stringify(hash);
return signature
},
generateRandomString(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
}
function init() {
// 设置文档结构
ZQL_multivideo.setVideoEl();
// 设置事件监听
// document.querySelector("#icon-oneviveo").addEventListener('click', () => {
// playingSource.videoNum = 1;
// setVideoEl();
// })
// document.querySelector("#icon-fourviveo").addEventListener('click', () => {
// playingSource.videoNum = 4;
// ZQL_multivideo.setVideoEl();
// })
// 1. 获取摄像头列表
Promise.all([ZQL_apis.getSources()]).then(res => {
let cameras = res[0].data.map(item => {
item.sourceId = item.id;
item.title = item.desc;
item.type = 'source';
item.checked = false;
ZQL_sources[item.id] = item
return item
});
layui.use(function () {
var tree = layui.tree;
var layer = layui.layer;
tree.render({
elem: '#ZQL_source_tree',
data: cameras,
// showCheckbox:true,
onlyIconControl: true, // 是否仅允许节点左侧图标控制展开收缩
click: function (obj) {
if (obj.data.sourceId) {
let key = obj.data.sourceId;
if (ZQL_sources[key].checked == false) {
ZQL_sources[key].checked = true
if (ZQL_playingSource.videoNum == 1) {
ZQL_playingSource[0] = key;
ZQL_multivideo.subscribeLive(key, 0);
ZQL_multivideo.setAlgList(0);
} else {
for (let i = 0; i < 4; i++) {
if (!ZQL_playingSource[i]) {
ZQL_playingSource[i] = key;
ZQL_multivideo.subscribeLive(key, i);
ZQL_multivideo.setAlgList(i)
break;
}
}
}
} else {
ZQL_sources[key].checked = false
if (ZQL_playingSource.videoNum == 1) {
ZQL_playingSource[0] = null;
ZQL_multivideo.destoryVideoByIndex(0);
ZQL_multivideo.clearAlgList(0);
ZQL_multivideo.liveStopLoading(0);
} else {
for (let i = 0; i < 4; i++) {
if (ZQL_playingSource[i] == key) {
ZQL_playingSource[i] = null;
ZQL_multivideo.destoryVideoByIndex(i);
ZQL_multivideo.clearAlgList(i)
ZQL_multivideo.liveStopLoading(i);
}
}
}
}
}
}
});
});
}).catch(err => {
console.log(err)
})
// 获取系统参数连接mqtt,通过mqtt获取实时检测结果和视频流状态码
ZQL_apis.sysArgs().then(res => {
ZQLGLOBAL = Object.assign(ZQLGLOBAL, res)
ZQL_multivideo.connectMqtt()
}).catch(err => { })
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,126 @@
/* Layui表格管理系统自定义样式 */
body {
background-color: #f2f2f2;
}
.layui-card-header h2 {
margin: 0;
color: #333;
font-size: 18px;
font-weight: 600;
}
/* 搜索区域样式优化 */
.layui-input-group {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.layui-input-group .layui-input {
border-right: none;
}
.layui-input-suffix {
background: #fff;
border: 1px solid #e6e6e6;
border-left: none;
border-radius: 0 2px 2px 0;
}
/* 表格样式优化 */
.layui-table {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.layui-table thead tr {
background-color: #f8f8f8;
}
.layui-table thead th {
font-weight: 600;
color: #333;
background-color: #f8f8f8;
}
/* 操作按钮样式 */
.layui-btn-xs {
margin-right: 5px;
}
/* 响应式优化 */
@media (max-width: 768px) {
.layui-container {
padding: 10px;
}
.layui-col-md8,
.layui-col-md4 {
margin-bottom: 10px;
}
.layui-input-group {
flex-direction: column;
}
.layui-input-suffix {
border: 1px solid #e6e6e6;
border-top: none;
border-radius: 0 0 2px 2px;
}
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
}
.empty-state h3 {
margin-bottom: 10px;
color: #666;
}
/* 通知样式 */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 4px;
color: white;
font-weight: 500;
z-index: 10000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
animation: slideIn 0.3s ease;
}
.notification.success {
background: #5FB878;
}
.notification.error {
background: #FF5722;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}

View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>视频流管理</title>
<link rel="stylesheet" href="https://unpkg.com/layui@2.8.18/dist/css/layui.css">
<link rel="stylesheet" href="public/style.css">
<script>
let ZQLGLOBAL = {
serverIp: '192.168.1.169',// 修改成您的盒子aiboxd的IP地址
source: `:9091/ks/source`, // 视频源增删改查
alg: `:9091/ks/alg`, // 算法列表
attr: `:9089/ks/stream/attr`, // 检测视频是否在线
group: `:9091/ks/group`, // 底库分组
};
</script>
</head>
<body>
<div class="layui-container" style="padding: 20px;">
<div class="layui-card">
<div class="layui-card-header">
<h2>视频流管理</h2>
</div>
<div class="layui-card-body">
<!-- 搜索和操作区域 -->
<div style="margin-bottom: 20px; display: flex;justify-content: space-between;">
<button class="layui-btn layui-btn-primary" id="searchBtn">
<i class="layui-icon layui-icon-search"></i> 查询
</button>
<button class="layui-btn layui-btn-normal" id="addBtn">
<i class="layui-icon layui-icon-add-1"></i> 添加数据
</button>
</div>
<!-- 表格区域 -->
<table id="dataTable" lay-filter="dataTable"></table>
</div>
</div>
</div>
<!-- 表格操作按钮模板 -->
<script type="text/html" id="operationBar" lay-filter="dataTable">
<a class="layui-btn layui-btn-xs" lay-event="edit">
<i class="layui-icon layui-icon-edit"></i>
</a>
<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">
<i class="layui-icon layui-icon-delete"></i>
</a>
<input type="checkbox" name="open" lay-skin="switch" lay-filter="switchTest" title="启用|停用" value="{{= d.id }}" {{= d.status == 1 ? "checked" : "" }}>
</script>
<!-- 引入Layui JS -->
<script src="https://unpkg.com/layui@2.8.18/dist/layui.js"></script>
<script src="public/jquery-1.10.2.min.js"></script>
<script src="source.js"></script>
</body>
</html>

View File

@ -0,0 +1,468 @@
// Layui表格数据管理类
class LayuiTableManager {
constructor() {
this.alg = {};
this.data = [];
this.isEditing = false;
this.currentEditId = null;
this.table = null;
this.init();
}
async init() {
this.initLayui();
this.bindEvents();
await sourceApis.getAlgs().then(res => {
res.data.forEach(alg => {
this.alg[alg.name] = alg
})
})
this.getTableData();
}
// 初始化Layui
initLayui() {
layui.use(['table', 'layer', 'form'], () => {
const table = layui.table;
const layer = layui.layer;
const form = layui.form;
// 初始化表格
this.table = table.render({
elem: '#dataTable',
data: this.data,
cols: [[
// { field: 'id', title: 'ID', width: 80, sort: true },
{ field: 'desc', title: '描述', width: 150 },
{ field: 'stream', title: '流地址', sort: true },
{ field: 'alg_ch_names', title: '算法', },
{ field: 'encoding', width: 100, title: '编码', },
{ title: '操作', width: 250, toolbar: '#operationBar', fixed: 'right' }
]],
page: true,
limit: 10,
limits: [10, 20, 50],
height: 'full-220',
text: {
none: '暂无数据'
}
});
// 监听工具条事件
table.on('tool(dataTable)', (obj) => {
const data = obj.data;
if (obj.event === 'edit') {
this.openEditModal(data, 'edit');
} else if (obj.event === 'del') {
this.deleteData(data.id);
}
});
form.on('switch(switchTest)', (obj) =>{
console.log(obj)
let status;
if(obj.elem.checked == true){
status = 1
}else{
status = -1
}
sourceApis.editSource({
id:obj.value,
status:status
}).then(res => {
this.showNotification('数据更新成功!');
})
});
});
}
getTableData() {
sourceApis.getSources().then(res => {
this.data = res.data.filter(item => item.type == 'stream').map(item => {
item.alg_ch_names = Object.keys(item.alg).map(alg_name => this.alg[alg_name].ch_name).join(',')
return item
});
if (this.table) {
layui.use('table', () => {
layui.table.reload('dataTable', {
data: this.data
});
});
}
})
}
// 绑定事件
bindEvents() {
// 添加按钮事件
document.getElementById('addBtn').addEventListener('click', () => {
this.openEditModal({
desc: '',
stream: '',
alg: {}
}, 'add');
});
// 搜索功能
document.getElementById('searchBtn').addEventListener('click', () => {
this.getTableData();
});
}
// 打开模态框
openEditModal(data, type) {
layui.use(['layer', 'form'], () => {
const layer = layui.layer;
const form = layui.form;
let algEl = ``;
for (let key in this.alg) {
if (data.alg[key]) {
algEl += `<input type="checkbox" name="${this.alg[key].name}" title="${this.alg[key].ch_name}" checked> `;
} else {
algEl += `<input type="checkbox" name="${this.alg[key].name}" title="${this.alg[key].ch_name}">`;
}
}
layer.open({
type: 1,
title: type == 'add' ? '添加数据' : '编辑数据',
area: ['80vw', '80vh'],
content: `
<form class="layui-form" lay-filter="dataForm" style="padding: 20px;">
<input type="hidden" name="id" value="${data.id}">
<div class="layui-form-item">
<label class="layui-form-label">描述</label>
<div class="layui-input-block">
<input type="text" name="desc" value="${data.desc}" required lay-verify="required" placeholder="请输入描述" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">流地址</label>
<div class="layui-input-block">
<input type="text" name="stream" value="${data.stream}" required lay-verify="required" placeholder="请输入流地址" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item" >
<label class="layui-form-label"></label>
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="detect">
检测是否在线
</button>
<input type="hidden" name="draw_size" value="${data.draw_size}">
</div>
<div class="layui-form-item">
<label class="layui-form-label">算法</label>
<div class="layui-input-block" id="sel-algs-container">
${algEl}
</div>
<input type="hidden" name="alg_ch_names" value="${data.alg_ch_names}">
<span style="color:#999">注本demo对选择的算法仅保存默认参数如果参数配置中有必须绘制检测区域的参数则检测区域默认设置为整张图片的坐标如果参数配置中有必须绘制线的参数则线默认设置为相对于图片从左到右垂直居中的横线。</span>
</div>
<div class="layui-form-item">
<label class="layui-form-label">算法参数</label>
<button class="layui-btn layui-btn-normal" lay-submit lay-filter="getAlgParams">
获取算法参数
</button>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="saveData">保存</button>
</div>
</div>
</form>
`,
success: () => {
form.render();
// 检测
form.on('submit(detect)', (formData) => {
sourceApis.getAttr(formData.field.stream).then(res => {
data.draw_size = res.data.size;
data.encoding = res.data.codec;
})
return false;
});
form.on('submit(getAlgParams)', (formData) => {
if (!data.draw_size) {
// 先检测
this.showNotification('请先检测是否在线', 'error');
return false;
}
// 算法参数含义可参考如下链接中的“3.前端配置文件”部分https://github.com/AIDrive-Research/Custom-Algorithm/tree/main/02_CustomAlgorithm/03_PackageStructure
let algEls = document.querySelectorAll("#sel-algs-container input");
for (let i = 0; i < algEls.length; i++) {
if (algEls[i].checked == true && !data.alg[algEls[i].name]) {
((alg) => {
sourceApis.getAlgJson(alg).then(res => {
// 算法配置文件中的basicParams为需要保存到后台的参数
data.alg[alg] = JSON.parse(JSON.stringify(res.basicParams));
// 算法配置文件中的renderParams为算法参数的展示、配置规则
if (res.renderParams.bbox) {
// 判断是否必须有检测区域,如离岗、区域入侵等必须绘制检测区域
let polygon = res.renderParams.bbox.polygons;
if (polygon && polygon.exits == 'must') {
data.alg[alg].bbox.polygons = [{
id: `polygon_${new Date().getTime()}`,
name: '',
polygon: [[0, 0], [data.draw_size[0], 0], [data.draw_size[0], data.draw_size[1]], [0, data.draw_size[1]]] // 以整图坐标为例
}]
}
// 判断是否必须有直线,如人员计数、车辆计数等必须绘制虚拟直线
let line = res.renderParams.bbox.lines;
if (line && line.exits == 'must') {
data.alg[alg].bbox.lines = [{
id: `line_${new Date().getTime()}`,
name: '',
line: [[0, data.draw_size[1] / 2], [data.draw_size[0], data.draw_size[1] / 2]],
direction: 'd+', // d+表示从上到下,
action: { count: "统计" }
}]
}
}
// 如果算法类型为match_开头说明需要配置底库分组
if (data.alg[alg].alg_type.indexOf('match_') >= 0) {
let lib_type = data.alg[alg].alg_type.replace('match_', '');
// 获取对应算法的底库组
sourceApis.getGrpup(lib_type).then(res => {
if(res.data.length > 0){
let groupId = res.data[0].id;
data.alg[alg].reserved_args.group_id = groupId;
} else {
this.showNotification(`没有查询到${lib_type}的底库分组,请先添加分组`, 'error');
}
})
}
})
})(algEls[i].name);
}
}
return false;
});
// 监听表单提交
form.on('submit(saveData)', (formData) => {
let alg = data.alg;
let algEls = document.querySelectorAll("#sel-algs-container input");
for (let i = 0; i < algEls.length; i++) {
// checked == false未勾选的算法
if (algEls[i].checked == false && data.alg[algEls[i].name]){
delete alg[algEls[i].name]; // 删除多余算法参数
}
}
this.handleFormSubmit(formData.field, data);
return false;
});
}
});
});
}
// 提交:添加/编辑
handleFormSubmit(formData, data) {
// 验证数据
if (!this.validateData(formData)) {
return;
}
let params = {
type: 'stream', // 仅以视频流为例
desc: formData.desc,
stream: formData.stream,
alg: data.alg,
draw_size: data.draw_size,
encoding: data.encoding,
video_record: 0,
ipv4: '',
name: '',
info: { rtsp_transport: "tcp", username: "", password: "" }
}
if (!data.id) {
sourceApis.addSource(params).then(res => {
this.getTableData();
this.showNotification('数据添加成功!');
// 关闭模态框
layui.use('layer', () => {
layui.layer.closeAll();
});
})
} else {
params.id = data.id;
sourceApis.editSource(params).then(res => {
this.getTableData();
this.showNotification('数据更新成功!');
// 关闭模态框
layui.use('layer', () => {
layui.layer.closeAll();
});
})
}
}
// 验证数据
validateData(data) {
if (!data.desc || data.desc.length < 2) {
this.showNotification('描述不能重复', 'error');
return false;
}
return true;
}
// 删除数据
deleteData(id) {
layui.use('layer', () => {
layui.layer.confirm('确定要删除这条数据吗?', {
icon: 3,
title: '提示'
}, (index) => {
sourceApis.delSource({ id: id }).then(res => {
this.getTableData();
this.showNotification('数据删除成功!');
layui.layer.close(index);
})
});
});
}
// 显示通知
showNotification(message, type = 'success') {
layui.use('layer', () => {
layui.layer.msg(message, {
icon: type === 'success' ? 1 : 2,
time: 2000
});
});
}
}
// 等待Layui加载完成后初始化
layui.use(['table', 'layer', 'form'], () => {
// 初始化表格管理器
window.tableManager = new LayuiTableManager();
});
const sourceApis = {
getSources: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.source}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getAlgs: () => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.alg}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getAlgJson: (alg_name) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}/algsjson/${alg_name}.json`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getAttr: (stream) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.attr}?stream=${stream}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
addSource: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "POST",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.source}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
editSource: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "PUT",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.source}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
delSource: (data) => {
return new Promise((resolve, reject) => {
$.ajax({
type: "DELETE",
contentType: "application/json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.source}`,
data: JSON.stringify(data),
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
},
getGrpup: (type) => {
// 获取底库分组
return new Promise((resolve, reject) => {
$.ajax({
type: "GET",
dataType: "json",
url: `http://${ZQLGLOBAL.serverIp}${ZQLGLOBAL.group}?alg=${type}`,
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
});
})
}
}