Refactor microphone selection handling in voice components

- Rename `setSelectedDeviceId` to `selectDevice` in `DebugVoicePanel` and `VoiceSessionControls` for clarity and consistency.
- Update `useVoicePreview` hook to implement the `selectDevice` function, enabling dynamic microphone switching during voice sessions.
- Enhance device selection logic to support real-time audio track replacement without requiring session reconnection.
This commit is contained in:
Xin Wang
2026-06-14 21:02:03 +08:00
parent 90e3e8a0c0
commit 86d9acce78
2 changed files with 57 additions and 8 deletions

View File

@@ -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({
<Select
value={selectedDeviceId || "default"}
onValueChange={(value) =>
setSelectedDeviceId(value === "default" ? "" : value)
selectDevice(value === "default" ? "" : value)
}
disabled={recording}
>
<SelectTrigger
size="sm"
@@ -2044,7 +2043,7 @@ function VoiceSessionControls({
assistantId,
audioInputs,
selectedDeviceId,
setSelectedDeviceId,
selectDevice,
connect,
disconnect,
}: {
@@ -2054,7 +2053,7 @@ function VoiceSessionControls({
assistantId: string | null;
audioInputs: MediaDeviceInfo[];
selectedDeviceId: string;
setSelectedDeviceId: (deviceId: string) => void;
selectDevice: (deviceId: string) => void;
connect: () => Promise<void>;
disconnect: () => void;
}) {
@@ -2096,9 +2095,8 @@ function VoiceSessionControls({
<Select
value={selectedDeviceId || "default"}
onValueChange={(value) =>
setSelectedDeviceId(value === "default" ? "" : value)
selectDevice(value === "default" ? "" : value)
}
disabled={recording}
>
<SelectTrigger
size="sm"

View File

@@ -373,6 +373,56 @@ export function useVoicePreview(assistantId: string | null) {
}
}, [assistantId, fail, refreshDevices]);
// 选择麦克风:更新选择;若会话正在发送麦克风音频,则用 WebRTC replaceTrack
// 热切换轨道(无需重新协商),并把波形可视化重新接到新流。
// 未连接时仅记下选择,留待下次 connect 生效。
const selectDevice = useCallback(
async (deviceId: string) => {
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,