|
@@ -35,6 +35,7 @@ const previewWindowStreamTypes = ref<Record<number, HikvisionStreamType>>({})
|
|
|
const interruptedWindowIndexes = ref<number[]>([])
|
|
const interruptedWindowIndexes = ref<number[]>([])
|
|
|
const previewError = ref('')
|
|
const previewError = ref('')
|
|
|
const captureAllLoading = ref(false)
|
|
const captureAllLoading = ref(false)
|
|
|
|
|
+const stopAllLoading = ref(false)
|
|
|
const soundWindowIndex = ref<number>()
|
|
const soundWindowIndex = ref<number>()
|
|
|
const soundControlLoading = ref(false)
|
|
const soundControlLoading = ref(false)
|
|
|
const talkWindowIndex = ref<number>()
|
|
const talkWindowIndex = ref<number>()
|
|
@@ -48,8 +49,10 @@ const hoveredWindowIndex = ref<number>()
|
|
|
// 鼠标进入页面叠加按钮后 SDK 会触发 windowEventOut,单独记录按钮悬浮以保持栏可见。
|
|
// 鼠标进入页面叠加按钮后 SDK 会触发 windowEventOut,单独记录按钮悬浮以保持栏可见。
|
|
|
const hoveredControlWindowIndex = ref<number>()
|
|
const hoveredControlWindowIndex = ref<number>()
|
|
|
const splitPopoverVisible = ref(false)
|
|
const splitPopoverVisible = ref(false)
|
|
|
|
|
+const splitControlLoading = ref(false)
|
|
|
const sidebarCollapsed = ref(false)
|
|
const sidebarCollapsed = ref(false)
|
|
|
const stageFullscreen = ref(false)
|
|
const stageFullscreen = ref(false)
|
|
|
|
|
+const fullscreenControlLoading = ref(false)
|
|
|
|
|
|
|
|
type SplitCount = 1 | 4 | 9 | 16
|
|
type SplitCount = 1 | 4 | 9 | 16
|
|
|
|
|
|
|
@@ -100,6 +103,8 @@ let componentUnmounted = false
|
|
|
// ResizeObserver 负责容器尺寸变化,requestAnimationFrame 用于合并同一帧内的重复 resize。
|
|
// ResizeObserver 负责容器尺寸变化,requestAnimationFrame 用于合并同一帧内的重复 resize。
|
|
|
let playerResizeObserver: ResizeObserver | undefined
|
|
let playerResizeObserver: ResizeObserver | undefined
|
|
|
let playerResizeFrame: number | undefined
|
|
let playerResizeFrame: number | undefined
|
|
|
|
|
+let playerResizeInProgress = false
|
|
|
|
|
+let playerResizePending = false
|
|
|
|
|
|
|
|
const currentSplitColumns = computed(() => Math.sqrt(currentSplitCount.value))
|
|
const currentSplitColumns = computed(() => Math.sqrt(currentSplitCount.value))
|
|
|
const currentSplitRows = computed(() => currentSplitColumns.value)
|
|
const currentSplitRows = computed(() => currentSplitColumns.value)
|
|
@@ -125,6 +130,7 @@ function clearPreviewError() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function showPreviewError(message: string) {
|
|
function showPreviewError(message: string) {
|
|
|
|
|
+ if (componentUnmounted) return
|
|
|
clearPreviewErrorTimer()
|
|
clearPreviewErrorTimer()
|
|
|
previewError.value = message
|
|
previewError.value = message
|
|
|
previewErrorTimer = setTimeout(() => {
|
|
previewErrorTimer = setTimeout(() => {
|
|
@@ -416,7 +422,9 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
|
|
|
szBasePath: getHikvisionPlayerBasePath(),
|
|
szBasePath: getHikvisionPlayerBasePath(),
|
|
|
iMaxSplit: MAX_SPLIT_COLUMNS,
|
|
iMaxSplit: MAX_SPLIT_COLUMNS,
|
|
|
iCurrentSplit: DEFAULT_SPLIT_COLUMNS,
|
|
iCurrentSplit: DEFAULT_SPLIT_COLUMNS,
|
|
|
- mseWorkerEnable: false,
|
|
|
|
|
|
|
+ // 指南建议高分辨率画面开启多线程解码;仅在页面具备跨源隔离时启用,
|
|
|
|
|
+ // 避免未配置 COOP/COEP 的部署环境错误使用 SharedArrayBuffer。
|
|
|
|
|
+ mseWorkerEnable: window.crossOriginIsolated,
|
|
|
bSupporDoubleClickFull: true,
|
|
bSupporDoubleClickFull: true,
|
|
|
oStyle: { borderSelect: '#facc15' }
|
|
oStyle: { borderSelect: '#facc15' }
|
|
|
})
|
|
})
|
|
@@ -526,8 +534,8 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
|
|
|
// 初始化失败也要销毁半成品实例,否则 Worker、WASM 或媒体资源可能残留。
|
|
// 初始化失败也要销毁半成品实例,否则 Worker、WASM 或媒体资源可能残留。
|
|
|
await initializingPlayer?.JS_StopRealPlayAll().catch(() => undefined)
|
|
await initializingPlayer?.JS_StopRealPlayAll().catch(() => undefined)
|
|
|
await Promise.resolve(initializingPlayer?.JS_Destroy()).catch(() => undefined)
|
|
await Promise.resolve(initializingPlayer?.JS_Destroy()).catch(() => undefined)
|
|
|
- console.error('H5player 初始化失败', error)
|
|
|
|
|
if (!componentUnmounted) {
|
|
if (!componentUnmounted) {
|
|
|
|
|
+ console.error('H5player 初始化失败', error)
|
|
|
showPreviewError(error instanceof Error ? error.message : '播放器初始化失败')
|
|
showPreviewError(error instanceof Error ? error.message : '播放器初始化失败')
|
|
|
}
|
|
}
|
|
|
throw error
|
|
throw error
|
|
@@ -540,6 +548,11 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function playCameraPreview(camera: HikvisionCameraNode) {
|
|
async function playCameraPreview(camera: HikvisionCameraNode) {
|
|
|
|
|
+ if (stopAllLoading.value || splitControlLoading.value) {
|
|
|
|
|
+ showPreviewError('播放器正在切换状态,请稍后再选择监控点')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 点击监控点时固定目标窗口,之后用户切换选中窗口不会改变本次请求的播放位置。
|
|
// 点击监控点时固定目标窗口,之后用户切换选中窗口不会改变本次请求的播放位置。
|
|
|
const windowIndex = activeWindowIndex.value
|
|
const windowIndex = activeWindowIndex.value
|
|
|
const requestId = beginWindowPreviewRequest(windowIndex)
|
|
const requestId = beginWindowPreviewRequest(windowIndex)
|
|
@@ -971,8 +984,13 @@ async function stopWindowPreview(windowIndex: number) {
|
|
|
|
|
|
|
|
async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColumns) {
|
|
async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColumns) {
|
|
|
splitPopoverVisible.value = false
|
|
splitPopoverVisible.value = false
|
|
|
- if (currentSplitCount.value === count) return
|
|
|
|
|
|
|
+ if (splitControlLoading.value || currentSplitCount.value === count) return
|
|
|
|
|
+ if (stopAllLoading.value) {
|
|
|
|
|
+ showPreviewError('正在关闭全部监控画面,请稍后切换分屏')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ splitControlLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
const currentPlayer = await initPlayer()
|
|
const currentPlayer = await initPlayer()
|
|
|
|
|
|
|
@@ -1005,6 +1023,8 @@ async function handleSplitChange(count: SplitCount, columns: HikvisionSplitColum
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('切换监控分屏失败', error)
|
|
console.error('切换监控分屏失败', error)
|
|
|
showPreviewError(error instanceof Error ? error.message : '切换分屏失败')
|
|
showPreviewError(error instanceof Error ? error.message : '切换分屏失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ splitControlLoading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1019,17 +1039,37 @@ function handleNodeClick(data: HikvisionTreeNode) {
|
|
|
void playCameraPreview(data)
|
|
void playCameraPreview(data)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function resizePlayer() {
|
|
|
|
|
|
|
+async function resizePlayer() {
|
|
|
playerResizeFrame = undefined
|
|
playerResizeFrame = undefined
|
|
|
|
|
+ const currentPlayer = player
|
|
|
|
|
+ if (!currentPlayer) return
|
|
|
|
|
+
|
|
|
|
|
+ // ResizeObserver 在侧栏动画期间可能连续触发。SDK resize 尚未结束时只保留一次尾调用,
|
|
|
|
|
+ // 避免多个 WASM/Canvas 尺寸计算并发堆积。
|
|
|
|
|
+ if (playerResizeInProgress) {
|
|
|
|
|
+ playerResizePending = true
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const playerContainer = playerContainerRef.value
|
|
const playerContainer = playerContainerRef.value
|
|
|
if (!playerContainer) {
|
|
if (!playerContainer) {
|
|
|
- player?.JS_Resize().catch(() => undefined)
|
|
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const { clientWidth, clientHeight } = playerContainer
|
|
const { clientWidth, clientHeight } = playerContainer
|
|
|
if (!clientWidth || !clientHeight) return
|
|
if (!clientWidth || !clientHeight) return
|
|
|
- player?.JS_Resize(clientWidth, clientHeight).catch(() => undefined)
|
|
|
|
|
|
|
+ playerResizeInProgress = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await currentPlayer.JS_Resize(clientWidth, clientHeight)
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 容器变化和播放器销毁可能发生在同一帧,尺寸同步失败无需打断预览。
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ playerResizeInProgress = false
|
|
|
|
|
+ if (playerResizePending && !componentUnmounted) {
|
|
|
|
|
+ playerResizePending = false
|
|
|
|
|
+ schedulePlayerResize()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 合并同一帧内的尺寸变化,避免侧栏动画或布局变化时连续调用昂贵的 SDK resize。 */
|
|
/** 合并同一帧内的尺寸变化,避免侧栏动画或布局变化时连续调用昂贵的 SDK resize。 */
|
|
@@ -1039,10 +1079,17 @@ function schedulePlayerResize() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function stopAllPreviews() {
|
|
async function stopAllPreviews() {
|
|
|
|
|
+ if (stopAllLoading.value) return
|
|
|
|
|
+ if (splitControlLoading.value) {
|
|
|
|
|
+ showPreviewError('正在切换分屏,请稍后关闭全部画面')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 先让所有未完成的接口请求失效,防止停止完成后某个旧请求又调用 JS_Play。
|
|
// 先让所有未完成的接口请求失效,防止停止完成后某个旧请求又调用 JS_Play。
|
|
|
cancelAllWindowPreviewRequests()
|
|
cancelAllWindowPreviewRequests()
|
|
|
clearPreviewError()
|
|
clearPreviewError()
|
|
|
|
|
|
|
|
|
|
+ stopAllLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
const currentTalkWindowIndex = talkWindowIndex.value ?? talkTargetWindowIndex.value
|
|
const currentTalkWindowIndex = talkWindowIndex.value ?? talkTargetWindowIndex.value
|
|
|
if (currentTalkWindowIndex !== undefined) await stopWindowTalk(currentTalkWindowIndex)
|
|
if (currentTalkWindowIndex !== undefined) await stopWindowTalk(currentTalkWindowIndex)
|
|
@@ -1056,16 +1103,23 @@ async function stopAllPreviews() {
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('关闭全部监控画面失败', error)
|
|
console.error('关闭全部监控画面失败', error)
|
|
|
showPreviewError(error instanceof Error ? error.message : '关闭全部监控画面失败')
|
|
showPreviewError(error instanceof Error ? error.message : '关闭全部监控画面失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ stopAllLoading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function toggleFullscreen() {
|
|
async function toggleFullscreen() {
|
|
|
|
|
+ if (fullscreenControlLoading.value) return
|
|
|
|
|
+
|
|
|
|
|
+ fullscreenControlLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
const currentPlayer = await initPlayer()
|
|
const currentPlayer = await initPlayer()
|
|
|
await currentPlayer.JS_FullScreenDisplay(!stageFullscreen.value)
|
|
await currentPlayer.JS_FullScreenDisplay(!stageFullscreen.value)
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('切换监控全屏失败', error)
|
|
console.error('切换监控全屏失败', error)
|
|
|
showPreviewError(error instanceof Error ? error.message : '切换全屏失败')
|
|
showPreviewError(error instanceof Error ? error.message : '切换全屏失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ fullscreenControlLoading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1084,6 +1138,7 @@ async function destroyPlayer() {
|
|
|
playerResizeObserver = undefined
|
|
playerResizeObserver = undefined
|
|
|
if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
|
|
if (playerResizeFrame !== undefined) cancelAnimationFrame(playerResizeFrame)
|
|
|
playerResizeFrame = undefined
|
|
playerResizeFrame = undefined
|
|
|
|
|
+ playerResizePending = false
|
|
|
if (!player) return
|
|
if (!player) return
|
|
|
|
|
|
|
|
const currentPlayer = player
|
|
const currentPlayer = player
|
|
@@ -1110,7 +1165,6 @@ onMounted(() => {
|
|
|
playerResizeObserver.observe(playerContainerRef.value)
|
|
playerResizeObserver.observe(playerContainerRef.value)
|
|
|
}
|
|
}
|
|
|
void loadRootTree()
|
|
void loadRootTree()
|
|
|
- void initPlayer().catch(() => undefined)
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
onBeforeUnmount(() => {
|
|
@@ -1346,8 +1400,11 @@ onBeforeUnmount(() => {
|
|
|
type="button"
|
|
type="button"
|
|
|
class="stop-all-trigger"
|
|
class="stop-all-trigger"
|
|
|
aria-label="关闭全部监控画面"
|
|
aria-label="关闭全部监控画面"
|
|
|
|
|
+ :aria-busy="stopAllLoading"
|
|
|
|
|
+ :disabled="stopAllLoading"
|
|
|
@click="stopAllPreviews">
|
|
@click="stopAllPreviews">
|
|
|
- <i class="i-lucide:video-off"></i>
|
|
|
|
|
|
|
+ <i v-if="stopAllLoading" class="i-lucide:loader-circle animate-spin"></i>
|
|
|
|
|
+ <i v-else class="i-lucide:video-off"></i>
|
|
|
</button>
|
|
</button>
|
|
|
</el-tooltip>
|
|
</el-tooltip>
|
|
|
|
|
|
|
@@ -1387,8 +1444,11 @@ onBeforeUnmount(() => {
|
|
|
class="layout-trigger"
|
|
class="layout-trigger"
|
|
|
:class="{ active: splitPopoverVisible }"
|
|
:class="{ active: splitPopoverVisible }"
|
|
|
aria-label="选择分屏布局"
|
|
aria-label="选择分屏布局"
|
|
|
- :aria-expanded="splitPopoverVisible">
|
|
|
|
|
- <i class="i-lucide:layout-grid"></i>
|
|
|
|
|
|
|
+ :aria-busy="splitControlLoading"
|
|
|
|
|
+ :aria-expanded="splitPopoverVisible"
|
|
|
|
|
+ :disabled="splitControlLoading">
|
|
|
|
|
+ <i v-if="splitControlLoading" class="i-lucide:loader-circle animate-spin"></i>
|
|
|
|
|
+ <i v-else class="i-lucide:layout-grid"></i>
|
|
|
</button>
|
|
</button>
|
|
|
</el-tooltip>
|
|
</el-tooltip>
|
|
|
|
|
|
|
@@ -1411,6 +1471,7 @@ onBeforeUnmount(() => {
|
|
|
:title="`${option.count} 分屏`"
|
|
:title="`${option.count} 分屏`"
|
|
|
:aria-label="`切换为 ${option.count} 分屏`"
|
|
:aria-label="`切换为 ${option.count} 分屏`"
|
|
|
:aria-pressed="currentSplitCount === option.count"
|
|
:aria-pressed="currentSplitCount === option.count"
|
|
|
|
|
+ :disabled="splitControlLoading"
|
|
|
@click="handleSplitChange(option.count, option.columns)">
|
|
@click="handleSplitChange(option.count, option.columns)">
|
|
|
<span
|
|
<span
|
|
|
class="split-menu-icon"
|
|
class="split-menu-icon"
|
|
@@ -1426,8 +1487,11 @@ onBeforeUnmount(() => {
|
|
|
class="fullscreen-trigger"
|
|
class="fullscreen-trigger"
|
|
|
:aria-label="stageFullscreen ? '退出全屏' : '全屏'"
|
|
:aria-label="stageFullscreen ? '退出全屏' : '全屏'"
|
|
|
:aria-pressed="stageFullscreen"
|
|
:aria-pressed="stageFullscreen"
|
|
|
|
|
+ :aria-busy="fullscreenControlLoading"
|
|
|
|
|
+ :disabled="fullscreenControlLoading"
|
|
|
@click="toggleFullscreen">
|
|
@click="toggleFullscreen">
|
|
|
- <i v-if="stageFullscreen" class="i-lucide:minimize"></i>
|
|
|
|
|
|
|
+ <i v-if="fullscreenControlLoading" class="i-lucide:loader-circle animate-spin"></i>
|
|
|
|
|
+ <i v-else-if="stageFullscreen" class="i-lucide:minimize"></i>
|
|
|
<i v-else class="i-lucide:maximize"></i>
|
|
<i v-else class="i-lucide:maximize"></i>
|
|
|
</button>
|
|
</button>
|
|
|
</el-tooltip>
|
|
</el-tooltip>
|
|
@@ -1899,20 +1963,24 @@ onBeforeUnmount(() => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.sidebar-toggle:hover,
|
|
.sidebar-toggle:hover,
|
|
|
-.stop-all-trigger:hover,
|
|
|
|
|
|
|
+.stop-all-trigger:hover:not(:disabled),
|
|
|
.capture-all-trigger:hover:not(:disabled),
|
|
.capture-all-trigger:hover:not(:disabled),
|
|
|
.sound-all-trigger:hover:not(:disabled),
|
|
.sound-all-trigger:hover:not(:disabled),
|
|
|
.sound-all-trigger.active,
|
|
.sound-all-trigger.active,
|
|
|
-.fullscreen-trigger:hover,
|
|
|
|
|
|
|
+.fullscreen-trigger:hover:not(:disabled),
|
|
|
.fullscreen-trigger[aria-pressed='true'],
|
|
.fullscreen-trigger[aria-pressed='true'],
|
|
|
-.layout-trigger:hover,
|
|
|
|
|
|
|
+.layout-trigger:hover:not(:disabled),
|
|
|
.layout-trigger.active {
|
|
.layout-trigger.active {
|
|
|
color: #fff;
|
|
color: #fff;
|
|
|
background: #2b3746;
|
|
background: #2b3746;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.stop-all-trigger:disabled,
|
|
|
.capture-all-trigger:disabled,
|
|
.capture-all-trigger:disabled,
|
|
|
-.sound-all-trigger:disabled {
|
|
|
|
|
|
|
+.sound-all-trigger:disabled,
|
|
|
|
|
+.fullscreen-trigger:disabled,
|
|
|
|
|
+.layout-trigger:disabled,
|
|
|
|
|
+.split-menu-button:disabled {
|
|
|
color: #64748b;
|
|
color: #64748b;
|
|
|
cursor: not-allowed;
|
|
cursor: not-allowed;
|
|
|
}
|
|
}
|