diff --git a/frontend/src/components/pages/AssistantPage.tsx b/frontend/src/components/pages/AssistantPage.tsx index 49a9eca..39dbde7 100644 --- a/frontend/src/components/pages/AssistantPage.tsx +++ b/frontend/src/components/pages/AssistantPage.tsx @@ -1838,7 +1838,7 @@ function DebugVoicePanel({ messages, audioInputs, selectedDeviceId, - setSelectedDeviceId, + selectDevice, sendText, connect, disconnect, @@ -1868,7 +1868,7 @@ function DebugVoicePanel({ assistantId={assistantId} audioInputs={audioInputs} selectedDeviceId={selectedDeviceId} - setSelectedDeviceId={setSelectedDeviceId} + selectDevice={selectDevice} connect={connect} disconnect={disconnect} /> @@ -1941,9 +1941,8 @@ function DebugVoicePanel({ - setSelectedDeviceId(value === "default" ? "" : value) + selectDevice(value === "default" ? "" : value) } - disabled={recording} > { + setSelectedDeviceId(deviceId); + selectedDeviceIdRef.current = deviceId; + + const pc = pcRef.current; + if (!pc) return; + // 只有本就在发送麦克风音频(存在 audio sender 轨道)时才热切换; + // 仅收听模式下加麦克风需重新协商,这里不处理,留到下次连接。 + const sender = pc + .getSenders() + .find((s) => s.track?.kind === "audio"); + if (!sender) return; + + try { + const audioConstraints: MediaTrackConstraints = { + echoCancellation: true, + noiseSuppression: true, + autoGainControl: true, + }; + if (deviceId) audioConstraints.deviceId = { exact: deviceId }; + const newStream = await navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + }); + // 切换期间可能已断开,丢弃刚拿到的流 + if (pcRef.current !== pc) { + newStream.getTracks().forEach((t) => t.stop()); + return; + } + const newTrack = newStream.getAudioTracks()[0]; + if (!newTrack) { + newStream.getTracks().forEach((t) => t.stop()); + return; + } + await sender.replaceTrack(newTrack); + // 旧轨道停掉,新流替换(波形/分析器随 localStream 变化自动重连) + localStreamRef.current?.getTracks().forEach((t) => t.stop()); + localStreamRef.current = newStream; + setLocalStream(newStream); + setMicWarning(null); + } catch (mediaError) { + setMicWarning(microphoneErrorMessage(mediaError)); + } + }, + [], + ); + // 发送文字消息:后端先打断当前播报,再按用户输入触发新回复。 // 成功返回 true;通道未就绪(未开始对话/连接中)返回 false。 const sendText = useCallback((text: string): boolean => { @@ -396,6 +446,7 @@ export function useVoicePreview(assistantId: string | null) { audioInputs, selectedDeviceId, setSelectedDeviceId, + selectDevice, sendText, connect, disconnect,