feat(room): выбор устройства аудиовывода в настройках
Третий селект в DeviceSettingsDialog (динамики/наушники/bluetooth) рядом с уже существующими микрофоном и камерой — на useMediaDeviceSelect(kind: 'audiooutput'). Выбор персистится отдельно от usePersistentUserChoices (LocalUserChoices LiveKit про аудиовыход не знает) и применяется заново при следующем подключении через RoomOptions.audioOutput. Поддержка определяется по наличию setSinkId в рантайме, не по User-Agent: там, где браузер не умеет переключать вывод (iOS Safari), список скрыт, вместо него — подсказка, что маршрутизация звука на стороне системы.
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user