import { Track } from 'livekit-client'; import * as React from 'react'; import { MediaDeviceMenu } from './MediaDeviceMenu'; import { DisconnectButton } from '../components/controls/DisconnectButton'; import { TrackToggle } from '../components/controls/TrackToggle'; import { ChatIcon, GearIcon, LeaveIcon } from '../assets/icons'; import { ChatToggle } from '../components/controls/ChatToggle'; import { useLocalParticipantPermissions, usePersistentUserChoices } from '../hooks'; import { useMediaQuery } from '../hooks/internal'; import { useMaybeLayoutContext } from '../context'; import { supportsScreenSharing } from '@livekit/components-core'; import { mergeProps } from '../utils'; import { StartMediaButton } from '../components/controls/StartMediaButton'; import { SettingsMenuToggle } from '../components/controls/SettingsMenuToggle'; /** @public */ export type ControlBarControls = { microphone?: boolean; camera?: boolean; chat?: boolean; screenShare?: boolean; leave?: boolean; settings?: boolean; }; const trackSourceToProtocol = (source: Track.Source) => { // NOTE: this mapping avoids importing the protocol package as that leads to a significant bundle size increase switch (source) { case Track.Source.Camera: return 1; case Track.Source.Microphone: return 2; case Track.Source.ScreenShare: return 3; default: return 0; } }; /** @public */ export interface ControlBarProps extends React.HTMLAttributes { onDeviceError?: (error: { source: Track.Source; error: Error }) => void; variation?: 'minimal' | 'verbose' | 'textOnly'; controls?: ControlBarControls; /** * If `true`, the user's device choices will be persisted. * This will enable the user to have the same device choices when they rejoin the room. * @defaultValue true * @alpha */ saveUserChoices?: boolean; } /** * The `ControlBar` prefab gives the user the basic user interface to control their * media devices (camera, microphone and screen share), open the `Chat` and leave the room. * * @remarks * This component is build with other LiveKit components like `TrackToggle`, * `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`. * * @example * ```tsx * * * * ``` * @public */ export function ControlBar({ variation, controls, saveUserChoices = true, onDeviceError, ...props }: ControlBarProps) { const [isChatOpen, setIsChatOpen] = React.useState(false); const layoutContext = useMaybeLayoutContext(); React.useEffect(() => { if (layoutContext?.widget.state?.showChat !== undefined) { setIsChatOpen(layoutContext?.widget.state?.showChat); } }, [layoutContext?.widget.state?.showChat]); const isTooLittleSpace = useMediaQuery(`(max-width: ${isChatOpen ? 1000 : 760}px)`); const defaultVariation = isTooLittleSpace ? 'minimal' : 'verbose'; variation ??= defaultVariation; const visibleControls = { leave: true, ...controls }; const localPermissions = useLocalParticipantPermissions(); if (!localPermissions) { visibleControls.camera = false; visibleControls.chat = false; visibleControls.microphone = false; visibleControls.screenShare = false; } else { const canPublishSource = (source: Track.Source) => { return ( localPermissions.canPublish && (localPermissions.canPublishSources.length === 0 || localPermissions.canPublishSources.includes(trackSourceToProtocol(source))) ); }; visibleControls.camera ??= canPublishSource(Track.Source.Camera); visibleControls.microphone ??= canPublishSource(Track.Source.Microphone); visibleControls.screenShare ??= canPublishSource(Track.Source.ScreenShare); visibleControls.chat ??= localPermissions.canPublishData && controls?.chat; } const showIcon = React.useMemo( () => variation === 'minimal' || variation === 'verbose', [variation], ); const showText = React.useMemo( () => variation === 'textOnly' || variation === 'verbose', [variation], ); const browserSupportsScreenSharing = supportsScreenSharing(); const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false); const onScreenShareChange = React.useCallback( (enabled: boolean) => { setIsScreenShareEnabled(enabled); }, [setIsScreenShareEnabled], ); const htmlProps = mergeProps({ className: 'lk-control-bar' }, props); const { saveAudioInputEnabled, saveVideoInputEnabled, saveAudioInputDeviceId, saveVideoInputDeviceId, } = usePersistentUserChoices({ preventSave: !saveUserChoices }); const microphoneOnChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => isUserInitiated ? saveAudioInputEnabled(enabled) : null, [saveAudioInputEnabled], ); const cameraOnChange = React.useCallback( (enabled: boolean, isUserInitiated: boolean) => isUserInitiated ? saveVideoInputEnabled(enabled) : null, [saveVideoInputEnabled], ); return (
{visibleControls.microphone && (
onDeviceError?.({ source: Track.Source.Microphone, error })} > {showText && 'Microphone'}
saveAudioInputDeviceId(deviceId ?? 'default') } />
)} {visibleControls.camera && (
onDeviceError?.({ source: Track.Source.Camera, error })} > {showText && 'Camera'}
saveVideoInputDeviceId(deviceId ?? 'default') } />
)} {visibleControls.screenShare && browserSupportsScreenSharing && ( onDeviceError?.({ source: Track.Source.ScreenShare, error })} > {showText && (isScreenShareEnabled ? 'Stop screen share' : 'Share screen')} )} {visibleControls.chat && ( {showIcon && } {showText && 'Chat'} )} {visibleControls.settings && ( {showIcon && } {showText && 'Settings'} )} {visibleControls.leave && ( {showIcon && } {showText && 'Leave'} )}
); }