Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffc3e4a656 | |||
| 634b8c03a0 | |||
| 6b622ddc44 | |||
| 8bfb4d3db3 | |||
| e79a167a3e | |||
| e75776070c | |||
| 0f72f0f727 |
@@ -102,11 +102,15 @@ export const Conference = ({
|
||||
audioCaptureDefaults: {
|
||||
deviceId: userConfig.audioDeviceId ?? undefined,
|
||||
},
|
||||
audioOutput: {
|
||||
deviceId: userConfig.audioOutputDeviceId ?? undefined,
|
||||
},
|
||||
}
|
||||
// do not rely on the userConfig object directly as its reference may change on every render
|
||||
}, [
|
||||
userConfig.videoDeviceId,
|
||||
userConfig.audioDeviceId,
|
||||
userConfig.audioOutputDeviceId,
|
||||
isAdaptiveStreamSupported,
|
||||
isDynacastSupported,
|
||||
])
|
||||
|
||||
@@ -31,6 +31,7 @@ import { openPermissionsDialog, permissionsStore } from '@/stores/permissions'
|
||||
import { ToggleDevice } from './join/ToggleDevice'
|
||||
import { SelectDevice } from './join/SelectDevice'
|
||||
import { useResolveDefaultDeviceId } from '../livekit/hooks/useResolveDefaultDevice'
|
||||
import { isSafari } from '@/utils/livekit'
|
||||
|
||||
const onError = (e: Error) => console.error('ERROR', e)
|
||||
|
||||
@@ -101,11 +102,13 @@ export const Join = ({
|
||||
audioEnabled,
|
||||
videoEnabled,
|
||||
audioDeviceId,
|
||||
audioOutputDeviceId,
|
||||
videoDeviceId,
|
||||
processorSerialized,
|
||||
username,
|
||||
},
|
||||
saveAudioInputEnabled,
|
||||
saveAudioOutputDeviceId,
|
||||
saveVideoInputEnabled,
|
||||
saveAudioInputDeviceId,
|
||||
saveVideoInputDeviceId,
|
||||
@@ -590,7 +593,7 @@ export const Join = ({
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
width: '50%',
|
||||
width: '30%',
|
||||
})}
|
||||
>
|
||||
<SelectDevice
|
||||
@@ -599,9 +602,22 @@ export const Join = ({
|
||||
onSubmit={saveAudioInputDeviceId}
|
||||
/>
|
||||
</div>
|
||||
{!isSafari() && (
|
||||
<div
|
||||
className={css({
|
||||
width: '30%',
|
||||
})}
|
||||
>
|
||||
<SelectDevice
|
||||
kind="audiooutput"
|
||||
id={audioOutputDeviceId}
|
||||
onSubmit={saveAudioOutputDeviceId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={css({
|
||||
width: '50%',
|
||||
width: '30%',
|
||||
})}
|
||||
>
|
||||
<SelectDevice
|
||||
|
||||
@@ -2,10 +2,11 @@ import {
|
||||
RemixiconComponentType,
|
||||
RiMicLine,
|
||||
RiVideoOnLine,
|
||||
RiVolumeDownLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||
import { useMemo } from 'react'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { Select } from '@/primitives/Select'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { permissionsStore } from '@/stores/permissions'
|
||||
@@ -44,6 +45,22 @@ const SelectDevicePermissions = ({
|
||||
label: d.label,
|
||||
}))
|
||||
|
||||
/**
|
||||
* FALLBACK AUDIO OUTPUT DEVICE SELECTION
|
||||
* Auto-selects the only available audio output device when currently on 'default'
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (
|
||||
kind !== 'audiooutput' ||
|
||||
items.length !== 1 ||
|
||||
items[0].value === 'default' ||
|
||||
activeDeviceId !== 'default'
|
||||
)
|
||||
return
|
||||
onSubmit?.(items[0].value)
|
||||
setActiveMediaDevice(items[0].value)
|
||||
}, [items, onSubmit, kind, setActiveMediaDevice, activeDeviceId])
|
||||
|
||||
return (
|
||||
<Select
|
||||
aria-label={t(`${kind}.choose`)}
|
||||
@@ -51,7 +68,11 @@ const SelectDevicePermissions = ({
|
||||
isDisabled={items.length === 0}
|
||||
items={items}
|
||||
iconComponent={config?.icon}
|
||||
placeholder={t('selectDevice.loading')}
|
||||
placeholder={
|
||||
items.length === 0
|
||||
? t('selectDevice.loading')
|
||||
: t('selectDevice.select')
|
||||
}
|
||||
selectedKey={id || activeDeviceId}
|
||||
onSelectionChange={(key) => {
|
||||
onSubmit?.(key as string)
|
||||
@@ -72,6 +93,10 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
||||
return {
|
||||
icon: RiMicLine,
|
||||
}
|
||||
case 'audiooutput':
|
||||
return {
|
||||
icon: RiVolumeDownLine,
|
||||
}
|
||||
case 'videoinput':
|
||||
return {
|
||||
icon: RiVideoOnLine,
|
||||
@@ -86,6 +111,9 @@ export const SelectDevice = ({ id, onSubmit, kind }: SelectDeviceProps) => {
|
||||
if (kind == 'videoinput') {
|
||||
return permissions.isCameraDenied || permissions.isCameraPrompted
|
||||
}
|
||||
if (kind == 'audiooutput') {
|
||||
return permissions.isMicrophoneDenied || permissions.isMicrophonePrompted
|
||||
}
|
||||
return false
|
||||
}, [kind, permissions])
|
||||
|
||||
|
||||
@@ -49,13 +49,13 @@ export const ToggleDevice = <T extends ToggleSource>({
|
||||
}
|
||||
try {
|
||||
if (isTrackEnabled) {
|
||||
await track.mute()
|
||||
setIsTrackEnabled(false)
|
||||
onChange?.(false, true)
|
||||
await track.mute()
|
||||
} else {
|
||||
await track.unmute()
|
||||
setIsTrackEnabled(true)
|
||||
onChange?.(true, true)
|
||||
await track.unmute()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle track:', error)
|
||||
|
||||
@@ -16,6 +16,9 @@ export function usePersistentUserChoices() {
|
||||
saveAudioInputDeviceId: (deviceId: string) => {
|
||||
userChoicesStore.audioDeviceId = deviceId
|
||||
},
|
||||
saveAudioOutputDeviceId: (deviceId: string) => {
|
||||
userChoicesStore.audioOutputDeviceId = deviceId
|
||||
},
|
||||
saveVideoInputDeviceId: (deviceId: string) => {
|
||||
userChoicesStore.videoDeviceId = deviceId
|
||||
},
|
||||
|
||||
@@ -95,6 +95,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
||||
userChoices: { noiseReductionEnabled },
|
||||
saveAudioInputDeviceId,
|
||||
saveNoiseReductionEnabled,
|
||||
saveAudioOutputDeviceId,
|
||||
} = usePersistentUserChoices()
|
||||
|
||||
const isSpeaking = useIsSpeaking(localParticipant)
|
||||
@@ -183,9 +184,10 @@ export const AudioTab = ({ id }: AudioTabProps) => {
|
||||
defaultSelectedKey={
|
||||
activeDeviceIdOut || getDefaultSelectedKey(itemsOut)
|
||||
}
|
||||
onSelectionChange={async (key) =>
|
||||
onSelectionChange={(key) => {
|
||||
setActiveMediaDeviceOut(key as string)
|
||||
}
|
||||
saveAudioOutputDeviceId(key as string)
|
||||
}}
|
||||
{...disabledProps}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"join": {
|
||||
"selectDevice": {
|
||||
"loading": "Laden…",
|
||||
"select": "Wählen Sie einen Wert",
|
||||
"permissionsNeeded": "Genehmigung erforderlich"
|
||||
},
|
||||
"videoinput": {
|
||||
@@ -27,6 +28,10 @@
|
||||
"enable": "Mikrofon aktivieren",
|
||||
"label": "Mikrofon"
|
||||
},
|
||||
"audiooutput": {
|
||||
"choose": "Lautsprecher auswählen",
|
||||
"permissionsNeeded": "Lautsprecher auswählen - genehmigung erforderlich"
|
||||
},
|
||||
"effects": {
|
||||
"description": "Effekte anwenden",
|
||||
"title": "Effekte",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"join": {
|
||||
"selectDevice": {
|
||||
"loading": "Loading…",
|
||||
"select": "Select a value",
|
||||
"permissionsNeeded": "Permission needed"
|
||||
},
|
||||
"videoinput": {
|
||||
@@ -27,6 +28,10 @@
|
||||
"enable": "Enable microphone",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"audiooutput": {
|
||||
"choose": "Select speaker",
|
||||
"permissionsNeeded": "Select speaker - permission needed"
|
||||
},
|
||||
"effects": {
|
||||
"description": "Apply effects",
|
||||
"title": "Effects",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"join": {
|
||||
"selectDevice": {
|
||||
"loading": "Chargement…",
|
||||
"select": "Sélectionnez une valeur",
|
||||
"permissionsNeeded": "Autorisations nécessaires"
|
||||
},
|
||||
"videoinput": {
|
||||
@@ -27,6 +28,10 @@
|
||||
"enable": "Activer le micro",
|
||||
"label": "Microphone"
|
||||
},
|
||||
"audiooutput": {
|
||||
"choose": "Choisir le haut-parleur",
|
||||
"permissionsNeeded": "Choisir le haut-parleur - autorisations nécessaires"
|
||||
},
|
||||
"heading": "Rejoindre la réunion ?",
|
||||
"effects": {
|
||||
"description": "Effets d'arrière plan",
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
"join": {
|
||||
"selectDevice": {
|
||||
"loading": "Bezig met laden…",
|
||||
"permissionNeeded": "Toestemming vereist"
|
||||
"select": "Selecteer een waarde",
|
||||
"permissionsNeeded": "Toestemming vereist"
|
||||
},
|
||||
"videoinput": {
|
||||
"choose": "Selecteer camera",
|
||||
"permissionNeeded": "Selecteer camera - Toestemming vereist",
|
||||
"permissionsNeeded": "Selecteer camera - Toestemming vereist",
|
||||
"disable": "Camera uitschakelen",
|
||||
"enable": "Camera inschakelen",
|
||||
"label": "Camera",
|
||||
@@ -22,11 +23,15 @@
|
||||
},
|
||||
"audioinput": {
|
||||
"choose": "Selecteer microfoon",
|
||||
"permissionNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||
"permissionsNeeded": "Selecteer microfoon - Toestemming vereist",
|
||||
"disable": "Microfoon dempen",
|
||||
"enable": "Microfoon dempen opheffen",
|
||||
"label": "Microfoon"
|
||||
},
|
||||
"audiooutput": {
|
||||
"choose": "Selecteer luidspreker",
|
||||
"permissionsNeeded": "Selecteer luidspreker - Toestemming vereist"
|
||||
},
|
||||
"effects": {
|
||||
"description": "Pas effecten toe",
|
||||
"title": "Effecten",
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
export type LocalUserChoices = LocalUserChoicesLK & {
|
||||
processorSerialized?: ProcessorSerialized
|
||||
noiseReductionEnabled?: boolean
|
||||
audioOutputDeviceId?: string
|
||||
}
|
||||
|
||||
function getUserChoicesState(): LocalUserChoices {
|
||||
return {
|
||||
noiseReductionEnabled: false,
|
||||
audioOutputDeviceId: 'default', // Use 'default' to match LiveKit's standard device selection behavior
|
||||
...loadUserChoices(),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user