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)