From a3ca31a1e0e46c315846df078b4d204bbcc87986 Mon Sep 17 00:00:00 2001
From: zhanghao <774378400@qq.com>
Date: Wed, 22 Jul 2026 14:01:38 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=89=AC=E5=A3=B0?=
=?UTF-8?q?=E5=99=A8=E9=9F=B3=E9=87=8F=E6=8E=A7=E5=88=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
server/index.js | 67 ++++++++++++++
src/api/audio.js | 8 ++
src/views/camera/VoiceConversation.vue | 116 ++++++++++++++++++++-----
3 files changed, 167 insertions(+), 24 deletions(-)
diff --git a/server/index.js b/server/index.js
index c150f48..cf9b0fc 100644
--- a/server/index.js
+++ b/server/index.js
@@ -8,6 +8,7 @@ import {
createAGVGrpcClient,
createARMGrpcClient,
createMicrophoneGrpcClient,
+ createSpeakerGrpcClient,
__dirname
} from './grpcClient.js';
import { attachAudioWebSocket } from './audioWebSocket.js';
@@ -83,6 +84,72 @@ app.post('/api/audio/microphone/volume/set', (req, res) => {
})
})
+// 获取机器人扬声器音量(0-100)。
+app.post('/api/audio/speaker/volume/get', (req, res) => {
+ const { ip, deviceId } = req.body
+ if (!ip || !deviceId) {
+ res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
+ return
+ }
+
+ const grpcClient = createSpeakerGrpcClient(ip)
+ grpcClient.getVolume(
+ { header: { device_id: deviceId } },
+ { deadline: Date.now() + 5000 },
+ (error, feedback) => {
+ grpcClient.close()
+ if (error) {
+ console.error('[audio] get speaker volume failed:', error)
+ res.status(502).json({ code: 502, message: error.details || error.message || '获取扬声器音量失败' })
+ return
+ }
+ if (feedback?.header?.success === false) {
+ res.status(502).json({
+ code: 502,
+ message: feedback.header.error_message || '机器人拒绝获取扬声器音量'
+ })
+ return
+ }
+ res.json({ code: 200, data: { volume: feedback?.volume ?? 0 } })
+ }
+ )
+})
+
+// 设置机器人扬声器音量(0-100)。
+app.post('/api/audio/speaker/volume/set', (req, res) => {
+ const { ip, deviceId } = req.body
+ const volume = Number(req.body.volume)
+ if (!ip || !deviceId) {
+ res.status(400).json({ code: 400, message: '机器人地址和扬声器设备 ID 不能为空' })
+ return
+ }
+ if (!Number.isInteger(volume) || volume < 0 || volume > 100) {
+ res.status(400).json({ code: 400, message: '音量必须是 0 到 100 的整数' })
+ return
+ }
+
+ const grpcClient = createSpeakerGrpcClient(ip)
+ grpcClient.setVolume({
+ header: { device_id: deviceId },
+ volume
+ }, { deadline: Date.now() + 5000 }, (error, feedback) => {
+ grpcClient.close()
+ if (error) {
+ console.error('[audio] set speaker volume failed:', error)
+ res.status(502).json({ code: 502, message: error.details || error.message || '设置扬声器音量失败' })
+ return
+ }
+ if (feedback?.header?.success === false) {
+ res.status(502).json({
+ code: 502,
+ message: feedback.header.error_message || '机器人拒绝设置扬声器音量'
+ })
+ return
+ }
+ res.json({ code: 200, data: { volume } })
+ })
+})
+
// 获取agv运行时状态
app.post('/api/agv/getRuntimeState', async (req, res) => {
const request = {
diff --git a/src/api/audio.js b/src/api/audio.js
index 905d8cd..18955ff 100644
--- a/src/api/audio.js
+++ b/src/api/audio.js
@@ -7,3 +7,11 @@ export function getMicrophoneVolume(params) {
export function setMicrophoneVolume(params) {
return request('/api/audio/microphone/volume/set', params, 'POST')
}
+
+export function getSpeakerVolume(params) {
+ return request('/api/audio/speaker/volume/get', params, 'POST')
+}
+
+export function setSpeakerVolume(params) {
+ return request('/api/audio/speaker/volume/set', params, 'POST')
+}
diff --git a/src/views/camera/VoiceConversation.vue b/src/views/camera/VoiceConversation.vue
index 022c7cb..cdad6d3 100644
--- a/src/views/camera/VoiceConversation.vue
+++ b/src/views/camera/VoiceConversation.vue
@@ -55,15 +55,29 @@
机器人麦克风音量
- {{ robotVolume }}%
+ {{ microphoneVolume }}%
+
+
+
+ 机器人扬声器音量
+
+ {{ speakerVolume }}%
@@ -87,7 +101,12 @@
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { ElMessage, ElNotification } from 'element-plus'
import { useRobotStore } from '@/stores/robot'
-import { getMicrophoneVolume, setMicrophoneVolume } from '@/api/audio'
+import {
+ getMicrophoneVolume,
+ getSpeakerVolume,
+ setMicrophoneVolume,
+ setSpeakerVolume
+} from '@/api/audio'
// 机器人实际音频流为 48kHz,因此浏览器上传也统一为 48kHz。
const TARGET_SAMPLE_RATE = 48000
@@ -101,9 +120,12 @@ const isStarting = ref(false)
const isMuted = ref(false)
const connectionStatus = ref('idle')
const errorMessage = ref('')
-const robotVolume = ref(50)
-const isVolumeLoading = ref(false)
-let confirmedRobotVolume = 50
+const microphoneVolume = ref(50)
+const speakerVolume = ref(50)
+const isMicrophoneVolumeLoading = ref(false)
+const isSpeakerVolumeLoading = ref(false)
+let confirmedMicrophoneVolume = 50
+let confirmedSpeakerVolume = 50
// 本次通话占用的浏览器资源。
const localStream = ref(null)
@@ -128,46 +150,89 @@ const statusMeta = computed(() => {
return statusMap[connectionStatus.value] || statusMap.idle
})
-function volumeRequestParams() {
+function microphoneVolumeRequestParams() {
return {
ip: robotStore.ip,
deviceId: robotStore.micDeviceId
}
}
-async function loadRobotVolume({ silent = false } = {}) {
+async function loadMicrophoneVolume({ silent = false } = {}) {
if (!robotStore.ip || !robotStore.micDeviceId) return
- isVolumeLoading.value = true
+ isMicrophoneVolumeLoading.value = true
try {
- const response = await getMicrophoneVolume(volumeRequestParams())
+ const response = await getMicrophoneVolume(microphoneVolumeRequestParams())
if (response.code !== 200) throw new Error(response.message || '获取机器人音量失败')
const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0))
- robotVolume.value = volume
- confirmedRobotVolume = volume
+ microphoneVolume.value = volume
+ confirmedMicrophoneVolume = volume
} catch (error) {
if (!silent) ElMessage.error(error.message || '获取机器人音量失败')
} finally {
- isVolumeLoading.value = false
+ isMicrophoneVolumeLoading.value = false
}
}
-async function updateRobotVolume(volume) {
- const previousVolume = confirmedRobotVolume
- isVolumeLoading.value = true
+async function updateMicrophoneVolume(volume) {
+ const previousVolume = confirmedMicrophoneVolume
+ isMicrophoneVolumeLoading.value = true
try {
const response = await setMicrophoneVolume({
- ...volumeRequestParams(),
+ ...microphoneVolumeRequestParams(),
volume: Math.round(volume)
})
if (response.code !== 200) throw new Error(response.message || '设置机器人音量失败')
- robotVolume.value = response.data.volume
- confirmedRobotVolume = response.data.volume
+ microphoneVolume.value = response.data.volume
+ confirmedMicrophoneVolume = response.data.volume
ElMessage.success(`机器人麦克风音量已设置为 ${response.data.volume}%`)
} catch (error) {
- robotVolume.value = previousVolume
+ microphoneVolume.value = previousVolume
ElMessage.error(error.message || '设置机器人音量失败')
} finally {
- isVolumeLoading.value = false
+ isMicrophoneVolumeLoading.value = false
+ }
+}
+
+function speakerVolumeRequestParams() {
+ return {
+ ip: robotStore.ip,
+ deviceId: robotStore.spkDeviceId
+ }
+}
+
+async function loadSpeakerVolume({ silent = false } = {}) {
+ if (!robotStore.ip || !robotStore.spkDeviceId) return
+ isSpeakerVolumeLoading.value = true
+ try {
+ const response = await getSpeakerVolume(speakerVolumeRequestParams())
+ if (response.code !== 200) throw new Error(response.message || '获取扬声器音量失败')
+ const volume = Math.max(0, Math.min(100, Number(response.data?.volume) || 0))
+ speakerVolume.value = volume
+ confirmedSpeakerVolume = volume
+ } catch (error) {
+ if (!silent) ElMessage.error(error.message || '获取扬声器音量失败')
+ } finally {
+ isSpeakerVolumeLoading.value = false
+ }
+}
+
+async function updateSpeakerVolume(volume) {
+ const previousVolume = confirmedSpeakerVolume
+ isSpeakerVolumeLoading.value = true
+ try {
+ const response = await setSpeakerVolume({
+ ...speakerVolumeRequestParams(),
+ volume: Math.round(volume)
+ })
+ if (response.code !== 200) throw new Error(response.message || '设置扬声器音量失败')
+ speakerVolume.value = response.data.volume
+ confirmedSpeakerVolume = response.data.volume
+ ElMessage.success(`机器人扬声器音量已设置为 ${response.data.volume}%`)
+ } catch (error) {
+ speakerVolume.value = previousVolume
+ ElMessage.error(error.message || '设置扬声器音量失败')
+ } finally {
+ isSpeakerVolumeLoading.value = false
}
}
@@ -472,7 +537,10 @@ function endCall() {
}
// 离开页面时释放麦克风、AudioWorklet 和 WebSocket。
-onMounted(() => loadRobotVolume())
+onMounted(() => {
+ loadMicrophoneVolume()
+ loadSpeakerVolume()
+})
onBeforeUnmount(cleanupResources)