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

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)
}
});
})
}
}