feat: 增加扬声器音量控制

This commit is contained in:
zhanghao 2026-07-22 14:01:38 +08:00
parent 974b4ed736
commit a3ca31a1e0
3 changed files with 167 additions and 24 deletions

View File

@ -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 = {

View File

@ -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')
}

View File

@ -55,15 +55,29 @@
<div class="robot-volume">
<span class="robot-volume__label">机器人麦克风音量</span>
<el-slider
v-model="robotVolume"
v-model="microphoneVolume"
class="robot-volume__slider"
:min="0"
:max="100"
:disabled="isVolumeLoading"
:disabled="isMicrophoneVolumeLoading"
:show-tooltip="true"
@change="updateRobotVolume"
@change="updateMicrophoneVolume"
/>
<span class="robot-volume__value">{{ robotVolume }}%</span>
<span class="robot-volume__value">{{ microphoneVolume }}%</span>
</div>
<div class="robot-volume">
<span class="robot-volume__label">机器人扬声器音量</span>
<el-slider
v-model="speakerVolume"
class="robot-volume__slider"
:min="0"
:max="100"
:disabled="isSpeakerVolumeLoading"
:show-tooltip="true"
@change="updateSpeakerVolume"
/>
<span class="robot-volume__value">{{ speakerVolume }}%</span>
</div>
<section class="status-card">
@ -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)
</script>