feat:优化视频预览
This commit is contained in:
1 parent
18b39813c0
commit
01508bf7a2
16 files changed
+3563
-665
No files matched your search
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+8
-5
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 ************************************* */
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user