|
|
|
@ -22,19 +22,18 @@ |
|
|
|
ref="cameraCards" |
|
|
|
class="camera-card" |
|
|
|
:class="{ active: activeIndex === index, empty: !slot }" |
|
|
|
@click="activeIndex = index" |
|
|
|
@click="selectSlot(index)" |
|
|
|
@dblclick="slot && fullscreen(index)" |
|
|
|
> |
|
|
|
<template v-if="slot"> |
|
|
|
<nvr-stream-player |
|
|
|
:source="slot.streamSource" |
|
|
|
:loading="slot.loading" |
|
|
|
:muted="muted" |
|
|
|
:muted="slot.muted !== false" |
|
|
|
@error="handleStreamError($event, index)" |
|
|
|
/> |
|
|
|
<div class="camera-label"> |
|
|
|
<span><i />{{ slot.name }}</span> |
|
|
|
<small>CH{{ slot.channel }}</small> |
|
|
|
</div> |
|
|
|
<div class="camera-actions"> |
|
|
|
<button type="button" title="历史回放" @click.stop="openPlayback(slot)"><i class="el-icon-time" /></button> |
|
|
|
@ -138,8 +137,8 @@ |
|
|
|
<div class="operation-grid"> |
|
|
|
<button type="button" class="wide" @click="capture"><i class="el-icon-camera" />抓图</button> |
|
|
|
<button type="button" @click="showPendingOperation('录像')"><i class="el-icon-video-camera" />录像</button> |
|
|
|
<button type="button" :class="{ active: !muted }" @click="muted = !muted"> |
|
|
|
<i :class="muted ? 'el-icon-turn-off-microphone' : 'el-icon-microphone'" />静音 |
|
|
|
<button type="button" :class="{ active: !activeMuted }" @click="toggleMuted"> |
|
|
|
<i :class="activeMuted ? 'el-icon-turn-off-microphone' : 'el-icon-microphone'" />静音 |
|
|
|
</button> |
|
|
|
<button type="button" @click="showPendingOperation('雨刷器')"><i class="el-icon-umbrella" />雨刷器</button> |
|
|
|
<button type="button" @click="showPendingOperation('3D定位')"><i class="el-icon-aim" />3D定位</button> |
|
|
|
@ -180,13 +179,14 @@ export default { |
|
|
|
], |
|
|
|
slots: Array.from({ length: 16 }, () => null), |
|
|
|
activeIndex: 0, |
|
|
|
controlTarget: null, |
|
|
|
placementCursor: 0, |
|
|
|
loading: false, |
|
|
|
connectionText: '读取配置', |
|
|
|
connectionState: '', |
|
|
|
muted: true, |
|
|
|
ptzSpeed: 4, |
|
|
|
ptzActive: false, |
|
|
|
ptzCameraCode: '', |
|
|
|
presets: [], |
|
|
|
presetId: null, |
|
|
|
newPresetId: null, |
|
|
|
@ -229,12 +229,14 @@ export default { |
|
|
|
computed: { |
|
|
|
capacity() { return this.layout }, |
|
|
|
visibleSlots() { return this.slots.slice(0, this.capacity) }, |
|
|
|
activeSlot() { return this.slots[this.activeIndex] || null }, |
|
|
|
activeMuted() { return this.controlTarget ? this.controlTarget.muted !== false : true }, |
|
|
|
gridStyle() { |
|
|
|
const columns = { 1: 1, 4: 2, 9: 3, 16: 4 }[this.layout] |
|
|
|
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${columns}, minmax(0, 1fr))` } |
|
|
|
}, |
|
|
|
selectedChannel() { return this.channels.find(item => item.cameraCode === this.selectedCode) || null }, |
|
|
|
selectedChannelName() { return this.selectedChannel ? (this.selectedChannel.name || `通道 ${this.selectedChannel.channel}`) : '未选择' } |
|
|
|
selectedChannel() { return this.controlTarget }, |
|
|
|
selectedChannelName() { return this.controlTarget ? (this.controlTarget.name || `通道 ${this.controlTarget.channel}`) : '未选择画面' } |
|
|
|
}, |
|
|
|
created() { |
|
|
|
this.$root.$on('video-layout-change', this.changeLayout) |
|
|
|
@ -269,7 +271,6 @@ export default { |
|
|
|
this.channels.push(channel) |
|
|
|
} |
|
|
|
this.selectedCode = channel.cameraCode |
|
|
|
this.loadPresets(true) |
|
|
|
}, |
|
|
|
openChannel(channel) { |
|
|
|
this.selectChannel(channel) |
|
|
|
@ -277,29 +278,39 @@ export default { |
|
|
|
}, |
|
|
|
async startChannel(channel) { |
|
|
|
if (!channel || !channel.cameraCode) return |
|
|
|
const existing = this.slots.findIndex(slot => slot && slot.cameraCode === channel.cameraCode) |
|
|
|
if (existing >= 0) { this.activeIndex = existing; return } |
|
|
|
let index = this.visibleSlots.findIndex(slot => !slot) |
|
|
|
if (index < 0) index = this.placementCursor % this.capacity |
|
|
|
const index = this.activeIndex |
|
|
|
const current = this.slots[index] |
|
|
|
if (current && current.cameraCode === channel.cameraCode) { |
|
|
|
this.controlTarget = current |
|
|
|
return |
|
|
|
} |
|
|
|
if (this.slots[index]) await this.closeSlot(index, false) |
|
|
|
const slot = { |
|
|
|
...channel, |
|
|
|
loading: true, |
|
|
|
streamSource: { hlsUrl: '', flvUrl: '' }, |
|
|
|
muted: true, |
|
|
|
errorNotified: false, |
|
|
|
timer: null |
|
|
|
} |
|
|
|
this.$set(this.slots, index, slot) |
|
|
|
this.activeIndex = index |
|
|
|
this.placementCursor = (index + 1) % this.capacity |
|
|
|
this.controlTarget = slot |
|
|
|
this.selectedCode = channel.cameraCode |
|
|
|
this.presets = [] |
|
|
|
this.presetId = null |
|
|
|
try { |
|
|
|
const result = await startLive(channel) |
|
|
|
if (this.slots[index] !== slot) return |
|
|
|
this.applyLiveResult(index, slot, result) |
|
|
|
if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) { |
|
|
|
throw new Error('实时视频接口未返回可播放地址') |
|
|
|
} |
|
|
|
this.loadPresets(true) |
|
|
|
} catch (error) { |
|
|
|
this.$set(this.slots, index, null) |
|
|
|
this.$message.error(error.message) |
|
|
|
if (this.slots[index] === slot) { |
|
|
|
this.$set(this.slots, index, null) |
|
|
|
this.$message.error(error.message) |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
applyLiveResult(index, slot, result) { |
|
|
|
@ -330,8 +341,14 @@ export default { |
|
|
|
async closeSlot(index, notify = true) { |
|
|
|
const slot = this.slots[index] |
|
|
|
if (!slot) return |
|
|
|
if (index === this.activeIndex && this.ptzActive) this.stopPtz() |
|
|
|
clearTimeout(slot.timer) |
|
|
|
this.$set(this.slots, index, null) |
|
|
|
if (this.controlTarget === slot) this.controlTarget = null |
|
|
|
if (index === this.activeIndex) { |
|
|
|
this.presets = [] |
|
|
|
this.presetId = null |
|
|
|
} |
|
|
|
try { await stopLive(slot.cameraCode) } catch (_) { /* release the UI slot first */ } |
|
|
|
if (notify) this.$message.success('视频通道已关闭') |
|
|
|
}, |
|
|
|
@ -345,13 +362,32 @@ export default { |
|
|
|
} |
|
|
|
this.layout = layout |
|
|
|
this.$root.$emit('video-layout-updated', layout) |
|
|
|
if (this.activeIndex >= layout) this.activeIndex = 0 |
|
|
|
if (this.activeIndex >= layout) this.selectSlot(0) |
|
|
|
this.placementCursor %= layout |
|
|
|
}, |
|
|
|
selectSlot(index) { |
|
|
|
if (index === this.activeIndex) { |
|
|
|
const current = this.slots[index] |
|
|
|
this.controlTarget = current || null |
|
|
|
if (current) this.selectedCode = current.cameraCode |
|
|
|
return |
|
|
|
} |
|
|
|
if (this.ptzActive) this.stopPtz() |
|
|
|
this.activeIndex = index |
|
|
|
const slot = this.slots[index] |
|
|
|
this.controlTarget = slot || null |
|
|
|
this.presets = [] |
|
|
|
this.presetId = null |
|
|
|
if (!slot) return |
|
|
|
this.selectedCode = slot.cameraCode |
|
|
|
this.loadPresets(true) |
|
|
|
}, |
|
|
|
handleStreamError(error, index) { |
|
|
|
const slot = this.slots[index] |
|
|
|
if (slot) slot.loading = false |
|
|
|
this.$message.error(error.message) |
|
|
|
if (!slot || slot.errorNotified) return |
|
|
|
slot.loading = false |
|
|
|
slot.errorNotified = true |
|
|
|
this.$message.error((error && error.message) || '视频播放失败') |
|
|
|
}, |
|
|
|
fullscreen(index) { |
|
|
|
const card = this.$refs.cameraCards && this.$refs.cameraCards[index] |
|
|
|
@ -360,31 +396,40 @@ export default { |
|
|
|
openPlayback(slot) { |
|
|
|
this.$router.push({ name: 'VideoHistoryPlayback', query: { cameraCode: slot.cameraCode }}) |
|
|
|
}, |
|
|
|
async sendPtz(controlCode, controlPara1 = '', controlPara2 = '') { |
|
|
|
if (!this.selectedCode) { this.$message.warning('请先选择摄像机'); return } |
|
|
|
const result = await controlPtz({ cameraCode: this.selectedCode, controlCode, controlPara1, controlPara2 }) |
|
|
|
async sendPtz(controlCode, controlPara1 = '', controlPara2 = '', cameraCode = '') { |
|
|
|
const targetCode = cameraCode || (this.controlTarget && this.controlTarget.cameraCode) |
|
|
|
if (!targetCode) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
|
const result = await controlPtz({ cameraCode: targetCode, controlCode, controlPara1, controlPara2 }) |
|
|
|
if (!result || Number(result.resultCode) !== 0) throw new Error('云台控制失败') |
|
|
|
}, |
|
|
|
startPtz(action) { |
|
|
|
if (action === 'STOP') { |
|
|
|
const cameraCode = this.ptzCameraCode || (this.controlTarget && this.controlTarget.cameraCode) |
|
|
|
this.ptzActive = false |
|
|
|
this.sendPtz(1).catch(error => this.$message.error(error.message)) |
|
|
|
this.ptzCameraCode = '' |
|
|
|
this.sendPtz(1, '', '', cameraCode).catch(error => this.$message.error(error.message)) |
|
|
|
return |
|
|
|
} |
|
|
|
const code = PTZ_CODES[action] |
|
|
|
if (!code || this.ptzActive) return |
|
|
|
if (!this.controlTarget) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
|
this.ptzActive = true |
|
|
|
this.sendPtz(code, '2', String(this.ptzSpeed)).catch(error => this.$message.error(error.message)) |
|
|
|
this.ptzCameraCode = this.controlTarget.cameraCode |
|
|
|
this.sendPtz(code, '2', String(this.ptzSpeed), this.ptzCameraCode).catch(error => this.$message.error(error.message)) |
|
|
|
}, |
|
|
|
stopPtz() { |
|
|
|
if (!this.ptzActive) return |
|
|
|
const cameraCode = this.ptzCameraCode |
|
|
|
this.ptzActive = false |
|
|
|
this.sendPtz(1).catch(error => this.$message.error(error.message)) |
|
|
|
this.ptzCameraCode = '' |
|
|
|
this.sendPtz(1, '', '', cameraCode).catch(error => this.$message.error(error.message)) |
|
|
|
}, |
|
|
|
async loadPresets(silent = false) { |
|
|
|
if (!this.selectedCode) return |
|
|
|
const cameraCode = this.controlTarget && this.controlTarget.cameraCode |
|
|
|
if (!cameraCode) return |
|
|
|
try { |
|
|
|
const result = await getPresetList(this.selectedCode) |
|
|
|
const result = await getPresetList(cameraCode) |
|
|
|
if (!this.controlTarget || this.controlTarget.cameraCode !== cameraCode) return |
|
|
|
const source = result && result.ptzPresetInfoList && result.ptzPresetInfoList.ptzPresetInfo |
|
|
|
this.presets = (Array.isArray(source) ? source : []).map(item => ({ id: Number(item.presetIndex), name: item.presetName || `预置位 ${item.presetIndex}` })).filter(item => item.id > 0) |
|
|
|
if (this.presets.length && !this.presets.some(item => item.id === this.presetId)) this.presetId = this.presets[0].id |
|
|
|
@ -425,20 +470,25 @@ export default { |
|
|
|
} catch (error) { if (error !== 'cancel' && error !== 'close') this.$message.error(error.message) } |
|
|
|
}, |
|
|
|
async capture() { |
|
|
|
if (!this.selectedCode) { this.$message.warning('请先选择摄像机'); return } |
|
|
|
const cameraCode = this.controlTarget && this.controlTarget.cameraCode |
|
|
|
if (!cameraCode) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
|
try { |
|
|
|
const command = await createSnapshot(this.selectedCode) |
|
|
|
const command = await createSnapshot(cameraCode) |
|
|
|
if (!command || Number(command.resultCode) !== 0 || !command.taskID) throw new Error('抓图任务创建失败') |
|
|
|
const blob = await downloadSnapshot(command.taskID) |
|
|
|
const url = URL.createObjectURL(blob) |
|
|
|
const link = document.createElement('a') |
|
|
|
link.href = url |
|
|
|
link.download = `camera-${this.selectedCode.replace(/[^A-Za-z0-9_-]/g, '_')}-${Date.now()}.jpg` |
|
|
|
link.download = `camera-${cameraCode.replace(/[^A-Za-z0-9_-]/g, '_')}-${Date.now()}.jpg` |
|
|
|
link.click() |
|
|
|
setTimeout(() => URL.revokeObjectURL(url), 2000) |
|
|
|
this.$message.success('抓图已下载') |
|
|
|
} catch (error) { this.$message.error(error.message) } |
|
|
|
}, |
|
|
|
toggleMuted() { |
|
|
|
if (!this.controlTarget) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
|
this.$set(this.controlTarget, 'muted', !(this.controlTarget.muted !== false)) |
|
|
|
}, |
|
|
|
showPendingOperation(name) { |
|
|
|
this.$message.info(`${name}控制接口暂未接入`) |
|
|
|
} |
|
|
|
|