11
This commit is contained in:
1 parent
01508bf7a2
commit
53f209d0d3
8 files changed
+351
-352
No files matched your search
@@ -628,20 +628,15 @@
|
||||
} else {
|
||||
data.info.count = data.info.count || 0
|
||||
data.info.count++
|
||||
clearTimeout(hwndInfo.notifyrealmonitorTimeout)
|
||||
hwndInfo.notifyrealmonitorTimeout = setTimeout(() => {
|
||||
if (Number(data.info.result) === 10704) {
|
||||
// 拉流失败
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10704, message: '打开视频失败', i18nKey: 'video.player.play.fail' })
|
||||
} else if (Number(data.info.result) === -405) {
|
||||
// 拉流超时
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10705, message: '打开视频超时', i18nKey: 'video.player.play.timeout' })
|
||||
} else {
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 204, message: '流异常断开,请重新连接~', i18nKey: 'video.player.stream.disconnect.reload' })
|
||||
}
|
||||
// channelInfo && videoInfo.startReal([channelInfo], { isReOpen: true, count: data.info.count++ })
|
||||
clearTimeout(hwndInfo.notifyrealmonitorTimeout)
|
||||
}, 2000)
|
||||
if (Number(data.info.result) === 10704) {
|
||||
// 拉流失败
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10704, message: '打开视频失败', i18nKey: 'video.player.play.fail' })
|
||||
} else if (Number(data.info.result) === -405) {
|
||||
// 拉流超时
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10705, message: '打开视频超时', i18nKey: 'video.player.play.timeout' })
|
||||
} else {
|
||||
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 204, message: '流异常断开,请重新连接~', i18nKey: 'video.player.stream.disconnect.reload' })
|
||||
}
|
||||
}
|
||||
videoInfo.setWindowDragEnable()
|
||||
break
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="video-container">
|
||||
<div v-if="loading" class="loading-mask">正在初始化...</div>
|
||||
<div id="video-plugin-preview-0" class="pane" />
|
||||
<div id="video-plugin-preview-3" class="pane" />
|
||||
<div id="video-plugin-preview-7" class="pane" />
|
||||
@@ -7,7 +8,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from "vue"
|
||||
import { onMounted, onUnmounted, ref } from "vue"
|
||||
import { ElMessage } from "element-plus";
|
||||
import dayjs from "dayjs"
|
||||
|
||||
@@ -19,20 +20,63 @@ import * as settingApi from "/@/api/com/comDhSetting"
|
||||
const TAG = "|视频预览|"
|
||||
|
||||
let VIDEO_PLAYER = null
|
||||
const divisionSettingList = [1, 2, 4, 6, 8, 9, 13, 16, 20, 25, 36, 64]
|
||||
let currentWindowType = null
|
||||
let isReady = false
|
||||
/** @type {Array<() => void>} */
|
||||
let readyQueue = []
|
||||
let lastRealRequest = null
|
||||
let lastPlaybackRequest = null
|
||||
let realFallbackAttempted = false
|
||||
const realFallbackAttempted = new Set()
|
||||
const players = new Map()
|
||||
const isReadyByType = new Map()
|
||||
const readyWaitersByType = new Map()
|
||||
const initPromisesByType = new Map()
|
||||
let latestRealToken = 0
|
||||
let latestPlaybackToken = 0
|
||||
const loading = ref(false)
|
||||
const REAL_RESTART_GUARD_MS = 800
|
||||
let lastRealSignature = ""
|
||||
let lastRealStartAt = 0
|
||||
let lastPlaybackSignature = ""
|
||||
let lastPlaybackStartAt = 0
|
||||
|
||||
function waitForReady(type = props.windowType) {
|
||||
return new Promise((resolve) => {
|
||||
if (isReadyByType.get(type)) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const arr = readyWaitersByType.get(type) || []
|
||||
arr.push(resolve)
|
||||
readyWaitersByType.set(type, arr)
|
||||
})
|
||||
}
|
||||
|
||||
function getContainerId(type) {
|
||||
return `video-plugin-preview-${type}`
|
||||
}
|
||||
|
||||
function getDivisionByCount(count) {
|
||||
const safeCount = Math.max(1, Number(count) || 1)
|
||||
const match = divisionSettingList.find((num) => num >= safeCount)
|
||||
return match || divisionSettingList[divisionSettingList.length - 1] || safeCount
|
||||
}
|
||||
|
||||
function applyDivisionByCount(count) {
|
||||
if (!VIDEO_PLAYER?.changeDivision) return
|
||||
const nextDivision = getDivisionByCount(count)
|
||||
const currentDivision = Number(VIDEO_PLAYER?.setting?.division)
|
||||
if (currentDivision === nextDivision) return
|
||||
VIDEO_PLAYER.changeDivision(nextDivision)
|
||||
}
|
||||
|
||||
function getRealFallbackKey(info) {
|
||||
if (info?.channelId) return `cid:${info.channelId}`
|
||||
if (typeof info?.snum === "number") return `snum:${info.snum}`
|
||||
return null
|
||||
}
|
||||
|
||||
function setActiveContainer(type) {
|
||||
const ids = [0, 3, 7].map(getContainerId)
|
||||
ids.forEach((id) => {
|
||||
@@ -43,6 +87,28 @@ function setActiveContainer(type) {
|
||||
})
|
||||
}
|
||||
|
||||
function buildRealSignature(list) {
|
||||
if (!Array.isArray(list)) return ""
|
||||
return list.map((item) => {
|
||||
const channelId = item?.channelId ?? ""
|
||||
const snum = typeof item?.snum === "number" ? item.snum : ""
|
||||
const streamType = item?.streamType ?? ""
|
||||
return `${channelId}|${snum}|${streamType}`
|
||||
}).join(";")
|
||||
}
|
||||
|
||||
function buildPlaybackSignature(list) {
|
||||
if (!Array.isArray(list)) return ""
|
||||
return list.map((item) => {
|
||||
const channelId = item?.channelId ?? ""
|
||||
const snum = typeof item?.snum === "number" ? item.snum : ""
|
||||
const streamType = item?.streamType ?? ""
|
||||
const startTime = item?.startTime ?? ""
|
||||
const endTime = item?.endTime ?? ""
|
||||
return `${channelId}|${snum}|${streamType}|${startTime}|${endTime}`
|
||||
}).join(";")
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任务加入就绪队列;若已就绪则立即执行
|
||||
* @param {() => void} fn 任务函数
|
||||
@@ -126,167 +192,195 @@ const props = defineProps({
|
||||
* @param {0|3|7} [type=props.windowType] 播放器类型(0预览/3回放/7回放支持倒放)
|
||||
*/
|
||||
async function initVideoPlayer(type = props.windowType) {
|
||||
if (players.has(type)) {
|
||||
VIDEO_PLAYER = players.get(type)
|
||||
currentWindowType = type
|
||||
isReady = !!isReadyByType.get(type)
|
||||
setActiveContainer(type)
|
||||
if (isReady) flushReadyQueue()
|
||||
return
|
||||
}
|
||||
if (initPromisesByType.get(type)) {
|
||||
return initPromisesByType.get(type)
|
||||
}
|
||||
loading.value = true
|
||||
// 查询配置
|
||||
const { data: setting, code: settingCode } = await settingApi.getObj()
|
||||
if (settingCode != 0) {
|
||||
ElMessage.error("查询配置失败")
|
||||
return
|
||||
loading.value = false
|
||||
return Promise.resolve()
|
||||
}
|
||||
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)
|
||||
const p = new Promise((resolve) => {
|
||||
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", "el-popper"],
|
||||
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
|
||||
coverShieldClass: [],
|
||||
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
|
||||
parentIframeShieldClass: [],
|
||||
// 创建播放器成功回调
|
||||
createSuccess: (versionInfo) => {
|
||||
// 该回调触发后,我们可以进行实时预览/录像回放等操作
|
||||
console.log(TAG, "创建播放器成功,versionInfo:", versionInfo)
|
||||
isReady = true
|
||||
isReadyByType.set(type, true)
|
||||
flushReadyQueue()
|
||||
const waiters = readyWaitersByType.get(type) || []
|
||||
waiters.forEach((fn) => {
|
||||
try { fn() } catch (_) { }
|
||||
})
|
||||
readyWaitersByType.delete(type)
|
||||
loading.value = false
|
||||
resolve(null)
|
||||
},
|
||||
// 创建播放器失败回调
|
||||
createError: (err) => {
|
||||
console.log(TAG, "创建播放器失败,err:", err)
|
||||
isReady = false
|
||||
isReadyByType.set(type, false)
|
||||
if ([1001].includes(err.code)) {
|
||||
ElMessage.error(err.message)
|
||||
}
|
||||
loading.value = false
|
||||
resolve(null)
|
||||
},
|
||||
// 插件公共回调
|
||||
dhPlayerMessage: (info, err) => {
|
||||
console.log(TAG, "插件公共消息,info:", info, "err:", err)
|
||||
},
|
||||
// 实时预览成功回调
|
||||
realSuccess: (info) => {
|
||||
console.log(TAG, "实时预览成功,info:", info)
|
||||
const key = getRealFallbackKey(info)
|
||||
if (key) realFallbackAttempted.delete(key)
|
||||
},
|
||||
// 实时预览失败回调
|
||||
realError: (info, err) => {
|
||||
console.log(TAG, "实时预览失败,info:", info, "err:", err)
|
||||
if ([204, 408, 10705].includes(err.code)) {
|
||||
try {
|
||||
const key = getRealFallbackKey(info)
|
||||
if (!key || realFallbackAttempted.has(key)) return
|
||||
if (info?.streamType !== 2) return
|
||||
const list = Array.isArray(lastRealRequest) ? lastRealRequest : []
|
||||
const index = list.findIndex((it) => {
|
||||
if (info?.channelId && it?.channelId) return it.channelId === info.channelId
|
||||
if (typeof info?.snum === "number") return it?.snum === info.snum
|
||||
return false
|
||||
})
|
||||
if (index < 0) return
|
||||
const current = list[index]
|
||||
if (current?.streamType !== 2) return
|
||||
realFallbackAttempted.add(key)
|
||||
const fallbackItem = { ...current, streamType: 1 }
|
||||
const nextList = [...list]
|
||||
nextList[index] = fallbackItem
|
||||
lastRealRequest = nextList
|
||||
console.log(TAG, "辅码流不可用,自动切换主码流重试", fallbackItem)
|
||||
VIDEO_PLAYER.startReal([fallbackItem])
|
||||
} 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)
|
||||
}
|
||||
},
|
||||
// 对讲成功回调
|
||||
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)
|
||||
})
|
||||
currentWindowType = type
|
||||
players.set(type, VIDEO_PLAYER)
|
||||
console.log(TAG, "播放器已初始化,windowType:", type)
|
||||
initPromisesByType.set(type, p)
|
||||
await p.finally(() => initPromisesByType.delete(type))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,40 +401,74 @@ function ensureWindowType(target) {
|
||||
* 开始实时预览
|
||||
* @param {RealOptions[]} channels 播放参数数组
|
||||
*/
|
||||
function startReal(channels) {
|
||||
async function startReal(channels) {
|
||||
const myToken = ++latestRealToken
|
||||
if (!VIDEO_PLAYER) {
|
||||
initVideoPlayer(0)
|
||||
} else {
|
||||
ensureWindowType(0)
|
||||
}
|
||||
lastRealRequest = channels
|
||||
realFallbackAttempted = false
|
||||
realFallbackAttempted.clear()
|
||||
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)
|
||||
}
|
||||
})
|
||||
loading.value = true
|
||||
await waitForReady(0)
|
||||
if (myToken !== latestRealToken) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
const signature = buildRealSignature(channels)
|
||||
if (signature && signature === lastRealSignature && now - lastRealStartAt < REAL_RESTART_GUARD_MS) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
lastRealSignature = signature
|
||||
lastRealStartAt = now
|
||||
applyDivisionByCount(Array.isArray(channels) ? channels.length : 1)
|
||||
console.log(TAG, "开始实时预览,channels:", channels)
|
||||
VIDEO_PLAYER.startReal(channels)
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
VIDEO_PLAYER.setTabControlBtn(showBtn, i)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始录像回放
|
||||
* @param {PlaybackOptions[]} items 回放参数数组
|
||||
*/
|
||||
function startPlayback(items) {
|
||||
async function startPlayback(items) {
|
||||
const myToken = ++latestPlaybackToken
|
||||
if (!VIDEO_PLAYER) {
|
||||
initVideoPlayer(3)
|
||||
} else {
|
||||
ensureWindowType(3)
|
||||
}
|
||||
lastPlaybackRequest = items
|
||||
enqueueReadyTask(() => {
|
||||
console.log(TAG, "开始录像回放,items:", items)
|
||||
VIDEO_PLAYER.startPlayback(items)
|
||||
})
|
||||
loading.value = true
|
||||
await waitForReady(3)
|
||||
if (myToken !== latestPlaybackToken) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
const now = Date.now()
|
||||
const signature = buildPlaybackSignature(items)
|
||||
if (signature && signature === lastPlaybackSignature && now - lastPlaybackStartAt < REAL_RESTART_GUARD_MS) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
lastPlaybackSignature = signature
|
||||
lastPlaybackStartAt = now
|
||||
applyDivisionByCount(Array.isArray(items) ? items.length : 1)
|
||||
console.log(TAG, "开始录像回放,items:", items)
|
||||
try {
|
||||
await closeVideo()
|
||||
} catch (e) {
|
||||
console.log(TAG, "开始回放前关闭旧视频失败,err:", e)
|
||||
}
|
||||
VIDEO_PLAYER.startPlayback(items)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,7 +486,7 @@ function updateVisible(visible) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
async function handleVisibilityChange() {
|
||||
if (document.hidden) {
|
||||
if (!VIDEO_PLAYER) return
|
||||
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
|
||||
@@ -376,15 +504,15 @@ function handleVisibilityChange() {
|
||||
if (!VIDEO_PLAYER) return
|
||||
VIDEO_PLAYER.show?.()
|
||||
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
|
||||
startReal(lastRealRequest)
|
||||
await waitForReady(0)
|
||||
await 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 })
|
||||
}
|
||||
})
|
||||
await waitForReady(currentWindowType)
|
||||
lastPlaybackRequest.forEach((item) => {
|
||||
if (item && typeof item.snum === "number") {
|
||||
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -447,7 +575,25 @@ function saveVideo(path, filename) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function closeVideo() {
|
||||
try {
|
||||
if (VIDEO_PLAYER?.closeVideo) {
|
||||
await Promise.resolve().then(() => VIDEO_PLAYER.closeVideo())
|
||||
}
|
||||
for (const p of players.values()) {
|
||||
try {
|
||||
if (p?.closeVideo) {
|
||||
await Promise.resolve().then(() => p.closeVideo())
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(TAG, "清空播放列表失败,err:", e)
|
||||
}
|
||||
}
|
||||
console.log(TAG, "关闭播放列表成功")
|
||||
} catch (e) {
|
||||
console.log(TAG, "关闭播放失败,err:", e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange)
|
||||
@@ -476,6 +622,7 @@ defineExpose({
|
||||
startReal,
|
||||
startPlayback,
|
||||
updateVisible,
|
||||
closeVideo,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -488,6 +635,7 @@ defineExpose({
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pane {
|
||||
@@ -499,4 +647,19 @@ defineExpose({
|
||||
.pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<el-dialog :title="form.id ? '编辑' : '新增'" width="40%" v-model="visible" :close-on-click-modal="false" draggable>
|
||||
<el-dialog destroy-on-close :title="form.id ? '编辑' : '新增'" width="40%" v-model="visible"
|
||||
:close-on-click-modal="false" draggable>
|
||||
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px"
|
||||
v-loading="loading">
|
||||
<el-row :gutter="24">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog title="绑定摄像头" v-model="visible" width="80%" :close-on-click-modal="false" draggable>
|
||||
<el-dialog destroy-on-close title="绑定摄像头" v-model="visible" width="80%" :close-on-click-modal="false" draggable>
|
||||
<div class="mb12" style="width: 100%">
|
||||
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog(
|
||||
state.queryForm.houseId
|
||||
|
||||
@@ -156,6 +156,7 @@ const handleNodeClick = (e: any) => {
|
||||
recordSource: Number(queryForm.recordSource)
|
||||
})
|
||||
}
|
||||
replayVideoRef.value?.closeVideo()
|
||||
replayVideoRef.value?.startPlayback([...channelList])
|
||||
return
|
||||
}
|
||||
@@ -174,6 +175,7 @@ const handleNodeClick = (e: any) => {
|
||||
recordSource: Number(queryForm.recordSource)
|
||||
}
|
||||
]
|
||||
replayVideoRef.value?.closeVideo()
|
||||
replayVideoRef.value?.startPlayback([...channelList])
|
||||
}
|
||||
|
||||
@@ -249,6 +251,7 @@ const getDeviceChannelList = async (codeStr: any) => {
|
||||
recordSource: Number(queryForm.recordSource)
|
||||
})
|
||||
}
|
||||
replayVideoRef.value?.closeVideo()
|
||||
replayVideoRef.value?.startPlayback([...channelList])
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
<template>
|
||||
<div class="layout-padding">
|
||||
<div class="layout-padding-auto layout-padding-view">
|
||||
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
|
||||
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
|
||||
:nodeClickable="false">
|
||||
<template #default>
|
||||
<div class="flex flex-col h-full">
|
||||
<el-row shadow="hover" v-show="showSearch" class="ml10">
|
||||
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
|
||||
<el-form-item style="width: 350px;" label="录像时间段" prop="videoDateList">
|
||||
<el-date-picker v-model="state.queryForm.videoDateList" value-format="YYYY-MM-DD" type="daterange"
|
||||
: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="state.queryForm.streamType" :options="dh_video_stream_type"
|
||||
placeholder="请选择码流" />
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 250px;" label="录像来源" prop="recordSource">
|
||||
<dict-select v-model="state.queryForm.recordSource" :options="dh_replay_source"
|
||||
placeholder="请选择录像来源" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button icon="search" type="primary" @click="replayVideoMounted">
|
||||
{{ $t('common.queryBtn') }}
|
||||
</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-row>
|
||||
<replay-video class="flex-1 overflow-hidden" ref="replayVideoRef" @mounted="replayVideoMounted" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable-org-Tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="systemDeviceCameraReplay">
|
||||
import { useMessage } from "/@/hooks/message";
|
||||
import { useDict } from '/@/hooks/dict';
|
||||
|
||||
import * as DeviceCameraReplayApi from "/@/api/third/deviceCameraReplay";
|
||||
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
|
||||
|
||||
// 引入组件
|
||||
const ReplayVideo = defineAsyncComponent(() => import('../components/replay-video/index.vue'));
|
||||
|
||||
// 定义查询字典
|
||||
const { dh_replay_source, dh_video_stream_type } = useDict("dh_replay_source", "dh_video_stream_type")
|
||||
|
||||
// 定义变量内容
|
||||
const draggableDepTreeRef = ref()
|
||||
const replayVideoRef = ref()
|
||||
// 搜索变量
|
||||
const queryRef = ref()
|
||||
const showSearch = ref(true)
|
||||
// 多选变量
|
||||
const selectObjs = ref([]) as any
|
||||
const multiple = ref(true)
|
||||
|
||||
const state = reactive<any>({
|
||||
queryForm: {
|
||||
ownerCode: null,
|
||||
orgCodes: null,
|
||||
isInclude: null,
|
||||
streamType: "1",
|
||||
recordSource: "1",
|
||||
videoDateList: getDefaultDateRange()
|
||||
},
|
||||
dataList: []
|
||||
})
|
||||
|
||||
const getDataList = async () => {
|
||||
const { data } = await DeviceChannelApi.getReplayList(state.queryForm)
|
||||
if (data && data.length) state.dataList = data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDataList()
|
||||
})
|
||||
|
||||
const replayVideoMounted = async () => {
|
||||
await getDataList()
|
||||
if (replayVideoRef.value) {
|
||||
replayVideoRef.value.init(state.dataList, state.queryForm)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认日期范围(前三天)
|
||||
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')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
return [formatDate(threeDaysAgo), formatDate(today)]
|
||||
}
|
||||
|
||||
// 禁用日期的逻辑
|
||||
const disabledDate = (time: Date) => {
|
||||
const today = new Date()
|
||||
today.setHours(23, 59, 59, 999) // 设置为今天的最后一刻
|
||||
// 禁用未来日期
|
||||
if (time.getTime() > today.getTime()) {
|
||||
return true
|
||||
}
|
||||
// 如果已经选择了开始日期,限制结束日期最多3天
|
||||
const selectedDates = state.queryForm.videoDateList
|
||||
if (selectedDates && selectedDates.length === 1) {
|
||||
const startDate = new Date(selectedDates[0])
|
||||
const maxEndDate = new Date(startDate.getTime() + 2 * 24 * 60 * 60 * 1000) // 最多3天
|
||||
const minEndDate = new Date(startDate.getTime() - 2 * 24 * 60 * 60 * 1000) // 最少3天前
|
||||
|
||||
return time.getTime() > maxEndDate.getTime() || time.getTime() < minEndDate.getTime()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 处理日期变化
|
||||
const handleDateChange = (dates: string[]) => {
|
||||
if (dates && dates.length === 2) {
|
||||
const startDate = new Date(dates[0])
|
||||
const endDate = new Date(dates[1])
|
||||
const diffTime = Math.abs(endDate.getTime() - startDate.getTime())
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1 // 包含起始日期
|
||||
|
||||
if (diffDays > 3) {
|
||||
useMessage().warning('最多只能选择3天的时间范围')
|
||||
// 重置为默认值
|
||||
state.queryForm.videoDateList = getDefaultDateRange()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索条件
|
||||
const resetQuery = () => {
|
||||
// 清空搜索条件
|
||||
queryRef.value?.resetFields()
|
||||
// 重置日期为默认前三天
|
||||
state.queryForm.videoDateList = getDefaultDateRange()
|
||||
// 重置码流为默认值
|
||||
state.queryForm.streamType = "1"
|
||||
// 重置录像来源为默认值
|
||||
state.queryForm.recordSource = "1"
|
||||
// 清空多选
|
||||
selectObjs.value = []
|
||||
draggableDepTreeRef.value.resetData()
|
||||
getDataList()
|
||||
}
|
||||
|
||||
// 树节点选择时
|
||||
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: boolean) => {
|
||||
state.queryForm.ownerCode = curClickedNode.id
|
||||
state.queryForm.orgCodes = checkedTreeNodes.map((item: any) => item.id)
|
||||
state.queryForm.isInclude = !isDeepTree
|
||||
getDataList()
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -150,6 +150,7 @@ function init(type: string) {
|
||||
streamType: 2
|
||||
})
|
||||
}
|
||||
previewVideoRef?.value?.closeVideo()
|
||||
previewVideoRef.value?.startReal([...doorChannelList])
|
||||
break;
|
||||
case 'REPLAY':
|
||||
@@ -169,6 +170,7 @@ function init(type: string) {
|
||||
recordSource: Number(state.replayForm.recordSource)
|
||||
})
|
||||
}
|
||||
replayVideoRef?.value?.closeVideo()
|
||||
replayVideoRef.value?.startPlayback([...replayChannelList])
|
||||
break;
|
||||
}
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
|
||||
minify: isDev ? 'esbuild' : 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true, // 删除 console
|
||||
drop_console: false, // 删除 console
|
||||
drop_debugger: true, // 删除 debugger
|
||||
},
|
||||
format: {
|
||||
|
||||
Reference in new issue
Block a user