Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b01c3f7bf5 | |||
| 1b17b33efc |
@@ -0,0 +1,123 @@
|
||||
import { Button } from '@/primitives'
|
||||
import { useMediaDeviceSelect } from '@livekit/components-react'
|
||||
import { RiCameraSwitchLine } from '@remixicon/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ButtonProps } from 'react-aria-components'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
enum FacingMode {
|
||||
USER = 'user',
|
||||
ENVIRONMENT = 'environment',
|
||||
}
|
||||
|
||||
export const CameraSwitchButton = (props: Partial<ButtonProps>) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
|
||||
const { devices, activeDeviceId, setActiveMediaDevice } =
|
||||
useMediaDeviceSelect({
|
||||
kind: 'videoinput',
|
||||
requestPermissions: true,
|
||||
})
|
||||
|
||||
// getCapabilities type is not available.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getDeviceFacingMode = (device: any): string[] => {
|
||||
if (!device.getCapabilities) {
|
||||
return []
|
||||
}
|
||||
const capabilities = device.getCapabilities()
|
||||
if (!capabilities) {
|
||||
return []
|
||||
}
|
||||
if (typeof capabilities.facingMode !== 'object') {
|
||||
return []
|
||||
}
|
||||
return capabilities.facingMode
|
||||
}
|
||||
|
||||
const detectCurrentFacingMode = (): FacingMode | null => {
|
||||
if (!activeDeviceId) {
|
||||
return null
|
||||
}
|
||||
const activeDevice = devices.find(
|
||||
(device) => device.deviceId === activeDeviceId
|
||||
)
|
||||
if (!activeDevice) {
|
||||
return null
|
||||
}
|
||||
const facingMode = getDeviceFacingMode(activeDevice)
|
||||
if (facingMode.indexOf(FacingMode.USER) >= 0) {
|
||||
return FacingMode.USER
|
||||
}
|
||||
if (facingMode.indexOf(FacingMode.ENVIRONMENT) >= 0) {
|
||||
return FacingMode.ENVIRONMENT
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const guessCurrentFacingMode = () => {
|
||||
const facingMode = detectCurrentFacingMode()
|
||||
if (facingMode) {
|
||||
return facingMode
|
||||
}
|
||||
// We consider by default if we have no clue that the user camera is used.
|
||||
return FacingMode.USER
|
||||
}
|
||||
|
||||
const [facingMode, setFacingMode] = useState<FacingMode | null>()
|
||||
|
||||
/**
|
||||
* Before setting the initial value of facingMode we need to wait for devices to
|
||||
* be loaded ( because in detectCurrentFacingMode we need to find the active device
|
||||
* in the devices list ), which is not the case at first render.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (devices.length == 0) {
|
||||
return
|
||||
}
|
||||
if (facingMode) {
|
||||
return
|
||||
}
|
||||
|
||||
setFacingMode(guessCurrentFacingMode())
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [devices])
|
||||
|
||||
const getUserDevice = (
|
||||
facingMode: FacingMode
|
||||
): MediaDeviceInfo | undefined => {
|
||||
return devices.find((device) => {
|
||||
return getDeviceFacingMode(device).indexOf(facingMode) >= 0
|
||||
})
|
||||
}
|
||||
|
||||
const toggle = () => {
|
||||
let target: FacingMode
|
||||
if (facingMode === FacingMode.USER) {
|
||||
target = FacingMode.ENVIRONMENT
|
||||
} else {
|
||||
target = FacingMode.USER
|
||||
}
|
||||
const device = getUserDevice(target)
|
||||
if (device) {
|
||||
setActiveMediaDevice(device.deviceId)
|
||||
setFacingMode(target)
|
||||
} else {
|
||||
console.error('Cannot get user device with facingMode ' + target)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onPress={(e) => {
|
||||
toggle()
|
||||
props.onPress?.(e)
|
||||
}}
|
||||
variant="primaryTextDark"
|
||||
aria-label={t('options.items.switchCamera')}
|
||||
tooltip={t('options.items.switchCamera')}
|
||||
description={true}
|
||||
>
|
||||
<RiCameraSwitchLine size={20} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
+38
-24
@@ -67,11 +67,13 @@ type SelectToggleDeviceProps<T extends ToggleSource> =
|
||||
UseTrackToggleProps<T> & {
|
||||
onActiveDeviceChange: (deviceId: string) => void
|
||||
source: SelectToggleSource
|
||||
hideMenu?: boolean
|
||||
variant?: 'dark' | 'light'
|
||||
}
|
||||
|
||||
export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
onActiveDeviceChange,
|
||||
hideMenu,
|
||||
variant = 'light',
|
||||
...props
|
||||
}: SelectToggleDeviceProps<T>) => {
|
||||
@@ -94,30 +96,42 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
||||
gap: '1px',
|
||||
})}
|
||||
>
|
||||
<ToggleDevice {...trackProps} config={config} />
|
||||
<Menu variant={variant}>
|
||||
<Button
|
||||
tooltip={selectLabel}
|
||||
aria-label={selectLabel}
|
||||
groupPosition="right"
|
||||
square
|
||||
variant={trackProps.enabled ? 'primaryDark' : 'error2'}
|
||||
>
|
||||
<RiArrowDownSLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={devices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMediaDevice(value as string)
|
||||
onActiveDeviceChange(value as string)
|
||||
}}
|
||||
variant={variant}
|
||||
/>
|
||||
</Menu>
|
||||
<ToggleDevice
|
||||
{...trackProps}
|
||||
config={config}
|
||||
toggleButtonProps={{
|
||||
...(hideMenu
|
||||
? {
|
||||
groupPosition: undefined,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
{!hideMenu && (
|
||||
<Menu variant={variant}>
|
||||
<Button
|
||||
tooltip={selectLabel}
|
||||
aria-label={selectLabel}
|
||||
groupPosition="right"
|
||||
square
|
||||
variant={trackProps.enabled ? 'primaryDark' : 'error2'}
|
||||
>
|
||||
<RiArrowDownSLine />
|
||||
</Button>
|
||||
<MenuList
|
||||
items={devices.map((d) => ({
|
||||
value: d.deviceId,
|
||||
label: d.label,
|
||||
}))}
|
||||
selectedItem={activeDeviceId}
|
||||
onAction={(value) => {
|
||||
setActiveMediaDevice(value as string)
|
||||
onActiveDeviceChange(value as string)
|
||||
}}
|
||||
variant={variant}
|
||||
/>
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,20 @@ import { SelectToggleDeviceConfig } from './SelectToggleDevice'
|
||||
import useLongPress from '@/features/shortcuts/useLongPress'
|
||||
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
|
||||
import { useIsSpeaking, useLocalParticipant } from '@livekit/components-react'
|
||||
import { ToggleButtonProps } from '@/primitives/ToggleButton'
|
||||
|
||||
export type ToggleDeviceProps = {
|
||||
enabled: boolean
|
||||
toggle: () => void
|
||||
config: SelectToggleDeviceConfig
|
||||
toggleButtonProps?: Partial<ToggleButtonProps>
|
||||
}
|
||||
|
||||
export const ToggleDevice = ({
|
||||
config,
|
||||
enabled,
|
||||
toggle,
|
||||
toggleButtonProps,
|
||||
}: ToggleDeviceProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
|
||||
|
||||
@@ -64,6 +67,7 @@ export const ToggleDevice = ({
|
||||
aria-label={toggleLabel}
|
||||
tooltip={toggleLabel}
|
||||
groupPosition="left"
|
||||
{...toggleButtonProps}
|
||||
>
|
||||
<Icon />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { LinkButton } from '@/primitives'
|
||||
import { useSettingsDialog } from '../../components/controls/SettingsDialogContext'
|
||||
import { ResponsiveMenu } from './ResponsiveMenu'
|
||||
import { TranscriptToggle } from '../../components/controls/TranscriptToggle'
|
||||
import { CameraSwitchButton } from '../../components/controls/CameraSwitchButton'
|
||||
|
||||
export function MobileControlBar({
|
||||
onDeviceError,
|
||||
@@ -55,7 +56,7 @@ export function MobileControlBar({
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '422px',
|
||||
width: '330px',
|
||||
})}
|
||||
>
|
||||
<LeaveButton />
|
||||
@@ -68,6 +69,7 @@ export function MobileControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveAudioInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
hideMenu={true}
|
||||
/>
|
||||
<SelectToggleDevice
|
||||
source={Track.Source.Camera}
|
||||
@@ -78,6 +80,7 @@ export function MobileControlBar({
|
||||
onActiveDeviceChange={(deviceId) =>
|
||||
saveVideoInputDeviceId(deviceId ?? '')
|
||||
}
|
||||
hideMenu={true}
|
||||
/>
|
||||
<HandToggle />
|
||||
<Button
|
||||
@@ -174,6 +177,7 @@ export function MobileControlBar({
|
||||
>
|
||||
<RiSettings3Line size={20} />
|
||||
</Button>
|
||||
<CameraSwitchButton onPress={() => setIsMenuOpened(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</ResponsiveMenu>
|
||||
|
||||
@@ -83,7 +83,8 @@
|
||||
"feedback": "",
|
||||
"settings": "",
|
||||
"username": "",
|
||||
"effects": ""
|
||||
"effects": "",
|
||||
"switchCamera": ""
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
"feedback": "Give us feedback",
|
||||
"settings": "Settings",
|
||||
"username": "Update Your Name",
|
||||
"effects": "Apply effects"
|
||||
"effects": "Apply effects",
|
||||
"switchCamera": "Switch camera"
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
"feedback": "Partager votre avis",
|
||||
"settings": "Paramètres",
|
||||
"username": "Choisir votre nom",
|
||||
"effects": "Appliquer des effets"
|
||||
"effects": "Appliquer des effets",
|
||||
"switchCamera": "Changer de caméra"
|
||||
}
|
||||
},
|
||||
"effects": {
|
||||
|
||||
Reference in New Issue
Block a user