69f12aa
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';
import { lkT } from '@/shared/livekit/lk-i18n';
/** @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<HTMLDivElement> {
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 built with other LiveKit components like `TrackToggle`,
* `DeviceSelectorButton`, `DisconnectButton` and `StartAudio`.
*
* @example
* ```tsx
* <LiveKitRoom>
* <ControlBar />
* </LiveKitRoom>
* ```
* @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 (
<div {...htmlProps}>
{visibleControls.microphone && (
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Microphone}
showIcon={showIcon}
onChange={microphoneOnChange}
onDeviceError={(error) => onDeviceError?.({ source: Track.Source.Microphone, error })}
>
{showText && lkT("livekit.microphone")}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="audioinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveAudioInputDeviceId(deviceId ?? 'default')
}
/>
</div>
</div>
)}
{visibleControls.camera && (
<div className="lk-button-group">
<TrackToggle
source={Track.Source.Camera}
showIcon={showIcon}
onChange={cameraOnChange}
onDeviceError={(error) => onDeviceError?.({ source: Track.Source.Camera, error })}
>
{showText && lkT("livekit.camera")}
</TrackToggle>
<div className="lk-button-group-menu">
<MediaDeviceMenu
kind="videoinput"
onActiveDeviceChange={(_kind, deviceId) =>
saveVideoInputDeviceId(deviceId ?? 'default')
}
/>
</div>
</div>
)}
{visibleControls.screenShare && browserSupportsScreenSharing && (
<TrackToggle
source={Track.Source.ScreenShare}
captureOptions={{ audio: true, selfBrowserSurface: 'include' }}
showIcon={showIcon}
onChange={onScreenShareChange}
onDeviceError={(error) => onDeviceError?.({ source: Track.Source.ScreenShare, error })}
>
{showText && lkT(isScreenShareEnabled ? "livekit.stopScreenShare" : "livekit.shareScreen")}
</TrackToggle>
)}
{visibleControls.chat && (
<ChatToggle>
{showIcon && <ChatIcon />}
{showText && lkT("livekit.chat")}
</ChatToggle>
)}
{visibleControls.settings && (
<SettingsMenuToggle>
{showIcon && <GearIcon />}
{showText && lkT("livekit.settings")}
</SettingsMenuToggle>
)}
{visibleControls.leave && (
<DisconnectButton>
{showIcon && <LeaveIcon />}
{showText && lkT("livekit.leave")}
</DisconnectButton>
)}
<StartMediaButton />
</div>
);
}