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:
@@ -1838,7 +1838,7 @@ function DebugVoicePanel({
|
|||||||
messages,
|
messages,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
selectedDeviceId,
|
selectedDeviceId,
|
||||||
setSelectedDeviceId,
|
selectDevice,
|
||||||
sendText,
|
sendText,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
@@ -1868,7 +1868,7 @@ function DebugVoicePanel({
|
|||||||
assistantId={assistantId}
|
assistantId={assistantId}
|
||||||
audioInputs={audioInputs}
|
audioInputs={audioInputs}
|
||||||
selectedDeviceId={selectedDeviceId}
|
selectedDeviceId={selectedDeviceId}
|
||||||
setSelectedDeviceId={setSelectedDeviceId}
|
selectDevice={selectDevice}
|
||||||
connect={connect}
|
connect={connect}
|
||||||
disconnect={disconnect}
|
disconnect={disconnect}
|
||||||
/>
|
/>
|
||||||
@@ -1941,9 +1941,8 @@ function DebugVoicePanel({
|
|||||||
<Select
|
<Select
|
||||||
value={selectedDeviceId || "default"}
|
value={selectedDeviceId || "default"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setSelectedDeviceId(value === "default" ? "" : value)
|
selectDevice(value === "default" ? "" : value)
|
||||||
}
|
}
|
||||||
disabled={recording}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -2044,7 +2043,7 @@ function VoiceSessionControls({
|
|||||||
assistantId,
|
assistantId,
|
||||||
audioInputs,
|
audioInputs,
|
||||||
selectedDeviceId,
|
selectedDeviceId,
|
||||||
setSelectedDeviceId,
|
selectDevice,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
}: {
|
}: {
|
||||||
@@ -2054,7 +2053,7 @@ function VoiceSessionControls({
|
|||||||
assistantId: string | null;
|
assistantId: string | null;
|
||||||
audioInputs: MediaDeviceInfo[];
|
audioInputs: MediaDeviceInfo[];
|
||||||
selectedDeviceId: string;
|
selectedDeviceId: string;
|
||||||
setSelectedDeviceId: (deviceId: string) => void;
|
selectDevice: (deviceId: string) => void;
|
||||||
connect: () => Promise<void>;
|
connect: () => Promise<void>;
|
||||||
disconnect: () => void;
|
disconnect: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -2096,9 +2095,8 @@ function VoiceSessionControls({
|
|||||||
<Select
|
<Select
|
||||||
value={selectedDeviceId || "default"}
|
value={selectedDeviceId || "default"}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
setSelectedDeviceId(value === "default" ? "" : value)
|
selectDevice(value === "default" ? "" : value)
|
||||||
}
|
}
|
||||||
disabled={recording}
|
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -373,6 +373,56 @@ export function useVoicePreview(assistantId: string | null) {
|
|||||||
}
|
}
|
||||||
}, [assistantId, fail, refreshDevices]);
|
}, [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。
|
// 成功返回 true;通道未就绪(未开始对话/连接中)返回 false。
|
||||||
const sendText = useCallback((text: string): boolean => {
|
const sendText = useCallback((text: string): boolean => {
|
||||||
@@ -396,6 +446,7 @@ export function useVoicePreview(assistantId: string | null) {
|
|||||||
audioInputs,
|
audioInputs,
|
||||||
selectedDeviceId,
|
selectedDeviceId,
|
||||||
setSelectedDeviceId,
|
setSelectedDeviceId,
|
||||||
|
selectDevice,
|
||||||
sendText,
|
sendText,
|
||||||
connect,
|
connect,
|
||||||
disconnect,
|
disconnect,
|
||||||
|
|||||||
Reference in New Issue
Block a user