feat:视频预览分页

This commit is contained in:
dzc committed 2025-11-06 13:29:22 +08:00
1 parent f1bcaef4cd
commit ac2085eb4c
11 files changed
+474 -423

No files matched your search

+14
View File
@@ -1,5 +1,6 @@
import request from "/@/utils/request"
// 通过deviceCode查询通道列表
export function list(query: any) {
return request({
url: '/admin/deviceChannel/list',
@@ -8,6 +9,19 @@ export function list(query: any) {
})
}
// 通过deviceCode查询通道列表
export const getChannelPage = (query:any) => {
return request({
url: '/admin/deviceChannel/panel/getPage',
method: 'post',
data:query,
params: {
current: query.current,
size: query.size,
}
})
}
export function getReplayList(data: any) {
return request({
url: '/admin/deviceChannel/getReplayList',
@@ -0,0 +1,126 @@
const pageDivId = "ws-video-page"
const rendererPage = (page = 1, pageSize = 10, total = 0, onChange = () => { }) => {
const tpl = document.createElement('template')
tpl.innerHTML = `
<div id="${pageDivId}" style="margin-left: 10px;flex:1;justify-content:center;height:100%;display:flex;flex-direction:column;">
</div>
`.trim()
const root = tpl.content.firstElementChild
// 初始化分页栏
mountPagination(root, { total, pageSize, current: page, onChange })
return root
}
const createPagination = ({ total = 0, pageSize = 10, current = 1, onChange = (page, pageSize) => { } }) => {
const totalPages = Math.max(1, Math.ceil(total / pageSize))
const bar = document.createElement('div')
bar.className = 'ws-pagination-bar'
bar.id = 'ws-pagination-bar'
bar.style.cssText = 'display:flex;gap:4px;align-items:center;flex-wrap:wrap;color:#e5e7eb;font-size:11px;'
// 将当前分页状态写入 dataset,事件从 dataset 读取最新值,避免闭包旧值
bar.dataset.current = String(current)
bar.dataset.pageSize = String(pageSize)
bar.dataset.total = String(total)
bar.dataset.totalPages = String(totalPages)
const makeBtn = (text, disabled, handler) => {
const btn = document.createElement('button')
btn.textContent = text
btn.disabled = !!disabled
btn.style.cssText = 'padding:2px 6px;border:1px solid #B4BAC2;background:transparent;color:#B4BAC2;border-radius:3px;cursor:pointer;font-size:14px;'
btn.addEventListener('click', handler)
return btn
}
const prev = makeBtn('上一页', current <= 1, () => {
const cur = Number(bar.dataset.current || 1)
const size = Number(bar.dataset.pageSize || 10)
onChange(Math.max(1, cur - 1), size)
})
prev.className = 'ws-pagination-prev'
const next = makeBtn('下一页', current >= totalPages, () => {
const cur = Number(bar.dataset.current || 1)
const size = Number(bar.dataset.pageSize || 10)
const tp = Number(bar.dataset.totalPages || 1)
onChange(Math.min(tp, cur + 1), size)
})
next.className = 'ws-pagination-next'
const info = document.createElement('span')
info.className = 'ws-pagination-info'
info.textContent = `第${current}/${totalPages}页,共${total}项`
info.style = "font-size:14px;color:#B4BAC2;"
bar.append(prev, info, next)
return bar
}
const mountPagination = ($container, { total, pageSize, current, onChange }) => {
// 就地更新,避免闪烁与重复 append
const updateUI = (bar, total, size, cur) => {
const tp = Math.max(1, Math.ceil(total / size))
// 写入最新状态
bar.dataset.current = String(cur)
bar.dataset.pageSize = String(size)
bar.dataset.total = String(total)
bar.dataset.totalPages = String(tp)
// 更新文案与禁用状态
const prevBtn = bar.querySelector('.ws-pagination-prev')
const nextBtn = bar.querySelector('.ws-pagination-next')
const info = bar.querySelector('.ws-pagination-info')
if (prevBtn) prevBtn.disabled = cur <= 1
if (nextBtn) nextBtn.disabled = cur >= tp
if (info) info.textContent = `第${cur}/${tp}页,共${total}项`
}
let bar = $container.querySelector('#ws-pagination-bar')
const handleChange = (page, size = pageSize) => {
updateUI(bar, total, size, page)
// 向外派发事件,便于宿主接收分页变更
$container.dispatchEvent(new CustomEvent('pagechange', { detail: { page, pageSize: size, total } }))
// 执行外部回调(透传 page 与 pageSize)
if (typeof onChange === 'function') {
onChange(page, size)
}
}
if (!bar) {
bar = createPagination({ total, pageSize, current, onChange: handleChange })
$container.appendChild(bar)
}
updateUI(bar, total, pageSize, current)
}
const clearClss = ($wsControl) => {
const $wsControlEnd = $wsControl.querySelector(".ws-flex.ws-flex-end")
if ($wsControlEnd) {
$wsControlEnd.style = "width: initial;"
}
// 删除$wsControl下面style为flex:1的元素
const $flex1 = $wsControl.querySelector("[style*='flex: 1']")
if ($flex1) $flex1.remove()
}
export const rendererVideoPage = (opts = {}) => {
const $wsControl = document.querySelector(".ws-control")
if (!$wsControl) return
clearClss($wsControl)
// 先查找有没有 video-page 元素
let $page = $wsControl.querySelector(`#${pageDivId}`)
const defaults = { total: 0, pageSize: 10, current: 1, onChange: () => { } }
const params = { ...defaults, ...opts }
if (!$page) {
const node = rendererPage(params.current, params.pageSize, params.total, params.onChange)
if (typeof $wsControl.prepend === 'function') {
$wsControl.prepend(node)
} else {
$wsControl.insertBefore(node, $wsControl.firstChild)
}
$page = node
} else {
// 如果容器已存在,确保分页挂载
mountPagination($page, params)
}
}
@@ -5,31 +5,37 @@
</template>
<script setup lang="ts" name="deviceGroupPlay">
import { useMessage, useMessageBox } from "/@/hooks/message";
import { useMessage } from "/@/hooks/message";
import * as DeviceCameraApi from "/@/api/device/deviceCamera"
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
// @ts-ignore
import PlayerManager from "../../components/lib/icc/PlayerManager";
// @ts-ignore
import { rendererVideoPage } from "../lib/custom/video-custom"
let realPlayer: any = null;
const videoState = reactive({
dialogVisible: false,
loading: false,
treeLoading: false,
videoNum: 0,
numList: [1, 4, 9, 16, 25],
const state = reactive<any>({
numList: [1, 4, 9, 16],
num: 16,
maxNum: 16,
channelList: [],
/**
* 码流类型:
* 1=主码流
* 2=辅码流
* 3=辅码流2
*/
streamType: 1,
current: 1,
total: 100,
})
const channel = reactive<any>({
playList: [],
deviceChannelList: []
})
onMounted(() => {
const query = reactive({
list: [],
deviceCode: null,
})
onUnmounted(() => {
@@ -37,84 +43,126 @@ onUnmounted(() => {
})
// 视频预览初始化
async function init(list: any) {
async function init(channelCodelist: any, deviceCode: any) {
try {
destroy()
if (list && list.length) {
// 查询设备通道
const { code: deviceChannelCode, data: deviceChannelList } = await DeviceChannelApi.list({ deviceChannelCodeList: list })
if (deviceChannelCode != 0) throw new Error()
if (deviceChannelList == null || deviceChannelList.length == 0) throw new Error()
channel.deviceChannelList = deviceChannelList
// 查询视频
const { code: cameraCode, data: channelList } = await DeviceCameraApi.getRtspVideoList({
channelIdList: list
})
if (cameraCode != 0) throw new Error()
channel.playList = channelList ?? []
initVideo()
if (channelCodelist) {
query.list = channelCodelist
}
if (deviceCode) {
query.deviceCode = deviceCode
}
state.current = 1
state.num = 16
initVideo()
} catch (error: any) {
console.error("视频预览初始化失败", error);
console.error("|视频预览初始化|,意外的错误", error);
}
}
// 初始化视频
function initVideo() {
async function initVideo() {
const _params = {
deviceChannelCodeList: query.list ?? [],
deviceCode: query.deviceCode ?? null,
current: state.current,
size: state.num,
};
try {
let { code, data } = await DeviceChannelApi.getChannelPage(_params);
if (code != 0) throw new Error();
let { records, total } = data
if (!records || !records.length) records = []
state.total = total
let suitableNum = state.num
if (state.total < state.numList[state.numList.length - 1]) {
suitableNum = findSuitableNum(records.length, state.numList);
state.maxNum = suitableNum
}
state.num = suitableNum;
state.channelList = records;
console.log(`查询参数:${JSON.stringify(_params)}原始通道数: ${records.length}, 调整后的显示路数: ${suitableNum}`);
if (records.length >= 50) return
// 初始化播放器
initPlayerManager();
} catch (error) {
console.error("|初始化视频|,意外的错误", error);
}
}
// 初始化播放器
function initPlayerManager() {
if (realPlayer) realPlayer = null;
videoState.videoNum = findSuitableNum(channel.playList.length, videoState.numList)
realPlayer = new PlayerManager({
el: "ws-real-player" /** 实时预览容器id,创建多个播放器,传入不同的容器id即可 **/,
type: "real" /** real - 实时预览 record - 录像回放 **/,
maxNum: 16 /** 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25 **/,
num: videoState.videoNum /** 初始化,页面显示的路数 **/,
showControl: true /** 是否显示工具栏,默认显示 **/,
prefixUrl: "lib" /** 解码库所在位置的前缀 **/,
isRTPStream: true /** true-标准码流拉流 false-私有码流拉流 **/,
// 实时预览容器id,创建多个播放器,传入不同的容器id即可
el: "ws-real-player",
// real - 实时预览 record - 录像回放
type: "real",
// 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25
maxNum: state.maxNum,
// 初始化,页面显示的路数
num: state.num,
// 是否显示工具栏,默认显示
showControl: true,
// 解码库所在位置的前缀
prefixUrl: "lib",
// true-标准码流拉流 false-私有码流拉流
isRTPStream: true,
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => {
/* 回调函数,可以在以下回调函数里面做监听 */
switch (methods) {
case "initializationCompleted":
console.log("初始化完成");
realPlayer.setPlayerAdapter("stretching");
// 初始化完成,可调用播放方法(适用于动态加载解码库)
// 若回调未触发时就使用实时预览/录像回放,则无法播放。
// 此时我们可以调用一个
if (realPlayer) {
realPlayer.setPlayerAdapter("stretching");
}
playReal();
refreshPage()
break;
case "changeStreamType": // 主/辅码流切换回调
console.log(data.channelData); // 通道数据
console.log(data.streamType); // 码流类型 0-主码流 1-辅码流1 2-辅码流2
console.log(data.selectIndex); // 窗口序号,从 0
// 主/辅码流切换回调
case "changeStreamType":
if (state.streamType != data.streamType) {
console.log("主/辅码流切换回调", data);
state.streamType = data.streamType;
destroy();
initVideo();
}
break;
case "realSuccess": // 实时预览成功
// 实时预览成功
case "realSuccess":
console.log("实时预览成功");
break;
case "realError": // 实时预览失败
console.log("实时预览失败", err);
// 实时预览失败
case "realError":
console.log("实时预览失败", data, err);
break;
// 点击窗口上的对讲按钮触发的事件回调
case "notifyTalk":
// 点击窗口上的对讲按钮触发的事件回调
console.log("notifyTalk", data);
case "talkError": // 对讲失败
console.log("对讲失败");
break;
case "selectWindowChanged": // 选中的窗口发生改变
// 选中的窗口发生改变
case "selectWindowChanged":
console.log(data, "返回窗口信息");
break;
case "windowNumChanged": // 播放器显示的路数发生改变
console.log(data, "返回显示的窗口数量");
// 播放器显示的路数发生改变
case "windowNumChanged":
console.log("每页", data);
if (data == state.num) return
state.num = data
refreshPage()
break;
case "closeVideo": // 视频关闭回调
// 点击关闭按钮引发的视频关闭进行提示
// 切换视频引发的视频关闭不进行提示
// 视频关闭回调
case "closeVideo":
if (!data.changeVideoFlag) {
console.log(`窗口${data.selectIndex}的视频已关闭`);
// console.log(`窗口${data.selectIndex}的视频已关闭`);
}
break;
case "statusChanged": // 视频状态发生改变
// 视频状态发生改变
case "statusChanged":
break;
case "errorInfo": // 错误信息汇总
// 错误信息汇总
case "errorInfo":
console.log(data, "可打印查看错误消息");
}
},
@@ -123,45 +171,73 @@ function initVideo() {
// 预览
async function playReal() {
if (!channel.playList || !channel.playList.length) return
try {
for (let index = 0; index < channel.playList.length; index++) {
const element = channel.playList[index]
const targetChannel = channel.deviceChannelList.find((v: any) => v.channelCode == element.channelId)
// 请求
let errorIndexList = []
for (let index = 0; index < state.channelList.length; index++) {
const element = state.channelList[index];
// 获取通道数据
const _params = {
channelId: element.channelCode,
streamType: state.streamType,
};
try {
const { code, data } = await DeviceCameraApi.getRtspVideo(_params);
if (code != 0 || !data) throw new Error();
realPlayer.realByUrl({
rtspURL: element.fullUrl, // 必传
wsURL: element.wsURL, // 必传
selectIndex: index, // 必传, 播放窗口号
channelId: element.channelId, // 必传, 通道id
streamType: element.streamType, // 必传, 码流类型,
playerAdapter: "stretching", // 选传, selfAdaption 自适应 | stretching 拉伸
rtspURL: data.fullUrl,
wsURL: data.wsURL,
selectIndex: index,
channelId: data.channelId,
streamType: data.streamType,
playerAdapter: "stretching",
channelData: {
// 建议传, 通道树信息
id: targetChannel.channelCode, // {String} 通道编码
deviceCode: targetChannel.deviceCode, // {String} 设备编码
deviceType: targetChannel.deviceType, // {String} 设备类型
channelSeq: targetChannel.channelSeq, // {String|Number} 通道序号
}, // 选传
id: element.channelCode,
deviceCode: element.deviceCode,
deviceType: element.deviceType,
channelSeq: element.channelSeq,
},
});
} catch (error) {
console.error("|预览|,意外的错误", error);
if (realPlayer) {
// realPlayer.close(index);
}
errorIndexList.push(index)
}
} catch (error: any) {
console.error(error);
useMessage().error(error?.msg ?? "视频播放错误");
}
if (errorIndexList.length > 0) {
useMessage().error(`监控${errorIndexList.map(v => v + 1).join(',')}视频加载错误`);
}
}
function refreshPage() {
rendererVideoPage({
current: state.current,
pageSize: state.num,
total: state.total,
onChange: (page: any, pageSize: any) => {
console.log("分页", page, pageSize);
if (page == state.current && pageSize == state.num) {
return
}
state.current = page;
state.num = pageSize;
destroy()
initVideo();
}
})
}
// 销毁
function destroy() {
console.log("摄像头预览销毁");
if (realPlayer) {
if (realPlayer) {
for (let index = 0; index < videoState.videoNum; index++) {
realPlayer.close(index)
}
}
realPlayer.destroy()
realPlayer = null;
if (!realPlayer) return
console.log("销毁的数量", state.num);
for (let index = 0; index < state.num; index++) {
realPlayer.close(index)
}
realPlayer.destroy()
realPlayer = null;
console.log("监控视频销毁成功");
}
// 根据当前数量在numList中找到合适的值(大于等于当前值的最小值)
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="state.dialogVisible" title="预览" width="60%" append-to-body :before-close="handleClose">
<el-dialog v-model="state.dialogVisible" title="预览" width="80%" append-to-body :before-close="handleClose">
<div class="flex justify-center items-start">
<div class="min-w-[20%]">
<el-tree :data="channel.treeList" :props="{
@@ -14,7 +14,7 @@
</el-tree>
</div>
<div class="flex-1 overflow-hidden video-container">
<div id="ws-real-player" style="height: 500px; margin-bottom: 20px" />
<preview-video ref="previewVideoRef" style="height: 500px; margin-bottom: 20px" />
</div>
</div>
<template #footer>
@@ -31,25 +31,19 @@ import { useMessage } from "/@/hooks/message";
import * as DeviceManageApi from "/@/api/device/deviceManage"
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
import * as DeviceCameraApi from "/@/api/device/deviceCamera"
// @ts-ignore
import PlayerManager from "../lib/icc/PlayerManager";
const PreviewVideo = defineAsyncComponent(() => import('./index.vue'));
let realPlayer: any = null;
const previewVideoRef = ref()
const state = reactive({
dialogVisible: false,
channelList: [],
loading: false,
treeLoading: false,
videoNum: 0,
numList: [1, 4, 9, 16, 25],
})
const detail = reactive<any>({
})
const detail = reactive<any>({})
const channel = reactive<any>({
treeList: [],
@@ -59,7 +53,7 @@ const channel = reactive<any>({
const handleClose = () => {
state.dialogVisible = false
destroy()
previewVideoRef.value.destroy()
}
// 打开弹窗
@@ -73,15 +67,11 @@ const openDialog = async (id: string) => {
// 点击树
const handleNodeClick = (e: any) => {
destroy()
if (e.id == -1) {
state.videoNum = findSuitableNum(channel.list.length, state.numList);
initVideo()
playReal()
previewVideoRef.value.init([], detail.deviceCode)
return
}
state.videoNum = 1
initVideo(e.channelCode)
previewVideoRef.value.init([e.channelCode], detail.deviceCode)
}
// 初始化表单数据
@@ -100,7 +90,7 @@ const getdeviceManageData = async (id: string) => {
}
};
// 查询通道列表
// 查询通道树
const getDeviceChannelList = async () => {
state.treeLoading = true
try {
@@ -108,7 +98,6 @@ const getDeviceChannelList = async () => {
if (code != 0) throw new Error()
channel.treeList = data
channel.list = data
state.videoNum = findSuitableNum(channel.list.length, state.numList);
// 处理树
if (data && data.length) {
const tree = data.map((v: any) => {
@@ -127,144 +116,15 @@ const getDeviceChannelList = async () => {
]
}
// 播放视频
initVideo()
previewVideoRef.value.init([], detail.deviceCode)
} catch (error: any) {
console.error(error);
console.error("|查询通道树|,意外的错误", error);
useMessage().error(error?.msg ?? "意外的错误");
} finally {
state.treeLoading = false
}
}
// 初始化视频
function initVideo(channelCode?: string) {
if (realPlayer) {
realPlayer = null;
}
realPlayer = new PlayerManager({
el: "ws-real-player" /** 实时预览容器id,创建多个播放器,传入不同的容器id即可 **/,
type: "real" /** real - 实时预览 record - 录像回放 **/,
maxNum: 16 /** 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25 **/,
num: state.videoNum /** 初始化,页面显示的路数 **/,
showControl: true /** 是否显示工具栏,默认显示 **/,
prefixUrl: "lib" /** 解码库所在位置的前缀 **/,
isRTPStream: true /** true-标准码流拉流 false-私有码流拉流 **/,
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => {
/* 回调函数,可以在以下回调函数里面做监听 */
switch (methods) {
case "initializationCompleted":
console.log("初始化完成");
realPlayer.setPlayerAdapter("stretching");
// 初始化完成,可调用播放方法(适用于动态加载解码库)
// 若回调未触发时就使用实时预览/录像回放,则无法播放。
// 此时我们可以调用一个
playReal(channelCode);
break;
case "changeStreamType": // 主/辅码流切换回调
console.log(data.channelData); // 通道数据
console.log(data.streamType); // 码流类型 0-主码流 1-辅码流1 2-辅码流2
console.log(data.selectIndex); // 窗口序号,从 0
break;
case "realSuccess": // 实时预览成功
console.log("实时预览成功");
break;
case "realError": // 实时预览失败
console.log("实时预览失败", err);
break;
case "notifyTalk":
// 点击窗口上的对讲按钮触发的事件回调
console.log("notifyTalk", data);
case "talkError": // 对讲失败
console.log("对讲失败");
break;
case "selectWindowChanged": // 选中的窗口发生改变
console.log(data, "返回窗口信息");
break;
case "windowNumChanged": // 播放器显示的路数发生改变
console.log(data, "返回显示的窗口数量");
break;
case "closeVideo": // 视频关闭回调
// 点击关闭按钮引发的视频关闭进行提示
// 切换视频引发的视频关闭不进行提示
if (!data.changeVideoFlag) {
console.log(`窗口${data.selectIndex}的视频已关闭`);
}
break;
case "statusChanged": // 视频状态发生改变
break;
case "errorInfo": // 错误信息汇总
console.log(data, "可打印查看错误消息");
}
},
});
}
// 预览
async function playReal(channelCode?: string) {
if (!channel.list || !channel.list.length) return
try {
// 获取流地址
const _data = {
channelIdList: channel.list.map((v: any) => v.channelCode)
}
if (channelCode) {
_data.channelIdList = [channelCode]
}
const { code, data: channelList } = await DeviceCameraApi.getRtspVideoList({
..._data
})
if (code != 0) throw new Error()
if (!channel.list || !channel.list.length) return
state.videoNum = channel.list.length
for (let index = 0; index < channelList.length; index++) {
const element = channelList[index]
const targetChannel = channel.list.find((v: any) => v.channelCode == element.channelId)
realPlayer.realByUrl({
rtspURL: element.fullUrl, // 必传
wsURL: element.wsURL, // 必传
selectIndex: index, // 必传, 播放窗口号
channelId: element.channelId, // 必传, 通道id
streamType: element.streamType, // 必传, 码流类型,
playerAdapter: "stretching", // 选传, selfAdaption 自适应 | stretching 拉伸
channelData: {
// 建议传, 通道树信息
id: targetChannel.channelCode, // {String} 通道编码
deviceCode: targetChannel.deviceCode, // {String} 设备编码
deviceType: targetChannel.deviceType, // {String} 设备类型
channelSeq: targetChannel.channelSeq, // {String|Number} 通道序号
}, // 选传
});
}
} catch (error: any) {
console.error(error);
useMessage().error(error?.msg ?? "视频播放错误");
}
}
// 销毁
function destroy() {
console.log("摄像头预览销毁");
if (realPlayer) {
if (realPlayer) {
for (let index = 0; index < state.videoNum; index++) {
realPlayer.close(index)
}
}
realPlayer.destroy()
realPlayer = null;
}
}
// 根据当前数量在numList中找到合适的值(大于等于当前值的最小值)
function findSuitableNum(currentNum: any, numList: any) {
const suitableNums = numList.filter((num: any) => num >= currentNum);
if (suitableNums.length > 0) {
return Math.min(...suitableNums);
} else {
return Math.max(...numList);
}
}
defineExpose({
openDialog
});
@@ -5,10 +5,12 @@
</template>
<script setup lang="ts" name="ReplayVideo">
import * as AlarmReplayVideoListApi from "/@/api/third/deviceCameraReplay"
import { formatDateRange, getDateRangeTimestamps } from "/@/utils/formatTime"
import { useMessage } from "/@/hooks/message";
import { formatDateRange } from "/@/utils/formatTime"
import * as AlarmReplayVideoListApi from "/@/api/third/deviceCameraReplay"
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
const emit = defineEmits(['mounted'])
// @ts-ignore
import PlayerManager from "../lib/icc/PlayerManager";
@@ -16,78 +18,129 @@ import PlayerManager from "../lib/icc/PlayerManager";
let replayVideo: any = null
const state = reactive<any>({
channelList: [],
startTimestamp: null,
endTimestamp: null,
queryForm: {},
dialogVisible: false,
channelList: [],
loading: false,
treeLoading: false,
currentNum: 4,
numList: [1, 4, 9, 16, 25],
maxCurrentNum: 16,
numList: [1, 4, 9, 16],
type: "device",
code: null,
current: 1,
total: 100,
})
onMounted(() => {
emit("mounted")
const query = reactive({
list: [],
deviceCode: null,
})
onUnmounted(() => {
destroy()
})
// 初始化播放器
const init = (dataList: any, queryForm: any) => {
destroy()
state.queryForm = queryForm
if (dataList && dataList.length) {
state.channelList = dataList
const init = (queryForm: any, replayForm: any) => {
if (queryForm) {
if (queryForm.list) {
query.list = queryForm.list
}
if (queryForm.deviceCode) {
query.deviceCode = queryForm.deviceCode
}
}
state.queryForm = replayForm
// 处理日期范围
if (queryForm.videoDateList && queryForm.videoDateList.length == 2) {
const dateRange = formatDateRange(queryForm.videoDateList)
state.startTimestamp = dateRange.startTimestamp
state.endTimestamp = dateRange.endTimestamp
if (replayForm.videoDateList) {
if (replayForm.videoDateList.length == 2) {
const dateRange = formatDateRange(replayForm.videoDateList)
state.startTimestamp = dateRange.startTimestamp
state.endTimestamp = dateRange.endTimestamp
}
}
state.currentNum = findSuitableNum(state.channelList.length, state.numList)
destroy()
initVideo()
}
// 初始化视频
async function initVideo() {
const _params = {
deviceChannelCodeList: query.list ?? [],
deviceCode: query.deviceCode ?? null,
current: state.current,
size: state.currentNum,
};
try {
let { code, data } = await DeviceChannelApi.getChannelPage(_params);
if (code != 0) throw new Error();
let { records, total } = data
if (!records || !records.length) records = []
state.total = total
let suitableNum = state.num
if (state.total < state.numList[state.numList.length - 1]) {
suitableNum = findSuitableNum(records.length, state.numList);
state.maxCurrentNum = suitableNum
}
state.currentNum = suitableNum;
state.channelList = records;
console.log(`原始通道数: ${records.length}, 调整后的显示路数: ${suitableNum}`);
// 初始化播放器
initPlayerManager();
} catch (error) {
console.error("|初始化视频|,意外的错误", error);
}
}
// 初始化视频
function initPlayerManager() {
if (replayVideo) replayVideo = null;
replayVideo = new PlayerManager({
el: "ws-record-player", /** 实时预览容器id,创建多个播放器,传入不同的容器id即可 **/
type: 'record', /** real - 实时预览 record - 录像回放 **/
maxNum: 16, /** 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25 **/
num: state.currentNum, /** 初始化,页面显示的路数 **/
showControl: true, /** 是否显示工具栏,默认显示 **/
prefixUrl: 'lib', /** 解码库所在位置的前缀 **/
isRTPStream: true, /** true-标准码流拉流 false-私有码流拉流 **/
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => { /* 回调函数,可以在以下回调函数里面做监听 */
// 实时预览容器id,创建多个播放器,传入不同的容器id即可
el: "ws-record-player",
// real - 实时预览 record - 录像回放
type: 'record',
// 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25
maxNum: state.maxCurrentNum,
// 初始化,页面显示的路数
num: state.currentNum,
// 是否显示工具栏,默认显示
showControl: true,
// 解码库所在位置的前缀
prefixUrl: 'lib',
// true-标准码流拉流 false-私有码流拉流
isRTPStream: true,
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => {
switch (methods) {
// 初始化完成,可以调用拉流方法,实现播放器加载后立即显示画面
case 'initializationCompleted':
// 初始化完成,可以调用拉流方法,实现播放器加载后立即显示画面
playRecord()
break;
// 录像回放成功
case "recordSuccess":
// 录像回放成功
console.log("录像回放成功")
break;
// 录像回放失败
case "recordError":
// 录像回放失败
console.log("录像回放失败", err)
break;
// 录像回放完成
case "recordFinish":
// 录像回放完成
console.log("录像回放完成");
break;
// 拖拽时间戳的回调
case 'switchStartTime':
console.log("拖拽时间戳的回调:", data);
break;
// 选中的窗口发生改变
case 'selectWindowChanged':
// 选中的窗口发生改变
console.log(data, "返回窗口信息")
break;
// 播放器显示的路数发生改变
case 'windowNumChanged':
// 播放器显示的路数发生改变
console.log(data, "返回显示的窗口数量")
if (state.currentNum == data) return
state.currentNum = data
if (replayVideo) {
@@ -97,9 +150,8 @@ const init = (dataList: any, queryForm: any) => {
}
playRecord()
break;
case 'closeVideo': // 视频关闭回调
// 点击关闭按钮引发的视频关闭进行提示
// 切换视频引发的视频关闭不进行提示
// 视频关闭回调
case 'closeVideo':
if (!data.changeVideoFlag) {
console.log(`窗口${data.selectIndex}的视频已关闭`)
}
@@ -117,45 +169,51 @@ const init = (dataList: any, queryForm: any) => {
// 流地址方式播放录像回放
const playRecord = async () => {
// 根据maxNum获取
if (!state.channelList || !state.channelList.length) return
const playList = state.channelList.slice(0, state.currentNum)
for (let index = 0; index < playList.length; index++) {
const element: any = playList[index];
// 根据maxNum获取
let list = state.channelList
if (!list || !list.length) return
let errorIndexList = []
for (let index = 0; index < list.length; index++) {
const element = list[index];
const _data = {
...state.queryForm,
channelId: element.channelCode
}
// 获取回放列表
const recordRes = await AlarmReplayVideoListApi.defaultReplayVideoList(_data)
if (recordRes.code != 0) continue
const videoRes = await AlarmReplayVideoListApi.getRtspReplayVideo(_data)
if (videoRes.code != 0) continue
console.log("data:", videoRes.data);
replayVideo.recordByUrl({
wsURL: videoRes.data.wsURL,
rtspURL: videoRes.data.fullUrl,
channelId: videoRes.data.channelId, // 必传, 通道id
records: recordRes.data || [], // 录像列表
selectIndex: index, // 必传
playRecordByTime: true, // 默认传true,拖拽异常时请传入false
startTime: state.startTimestamp, // 开始时间戳(秒)
endTime: state.endTimestamp, // 结束时间戳(秒)
// playerAdapter: opt.playerAdapter, // 选传, , selfAdaption 自适应 | stretching 拉伸
channelData: {
records: recordRes.data || []
}, // 选传, 通道信息
})
try {
const recordRes = await AlarmReplayVideoListApi.defaultReplayVideoList(_data)
if (recordRes.code != 0 || !recordRes.data) throw new Error();
const videoRes = await AlarmReplayVideoListApi.getRtspReplayVideo(_data)
if (videoRes.code != 0 || !videoRes.data) throw new Error();
replayVideo.recordByUrl({
wsURL: videoRes.data.wsURL,
rtspURL: videoRes.data.fullUrl,
channelId: videoRes.data.channelId,
records: recordRes.data || [],
selectIndex: index,
playRecordByTime: true,
startTime: state.startTimestamp,
endTime: state.endTimestamp,
channelData: {
records: recordRes.data || []
},
})
} catch (error) {
console.error("|流地址方式播放录像回放|,意外的错误", error);
errorIndexList.push(index)
}
if (errorIndexList.length > 0) {
useMessage().error(`监控${errorIndexList.map(v => v + 1).join(',')}视频加载错误`);
}
}
}
// 销毁
function destroy() {
if (!replayVideo) return
if (replayVideo) {
for (let index = 0; index < state.currentNum; index++) {
replayVideo.close(index)
}
for (let index = 0; index < state.currentNum; index++) {
replayVideo.close(index)
}
replayVideo.destroy()
replayVideo = null;
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="state.dialogVisible" title="回放" width="75%" :before-close="handleClose">
<el-dialog v-model="state.dialogVisible" title="回放" width="80%" :before-close="handleClose">
<div class="flex justify-center items-start">
<div v-if="state.type == 'device'" class="min-w-[20%]">
<el-tree :data="channel.treeList" :props="{
@@ -34,7 +34,7 @@
</el-button>
</el-form-item>
</el-form>
<div id="ws-record-player" style="height: 500px; margin-bottom: 20px" />
<ReplayVideo ref="replayVideoRef" style="height: 500px; margin-bottom: 20px" />
</div>
</div>
<template #footer>
@@ -54,14 +54,15 @@ import * as AlarmReplayVideoListApi from "/@/api/third/deviceCameraReplay"
import * as DeviceManageApi from "/@/api/device/deviceManage"
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
const ReplayVideo = defineAsyncComponent(() => import("./index.vue"));
// 定义查询字典
const { dh_replay_source, dh_video_stream_type } = useDict("dh_replay_source", "dh_video_stream_type")
// @ts-ignore
import PlayerManager from "../lib/icc/PlayerManager";
let replayVideo: any = null
const replayVideoRef = ref()
const state = reactive<any>({
startTimestamp: null,
endTimestamp: null,
@@ -92,13 +93,11 @@ const channel = reactive<any>({
// 打开弹窗
const openDialog = async (type: string, code: string, timeRange?: { startTime: string, endTime: string }) => {
console.log("type:", type, "code:", code, "timeRange:", timeRange);
if (!type || !code) return
state.type = type
state.code = code
state.dialogVisible = true
if (!code) return
// 如果传入了时间范围,则设置查询表单的时间
if (timeRange) {
queryForm.videoDateList = [timeRange.startTime, timeRange.endTime]
@@ -119,21 +118,24 @@ const doSearch = async () => {
}
const handleClose = () => {
replayVideoRef.value.destroy()
state.dialogVisible = false
destroy()
}
// 点击树
const handleNodeClick = (e: any) => {
console.log("点击树:", e);
destroy()
if (e.id == -1) {
state.currentNum = findSuitableNum(channel.list.length, state.numList);
initVideo()
replayVideoRef.value.init(
{ list: [], deviceCode: state.code },
{}
)
return
}
state.currentNum = 1
initVideo(e.channelCode)
replayVideoRef.value.init(
{ list: [e.channelCode], deviceCode: state.code },
{}
)
}
// 初始化表单数据
@@ -168,15 +170,12 @@ const getDeviceChannelList = async (codeStr: any) => {
}
const { data, code } = await DeviceChannelApi.list(_data)
if (code != 0) throw new Error()
if (!data || !data.length) {
useMessage().error("查询不到设备通道")
return
}
channel.treeList = data
channel.list = data
state.currentNum = findSuitableNum(channel.list.length, state.numList);
// 处理树
if (data && data.length) {
const tree = data.map((v: any) => {
@@ -194,8 +193,11 @@ const getDeviceChannelList = async (codeStr: any) => {
}
]
}
// 播放回放
initVideo()
console.log("replayVideoRef:", replayVideoRef);
replayVideoRef.value.init(
{ list: [], deviceCode: state.code },
{}
)
} catch (error: any) {
console.error(error);
useMessage().error(error?.msg ?? "意外的错误");
@@ -242,84 +244,6 @@ const playRecord = async (channelCode?: string) => {
}
}
// 初始化视频
function initVideo(channelCode?: string) {
if (replayVideo) replayVideo = null;
replayVideo = new PlayerManager({
el: "ws-record-player", /** 实时预览容器id,创建多个播放器,传入不同的容器id即可 **/
type: 'record', /** real - 实时预览 record - 录像回放 **/
maxNum: 16, /** 一个播放器上限能播放的路数,可根据实际情况设置,支持 1 4 9 16 25 **/
num: state.currentNum, /** 初始化,页面显示的路数 **/
showControl: true, /** 是否显示工具栏,默认显示 **/
prefixUrl: 'lib', /** 解码库所在位置的前缀 **/
isRTPStream: true, /** true-标准码流拉流 false-私有码流拉流 **/
receiveMessageFromWSPlayer: (methods: any, data: any, err: any) => { /* 回调函数,可以在以下回调函数里面做监听 */
switch (methods) {
case 'initializationCompleted':
// 初始化完成,可以调用拉流方法,实现播放器加载后立即显示画面
playRecord(channelCode)
break;
case "recordSuccess":
// 录像回放成功
console.log("录像回放成功")
break;
case "recordError":
// 录像回放失败
console.log("录像回放失败", err)
break;
case "recordFinish":
// 录像回放完成
console.log("录像回放完成");
break;
// 拖拽时间戳的回调
case 'switchStartTime':
console.log("拖拽时间戳的回调:", data);
break;
case 'selectWindowChanged':
// 选中的窗口发生改变
console.log(data, "返回窗口信息")
break;
case 'windowNumChanged':
// 播放器显示的路数发生改变
console.log(data, "返回显示的窗口数量")
if (state.currentNum == data) return
state.currentNum = data
if (replayVideo) {
for (let index = 0; index < state.currentNum; index++) {
replayVideo.close(index)
}
}
playRecord()
break;
case 'closeVideo': // 视频关闭回调
// 点击关闭按钮引发的视频关闭进行提示
// 切换视频引发的视频关闭不进行提示
if (!data.changeVideoFlag) {
console.log(`窗口${data.selectIndex}的视频已关闭`)
}
break;
case 'statusChanged':
// 视频状态发生改变
break;
case 'errorInfo':
// 错误信息汇总
console.log(data, "可打印查看错误消息");
}
}
})
}
// 销毁
function destroy() {
if (!replayVideo) return
if (replayVideo) {
for (let index = 0; index < state.currentNum; index++) {
replayVideo.close(index)
}
}
replayVideo.destroy()
replayVideo = null;
}
// 获取默认日期范围(前三天)
function getDefaultDateRange() {
@@ -378,16 +302,6 @@ const disabledDate = (time: Date) => {
return false
}
// 根据当前数量在numList中找到合适的值(大于等于当前值的最小值)
function findSuitableNum(currentNum: any, numList: any) {
const suitableNums = numList.filter((num: any) => num >= currentNum);
if (suitableNums.length > 0) {
return Math.min(...suitableNums);
} else {
return Math.max(...numList);
}
}
// 暴露变量
defineExpose({
openDialog
+12 -6
View File
@@ -24,7 +24,7 @@
</el-form-item>
</el-form>
</el-row>
<el-row>
<el-row v-if="false">
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
v-auth="'admin_deviceManage_add'">
@@ -80,11 +80,17 @@
<el-button icon="VideoPlay" text type="primary"
@click="previewVideoRef.openDialog(row.id)">预览</el-button>
<el-button icon="VideoCamera" text type="primary"
@click="replayVideoDialogRef.openDialog('device', row.deviceCode)">回放</el-button>
<el-button icon="edit-pen" text type="primary" v-auth="'admin_deviceManage_edit'"
@click="formDialogRef.openDialog(row.id)">编辑</el-button>
<el-button icon="delete" text type="primary" v-auth="'admin_deviceManage_del'"
@click="handleDelete([row.id])">删除</el-button>
@click="replayVideoDialogRef.openDialog('device', row.deviceCode)">
回放
</el-button>
<el-button v-if="false" icon="edit-pen" text type="primary"
v-auth="'admin_deviceManage_edit'" @click="formDialogRef.openDialog(row.id)">
编辑
</el-button>
<el-button v-if="false" icon="delete" text type="primary"
v-auth="'admin_deviceManage_del'" @click="handleDelete([row.id])">
删除
</el-button>
</div>
</template>
</el-table-column>
@@ -38,8 +38,7 @@
</template>
<script setup lang="ts" name="systemDeviceCameraReplay">
import { BasicTableProps, useTable } from "/@/hooks/table";
import { useMessage, useMessageBox } from "/@/hooks/message";
import { useMessage } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import * as DeviceCameraReplayApi from "/@/api/third/deviceCameraReplay";
+1 -2
View File
@@ -1,7 +1,6 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
:nodeClickable="false">
@@ -25,7 +24,7 @@
</el-form-item>
</el-form>
</el-row>
<el-row>
<el-row v-if="false">
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()"
v-auth="'admin_deviceManage_add'">
+2 -2
View File
@@ -33,8 +33,8 @@
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="80" />
<el-table-column label="类型" prop="syncConfigType">
</el-table-column>
<!-- <el-table-column label="类型" prop="syncConfigType">
</el-table-column> -->
<el-table-column min-width="200" label="报警类型">
<template #default="{ row }">
-1
View File
@@ -116,7 +116,6 @@ const getThemeConfig = computed(() => {
* 其他正常
*/
const jumpAdminPanel = () => {
console.log("userInfos", useUserInfoStore.userInfos);
const isDev = import.meta.env.MODE == "development"
const baseAdminUrl = isDev ? 'http://localhost:5173/device' : 'http://192.168.50.10:7001/device';
const roleCodes = useUserInfoStore.userInfos?.roleCodes ?? []