我来为您介绍在SpringBoot + Vue项目中实现海康摄像头喊话功能的完整方案:

一、技术架构
scss
前端(Vue) → 后端(SpringBoot) → 海康SDK/ISAPI → 海康摄像头
二、后端实现(SpringBoot)
1. 添加依赖
xml
<!-- 可选:WebSocket支持实时语音流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 海康SDK集成(基于HCNetSDK)
VoiceBroadcastService.java
java
@Service
public class VoiceBroadcastService {
// 加载海康SDK
static {
System.loadLibrary("hcnetsdk");
System.loadLibrary("PlayCtrl");
System.loadLibrary("HCNetSDK");
}
public boolean startBroadcast(String cameraIp, String text) {
HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
IntByReference loginHandle = new IntByReference(0);
// 1. 登录设备
HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V30();
loginHandle.setValue(hCNetSDK.NET_DVR_Login_V30(
cameraIp, (short)8000, "admin", "password",
deviceInfo, null
));
if (loginHandle.getValue() < 0) {
return false;
}
try {
// 2. 开启语音对讲
HCNetSDK.NET_DVR_VOICECOM_START voiceStart = new HCNetSDK.NET_DVR_VOICECOM_START();
voiceStart.dwSize = voiceStart.size();
voiceStart.dwVoiceChan = 1; // 通道号
voiceStart.byVoiceMode = 0; // 0-客户端发起
int voiceHandle = hCNetSDK.NET_DVR_StartVoiceCom_V30(
loginHandle.getValue(), voiceStart, null, null
);
if (voiceHandle < 0) {
return false;
}
// 3. 发送语音数据(这里需要音频输入)
// 实际实现需要从麦克风获取音频流
// 4. 停止对讲
hCNetSDK.NET_DVR_StopVoiceCom(voiceHandle);
return true;
} finally {
// 5. 注销登录
hCNetSDK.NET_DVR_Logout(loginHandle.getValue());
}
}
}
3. 基于ISAPI的文本转语音方案(推荐)
HikvisionISAPIService.java
ini
@Service
public class HikvisionISAPIService {
@Value("${hikvision.username}")
private String username;
@Value("${hikvision.password}")
private String password;
/**
* 文本转语音广播
*/
public boolean textToSpeech(String cameraIp, String text) {
String url = String.format("http://%s/ISAPI/System/Audio/channels/1/audioData", cameraIp);
try {
// 1. 构建语音数据(需要将文本转为G.711/G.726等格式)
byte[] audioData = convertTextToAudio(text);
// 2. 发送HTTP PUT请求
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(username, password);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
HttpEntity<byte[]> entity = new HttpEntity<>(audioData, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.PUT, entity, String.class
);
return response.getStatusCode() == HttpStatus.OK;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 获取音频通道信息
*/
public String getAudioChannels(String cameraIp) {
String url = String.format("http://%s/ISAPI/System/Audio/channels", cameraIp);
try {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setBasicAuth(username, password);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
url, HttpMethod.GET, entity, String.class
);
return response.getBody();
} catch (Exception e) {
return null;
}
}
}
4. WebSocket实现实时语音流
WebSocketConfig.java
typescript
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(voiceHandler(), "/voice")
.setAllowedOrigins("*");
}
@Bean
public WebSocketHandler voiceHandler() {
return new VoiceWebSocketHandler();
}
}
VoiceWebSocketHandler.java
scala
@Component
public class VoiceWebSocketHandler extends BinaryWebSocketHandler {
@Autowired
private VoiceBroadcastService voiceService;
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
// 接收前端发送的音频流,转发给摄像头
ByteBuffer payload = message.getPayload();
byte[] audioData = new byte[payload.remaining()];
payload.get(audioData);
// 这里实现将音频流发送给摄像头
voiceService.sendAudioToCamera(audioData);
}
}
三、前端实现(Vue3 + TypeScript)
1. 音频录制组件
VoiceBroadcast.vue
xml
<template>
<div class="voice-broadcast">
<!-- 文本喊话 -->
<div v-if="mode === 'text'">
<el-input
v-model="textMessage"
type="textarea"
placeholder="输入要喊话的内容"
:rows="4"
/>
<el-button @click="sendText" :loading="loading">
发送喊话
</el-button>
</div>
<!-- 实时语音 -->
<div v-else>
<el-button
@mousedown="startRecording"
@mouseup="stopRecording"
:disabled="recording"
type="primary"
size="large"
>
🎤 {{ recording ? '正在喊话...' : '按住说话' }}
</el-button>
<div v-if="recordingTime > 0" class="recording-indicator">
录音时长: {{ recordingTime }}秒
</div>
</div>
<!-- 模式切换 -->
<div class="mode-switch">
<el-radio-group v-model="mode" size="small">
<el-radio-button label="text">文本喊话</el-radio-button>
<el-radio-button label="voice">实时语音</el-radio-button>
</el-radio-group>
</div>
<!-- 设备选择 -->
<div class="device-select">
<el-select v-model="selectedCamera" placeholder="选择摄像头">
<el-option
v-for="camera in cameras"
:key="camera.id"
:label="camera.name"
:value="camera.ip"
/>
</el-select>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { ElMessage } from 'element-plus'
import { textToSpeech, startVoiceStream, stopVoiceStream } from '@/api/broadcast'
// 状态
const mode = ref<'text' | 'voice'>('text')
const textMessage = ref('')
const selectedCamera = ref('')
const cameras = ref<any[]>([])
const loading = ref(false)
const recording = ref(false)
const recordingTime = ref(0)
let recorder: MediaRecorder | null = null
let audioChunks: Blob[] = []
let timer: number | null = null
let ws: WebSocket | null = null
// 发送文本喊话
const sendText = async () => {
if (!textMessage.value.trim()) {
ElMessage.warning('请输入喊话内容')
return
}
if (!selectedCamera.value) {
ElMessage.warning('请选择摄像头')
return
}
loading.value = true
try {
const res = await textToSpeech(selectedCamera.value, textMessage.value)
if (res.success) {
ElMessage.success('喊话发送成功')
textMessage.value = ''
} else {
ElMessage.error('喊话失败')
}
} catch (error) {
ElMessage.error('发送失败')
} finally {
loading.value = false
}
}
// 开始录音
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 8000, // 8kHz适合语音
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
})
recorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus' // 或 'audio/ogg;codecs=opus'
})
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunks.push(event.data)
// 通过WebSocket发送音频数据
sendAudioData(event.data)
}
}
recorder.start(100) // 每100ms发送一次数据
recording.value = true
recordingTime.value = 0
// 计时器
timer = setInterval(() => {
recordingTime.value++
}, 1000)
} catch (error) {
ElMessage.error('无法访问麦克风')
}
}
// 停止录音
const stopRecording = () => {
if (recorder && recording.value) {
recorder.stop()
recorder.stream.getTracks().forEach(track => track.stop())
recording.value = false
if (timer) {
clearInterval(timer)
timer = null
}
// 关闭WebSocket连接
if (ws) {
ws.close()
ws = null
}
}
}
// 通过WebSocket发送音频数据
const sendAudioData = (audioBlob: Blob) => {
if (!ws) {
// 建立WebSocket连接
ws = new WebSocket(`ws://${location.host}/voice?cameraIp=${selectedCamera.value}`)
ws.onopen = () => {
console.log('WebSocket连接已建立')
}
ws.onerror = (error) => {
console.error('WebSocket错误:', error)
}
}
// 转换为ArrayBuffer发送
const reader = new FileReader()
reader.onload = () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(reader.result as ArrayBuffer)
}
}
reader.readAsArrayBuffer(audioBlob)
}
// 加载摄像头列表
const loadCameras = async () => {
// 这里调用API获取摄像头列表
cameras.value = [
{ id: 1, name: '大门摄像头', ip: '192.168.1.100' },
{ id: 2, name: '停车场摄像头', ip: '192.168.1.101' }
]
}
onMounted(() => {
loadCameras()
})
onUnmounted(() => {
if (recorder) {
recorder.stop()
}
if (ws) {
ws.close()
}
})
</script>
<style scoped>
.voice-broadcast {
padding: 20px;
max-width: 500px;
margin: 0 auto;
}
.recording-indicator {
margin-top: 10px;
color: #f56c6c;
font-weight: bold;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.mode-switch, .device-select {
margin-top: 20px;
}
</style>
2. API接口封装
broadcast.ts
typescript
import request from '@/utils/request'
// 文本转语音喊话
export const textToSpeech = (cameraIp: string, text: string) => {
return request.post('/api/broadcast/text-to-speech', {
cameraIp,
text
})
}
// 开始语音流
export const startVoiceStream = (cameraIp: string) => {
return request.post('/api/broadcast/voice/start', { cameraIp })
}
// 停止语音流
export const stopVoiceStream = (cameraIp: string) => {
return request.post('/api/broadcast/voice/stop', { cameraIp })
}
// 获取摄像头列表
export const getCameras = () => {
return request.get('/api/cameras')
}
四、配置说明
application.yml
yaml
hikvision:
default-username: admin
default-password: 123456
isapi-port: 80
audio:
format: G711 # 音频格式:G711, G726, AAC
sample-rate: 8000
五、音频格式转换工具类
AudioConverter.java
arduino
@Component
public class AudioConverter {
/**
* 将文本转为语音音频
* 需要集成TTS引擎,如讯飞、百度、阿里云等
*/
public byte[] textToAudio(String text, AudioFormat format) {
// 这里调用第三方TTS服务
// 1. 调用TTS API获取音频流
// 2. 转换为摄像头支持的格式(G.711/G.726)
// 3. 返回音频字节数组
return convertToG711(text);
}
/**
* PCM转G.711
*/
private byte[] convertToG711(byte[] pcmData) {
// 实现PCM到G.711的转换逻辑
// 可以使用Jave、FFmpeg等库
return pcmData;
}
}
六、安全注意事项
- 认证加密:使用HTTPS和WSS协议
- 权限控制:限制用户喊话权限
- 频率限制:防止恶意频繁喊话
- 日志记录:记录所有喊话操作
- 音频压缩:减少带宽占用
七、常见问题解决
- 编码格式问题:确保音频格式为摄像头支持的格式
- 网络延迟:使用UDP协议传输实时音频
- 兼容性问题:不同型号摄像头API可能有差异
- 防火墙:确保端口(8000, 554, 80)开放
这个方案提供了两种喊话方式:文本转语音和实时语音。文本转语音更简单稳定,实时语音体验更好但实现复杂度高。您可以根据实际需求选择合适的方案。