海康威视webSDK语音对讲、云台控制功能记录
·
先从官网下载webSDKdemo文件,将以下文件放入public文件夹下
还需要一个jq文件,这里我放在public文件夹下的
![]()
index.html文件中引入

我的项目有两种播放实时监控的方式,一种是RTSP,一种是webSDK. RTSP很简单,最后再讲。下面是组件的代码
<template>
<div id="video-container" class="video-container">
<!-- WebRTC 方式播放 RTSP -->
<video
class="video"
ref="video"
preload="auto"
autoplay="autoplay"
muted
width="100%"
height="100%"
v-if="!useSdk"
/>
<!-- 海康 SDK 播放容器 -->
<div v-else id="divPlugin" class="plugin-container"></div>
<!-- 点击遮罩层,仅用于选择视频(无双击全屏功能) -->
<div class="mask" @click="handleClickVideo" :class="{ 'active-video-border': selectStatus }"></div>
</div>
</template>
<script>
import WebRtcStreamer from '../../../../public/webrtcstreamer'
export default {
name: 'videoCom',
props: {
rtsp: {
type: String,
required: true
},
isOn: {
type: Boolean,
default: false
},
spareId: {
type: Number
},
selectStatus: {
type: Boolean,
default: false
},
deviceInfo: {
type: Object,
default: function() {
return {
netWork: '',
port: '',
account: '',
password: ''
}
}
},
useSdk: {
type: Boolean,
default: false
},
useProxy: {
type: Boolean,
default: false
}
},
data: function() {
return {
webRtcServer: null,
g_iWndIndex: 0,
sdkReady: false,
isLoggedIn: false,
isPlaying: false,
deviceIdentify: '',
channelId: 1,
rtspPort: null,
g_szRecordType: '',
audioChannels: [],
selectedAudioChannel: 1,
pendingOperations: [],
loginRetryCount: 0,
szIP: '',
szPort: '',
szUsername: '',
szPassword: '',
szDeviceIdentify: '',
pluginInserted: false,
initRetryCount: 0,
currentRecordPath: ''
}
},
watch: {
rtsp: {
handler: function(newVal, oldVal) {
if (newVal && newVal !== oldVal && this.useSdk && this.isPlaying) {
this.switchChannel()
}
}
},
useSdk: {
handler: function(newVal) {
if (newVal) {
this.destroyWebRTC()
this.$nextTick(() => {
this.initSDK()
})
} else {
this.destroySDK()
this.$nextTick(() => {
this.initWebRTC()
})
}
}
}
},
mounted: function() {
if (this.useSdk) {
this.initSDK()
} else {
this.initWebRTC()
}
},
beforeDestroy: function() {
if (this.useSdk) {
this.destroySDK()
} else {
this.destroyWebRTC()
}
},
methods: {
openSound: function() {
var self = this
return new Promise(function(resolve, reject) {
WebVideoCtrl.I_OpenSound()
.then(function() {
console.log('打开声音成功')
resolve()
})
.catch(function(e) {
console.error('打开声音失败:', e)
reject(new Error('打开声音失败'))
})
})
},
closeSound: function() {
var self = this
return new Promise(function(resolve, reject) {
WebVideoCtrl.I_CloseSound()
.then(function() {
console.log('关闭声音成功')
resolve()
})
.catch(function(e) {
console.error('关闭声音失败:', e)
reject(new Error('关闭声音失败'))
})
})
},
initWebRTC: function() {
if (!this.rtsp) return
try {
this.webRtcServer = new WebRtcStreamer(
this.$refs.video,
window.location.protocol + '//' + window.location.hostname + ':8000'
)
this.webRtcServer.connect(this.rtsp)
console.log('WebRTC 初始化成功')
} catch (error) {
console.error('WebRTC 初始化失败:', error)
this.$emit('error', 'WebRTC 初始化失败')
}
},
destroyWebRTC: function() {
if (this.webRtcServer) {
this.webRtcServer.disconnect()
this.webRtcServer = null
}
},
initSDK: function() {
var self = this
if (typeof WebVideoCtrl === 'undefined') {
console.log('等待 WebVideoCtrl 加载...')
setTimeout(function() {
self.initSDK()
}, 500)
return
}
var container = document.getElementById('divPlugin')
if (!container) {
console.log('等待 divPlugin 容器...')
setTimeout(function() {
self.initSDK()
}, 200)
return
}
// 保存设备信息
this.szIP = this.deviceInfo.netWork
this.szPort = this.deviceInfo.port || '80'
this.szUsername = this.deviceInfo.account || 'admin'
this.szPassword = this.deviceInfo.password || ''
this.szDeviceIdentify = this.szIP + '_' + this.szPort
console.log('设备信息:', {
ip: this.szIP,
port: this.szPort,
username: this.szUsername,
identify: this.szDeviceIdentify
})
WebVideoCtrl.I_InitPlugin({
bWndFull: true,
iWndowType: 1,
cbSelWnd: function(xmlDoc) {
self.g_iWndIndex = parseInt($(xmlDoc).find('SelectWnd').eq(0).text(), 10)
console.log('当前选择的窗口编号:', self.g_iWndIndex)
self.$emit('window-selected', self.g_iWndIndex)
},
cbDoubleClickWnd: function(iWndIndex, bFullScreen) {
console.log('双击窗口:', iWndIndex, '全屏:', bFullScreen)
},
cbEvent: function(iEventType, iParam1, iParam2) {
console.log('插件事件:', iEventType, iParam1, iParam2)
if (3002 === iEventType) {
self.$emit('error', '回放异常')
} else if (3003 === iEventType) {
console.log('回放停止')
} else if (3004 === iEventType) {
self.$emit('error', '硬盘空间不足')
} else if (5000 === iEventType) {
self.$emit('error', '语音对讲失败')
}
},
cbInitPluginComplete: function() {
console.log('插件初始化完成')
WebVideoCtrl.I_InsertOBJECTPlugin('divPlugin').then(function() {
console.log('插件插入成功')
self.pluginInserted = true
self.getDevicePortAndLogin()
}, function(err) {
console.error('插件插入失败:', err)
self.pluginInserted = false
self.$emit('error', '插件插入失败,请确认已安装HCWebSDK插件')
})
}
})
},
getDevicePortAndLogin: function() {
var self = this
try {
var oPort = WebVideoCtrl.I_GetDevicePort(this.szDeviceIdentify)
if (oPort) {
self.rtspPort = oPort.iRtspPort
console.log('获取端口成功:', oPort)
} else {
console.warn('获取端口返回空,使用默认端口 554')
self.rtspPort = 554
}
} catch(e) {
console.error('获取端口异常:', e)
self.rtspPort = 554
}
self.loginDevice()
},
loginDevice: function() {
var self = this
if (!this.szIP) {
this.$emit('error', '设备IP为空')
return
}
console.log('开始登录设备:', this.szIP, this.szPort, this.szUsername)
WebVideoCtrl.I_Login(this.szIP, 1, this.szPort, this.szUsername, this.szPassword, {
timeout: 30000,
success: function(xmlDoc) {
console.log('登录成功:', self.szIP)
self.isLoggedIn = true
self.loginRetryCount = 0
self.$emit('login-success')
self.getChannelInfo()
self.getAudioInfo()
},
error: function(status, xmlDoc) {
console.error('登录失败:', status, xmlDoc)
if (status === 2001) {
console.log('设备已登录')
self.isLoggedIn = true
self.getChannelInfo()
self.getAudioInfo()
} else if (self.loginRetryCount < 3) {
self.loginRetryCount++
setTimeout(function() {
self.loginDevice()
}, 2000)
} else {
self.$emit('error', '登录失败:请检查设备IP、端口、用户名和密码')
}
}
})
},
getChannelInfo: function() {
var self = this
var szDeviceIdentify = this.szDeviceIdentify
WebVideoCtrl.I_GetAnalogChannelInfo(szDeviceIdentify, {
success: function(xmlDoc) {
var channels = $(xmlDoc).find('VideoInputChannel')
if (channels.length > 0) {
self.channelId = parseInt($(channels[0]).find('id').eq(0).text(), 10)
console.log('使用模拟通道:', self.channelId)
self.startPreview()
return
}
},
error: function() {
WebVideoCtrl.I_GetDigitalChannelInfo(szDeviceIdentify, {
success: function(xmlDoc) {
var channels = $(xmlDoc).find('InputProxyChannelStatus')
if (channels.length > 0) {
self.channelId = parseInt($(channels[0]).find('id').eq(0).text(), 10)
console.log('使用数字通道:', self.channelId)
}
self.startPreview()
},
error: function() {
self.channelId = 1
self.startPreview()
}
})
}
})
},
getAudioInfo: function() {
var self = this
WebVideoCtrl.I_GetAudioInfo(this.szDeviceIdentify, {
success: function(xmlDoc) {
var oAudioChannels = $(xmlDoc).find('TwoWayAudioChannel')
var channels = []
$.each(oAudioChannels, function() {
var id = $(this).find('id').eq(0).text()
channels.push({ id: parseInt(id, 10) })
})
self.audioChannels = channels
if (channels.length > 0) {
self.selectedAudioChannel = channels[0].id
}
console.log('获取对讲通道成功:', channels)
self.$emit('audio-channels-loaded', channels)
},
error: function(oError) {
console.error('获取对讲通道失败:', oError)
}
})
},
startPreview: function() {
var self = this
if (!this.isLoggedIn) {
console.warn('设备未登录,等待登录...')
setTimeout(function() {
if (self.isLoggedIn) {
self.startPreview()
}
}, 1000)
return
}
console.log('========== 开始预览 ==========')
console.log('设备标识:', this.szDeviceIdentify)
console.log('窗口索引:', this.g_iWndIndex)
console.log('通道ID:', this.channelId)
console.log('RTSP端口:', this.rtspPort)
console.log('useProxy:', this.useProxy)
var startRealPlay = function() {
var streamType = 1
var playParams = {
iWndIndex: self.g_iWndIndex,
iStreamType: streamType,
iChannelID: self.channelId,
bZeroChannel: false,
iPort: self.rtspPort,
success: function() {
console.log('========== 开始预览成功! ==========')
self.isPlaying = true
self.sdkReady = true
self.$emit('play-start')
self.processPendingOperations()
},
error: function(status, xmlDoc) {
console.error('开始预览失败:', status, xmlDoc)
if (streamType === 1) {
console.log('主码流失败,尝试子码流...')
streamType = 2
playParams.iStreamType = streamType
WebVideoCtrl.I_StartRealPlay(self.szDeviceIdentify, playParams)
} else {
self.$emit('error', '预览失败,请检查设备是否支持WebSocket取流')
}
}
}
WebVideoCtrl.I_StartRealPlay(self.szDeviceIdentify, playParams)
}
var oWndInfo = WebVideoCtrl.I_GetWindowStatus(this.g_iWndIndex)
if (oWndInfo && oWndInfo.bIsPlay) {
console.log('窗口已在播放,先停止')
WebVideoCtrl.I_Stop({
iWndIndex: this.g_iWndIndex,
success: startRealPlay,
error: startRealPlay
})
} else {
startRealPlay()
}
},
stopPreview: function() {
if (this.isPlaying) {
WebVideoCtrl.I_Stop({
iWndIndex: this.g_iWndIndex,
success: function() {
console.log('停止预览成功')
this.isPlaying = false
}.bind(this)
})
}
},
switchChannel: function() {
var match = this.rtsp.match(/Channels\/(\d+)/)
if (match) {
var newChannelId = parseInt(match[1].toString().slice(0, -1), 10)
if (newChannelId && newChannelId !== this.channelId) {
this.channelId = newChannelId
if (this.isPlaying) {
this.startPreview()
}
}
}
},
destroySDK: function() {
this.stopPreview()
if (this.isLoggedIn && this.szDeviceIdentify) {
try {
WebVideoCtrl.I_Logout(this.szDeviceIdentify)
} catch (e) {}
this.isLoggedIn = false
}
try {
WebVideoCtrl.I_DestroyPlugin()
} catch(e) {}
this.sdkReady = false
this.pendingOperations = []
this.pluginInserted = false
},
addPendingOperation: function(operation) {
var self = this
return new Promise(function(resolve, reject) {
if (self.sdkReady && self.isPlaying) {
operation()
.then(resolve)
.catch(reject)
} else {
self.pendingOperations.push({ operation: operation, resolve: resolve, reject: reject })
}
})
},
processPendingOperations: function() {
while (this.pendingOperations.length > 0) {
var pending = this.pendingOperations.shift()
pending
.operation()
.then(pending.resolve)
.catch(pending.reject)
}
},
capturePic: function() {
var self = this
return this.addPendingOperation(function() {
return new Promise(function(resolve, reject) {
var deviceName = self.deviceInfo.name || 'camera'
var now = new Date()
var formattedTime = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0') + '_' +
String(now.getHours()).padStart(2, '0') + '.' +
String(now.getMinutes()).padStart(2, '0') + '.' +
String(now.getSeconds()).padStart(2, '0')
var szPicName = deviceName + '_' + formattedTime + '.jpg'
WebVideoCtrl.I_CapturePic(szPicName, {})
.then(function() {
console.log('截图成功:', szPicName)
resolve({ name: szPicName, path: '桌面' })
})
.catch(function(oError) {
console.error('抓图失败:', oError)
reject(new Error('抓图失败:' + (oError.errorMsg || JSON.stringify(oError))))
})
})
})
},
startRecord: function() {
var self = this
return this.addPendingOperation(function() {
return new Promise(function(resolve, reject) {
var deviceName = self.deviceInfo.name || 'camera'
var now = new Date()
var formattedTime = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0')
var szFileName = deviceName + '_' + formattedTime
WebVideoCtrl.I_StartRecord(szFileName, {
iWndIndex: self.g_iWndIndex,
success: function() {
console.log('开始录像成功')
self.g_szRecordType = 'realplay'
resolve()
},
error: function(error) {
console.error('开始录像失败:', error)
reject(new Error('开始录像失败'))
}
})
})
})
},
stopRecord: function() {
var self = this
return this.addPendingOperation(function() {
return new Promise(function(resolve, reject) {
WebVideoCtrl.I_StopRecord({
iWndIndex: self.g_iWndIndex,
success: function() {
console.log('停止录像成功')
resolve({ path: '当前配置路径' })
},
error: function(error) {
console.error('停止录像失败:', error)
reject(new Error('停止录像失败'))
}
})
})
})
},
startZoomIn: function() {
if (!this.sdkReady || !this.isPlaying) {
console.warn('SDK未就绪或未播放')
return
}
WebVideoCtrl.I_PTZControl(10, false, {
iWndIndex: this.g_iWndIndex,
iPTZSpeed: 4,
success: function() {
console.log('调焦+成功')
},
error: function(status) {
console.error('调焦+失败:', status)
}
})
},
startZoomOut: function() {
if (!this.sdkReady || !this.isPlaying) {
console.warn('SDK未就绪或未播放')
return
}
WebVideoCtrl.I_PTZControl(11, false, {
iWndIndex: this.g_iWndIndex,
iPTZSpeed: 4,
success: function() {
console.log('调焦-成功')
},
error: function(status) {
console.error('调焦-失败:', status)
}
})
},
stopPTZ: function() {
if (!this.sdkReady) return
WebVideoCtrl.I_PTZControl(11, true, {
iWndIndex: this.g_iWndIndex,
success: function() {
console.log('调焦停止成功')
},
error: function(status) {
console.error('调焦停止失败:', status)
}
})
},
ptzZoom: function(action, continuous) {
if (action === 'in') {
this.startZoomIn()
if (!continuous) {
setTimeout(this.stopPTZ.bind(this), 300)
}
} else if (action === 'out') {
this.startZoomOut()
if (!continuous) {
setTimeout(this.stopPTZ.bind(this), 300)
}
}
},
startVoiceTalk: function(audioChannel) {
var self = this
var channel = audioChannel || this.selectedAudioChannel || 1
if (!this.isLoggedIn) {
return Promise.reject(new Error('未登录'))
}
console.log('开始对讲,设备:', this.szDeviceIdentify, '通道:', channel)
return new Promise(function(resolve, reject) {
WebVideoCtrl.I_StartVoiceTalk(self.szDeviceIdentify, channel)
.then(function() {
console.log('开始对讲成功')
resolve()
})
.catch(function(error) {
console.error('开始对讲失败:', error)
reject(new Error('开始对讲失败'))
})
})
},
stopVoiceTalk: function() {
var self = this
return new Promise(function(resolve, reject) {
WebVideoCtrl.I_StopVoiceTalk()
.then(function() {
console.log('停止对讲成功')
resolve()
})
.catch(function(error) {
console.error('停止对讲失败:', error)
reject(new Error('停止对讲失败'))
})
})
},
getControlStatus: function() {
return {
ready: this.sdkReady,
playing: this.isPlaying,
loggedIn: this.isLoggedIn,
windowIndex: this.g_iWndIndex
}
},
handleClickVideo: function() {
if (this.isOn) {
this.$emit('selectVideo', this.spareId)
}
}
}
}
</script>
<style scoped lang="scss">
.video-container {
position: relative;
width: 100%;
height: 100%;
.video {
width: 100%;
height: 100%;
object-fit: fill;
}
.plugin-container {
width: 100%;
height: 100%;
:deep(object) {
width: 100%;
height: 100%;
}
}
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.active-video-border {
border: 2px solid salmon;
box-sizing: border-box;
}
}
</style>
下面是使用示例,loadSpeakCapabilities调的后端的接口,不用也可以运行。代码中已注释
<template>
<div class="ai-monitor-modal" v-if="visible">
<div class="modal-overlay" @click="handleClose"></div>
<div class="modal-container" ref="modalContainer">
<!-- 标题栏 -->
<div class="modal-header">
<div class="header-left">
<div class="status-tag">
<span class="pulse-dot"></span>
实时监控
</div>
<div class="title-group">
<div class="title-text">
<span class="main-title">
<a-icon class="camera-icon" type="video-camera" theme="filled" />
{{ cameraData.name || '监控设备' }}
</span>
<span class="sub-title">设备编号:{{ cameraData.serialNumber || '--' }} | 所属部门:{{ cameraData.sysOrgCodeStr || '--' }}</span>
</div>
</div>
</div>
<div class="header-right">
<button class="header-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏'">
<a-icon class="icon" v-if="!isFullscreen" type="fullscreen" />
<a-icon class="icon" v-else type="fullscreen-exit" />
</button>
<button class="header-btn close-btn" @click="handleClose" title="关闭">
<a-icon class="icon" type="close" />
</button>
</div>
</div>
<!-- 主内容区 -->
<div class="modal-content">
<!-- 左侧视频区 -->
<div class="video-left">
<!-- 视频插件容器 -->
<div class="video-wrapper">
<Video
ref="videoComponent"
:rtsp="rtspUrl"
:selectStatus="false"
:deviceInfo="cameraData"
:useSdk="true"
:useProxy="false"
@play-start="onPlayStart"
@error="onVideoError"
@login-success="onLoginSuccess"
@audio-channels-loaded="onAudioChannelsLoaded"
@window-selected="onWindowSelected"
/>
<div id="divPlugin" class="plugin-container"></div>
</div>
<!-- 视频控制栏(放在视频插件下方) -->
<div class="video-control-bar">
<!-- 左侧状态信息 -->
<div class="control-left">
<div class="recording-indicator" v-if="isRecording">
<span class="rec-dot"></span>
<span class="rec-text">正在录像</span>
<span class="rec-time">{{ recordingTime }}</span>
</div>
<div class="control-status" v-if="!controlReady && controlLoading">
<a-icon type="loading" />
<span class="status-text">正在连接设备控制...</span>
</div>
<div class="audio-debug" v-if="audioDebugInfo">
<span class="debug-text">{{ audioDebugInfo }}</span>
</div>
</div>
<!-- 右侧控制按钮 -->
<div class="control-right">
<div class="video-controls">
<button class="control-btn" @click="handleScreenshot" title="截图" :disabled="!controlReady">
<a-icon type="picture" />
<span>截图</span>
</button>
<button class="control-btn" @click="toggleRecording" :title="isRecording ? '停止录像' : '开始录像'" :disabled="!controlReady">
<a-icon v-if="isRecording" type="video-camera" theme="twoTone" twoToneColor="#ff4d4f" />
<a-icon v-else type="video-camera" />
<span>{{ isRecording ? '停止录像' : '开始录像' }}</span>
</button>
<button
class="control-btn"
@mousedown="startZoomIn"
@mouseup="stopZoom"
@mouseleave="stopZoom"
title="按住放大"
:disabled="!controlReady"
>
<a-icon type="plus" />
<span>放大</span>
</button>
<button
class="control-btn"
@mousedown="startZoomOut"
@mouseup="stopZoom"
@mouseleave="stopZoom"
title="按住缩小"
:disabled="!controlReady"
>
<a-icon type="minus" />
<span>缩小</span>
</button>
<button
class="control-btn"
@click="toggleSound"
:title="isSoundOpen ? '关闭声音' : '打开声音'"
:disabled="!controlReady"
>
<a-icon v-if="isSoundOpen" type="sound" theme="twoTone" twoToneColor="#00d4ff" />
<a-icon v-else type="sound" />
<span>{{ isSoundOpen ? '静音' : '声音' }}</span>
</button>
</div>
</div>
</div>
</div>
<!-- 右侧边栏 -->
<div class="sidebar-right">
<!-- 现场喊话 -->
<div class="sidebar-section intercom-section">
<div class="section-header">
<a-icon class="section-icon" type="audio" theme="filled" />
<span>现场喊话</span>
</div>
<div class="intercom-content">
<button
class="intercom-btn push-to-talk-btn"
:class="{ 'recording': isSpeaking }"
@click="toggleSpeaking()"
:disabled="!controlReady || !speakCapable"
>
<a-icon class="mic-icon" type="audio" />
<span class="btn-text">{{ isSpeaking ? '点击停止讲话' : '点击开始讲话' }}</span>
</button>
<select v-if="audioChannels.length > 0" v-model="selectedAudioChannelId" class="audio-channel-select" @change="onAudioChannelChange">
<option v-for="ch in audioChannels" :key="ch.id" :value="ch.id">对讲通道 {{ ch.id }}</option>
</select>
<div class="device-status">
<div class="status-indicator" :class="{ 'ready': controlReady && speakCapable }"></div>
<span>{{ getControlStatusText() }}</span>
</div>
</div>
</div>
<!-- 设备信息 -->
<div class="sidebar-section device-section">
<div class="section-header">
<a-icon class="section-icon" type="desktop" />
<span>设备信息</span>
</div>
<div class="device-info-list">
<!-- <div class="info-item">
<span class="info-label">设备ID</span>
<span class="info-value device-id">{{ cameraData.id || '--' }}</span>
</div> -->
<div class="info-item">
<span class="info-label">设备名称</span>
<span class="info-value">{{ cameraData.name || '--' }}</span>
</div>
<div class="info-item">
<span class="info-label">设备编号</span>
<span class="info-value">{{ cameraData.serialNumber || '--' }}</span>
</div>
<div class="info-item">
<span class="info-label">IP地址</span>
<span class="info-value">{{ cameraData.netWork || '--' }}</span>
</div>
<!-- <div class="info-item">
<span class="info-label">端口</span>
<span class="info-value">{{ cameraData.port || '80' }}</span>
</div> -->
<div class="info-item">
<span class="info-label">播放状态</span>
<span class="info-value">{{ isPlaying ? '播放中' : '未播放' }}</span>
</div>
<div class="info-item">
<span class="info-label">声音状态</span>
<span class="info-value" :class="{ 'ready-text': isSoundOpen }">{{ isSoundOpen ? '已开启' : '已关闭' }}</span>
</div>
<div class="info-item">
<span class="info-label">控制状态</span>
<span class="info-value" :class="{ 'ready-text': controlReady }">{{ controlReady ? '已连接' : (controlLoading ? '连接中' : '未连接') }}</span>
</div>
<div class="info-item">
<span class="info-label">对讲能力</span>
<span class="info-value" :class="{ 'ready-text': speakCapable }">{{ speakCapable ? '支持' : '不支持' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 插件未安装弹窗 -->
<a-modal
title="插件未安装"
:visible="pluginNotInstalledVisible"
:footer="null"
@cancel="pluginNotInstalledVisible = false"
width="400px"
>
<div style="text-align: center; padding: 20px;">
<a-icon type="warning" style="font-size: 48px; color: #ff9800; margin-bottom: 16px;" />
<p style="margin-bottom: 16px; font-size: 14px;">未检测到海康Web插件,请先安装插件后再使用视频监控功能。</p>
<a-button type="primary" @click="downloadPlugin" style="margin-right: 12px;">
<a-icon type="download" /> 立即下载
</a-button>
<a-button @click="pluginNotInstalledVisible = false">
取消
</a-button>
</div>
</a-modal>
<!-- 安装完成提示弹窗 -->
<a-modal
title="提示"
:visible="installCompleteVisible"
:footer="null"
@cancel="installCompleteVisible = false"
width="400px"
>
<div style="text-align: center; padding: 20px;">
<a-icon type="check-circle" style="font-size: 48px; color: #52c41a; margin-bottom: 16px;" />
<p style="margin-bottom: 16px; font-size: 14px;">插件下载成功!</p>
<p style="margin-bottom: 16px; font-size: 12px; color: #666;">请双击运行安装程序,安装完成后点击下方按钮刷新页面。</p>
<a-button type="primary" @click="refreshPage">
<a-icon type="reload" /> 安装完成,刷新页面
</a-button>
</div>
</a-modal>
</div>
</template>
<script>
import Video from './video.vue'
import { getTwoWayAudioCapabilities } from '@/api/hikvision'
export default {
name: 'AiMonitorModal',
components: {
Video
},
props: {
visible: {
type: Boolean,
default: false
},
cameraData: {
type: Object,
default: function() {
return {}
}
}
},
computed: {
rtspUrl: function() {
var camera = this.cameraData
if (!camera.netWork || !camera.account) {
return ''
}
return 'rtsp://' + camera.account + ':' + camera.password + '@' + camera.netWork + ':554/Streaming/Channels/101'
}
},
data: function() {
return {
// 视频控制相关
isFullscreen: false,
isPlaying: false,
isRecording: false,
isSoundOpen: false,
recordingTime: '00:00:00',
recordingInterval: null,
controlReady: false,
controlLoading: false,
// 对讲相关
isSpeaking: false,
audioChannels: [],
selectedAudioChannelId: null,
speakCapable: false,
// 窗口相关
currentWindowIndex: 0,
// 调试信息
audioDebugInfo: '',
lastDebugTime: 0,
// 弹窗状态
pluginNotInstalledVisible: false,
installCompleteVisible: false
}
},
watch: {
visible: {
handler: function(val) {
if (val) {
this.$nextTick(() => {
this.initModal()
})
} else {
this.cleanupAll()
}
},
immediate: false
}
},
mounted() {
document.addEventListener('fullscreenchange', this.handleFullscreenChange)
document.addEventListener('webkitfullscreenchange', this.handleFullscreenChange)
document.addEventListener('mozfullscreenchange', this.handleFullscreenChange)
document.addEventListener('MSFullscreenChange', this.handleFullscreenChange)
},
beforeDestroy: function() {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange)
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange)
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange)
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange)
this.cleanupAll()
},
methods: {
toggleSpeaking(){
this.isSpeaking? this.stopSpeaking() : this.startSpeaking()
},
handleFullscreenChange() {
const isFull = !!(document.fullscreenElement ||
document.webkitFullscreenElement ||
document.mozFullScreenElement ||
document.msFullscreenElement)
this.isFullscreen = isFull
},
getDeviceId: function() {
return this.cameraData.id || this.cameraData.serialNumber || this.cameraData.serialNo
},
getVideoComp: function() {
return this.$refs.videoComponent
},
updateAudioDebug: function(msg) {
var now = Date.now()
if (now - this.lastDebugTime > 500 || msg.includes('失败') || msg.includes('完成') || msg.includes('成功')) {
this.audioDebugInfo = msg
this.lastDebugTime = now
var self = this
setTimeout(function() {
if (self.audioDebugInfo === msg) {
self.audioDebugInfo = ''
}
}, 3000)
}
console.log('[调试]', msg)
},
// ==================== 初始化 ====================
initModal: function() {
this.controlLoading = true
this.controlReady = false
this.isSoundOpen = false
this.speakCapable = false
this.audioChannels = []
this.selectedAudioChannelId = null
this.audioDebugInfo = ''
this.isPlaying = false
this.pluginNotInstalledVisible = false
this.installCompleteVisible = false
},
downloadPlugin: function() {
this.pluginNotInstalledVisible = false
var pluginUrl = '/codebase/HCWebSDKPluginsUserSetup.exe'
var link = document.createElement('a')
link.href = pluginUrl
link.download = 'HCWebSDKPluginsUserSetup.exe'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
this.installCompleteVisible = true
},
refreshPage: function() {
window.location.reload()
},
// ==================== 语音对讲能力获取 ====================
loadSpeakCapabilities: function() {
var self = this
var deviceId = this.getDeviceId()
if (!deviceId) return
getTwoWayAudioCapabilities(deviceId).then(function(response) {
var capable = false
if (response && response.success === true && response.result) {
capable = response.result.supported === true
}
self.speakCapable = capable
console.log('语音对讲能力:', capable)
}).catch(function(error) {
console.error('获取语音对讲能力失败:', error)
self.speakCapable = false
})
},
onAudioChannelsLoaded: function(channels) {
this.audioChannels = channels
if (channels.length > 0) {
this.selectedAudioChannelId = channels[0].id
}
this.speakCapable = true
console.log('音频通道加载成功:', channels)
},
onWindowSelected: function(windowIndex) {
this.currentWindowIndex = windowIndex
console.log('窗口选中:', windowIndex)
},
getControlStatusText: function() {
if (this.controlReady && this.speakCapable) return '设备就绪'
if (this.controlLoading) return '连接中...'
if (!this.speakCapable) return '不支持对讲'
return '设备离线'
},
// ==================== 对讲功能 ====================
startSpeaking: function() {
if (this.isSpeaking) return
if (!this.controlReady || !this.speakCapable) {
this.$message && this.$message.warning('设备未就绪')
return
}
var channelId = this.selectedAudioChannelId
if (!channelId) {
this.$message && this.$message.warning('未找到对讲通道')
return
}
var videoComp = this.getVideoComp()
if (!videoComp) {
this.$message && this.$message.warning('视频组件未就绪')
return
}
console.log('开始对讲,通道:', channelId)
this.updateAudioDebug('正在开启对讲...')
videoComp.startVoiceTalk(channelId).then(() => {
console.log('对讲开启成功')
this.updateAudioDebug('对讲已开启,请讲话...')
this.isSpeaking = true
}).catch((error) => {
console.error('开启对讲失败:', error)
this.updateAudioDebug('开启对讲失败')
this.$message && this.$message.error('开启对讲失败')
})
},
stopSpeaking: function() {
if (!this.isSpeaking) return
console.log('停止对讲')
this.updateAudioDebug('正在关闭对讲...')
var videoComp = this.getVideoComp()
if (videoComp) {
videoComp.stopVoiceTalk().then(() => {
console.log('对讲关闭成功')
this.updateAudioDebug('对讲已关闭')
this.isSpeaking = false
}).catch((error) => {
console.error('关闭对讲失败:', error)
this.updateAudioDebug('关闭对讲失败')
this.isSpeaking = false
})
} else {
this.isSpeaking = false
}
},
onAudioChannelChange: function() {
var newChannelId = this.selectedAudioChannelId
if (!newChannelId) return
console.log('切换对讲通道:', newChannelId)
},
// ==================== 截图 ====================
handleScreenshot: function() {
var videoComp = this.getVideoComp()
if (!videoComp) {
this.$message && this.$message.error('视频组件未就绪')
return
}
videoComp.capturePic().then(function(result) {
var msg = result.name ? '截图成功: ' + result.name : '截图成功'
if (result.path) {
msg += ' (保存路径: ' + result.path + ')'
}
console.log(msg)
if (this.$message) this.$message.success('截图已保存至桌面'+this.getCurrentDateFolder() +'文件夹')
}.bind(this)).catch(function(error) {
console.error('截图失败:', error)
if (this.$message) this.$message.error('截图失败')
}.bind(this))
},
// ==================== 录像 ====================
// 获取当前年月日字符串
getCurrentDateFolder: function() {
var now = new Date()
var year = now.getFullYear()
var month = String(now.getMonth() + 1).padStart(2, '0')
var day = String(now.getDate()).padStart(2, '0')
return year + '-' + month + '-' + day
},
toggleRecording: function() {
var videoComp = this.getVideoComp()
var self = this
if (!videoComp) return
if (this.isRecording) {
videoComp.stopRecord().then(function(result) {
self.isRecording = false
if (self.recordingInterval) clearInterval(self.recordingInterval)
self.recordingTime = '00:00:00'
var msg = result.path ? '录像已保存到: ' + self.getCurrentDateFolder() : '录像已保存'
console.log(msg)
if (self.$message) self.$message.success('录像已保存至桌面'+self.getCurrentDateFolder() +'文件夹')
}).catch(function(error) {
console.error('停止录像失败:', error)
if (self.$message) self.$message.error('停止录像失败')
})
} else {
videoComp.startRecord().then(function() {
self.isRecording = true
self.startRecordingTimer()
if (self.$message) self.$message.success('开始录像')
}).catch(function(error) {
console.error('开始录像失败:', error)
if (self.$message) self.$message.error('开始录像失败')
})
}
},
startRecordingTimer: function() {
var seconds = 0
var self = this
this.recordingInterval = setInterval(function() {
seconds++
var hrs = String(Math.floor(seconds / 3600)).padStart(2, '0')
var mins = String(Math.floor((seconds % 3600) / 60)).padStart(2, '0')
var secs = String(seconds % 60).padStart(2, '0')
self.recordingTime = hrs + ':' + mins + ':' + secs
}, 1000)
},
// ==================== 变倍控制 ====================
startZoomIn: function() {
var videoComp = this.getVideoComp()
if (videoComp) videoComp.startZoomIn()
},
startZoomOut: function() {
var videoComp = this.getVideoComp()
if (videoComp) videoComp.startZoomOut()
},
stopZoom: function() {
var videoComp = this.getVideoComp()
if (videoComp) videoComp.stopPTZ()
},
// ==================== 声音控制 ====================
openSound: function() {
var videoComp = this.getVideoComp()
var self = this
if (!videoComp) {
this.$message && this.$message.warning('视频组件未就绪')
return
}
videoComp.openSound().then(function() {
self.isSoundOpen = true
console.log('打开声音成功')
if (self.$message) self.$message.success('打开声音成功')
}).catch(function(error) {
console.error('打开声音失败:', error)
if (self.$message) self.$message.error('打开声音失败')
})
},
closeSound: function() {
var videoComp = this.getVideoComp()
var self = this
if (!videoComp) {
this.$message && this.$message.warning('视频组件未就绪')
return
}
videoComp.closeSound().then(function() {
self.isSoundOpen = false
console.log('关闭声音成功')
if (self.$message) self.$message.success('关闭声音成功')
}).catch(function(error) {
console.error('关闭声音失败:', error)
if (self.$message) self.$message.error('关闭声音失败')
})
},
toggleSound: function() {
if (this.isSoundOpen) {
this.closeSound()
} else {
this.openSound()
}
},
// ==================== 回调事件 ====================
onPlayStart: function() {
this.isPlaying = true
this.controlReady = true
this.controlLoading = false
console.log('视频开始播放,控制就绪')
// this.loadSpeakCapabilities()
},
onVideoError: function(error) {
console.error('视频错误:', error)
this.controlReady = false
this.controlLoading = false
if (error && typeof error === 'string' && error.indexOf('插件插入失败') !== -1) {
this.pluginNotInstalledVisible = true
} else if (error && typeof error === 'string') {
this.$message && this.$message.error('视频错误: ' + error)
}
},
onLoginSuccess: function() {
console.log('设备登录成功')
},
// ==================== 全屏 ====================
toggleFullscreen: function() {
var container = this.$refs.modalContainer
var self = this
if (!document.fullscreenElement) {
container.requestFullscreen().then(function() {
self.isFullscreen = true
}).catch(function() {
self.isFullscreen = true
})
} else {
document.exitFullscreen().then(function() {
self.isFullscreen = false
}).catch(function() {
self.isFullscreen = false
})
}
},
// ==================== 关闭和清理 ====================
handleClose: function() {
this.cleanupAll()
this.$emit('close')
},
cleanupAll: function() {
if (this.isSpeaking) {
var videoComp = this.getVideoComp()
if (videoComp) {
videoComp.stopVoiceTalk().catch(function() {})
}
this.isSpeaking = false
}
if (this.isRecording) {
var videoComp = this.getVideoComp()
if (videoComp) {
videoComp.stopRecord().catch(function() {})
}
}
if (this.recordingInterval) clearInterval(this.recordingInterval)
if (this.isSoundOpen) {
var videoComp = this.getVideoComp()
if (videoComp) {
videoComp.closeSound().catch(function() {})
}
}
this.isRecording = false
this.controlReady = false
this.controlLoading = false
this.audioChannels = []
this.selectedAudioChannelId = null
this.speakCapable = false
this.isSpeaking = false
this.audioDebugInfo = ''
this.isPlaying = false
this.isSoundOpen = false
this.pluginNotInstalledVisible = false
this.installCompleteVisible = false
}
}
}
</script>
<style lang="less" scoped>
@primary-color: #00d4ff;
@success-color: #52c41a;
@danger-color: #f87171;
@border-color: rgba(13, 124, 149, 0.4);
@text-primary: #ffffff;
@text-secondary: #a5b8d0;
@text-dim: #6a8aa0;
.ai-monitor-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
}
.modal-container {
position: relative;
width: 56vw;
height: 65vh;
background: linear-gradient(180deg, #0a1e3d 0%, #061223 100%);
border: 0.052vw solid @border-color;
border-radius: 0.625vw;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 0 1.563vw rgba(0, 212, 255, 0.2);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.833vw 1.042vw;
background: linear-gradient(180deg, rgba(0, 212, 255, 0.1) 0%, transparent 100%);
border-bottom: 0.052vw solid @border-color;
flex-shrink: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 0.833vw;
}
.status-tag {
display: flex;
align-items: center;
gap: 0.3125vw;
padding: 0.2vw 0.625vw;
background: rgba(82, 196, 26, 0.2);
border: 0.052vw solid rgba(82, 196, 26, 0.4);
border-radius: 1.042vw;
font-size: 0.625vw;
color: @success-color;
}
.pulse-dot {
width: 0.417vw;
height: 0.417vw;
border-radius: 50%;
background: @success-color;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.title-group {
display: flex;
align-items: center;
gap: 0.521vw;
}
.camera-icon {
font-size: 1vw;
color: @primary-color;
}
.title-text {
display: flex;
flex-direction: column;
}
.main-title {
font-size: 0.833vw;
font-weight: 600;
color: #ffffff;
display: flex;
align-items: center;
gap: 0.4vw;
}
.sub-title {
font-size: 0.625vw;
color: @text-secondary;
}
.header-right {
display: flex;
align-items: center;
gap: 0.417vw;
}
.header-btn {
width: 1.875vw;
height: 1.875vw;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.05);
border: 0.052vw solid @border-color;
border-radius: 0.417vw;
cursor: pointer;
transition: all 0.2s ease;
.icon {
font-size: 1vw;
color: @text-secondary;
}
&:hover {
background: rgba(0, 212, 255, 0.2);
border-color: @primary-color;
.icon { color: @primary-color; }
}
}
.close-btn:hover {
background: rgba(248, 113, 113, 0.2);
border-color: @danger-color;
.icon { color: @danger-color; }
}
.modal-content {
flex: 1;
display: flex;
min-height: 0;
position: relative;
gap: 0.052vw;
padding: 0.052vw;
}
.video-left {
flex: 1;
min-width: 40vw;
display: flex;
flex-direction: column;
}
.video-wrapper {
flex: 1;
background: #1a1a2e;
border-radius: 0.3125vw;
overflow: hidden;
position: relative;
}
.plugin-container {
width: 100%;
height: 100%;
:deep(object) {
width: 100%;
height: 100%;
}
}
.video-control-bar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.521vw 0.625vw;
background: rgba(0, 0, 0, 0.7);
border-radius: 0.3125vw;
gap: 0.625vw;
}
.control-left {
display: flex;
align-items: center;
gap: 0.625vw;
flex-wrap: wrap;
}
.recording-indicator {
display: flex;
align-items: center;
gap: 0.3125vw;
padding: 0.208vw 0.521vw;
background: rgba(248, 113, 113, 0.9);
border-radius: 0.208vw;
font-size: 0.625vw;
color: white;
}
.rec-dot {
width: 0.417vw;
height: 0.417vw;
border-radius: 50%;
background: white;
animation: pulse 1s ease-in-out infinite;
}
.control-status {
display: flex;
align-items: center;
gap: 0.3125vw;
padding: 0.208vw 0.521vw;
background: rgba(0, 212, 255, 0.3);
border-radius: 0.208vw;
font-size: 0.625vw;
color: @primary-color;
}
.audio-debug {
display: flex;
align-items: center;
gap: 0.3125vw;
padding: 0.208vw 0.521vw;
background: rgba(0, 0, 0, 0.7);
border-radius: 0.208vw;
font-size: 0.625vw;
color: #ffaa00;
}
.debug-text {
font-family: monospace;
}
.control-right {
flex-shrink: 0;
}
.video-controls {
display: flex;
gap: 0.417vw;
}
.control-btn {
min-width: 2.5vw;
padding: 0.313vw 0.521vw;
display: flex;
align-items: center;
justify-content: center;
gap: 0.208vw;
background: rgba(255, 255, 255, 0.08);
border: 0.052vw solid @border-color;
border-radius: 0.3125vw;
cursor: pointer;
color: @text-primary;
font-size: 0.625vw;
transition: all 0.2s ease;
&:hover:not(:disabled) {
background: rgba(0, 212, 255, 0.2);
border-color: @primary-color;
color: @primary-color;
}
&:active:not(:disabled) {
transform: scale(0.95);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
span {
font-size: 0.625vw;
}
}
.sidebar-right {
width: 14.583vw;
background: rgba(11, 30, 61, 0.5);
border-left: 0.052vw solid @border-color;
display: flex;
flex-direction: column;
flex-shrink: 0;
border-radius: 0.3125vw;
overflow: hidden;
}
.sidebar-section {
padding: 0.833vw;
border-bottom: 0.052vw solid @border-color;
&:last-child { border-bottom: none; }
}
.section-header {
display: flex;
align-items: center;
gap: 0.417vw;
margin-bottom: 0.833vw;
font-size: 0.729vw;
font-weight: 500;
color: @text-primary;
}
.section-icon {
font-size: 0.8vw;
color: @primary-color;
}
.intercom-section {
flex-shrink: 0;
}
.intercom-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.833vw;
}
.audio-channel-select {
width: 100%;
padding: 0.417vw;
background: rgba(255, 255, 255, 0.08);
border: 0.052vw solid @border-color;
border-radius: 0.3125vw;
color: @text-primary;
font-size: 0.625vw;
option {
background: #0a1e3d;
}
}
.push-to-talk-btn {
width: 100%;
height: 2.333vw;
display: flex;
align-items: center;
justify-content: center;
gap: 0.208vw;
background: linear-gradient(180deg, #ff8c00 0%, #ff6600 100%);
border: none;
border-radius: 0.625vw;
cursor: pointer;
transition: all 0.1s ease;
user-select: none;
&:hover:not(:disabled) {
transform: scale(1.02);
}
&:active:not(:disabled), &.recording {
transform: scale(0.98);
background: linear-gradient(180deg, #ff6600 0%, #ff4400 100%);
}
&:disabled {
background: linear-gradient(180deg, #666 0%, #444 100%);
cursor: not-allowed;
transform: none;
}
}
.mic-icon {
font-size: 1vw;
color: white;
}
.push-to-talk-btn .btn-text {
font-size: 0.729vw;
font-weight: 600;
color: white;
}
.device-status {
display: flex;
align-items: center;
gap: 0.417vw;
font-size: 0.625vw;
color: @text-secondary;
}
.status-indicator {
width: 0.417vw;
height: 0.417vw;
border-radius: 50%;
background: @text-dim;
&.ready {
background: @success-color;
box-shadow: 0 0 0.417vw rgba(82, 196, 26, 0.5);
}
}
.device-section {
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.11vw;
}
&::-webkit-scrollbar-track {
background: rgba(13, 124, 149, 0.1);
}
&::-webkit-scrollbar-thumb {
background: @border-color;
border-radius: 0.052vw;
}
}
.device-info-list {
display: flex;
flex-direction: column;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.521vw 0.625vw;
background: rgba(255, 255, 255, 0.03);
}
.info-label {
font-size: 0.625vw;
color: @text-dim;
}
.info-value {
font-size: 0.625vw;
color: @text-primary;
&.device-id {
color: @primary-color;
font-weight: 600;
}
&.ready-text {
color: @success-color;
}
}
</style>
以上包含了截图、录像、变倍+-、语音监听、语音对讲功能,如果需要其他功能,可以去海康威视官网查看文档
RTSP实时视频流,生产环境中只需要服务器端运行一个程序,开发环境可以在本地运行一个程序,前端只需要拼接地址。具体可以看这个大佬的文章,我就懒得写了。vue2使用rtsp视频流接入海康威视摄像头(纯前端)-CSDN博客
更多推荐

所有评论(0)