Enhance audio and video device selection in MobileCallPage
- Refactor `CallDeviceSelect` to `CallAudioDeviceSelect`, allowing simultaneous selection of microphone and output devices. - Introduce dropdown menus for selecting audio input and output devices, improving user experience. - Update `useVoicePreview` hook to manage audio output devices, including support for selecting and applying output devices. - Modify `MobileCallPage` to integrate new device selection components, ensuring seamless audio and video call functionality.
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Camera, Loader2, Mic, Phone, PhoneOff, Video } from "lucide-react";
|
||||
import { Camera, Headphones, Loader2, Mic, Phone, PhoneOff, Video } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -13,22 +22,99 @@ import {
|
||||
import { useCameraPreview } from "@/hooks/use-camera-preview";
|
||||
import { useVoicePreview } from "@/hooks/use-voice-preview";
|
||||
|
||||
function CallDeviceSelect({
|
||||
kind,
|
||||
function CallAudioDeviceSelect({
|
||||
micValue,
|
||||
micDevices,
|
||||
onMicSelect,
|
||||
outputValue,
|
||||
outputDevices,
|
||||
onOutputSelect,
|
||||
outputSupported,
|
||||
disabled,
|
||||
}: {
|
||||
micValue: string;
|
||||
micDevices: MediaDeviceInfo[];
|
||||
onMicSelect: (deviceId: string) => void | Promise<void>;
|
||||
outputValue: string;
|
||||
outputDevices: MediaDeviceInfo[];
|
||||
onOutputSelect: (deviceId: string) => void;
|
||||
outputSupported: boolean;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="选择麦克风和耳机"
|
||||
title="选择麦克风和耳机"
|
||||
className="flex size-12 items-center justify-center rounded-full border border-white/15 bg-black/45 text-white shadow-xl backdrop-blur-md transition-colors hover:bg-black/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40 disabled:pointer-events-none disabled:opacity-50 min-[380px]:size-14"
|
||||
>
|
||||
<Mic className="size-5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="max-w-[min(20rem,calc(100vw-2rem))] min-w-56"
|
||||
>
|
||||
<DropdownMenuLabel>麦克风</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={micValue || "default"}
|
||||
onValueChange={(next) =>
|
||||
void onMicSelect(next === "default" ? "" : next)
|
||||
}
|
||||
>
|
||||
<DropdownMenuRadioItem value="default">默认麦克风</DropdownMenuRadioItem>
|
||||
{micDevices.map((device, index) => (
|
||||
<DropdownMenuRadioItem key={device.deviceId} value={device.deviceId}>
|
||||
{device.label || `麦克风 ${index + 1}`}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
{outputSupported && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="flex items-center gap-2">
|
||||
<Headphones className="size-3.5" />
|
||||
耳机 / 扬声器
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={outputValue || "default"}
|
||||
onValueChange={(next) =>
|
||||
onOutputSelect(next === "default" ? "" : next)
|
||||
}
|
||||
>
|
||||
<DropdownMenuRadioItem value="default">
|
||||
默认输出设备
|
||||
</DropdownMenuRadioItem>
|
||||
{outputDevices.map((device, index) => (
|
||||
<DropdownMenuRadioItem
|
||||
key={device.deviceId}
|
||||
value={device.deviceId}
|
||||
>
|
||||
{device.label || `输出设备 ${index + 1}`}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function CallCameraDeviceSelect({
|
||||
value,
|
||||
devices,
|
||||
onSelect,
|
||||
disabled,
|
||||
}: {
|
||||
kind: "microphone" | "camera";
|
||||
value: string;
|
||||
devices: MediaDeviceInfo[];
|
||||
onSelect: (deviceId: string) => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const isMic = kind === "microphone";
|
||||
const label = isMic ? "选择麦克风" : "选择摄像头";
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value || "default"}
|
||||
@@ -39,30 +125,24 @@ function CallDeviceSelect({
|
||||
>
|
||||
<SelectTrigger
|
||||
size="default"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
aria-label="选择摄像头"
|
||||
title="选择摄像头"
|
||||
className="w-12 justify-center overflow-hidden rounded-full border-white/15 bg-black/45 p-0 text-white shadow-xl backdrop-blur-md hover:bg-black/60 focus-visible:ring-white/40 data-[size=default]:h-12 min-[380px]:w-14 min-[380px]:data-[size=default]:h-14 [&>svg:last-child]:hidden"
|
||||
>
|
||||
<span className="sr-only">
|
||||
<SelectValue />
|
||||
</span>
|
||||
{isMic ? (
|
||||
<Mic className="size-5" />
|
||||
) : (
|
||||
<Camera className="size-5" />
|
||||
)}
|
||||
<Camera className="size-5" />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
side="top"
|
||||
align={isMic ? "start" : "end"}
|
||||
align="end"
|
||||
className="max-w-[min(20rem,calc(100vw-2rem))]"
|
||||
>
|
||||
<SelectItem value="default">
|
||||
{isMic ? "默认麦克风" : "默认摄像头"}
|
||||
</SelectItem>
|
||||
<SelectItem value="default">默认摄像头</SelectItem>
|
||||
{devices.map((device, index) => (
|
||||
<SelectItem key={device.deviceId} value={device.deviceId}>
|
||||
{device.label || `${isMic ? "麦克风" : "摄像头"} ${index + 1}`}
|
||||
{device.label || `摄像头 ${index + 1}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -77,8 +157,12 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
status,
|
||||
error: previewError,
|
||||
audioInputs,
|
||||
audioOutputs,
|
||||
selectedDeviceId,
|
||||
selectedOutputDeviceId,
|
||||
selectDevice,
|
||||
selectOutputDevice,
|
||||
supportsOutputSelection,
|
||||
connect,
|
||||
replaceVideoStream,
|
||||
disconnect,
|
||||
@@ -203,11 +287,14 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
|
||||
{inCall && (
|
||||
<div className="absolute bottom-[max(1.25rem,env(safe-area-inset-bottom))] left-1/2 flex -translate-x-1/2 items-center gap-3 min-[380px]:gap-5 landscape:bottom-[max(1rem,env(safe-area-inset-bottom))]">
|
||||
<CallDeviceSelect
|
||||
kind="microphone"
|
||||
value={selectedDeviceId}
|
||||
devices={audioInputs}
|
||||
onSelect={selectDevice}
|
||||
<CallAudioDeviceSelect
|
||||
micValue={selectedDeviceId}
|
||||
micDevices={audioInputs}
|
||||
onMicSelect={selectDevice}
|
||||
outputValue={selectedOutputDeviceId}
|
||||
outputDevices={audioOutputs}
|
||||
onOutputSelect={selectOutputDevice}
|
||||
outputSupported={supportsOutputSelection}
|
||||
disabled={connecting}
|
||||
/>
|
||||
<button
|
||||
@@ -219,8 +306,7 @@ export function MobileCallPage({ assistantId }: { assistantId: string }) {
|
||||
>
|
||||
<PhoneOff className="size-6 min-[380px]:size-7" />
|
||||
</button>
|
||||
<CallDeviceSelect
|
||||
kind="camera"
|
||||
<CallCameraDeviceSelect
|
||||
value={cameraDeviceId}
|
||||
devices={cameraDevices}
|
||||
onSelect={selectCamera}
|
||||
|
||||
Reference in New Issue
Block a user