feat(room): выбор устройства аудиовывода в настройках

Третий селект в DeviceSettingsDialog (динамики/наушники/bluetooth) рядом
с уже существующими микрофоном и камерой — на useMediaDeviceSelect(kind:
'audiooutput'). Выбор персистится отдельно от usePersistentUserChoices
(LocalUserChoices LiveKit про аудиовыход не знает) и применяется заново
при следующем подключении через RoomOptions.audioOutput.

Поддержка определяется по наличию setSinkId в рантайме, не по User-Agent:
там, где браузер не умеет переключать вывод (iOS Safari), список скрыт,
вместо него — подсказка, что маршрутизация звука на стороне системы.
This commit is contained in:
2026-07-27 17:31:35 +03:00
parent 35a1455645
commit e07217657f
4 changed files with 94 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
import { X } from 'lucide-react'
import { useMediaDeviceSelect, usePersistentUserChoices } from '@livekit/components-react'
import { useToast } from '@/components/ui/ToastProvider'
import { isAudioOutputSelectable, saveAudioOutputDeviceId } from '@/lib/audioOutputDevice'
interface DeviceSettingsDialogProps {
onClose: () => void
@@ -12,12 +13,20 @@ function deviceLabel(device: MediaDeviceInfo, index: number, fallback: string):
}
/**
* Диалог «Настройки устройств» — два селекта
* Диалог «Настройки устройств» — три селекта
* на хуках `@livekit/components-react`: список устройств и переключение —
* целиком в `useMediaDeviceSelect` (сама подписана на
* `RoomEvent.MediaDevicesChanged`), персист выбора — в `usePersistentUserChoices`
* (localStorage, читается заново при следующем входе в комнату — см.
* `RoomPage.tsx`, `options` пропс `LiveKitRoom`).
* `RoomEvent.MediaDevicesChanged`), персист выбора микрофона/камеры — в
* `usePersistentUserChoices` (localStorage, читается заново при следующем
* входе в комнату — см. `RoomPage.tsx`, `options` пропс `LiveKitRoom`).
* Аудиовыход персистится отдельно (`lib/audioOutputDevice.ts`) —
* `LocalUserChoices` LiveKit про него не знает, а `RoomOptions.audioOutput`
* читает сохранённый `deviceId` при следующем подключении.
*
* Выбор устройства вывода показываем только там, где браузер реально умеет
* им управлять (`isAudioOutputSelectable` — проверка по возможностям
* `setSinkId`, не по User-Agent): в iOS Safari метода нет вообще, вместо
* списка — подсказка, что переключение звука там на стороне системы.
*
* ДОЛЖЕН рендериться внутри `<LiveKitRoom>`: `useMediaDeviceSelect` без явно
* переданного `room` берёт активную комнату из `RoomContext` — вне контекста
@@ -29,6 +38,8 @@ export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
const { saveAudioInputDeviceId, saveVideoInputDeviceId } = usePersistentUserChoices()
const mic = useMediaDeviceSelect({ kind: 'audioinput' })
const camera = useMediaDeviceSelect({ kind: 'videoinput' })
const speaker = useMediaDeviceSelect({ kind: 'audiooutput' })
const speakerSelectable = isAudioOutputSelectable()
async function handleMicChange(deviceId: string) {
try {
@@ -49,6 +60,15 @@ export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
}
}
async function handleSpeakerChange(deviceId: string) {
try {
await speaker.setActiveMediaDevice(deviceId)
saveAudioOutputDeviceId(deviceId)
} catch {
toast.show('Не удалось переключить вывод звука — устройство занято или отключено', 'error')
}
}
return (
<div
className="room-modal-overlay"
@@ -104,6 +124,34 @@ export function DeviceSettingsDialog({ onClose }: DeviceSettingsDialogProps) {
))}
</select>
</div>
{speakerSelectable ? (
<div className="room-field">
<label htmlFor="device-settings-speaker">Динамики</label>
<select
id="device-settings-speaker"
value={speaker.activeDeviceId}
onChange={(e) => void handleSpeakerChange(e.target.value)}
>
<option value="" disabled>
Определяется
</option>
{speaker.devices.map((device, index) => (
<option key={device.deviceId} value={device.deviceId}>
{deviceLabel(device, index, 'Динамик')}
</option>
))}
</select>
</div>
) : (
<div className="room-field">
<span className="room-field-label">Динамики</span>
<p className="room-field-hint">
Вывод звука (динамик, наушники, Bluetooth) переключается средствами системы этот
браузер не позволяет управлять им со страницы.
</p>
</div>
)}
</div>
</div>
)