feat:优化视频预览

This commit is contained in:
dzc committed 2026-03-02 19:00:00 +08:00
1 parent 18b39813c0
commit 01508bf7a2
16 files changed
+3563 -665

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
{
"type": "module"
}
-3
View File
@@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar"]
}
+1
View File
@@ -26,6 +26,7 @@
"axios": "^1.3.3",
"codemirror": "5.65.5",
"crypto-js": "^3.1.9-1",
"dayjs": "^1.11.19",
"disable-devtool": "^0.3.8",
"driver.js": "^0.9.8",
"echarts": "^5.4.1",
+8 -5
View File
@@ -47,6 +47,9 @@ importers:
crypto-js:
specifier: ^3.1.9-1
version: 3.3.0
dayjs:
specifier: ^1.11.19
version: 1.11.19
disable-devtool:
specifier: ^0.3.8
version: 0.3.8
@@ -1570,8 +1573,8 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
dayjs@1.11.19:
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
@@ -4490,7 +4493,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.27.6
dayjs@1.11.13: {}
dayjs@1.11.19: {}
debug@4.4.1:
dependencies:
@@ -4567,7 +4570,7 @@ snapshots:
'@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.4.15(typescript@4.9.5))
async-validator: 4.2.5
dayjs: 1.11.13
dayjs: 1.11.19
escape-html: 1.0.3
lodash: 4.17.21
lodash-es: 4.17.21
@@ -4588,7 +4591,7 @@ snapshots:
'@types/lodash-es': 4.17.12
'@vueuse/core': 9.13.0(vue@3.4.15(typescript@4.9.5))
async-validator: 4.2.5
dayjs: 1.11.13
dayjs: 1.11.19
escape-html: 1.0.3
lodash: 4.17.21
lodash-es: 4.17.21
+4
View File
@@ -40,6 +40,8 @@ import ColumnBtn from "/@/components/Column/ColumnBtn.vue"
import PreviewJson from "/@/components/PreviewJson/index.vue"
import RtspPluginVideo from "/@/components/rtsp-plugin-video/video-plugin.vue"
// 第三方组件
import ElementPlus from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
@@ -102,6 +104,8 @@ export default {
app.component('PreviewJson', PreviewJson);
app.component('RtspPluginVideo', RtspPluginVideo)
// 导入全部的elmenet-plus的图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,502 @@
<template>
<div class="video-container">
<div id="video-plugin-preview-0" class="pane" />
<div id="video-plugin-preview-3" class="pane" />
<div id="video-plugin-preview-7" class="pane" />
</div>
</template>
<script setup>
import { onMounted, onUnmounted } from "vue"
import { ElMessage } from "element-plus";
import dayjs from "dayjs"
import * as VideoPlayer from "./lib/videoPlayer";
import * as settingApi from "/@/api/com/comDhSetting"
const TAG = "|视频预览|"
let VIDEO_PLAYER = null
let currentWindowType = null
let isReady = false
/** @type {Array<() => void>} */
let readyQueue = []
let lastRealRequest = null
let lastPlaybackRequest = null
let realFallbackAttempted = false
const players = new Map()
const isReadyByType = new Map()
function getContainerId(type) {
return `video-plugin-preview-${type}`
}
function setActiveContainer(type) {
const ids = [0, 3, 7].map(getContainerId)
ids.forEach((id) => {
const el = document.getElementById(id)
if (!el) return
if (id === getContainerId(type)) el.classList.add("active")
else el.classList.remove("active")
})
}
/**
* 将任务加入就绪队列;若已就绪则立即执行
* @param {() => void} fn 任务函数
*/
function enqueueReadyTask(fn) {
if (isReady) {
try {
fn()
} catch (e) {
console.log(TAG, "就绪任务执行失败,err:", e)
}
} else {
readyQueue.push(fn)
}
}
/**
* 清空就绪队列并执行
*/
function flushReadyQueue() {
const tasks = readyQueue.splice(0)
tasks.forEach((fn) => {
try {
fn()
} catch (e) {
console.log(TAG, "就绪队列任务执行失败,err:", e)
}
})
}
/**
* 登录信息
* @typedef {Object} LoginInfo
* @property {string} host icc 平台的 ip
* @property {string} port icc 平台的端口(https 通常为 443)
* @property {string} username 登录用户名
* @property {string} password 登录密码
*/
/**
* 实时预览参数
* @typedef {Object} RealOptions
* @property {string} channelId 通道 id(必传)
* @property {string} [channelName] 通道名称(用于本地录像下载)
* @property {number} snum 播放窗口序号(从 0 开始,必传)
* @property {number} [streamType] 码流类型:1-主码流 2-辅码流(默认主码流)
* @property {number} [deviceType] 设备类别(用于对讲)
* @property {string} [cameraType] 摄像头类型(用于云台)
* @property {string} [capability] 能力集(用于云台)
*/
/**
* 录像回放参数
* @typedef {Object} PlaybackOptions
* @property {string} channelId 通道 id
* @property {string} [channelName] 通道名称(用于本地录像下载和错误提示)
* @property {string} startTime 开始时间(格式:YYYY-MM-DD HH:mm:ss)
* @property {string} endTime 结束时间(格式:YYYY-MM-DD HH:mm:ss)
* @property {number} [recordSource] 录像来源:2-设备录像 3-中心录像
* @property {number} [streamType] 码流选择:0-所有码流 1-主码流 2-辅码流
* @property {number} snum 播放窗口序号(从 0 开始)
*/
/**
* 组件入参
* - 提供登录信息与播放器基础参数,支持父组件通过 ref 调用预览与回放方法
*/
const props = defineProps({
/** 播放器类型:0-实时预览,3-录像回放,7-录像回放(支持倒放) */
windowType: { type: Number, default: 0 },
/** 默认展示的窗口数量 */
division: { type: Number, default: 1 },
/** 窗口拖拽(暂不支持,默认 false) */
draggable: { type: Boolean, default: false },
/** 是否显示底部操作栏 */
showBar: { type: Boolean, default: true },
})
/**
* 初始化播放器
* @param {0|3|7} [type=props.windowType] 播放器类型(0预览/3回放/7回放支持倒放)
*/
async function initVideoPlayer(type = props.windowType) {
// 查询配置
const { data: setting, code: settingCode } = await settingApi.getObj()
if (settingCode != 0) {
ElMessage.error("查询配置失败")
return
}
const dhIccSetting = setting.dhIccSetting
try {
// 若已有缓存实例,直接复用并切换容器
if (players.has(type)) {
VIDEO_PLAYER = players.get(type)
currentWindowType = type
isReady = !!isReadyByType.get(type)
setActiveContainer(type)
console.log("缓存-VIDEO_PLAYER:", VIDEO_PLAYER);
console.log(TAG, "复用缓存播放器,windowType:", type)
if (isReady) flushReadyQueue()
return
}
} catch (e) {
console.log(TAG, "旧播放器销毁失败,err:", e)
}
isReady = false
isReadyByType.set(type, false)
setActiveContainer(type)
VIDEO_PLAYER = new window.VideoPlayer({
videoId: getContainerId(type),
// 播放器类型:0-实时预览,3-录像回放,7-录像回放(支持倒放)
windowType: type,
// zh-中文 en-英文
// V1.2.1新增属性,支持语言切换,初始化时设置好语言.
language: 'zh',
// 写死true即可
usePluginLogin: true,
// 必传
pluginLoginInfo: {
// icc平台的ip
host: dhIccSetting.videoHost ?? "",
// icc 平台的端口 https (默认 443)
port: dhIccSetting?.videoPort ?? "",
// 登录 icc 平台的用户名 用户名也可能是 admin/zzz/...
username: dhIccSetting?.videoUsername ?? "",
// 登录 icc 平台的密码
password: dhIccSetting?.videoPassword ?? "",
},
// 默认展示的窗口数量, 必传
division: props.division,
// 窗口拖拽 【暂不支持】
draggable: props.draggable,
// 底部操作栏, 选传,【true - 显示, false - 隐藏】
showBar: props.showBar,
// 如果DOM元素被插件挡住了,把DOM元素的类名传入。
shieldClass: ["el-select__popper"],
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
coverShieldClass: [],
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
parentIframeShieldClass: [],
// 创建播放器成功回调
createSuccess: (versionInfo) => {
// 该回调触发后,我们可以进行实时预览/录像回放等操作
console.log(TAG, "创建播放器成功,versionInfo:", versionInfo)
isReady = true
isReadyByType.set(type, true)
flushReadyQueue()
},
// 创建播放器失败回调
createError: (err) => {
console.log(TAG, "创建播放器失败,err:", err)
isReady = false
isReadyByType.set(type, false)
if ([1001].includes(err.code)) {
ElMessage.error(err.message)
}
},
// 插件公共回调
dhPlayerMessage: (info, err) => {
console.log(TAG, "插件公共消息,info:", info, "err:", err)
},
// 实时预览成功回调
realSuccess: (info) => {
console.log(TAG, "实时预览成功,info:", info)
realFallbackAttempted = false
},
// 实时预览失败回调
realError: (info, err) => {
console.log(TAG, "实时预览失败,info:", info, "err:", err)
if ([408].includes(err.code)) {
try {
const hasSubStream = Array.isArray(lastRealRequest) && lastRealRequest.some(it => (it?.streamType === 2))
if (hasSubStream && !realFallbackAttempted) {
realFallbackAttempted = true
const fallback = lastRealRequest.map(it => ({ ...it, streamType: 1 }))
lastRealRequest = fallback
console.log(TAG, "辅码流不可用,自动切换主码流重试")
VIDEO_PLAYER.startReal(fallback)
}
} catch (e) {
console.log(TAG, "自动切换主码流失败,err:", e)
}
}
},
// 对讲成功回调
talkSuccess: (info) => {
console.log(TAG, "对讲成功,info:", info)
},
// 对讲失败回调
talkError: (info, err) => {
console.log(TAG, "对讲失败,info:", info, "err:", err)
},
// 录像播放成功回调
playbackSuccess: (info) => {
console.log(TAG, "录像播放成功,info:", info)
},
// 录像播放失败回调
playbackError: (info, err) => {
console.log(TAG, "录像播放失败,info:", info, "err:", err)
},
// 录像播放完成回调
playbackFinish: (info) => {
console.log(TAG, "录像播放完成,info:", info)
},
// 抓图成功回调
snapshotSuccess: ({ base64Url, path }, info) => {
console.log(TAG, "抓图成功,base64Url:", base64Url, "path:", path, "info:", info)
// 可选:保存图片到本地(演示示例)
saveSnapshot(base64Url, `${info.channelName}-抓图-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.jpg`)
},
// 录制视频成功回调
videoDownloadSuccess: (path, info) => {
console.log(TAG, "录制视频成功,path:", path, "info:", info)
// 可选:保存视频到本地(演示示例)
saveVideo(path, `${info.channelName}-录像-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.mp4`)
},
// 关闭视频窗口回调
closeWindowSuccess: ({ isAll, snum, channelList }) => {
console.log(TAG, "关闭视频窗口,isAll:", isAll, "snum:", snum, "channelList:", channelList)
},
// 鼠标单击窗口回调
clickWindow: (snum) => {
console.log(TAG, "鼠标单击窗口,snum:", snum)
},
// 鼠标双击窗口回调
dbClickWindow: (snum) => {
console.log(TAG, "鼠标双击窗口,snum:", snum)
},
// 播放器窗口的数量回调
changeDivision: (division) => {
console.log(TAG, "窗口数量变更,division:", division)
},
// rtsp 流下载录像成功回调
downloadRecordSuccess: (info) => {
console.log(TAG, "RTSP录像下载成功,info:", info)
},
// rtsp 流下载录像失败回调
downloadRecordError: (info, err) => {
console.log(TAG, "RTSP录像下载失败,info:", info, "err:", err)
}
})
currentWindowType = type
players.set(type, VIDEO_PLAYER)
console.log(TAG, "播放器已初始化,windowType:", type)
}
/**
* 确保播放器类型与目标一致,不一致则重新创建
* @param {0|3|7} target 目标类型
* @returns {boolean} 是否发生重新创建
*/
function ensureWindowType(target) {
if (currentWindowType !== target) {
console.log(TAG, "检测到窗口类型不一致,当前:", currentWindowType, "目标:", target, "正在重新创建")
initVideoPlayer(target)
return true
}
return false
}
/**
* 开始实时预览
* @param {RealOptions[]} channels 播放参数数组
*/
function startReal(channels) {
if (!VIDEO_PLAYER) {
initVideoPlayer(0)
} else {
ensureWindowType(0)
}
lastRealRequest = channels
realFallbackAttempted = false
const showBtn = ["BTN_STREAM", "BTN_VOICE", "BTN_TALK", "BTN_RECORD", "BTN_PIC", "BTN_ENLARGE"]
enqueueReadyTask(() => {
console.log(TAG, "开始实时预览,channels:", channels)
VIDEO_PLAYER.startReal(channels)
// 实时预览成功后,设置操作按钮
for (let i = 0; i < channels.length; i++) {
VIDEO_PLAYER.setTabControlBtn(showBtn, i)
}
})
}
/**
* 开始录像回放
* @param {PlaybackOptions[]} items 回放参数数组
*/
function startPlayback(items) {
if (!VIDEO_PLAYER) {
initVideoPlayer(3)
} else {
ensureWindowType(3)
}
lastPlaybackRequest = items
enqueueReadyTask(() => {
console.log(TAG, "开始录像回放,items:", items)
VIDEO_PLAYER.startPlayback(items)
})
}
/**
* 显示播放器(两种方式演示)
* @param {boolean} visible 是否显示
*/
function updateVisible(visible) {
if (!VIDEO_PLAYER) return
try {
if (visible) VIDEO_PLAYER.show?.()
else VIDEO_PLAYER.hide?.()
console.log(TAG, "更新可见性,visible:", VIDEO_PLAYER.setting)
} catch (e) {
console.log(TAG, "更新可见性失败,err:", e)
}
}
function handleVisibilityChange() {
if (document.hidden) {
if (!VIDEO_PLAYER) return
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
VIDEO_PLAYER.closeVideo?.()
}
if ((currentWindowType === 3 || currentWindowType === 7) && Array.isArray(lastPlaybackRequest)) {
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 0 })
}
})
}
VIDEO_PLAYER.hide?.()
} else {
if (!VIDEO_PLAYER) return
VIDEO_PLAYER.show?.()
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
startReal(lastRealRequest)
}
if ((currentWindowType === 3 || currentWindowType === 7) && Array.isArray(lastPlaybackRequest)) {
enqueueReadyTask(() => {
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
}
})
})
}
}
}
/**
* 保存截图到本地(可选)
* @param {string} base64Url base64 图片
* @param {string} filename 文件名
*/
function saveSnapshot(base64Url, filename) {
try {
const base64 = base64Url.replace(/^data:image\/(png|jpeg|jpg);base64,/, "")
const byteChars = atob(base64)
const byteNums = new Array(byteChars.length)
for (let i = 0; i < byteChars.length; i++) byteNums[i] = byteChars.charCodeAt(i)
const blob = new Blob([new Uint8Array(byteNums)], { type: "image/jpeg" })
const a = document.createElement("a")
a.download = filename || "snapshot.jpg"
a.href = URL.createObjectURL(blob)
a.click()
console.log(TAG, "保存截图成功,filename:", filename)
} catch (e) {
console.log(TAG, "保存截图失败,err:", e)
}
}
/**
* 保存视频到本地(可选)
* @param {string} path 视频路径
* @param {string} filename 文件名
*/
function saveVideo(path, filename) {
try {
if (!path) {
console.log(TAG, "保存视频失败,原因:", "路径为空")
return
}
const p = String(path)
const isLocal = p.startsWith("file://") || /^[a-zA-Z]:/.test(p)
if (isLocal) {
console.log(TAG, "无法通过浏览器下载本地文件,路径:", p)
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(p)
.then(() => console.log(TAG, "已复制到剪贴板,请在资源管理器打开"))
.catch((e) => console.log(TAG, "复制到剪贴板失败,err:", e))
}
return
}
const a = document.createElement("a")
a.download = filename || "video.mp4"
a.href = p
a.target = "_blank"
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
console.log(TAG, "保存视频成功,filename:", filename)
} catch (e) {
console.log(TAG, "保存视频失败,err:", e)
}
}
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange)
})
onUnmounted(() => {
try {
document.removeEventListener("visibilitychange", handleVisibilityChange)
VIDEO_PLAYER?.closeVideo?.()
VIDEO_PLAYER?.hide?.()
// VIDEO_PLAYER?.destroy?.()
players.forEach((p) => {
try { p?.closeVideo?.() } catch (e) { console.log(TAG, "清空播放列表失败,err:", e) }
try { p?.hide?.() } catch (e) { console.log(TAG, "隐藏缓存播放器失败,err:", e) }
// try { p?.destroy?.() } catch (e) { console.log(TAG, "销毁缓存播放器失败,err:", e) }
})
console.log(TAG, "组件卸载,已隐藏播放器与缓存")
} catch (e) {
console.log(TAG, "隐藏播放器或缓存失败,err:", e)
}
})
// 暴露方法给父组件,通过 ref 调用
defineExpose({
initVideoPlayer,
startReal,
startPlayback,
updateVisible,
})
</script>
<style scoped>
.video-container {
flex: 1;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
.pane {
width: 100%;
height: 100%;
display: none;
}
.pane.active {
display: block;
}
</style>
+10
View File
@@ -1,5 +1,10 @@
<template>
<div class="layout-navbars-breadcrumb-user pr15" :style="{ flex: layoutUserFlexNum }">
<div class="layout-navbars-breadcrumb-user-icon" @click="downloadDhPlugin">
<el-icon title="下载大华播放插件">
<ele-CameraFilled />
</el-icon>
</div>
<div class="layout-navbars-breadcrumb-user-icon" @click="downloadChrome">
<el-icon title="下载谷歌浏览器">
<ele-ChromeFilled />
@@ -132,6 +137,11 @@ const downloadChrome = async () => {
await other.downBlobFile(other.adaptationUrl("/admin/sys-file/local/file/109.0.5414.120_chrome_installer_x64.exe"), {}, 'ChromeStandaloneSetup64.exe');
}
// 下载大华播放插件
const downloadDhPlugin = async () => {
await other.downBlobFile(other.adaptationUrl("/admin/sys-file/local/file/DHPlayerSetup.exe"), {}, 'DHPlayerSetup.exe');
}
// 全屏点击时
const onScreenfullClick = () => {
if (!screenfull.isEnabled) {
@@ -48,9 +48,6 @@
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button v-if="false" icon="VideoPlay" text type="primary"
@click="previewVideoDialogRef.openDialog('channel', row.channelCode)">
预览</el-button>
<el-button icon="edit-pen" text type="primary" v-auth="'area_areaHouse_edit'"
@click="formDialogRef.openDialog(state.queryForm.houseId, row.id)">编辑</el-button>
<el-button icon="delete" text type="primary" v-auth="'area_areaHouse_del'"
@@ -67,7 +64,6 @@
</template>
<BindCameraFormDialog ref="formDialogRef" @refresh="getDataList" />
<PreviewVideoDialog ref="previewVideoDialogRef" />
</el-dialog>
</template>
@@ -82,11 +78,9 @@ import * as AreaHouseApi from '/@/api/area/areaHouse'
// 引入组件
const BindCameraFormDialog = defineAsyncComponent(() => import('./bindCameraForm.vue'));
const PreviewVideoDialog = defineAsyncComponent(() => import('../../device/components/preview-video/index.vue'));
// 定义变量内容
const formDialogRef = ref()
const previewVideoDialogRef = ref()
const dataFormRef = ref();
const visible = ref(false)
@@ -1,271 +0,0 @@
<template>
<div class="w-full h-full">
<div id="ws-real-player" class="w-full h-full" />
</div>
</template>
<script setup lang="ts" name="deviceGroupPlay">
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"
const emit = defineEmits(["windowNumChanged"])
let realPlayer: any = null;
const state = reactive<any>({
numList: [1, 4, 9],
num: 9,
maxNum: 9,
channelList: [],
/**
* 码流类型:
* 1=主码流
* 2=辅码流
* 3=辅码流2
*/
streamType: 2,
current: 1,
total: 100,
})
const query = reactive({
list: [],
deviceMap: [],
deviceCode: null,
})
onUnmounted(() => {
destroy()
})
// 视频预览初始化
async function init(channelCodelist: any, deviceCode: any, deviceMap: any) {
try {
destroy()
if (channelCodelist) {
query.list = channelCodelist
}
if (deviceCode) {
query.deviceCode = deviceCode
}
if (deviceMap) {
query.deviceMap = deviceMap
} else {
query.deviceMap = []
}
state.current = 1
state.num = state.numList[state.numList.length - 1]
refreshPage()
} catch (error: any) {
console.error("|视频预览初始化|,意外的错误", error);
}
}
// 初始化视频
async function initVideo() {
const _params = {
deviceChannelCodeList: query.list ?? [],
deviceCode: query.deviceCode ?? null,
deviceTreeMap: query.deviceMap ?? 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(state.total, 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;
realPlayer = new PlayerManager({
// 实时预览容器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":
if (realPlayer) {
realPlayer.setPlayerAdapter("stretching");
}
playReal();
break;
// 主/辅码流切换回调
case "changeStreamType":
if (state.streamType != data.streamType) {
console.log("主/辅码流切换回调", data);
state.streamType = data.streamType;
destroy();
initVideo();
}
break;
// 实时预览成功
case "realSuccess":
console.log("实时预览成功");
break;
// 实时预览失败
case "realError":
console.log("实时预览失败", data, err);
break;
// 点击窗口上的对讲按钮触发的事件回调
case "notifyTalk":
console.log("notifyTalk", data);
case "talkError": // 对讲失败
console.log("对讲失败");
break;
// 选中的窗口发生改变
case "selectWindowChanged":
console.log(data, "返回窗口信息");
break;
// 播放器显示的路数发生改变
case "windowNumChanged":
emit("windowNumChanged", data)
if (data == state.num) return
state.current = 1
state.num = data
refreshPage()
break;
// 视频关闭回调
case "closeVideo":
if (!data.changeVideoFlag) {
// console.log(`窗口${data.selectIndex}的视频已关闭`);
}
break;
// 视频状态发生改变
case "statusChanged":
break;
// 错误信息汇总
case "errorInfo":
console.log(data, "可打印查看错误消息");
}
},
});
}
// 预览
async function playReal() {
// 请求
let errorIndexList = []
for (let index = 0; index < state.channelList.length; index++) {
const element = state.channelList[index];
const currentStreamType = element?.deviceChannelStreamType ?? state.streamType
// 获取通道数据
const _params = {
channelId: element.channelCode,
streamType: currentStreamType,
};
try {
const { code, data } = await DeviceCameraApi.getRtspVideo(_params);
if (code != 0 || !data) throw new Error();
realPlayer.realByUrl({
rtspURL: data.fullUrl,
wsURL: data.wsURL,
selectIndex: index,
channelId: data.channelId,
streamType: currentStreamType,
playerAdapter: "stretching",
channelData: {
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)
}
}
if (errorIndexList.length > 0) {
useMessage().error(`监控${errorIndexList.map(v => v + 1).join(',')}视频加载错误`);
}
}
function refreshPage() {
rendererVideoPage({
current: state.current,
pageSize: state.num,
total: state.total,
onInit: async () => {
destroy();
await initVideo();
return state.total
},
onChange: (page: any, pageSize: any) => {
if (page == state.current && pageSize == state.num) {
return
}
state.current = page;
state.num = pageSize;
refreshPage()
}
})
}
// 销毁
function destroy() {
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中找到合适的值(大于等于当前值的最小值)
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({
init,
destroy,
});
</script>
@@ -1,5 +1,6 @@
<template>
<el-dialog v-model="state.dialogVisible" title="预览" width="80%" append-to-body :before-close="handleClose">
<el-dialog destroy-on-close v-model="state.dialogVisible" title="预览" width="80%" append-to-body
:before-close="handleClose">
<div class="flex justify-center items-start">
<div class="min-w-[25%]">
<el-tree :data="channel.treeList" :props="{
@@ -14,7 +15,7 @@
</el-tree>
</div>
<div class="flex-1 overflow-hidden video-container">
<preview-video ref="previewVideoRef" style="height: 500px; margin-bottom: 20px" />
<RtspPluginVideo ref="previewVideoRef" style="height: 500px;" />
</div>
</div>
<template #footer>
@@ -32,8 +33,6 @@ import { useMessage } from "/@/hooks/message";
import * as DeviceManageApi from "/@/api/device/deviceManage"
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
const PreviewVideo = defineAsyncComponent(() => import('./index.vue'));
const previewVideoRef = ref()
const state = reactive({
@@ -53,7 +52,6 @@ const channel = reactive<any>({
const handleClose = () => {
state.dialogVisible = false
previewVideoRef.value.destroy()
}
// 打开弹窗
@@ -68,10 +66,31 @@ const openDialog = async (id: string) => {
// 点击树
const handleNodeClick = (e: any) => {
if (e.id == -1) {
previewVideoRef.value.init([], detail.deviceCode)
// 获取所有的channelCodeList
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: 2
})
}
previewVideoRef.value?.startReal(channelList)
return
}
previewVideoRef.value.init([e.channelCode], detail.deviceCode)
const targetChannel = channel.list.find((v: any) => v.channelCode == e.channelCode)
previewVideoRef.value?.startReal([
{
channelId: targetChannel.channelCode,
channelName: targetChannel.channelName,
snum: 0,
// 默认辅码流
streamType: 2
}
])
}
// 初始化表单数据
@@ -115,8 +134,19 @@ const getDeviceChannelList = async () => {
}
]
}
// 播放视频
previewVideoRef.value.init([], detail.deviceCode)
// 获取所有的channelCodeList
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: 2
})
}
previewVideoRef.value?.startReal(channelList)
} catch (error: any) {
console.error("|查询通道树|,意外的错误", error);
useMessage().error(error?.msg ?? "意外的错误");
@@ -148,4 +178,4 @@ defineExpose({
justify-content: space-between;
align-content: center;
}
</style>
</style>
@@ -1,247 +0,0 @@
<template>
<div class="w-full h-full">
<div id="ws-record-player" class="w-full h-full" />
</div>
</template>
<script setup lang="ts" name="ReplayVideo">
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(["windowNumChanged"])
// @ts-ignore
import PlayerManager from "../lib/icc/PlayerManager";
let replayVideo: any = null
const state = reactive<any>({
channelList: [],
startTimestamp: null,
endTimestamp: null,
dialogVisible: false,
loading: false,
treeLoading: false,
currentNum: 4,
maxCurrentNum: 9,
numList: [1, 4, 9],
type: "device",
code: null,
current: 1,
total: 100,
})
const query = reactive({
list: [],
deviceCode: null,
})
onUnmounted(() => {
destroy()
})
// 初始化播放器
const init = (queryForm: any, replayForm: any) => {
console.log("queryForm:", queryForm);
console.log("replayForm:", replayForm);
if (queryForm) {
if (queryForm.list) {
query.list = queryForm.list
}
if (queryForm.deviceCode) {
query.deviceCode = queryForm.deviceCode
}
}
state.queryForm = replayForm
// 处理日期范围
if (replayForm.videoDateList) {
if (replayForm.videoDateList.length == 2) {
const dateRange = formatDateRange(replayForm.videoDateList)
state.startTimestamp = dateRange.startTimestamp
state.endTimestamp = dateRange.endTimestamp
}
}
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.currentNum
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({
// 实时预览容器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':
emit("windowNumChanged", 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, "可打印查看错误消息");
}
}
})
}
// 流地址方式播放录像回放
const playRecord = async () => {
if (!state.channelList || !state.channelList.length) return
// 根据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
}
// 获取回放列表
try {
const recordRes = await AlarmReplayVideoListApi.defaultReplayVideoList(_data)
if (recordRes.code != 0 || !recordRes.data) {
errorIndexList.push(index)
continue
}
const videoRes = await AlarmReplayVideoListApi.getRtspReplayVideo(_data)
if (videoRes.code != 0 || !videoRes.data) {
errorIndexList.push(index)
continue
}
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
for (let index = 0; index < state.currentNum; index++) {
replayVideo.close(index)
}
replayVideo.destroy()
replayVideo = 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({
init,
destroy
});
</script>
@@ -1,5 +1,5 @@
<template>
<el-dialog v-model="state.dialogVisible" title="回放" width="80%" :before-close="handleClose">
<el-dialog destroy-on-close 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-[25%]">
<el-tree :data="channel.treeList" :props="{
@@ -16,9 +16,10 @@
<div class="flex-1 flex flex-col overflow-hidden video-container">
<el-form :model="queryForm" ref="queryRef" :inline="true" @keyup.enter="doSearch">
<el-form-item style="width: 450px;" label="录像时间" prop="videoDateList">
<el-date-picker v-model="queryForm.videoDateList" value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange" :disabled-date="disabledDate" placeholder="请选择日期范围"
start-placeholder="开始日期" end-placeholder="结束日期" @change="handleDateChange" />
<el-date-picker v-model="queryForm.videoDateList" date-format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange" :disabled-date="disabledDate"
placeholder="请选择日期范围" start-placeholder="开始日期" end-placeholder="结束日期"
@change="handleDateChange" />
</el-form-item>
<el-form-item style="width: 250px;" label="码流" prop="streamType">
<dict-select v-model="queryForm.streamType" :options="dh_video_stream_type"
@@ -34,7 +35,9 @@
</el-button>
</el-form-item>
</el-form>
<ReplayVideo ref="replayVideoRef" style="height: 500px; margin-bottom: 20px" />
<div class="flex-1 overflow-hidden video-container" style="height: 500px;">
<RtspPluginVideo ref="replayVideoRef" style="height: 500px;" />
</div>
</div>
</div>
<template #footer>
@@ -49,13 +52,12 @@
<script setup lang="ts" name="ReplayVideoDialog">
import { useMessage } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import dayjs from "dayjs";
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")
@@ -69,7 +71,6 @@ const state = reactive<any>({
dialogVisible: false,
loading: false,
treeLoading: false,
currentNum: 4,
numList: [1, 4, 9, 16, 25],
type: "device",
code: null
@@ -81,7 +82,7 @@ const queryForm = reactive({
ownerCode: null,
orgCodes: null,
isInclude: null,
streamType: "1",
streamType: "0",
recordSource: "2",
videoDateList: getDefaultDateRange()
})
@@ -91,24 +92,38 @@ const channel = reactive<any>({
list: [],
})
// 同步时间戳(秒)到 state,来源于 YYYY-MM-DD HH:mm:ss 字符串
function syncTimestampsFromQuery() {
const range = queryForm.videoDateList || []
if (Array.isArray(range) && range.length === 2 && range[0] && range[1]) {
state.startTimestamp = dayjs(range[0]).unix()
state.endTimestamp = dayjs(range[1]).unix()
}
}
// 初始化一次
syncTimestampsFromQuery()
// 打开弹窗
const openDialog = async (type: string, code: string, timeRange?: { startTime: string, endTime: string }) => {
if (!type || !code) return
state.type = type
state.code = code
state.dialogVisible = true
if (!code) return
// 如果传入了时间范围,则设置查询表单的时间
if (timeRange) {
queryForm.videoDateList = [timeRange.startTime, timeRange.endTime]
// 同时设置时间戳
state.startTimestamp = Math.floor(new Date(timeRange.startTime).getTime() / 1000)
state.endTimestamp = Math.floor(new Date(timeRange.endTime).getTime() / 1000)
state.startTimestamp = dayjs(timeRange.startTime).unix()
state.endTimestamp = dayjs(timeRange.endTime).unix()
}
// 保底同步一次(适配未传 timeRange 的情况)
syncTimestampsFromQuery()
await doSearch()
}
const doSearch = async () => {
// 确保接口查询与播放器时间戳一致
syncTimestampsFromQuery()
// 设备详情
if (state.type === "device") {
await getdeviceManageData(state.code);
@@ -118,24 +133,48 @@ const doSearch = async () => {
}
const handleClose = () => {
replayVideoRef.value.destroy()
state.dialogVisible = false
}
// 点击树
const handleNodeClick = (e: any) => {
if (e.id == -1) {
replayVideoRef.value.init(
{ list: [], deviceCode: state.code },
queryForm,
)
// 获取所有的channelCodeList
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: Number(queryForm.streamType),
// 默认前3天(格式YYYY-MM-DD HH:mm:ss)
startTime: queryForm.videoDateList[0],
endTime: queryForm.videoDateList[1],
// 录像来源:2-设备录像 3-中心录像
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value?.startPlayback([...channelList])
return
}
state.currentNum = 1
replayVideoRef.value.init(
{ list: [e.channelCode], deviceCode: state.code },
queryForm,
)
const targetChannel = channel.list.find((v: any) => v.channelCode == e.channelCode)
const channelList = [
{
channelId: targetChannel.channelCode,
channelName: targetChannel.channelName,
snum: 0,
// 默认辅码流
streamType: Number(queryForm.streamType),
// 默认前3天(格式YYYY-MM-DD HH:mm:ss)
startTime: queryForm.videoDateList[0],
endTime: queryForm.videoDateList[1],
// 录像来源:2-设备录像 3-中心录像
recordSource: Number(queryForm.recordSource)
}
]
replayVideoRef.value?.startPlayback([...channelList])
}
// 初始化表单数据
@@ -193,17 +232,24 @@ const getDeviceChannelList = async (codeStr: any) => {
}
]
}
console.log("replayVideoRef:", replayVideoRef);
let list = []
let deviceCode = state.code
if (_data.playbackGroupChannelCode) {
list = data.map((item: any) => item.channelCode)
deviceCode = null
// 获取所有的channelCodeList
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: Number(queryForm.streamType),
// 默认前3天(格式YYYY-MM-DD HH:mm:ss)
startTime: queryForm.videoDateList[0],
endTime: queryForm.videoDateList[1],
// 录像来源:2-设备录像 3-中心录像
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value.init(
{ list: list, deviceCode: deviceCode },
queryForm,
)
replayVideoRef.value?.startPlayback([...channelList])
} catch (error: any) {
console.error(error);
useMessage().error(error?.msg ?? "意外的错误");
@@ -212,63 +258,15 @@ const getDeviceChannelList = async (codeStr: any) => {
}
}
// 流地址方式播放录像回放
const playRecord = async (channelCode?: string) => {
if (!channel.list || !channel.list.length) return
// 根据maxNum获取
let list = channel.list
if (channelCode) list = channel.list.filter((v: any) => v.channelCode == channelCode)
if (!list || !list.length) return
console.log("list:", list);
for (let index = 0; index < list.length; index++) {
const element: any = list[index];
const _data = {
...queryForm,
channelId: element.channelCode
}
// 获取回放列表
const recordRes = await AlarmReplayVideoListApi.defaultReplayVideoList(_data)
if (recordRes.code != 0 || !recordRes.data) continue
const videoRes = await AlarmReplayVideoListApi.getRtspReplayVideo(_data)
if (videoRes.code != 0 || !videoRes.data) 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 || []
}, // 选传, 通道信息
})
}
}
// 获取默认日期范围(前三天)
function getDefaultDateRange() {
const today = new Date()
const threeDaysAgo = new Date(today.getTime() - 2 * 24 * 60 * 60 * 1000) // 前三天(包含今天)
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 设置开始时间为三天前的 00:00:00
threeDaysAgo.setHours(0, 0, 0, 0)
// 设置结束时间为今天的 23:59:59
today.setHours(23, 59, 59, 999)
return [formatDate(threeDaysAgo), formatDate(today)]
return [dayjs(threeDaysAgo).format("YYYY-MM-DD HH:mm:ss"), dayjs(today).format("YYYY-MM-DD HH:mm:ss")]
}
// 点击时间
@@ -285,6 +283,9 @@ const handleDateChange = (dates: string[]) => {
queryForm.videoDateList = getDefaultDateRange()
return
}
// 选择合法后,同步秒级时间戳
state.startTimestamp = dayjs(dates[0]).unix()
state.endTimestamp = dayjs(dates[1]).unix()
}
}
@@ -312,4 +313,4 @@ const disabledDate = (time: Date) => {
defineExpose({
openDialog
});
</script>
</script>
+53 -33
View File
@@ -10,12 +10,11 @@
<el-tab-pane v-for="(v, i) in data.tabs" :key="v.name" :label="v.label" :name="v.name" />
</el-tabs>
<div class="w-full h-full flex-1 overflow-hidden" v-loading="videoLoading">
<preview-video ref="previewVideoRef" v-if="state.currentTab == 'PREVIEW'"
@windowNumChanged="windowNumChanged" />
<div class="w-full h-full flex flex-col" v-if="state.currentTab == 'DOOR'">
<RtspPluginVideo ref="previewVideoRef" v-if="state.currentTab == 'PREVIEW'" />
<div class="w-full h-full flex flex-col" v-if="state.currentTab == 'REPLAY'">
<el-row shadow="hover" class="ml10">
<el-form :model="state.replayForm" ref="queryRef" :inline="true"
@keyup.enter="getDataList">
@keyup.enter="init('REPLAY')">
<el-form-item style="width: 450px;" label="录像时间段" prop="videoDateList">
<el-date-picker v-model="state.replayForm.videoDateList"
value-format="YYYY-MM-DD HH:mm:ss" type="datetimerange"
@@ -32,7 +31,7 @@
:options="dh_replay_source" placeholder="请选择录像来源" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
<el-button icon="search" type="primary" @click="init('REPLAY')">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">
@@ -41,8 +40,7 @@
</el-form-item>
</el-form>
</el-row>
<replay-video class="flex-1 overflow-hidden" ref="replayVideoRef"
@windowNumChanged="windowNumChanged" />
<RtspPluginVideo class="flex-1 overflow-hidden" ref="replayVideoRef" />
</div>
</div>
</div>
@@ -56,11 +54,7 @@
import { useMessage } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
// 引入组件
const PreviewVideo = defineAsyncComponent(() => import("../components/preview-video/index.vue"));
const ReplayVideo = defineAsyncComponent(() => import("../components/replay-video/index.vue"));
import * as deviceChannelApi from "/@/api/device/deviceChannel"
const previewVideoRef = ref()
const replayVideoRef = ref()
@@ -80,7 +74,7 @@ const data = {
},
{
label: "回放",
name: "DOOR",
name: "REPLAY",
}
]
}
@@ -127,12 +121,55 @@ function init(type: string) {
nextTick(async () => {
videoLoading.value = true
await new Promise(resolve => setTimeout(resolve, 500))
// 查询通道列表
let channelRecords = []
try {
const _params = {
current: 1,
size: 64,
deviceTreeMap: state.deviceTreeMap ?? null
}
let { code, data } = await deviceChannelApi.getChannelPage(_params);
if (code != 0) throw new Error("")
if (data.records && data.records.length) {
channelRecords = data.records
}
} catch (error) {
console.log("分组播放意外的错误error:", error);
}
switch (type) {
case 'PREVIEW':
previewVideoRef.value.init(state.channelCodeList, null, state.deviceTreeMap)
const doorChannelList: any[] = []
for (let i = 0; i < channelRecords.length; i++) {
const v: any = channelRecords[i]
doorChannelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: 2
})
}
previewVideoRef.value?.startReal([...doorChannelList])
break;
case 'DOOR':
await getDataList()
case 'REPLAY':
const replayChannelList: any[] = []
for (let i = 0; i < channelRecords.length; i++) {
const v: any = channelRecords[i]
replayChannelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: Number(state.replayForm.streamType),
// 默认前3天(格式YYYY-MM-DD HH:mm:ss)
startTime: state.replayForm.videoDateList[0],
endTime: state.replayForm.videoDateList[1],
// 录像来源:2-设备录像 3-中心录像
recordSource: Number(state.replayForm.recordSource)
})
}
replayVideoRef.value?.startPlayback([...replayChannelList])
break;
}
videoLoading.value = false
@@ -151,22 +188,6 @@ async function destroy() {
}
}
// 获取回放列表
const getDataList = async () => {
try {
const { data } = await DeviceChannelApi.getReplayList({
...state.replayForm,
deviceChannelCodeList: state.channelCodeList
})
state.dataList = data ?? []
if (replayVideoRef.value) {
replayVideoRef.value.init(state.dataList, state.replayForm)
}
} catch (error: any) {
console.error("获取回放列表错误", error);
}
}
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
@@ -177,7 +198,6 @@ const resetQuery = () => {
state.replayForm.streamType = "1"
// 重置录像来源为默认值
state.replayForm.recordSource = "1"
getDataList()
}
/* ************************************* start 回放时间处理 start ************************************* */
-1
View File
@@ -104,7 +104,6 @@ const storesPanel = usePanel()
// 引入组件
const FormDialog = defineAsyncComponent(() => import('../components/deviceForm.vue'));
const PreviewVideo = defineAsyncComponent(() => import("../components/preview-video/index.vue"));
// 定义查询字典
const { device_category, is_sync, is_nline, dh_device_manufacturer } = useDict("device_category", "is_sync", "is_nline", "dh_device_manufacturer")
// 定义变量内容
@@ -5,7 +5,7 @@
<select-device-tree v-model="refreshForm.dutyId" autoFirst style="width: 250px;" @change="groupChange" />
</com-title>
<div class="h-[10px]" />
<preview-video ref="previewVideoRef" />
<RtspPluginVideo ref="rtspPluginVideoRef" />
</div>
</template>
@@ -13,7 +13,6 @@
import * as deviceChannelApi from "/@/api/device/deviceChannel"
const comTitle = defineAsyncComponent(() => import('./com-title.vue'));
const PreviewVideo = defineAsyncComponent(() => import("../../../device/components/preview-video/index.vue"));
const refreshForm = reactive<any>({
dutyId: null,
@@ -22,20 +21,42 @@ const refreshForm = reactive<any>({
deviceChannelCodeList: []
})
const previewVideoRef = ref<any>(null)
const rtspPluginVideoRef = ref<any>(null)
const refreshVideo = async () => {
previewVideoRef.value.init(null, null, refreshForm.deviceTreeMap)
// 查询通道列表
try {
const _params = {
current: 1,
size: 64,
deviceTreeMap: refreshForm.deviceTreeMap ?? null
}
let { code, data } = await deviceChannelApi.getChannelPage(_params);
if (code != 0) throw new Error("")
if (data.records && data.records.length) {
const channelList = []
for (let index = 0; index < data.records.length; index++) {
const element = data.records[index];
channelList.push({
channelId: element.channelCode,
channelName: element.channelName,
snum: index,
// 默认辅码流
streamType: 2
})
}
rtspPluginVideoRef.value?.startReal(channelList)
}
} catch (error) {
console.log("分组播放意外的错误error:", error);
}
}
const groupChange = (e: any) => {
if (!e) return
const [groupIds, channelCodes, parentChildGroup] = e
if (!groupIds.length && !channelCodes.length) {
if (previewVideoRef.value) {
previewVideoRef.value?.close()
}
return
}
refreshForm.groupIdList = groupIds