import { useCallback, useMemo } from 'react'; import { Track } from 'livekit-client'; import { type TrackReferenceOrPlaceholder, useLocalParticipant, useLocalParticipantPermissions, usePersistentUserChoices, useTrackToggle, } from '@livekit/components-react'; 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; } }; export interface PublishPermissions { camera: boolean; microphone: boolean; screenShare: boolean; data: boolean; } export function usePublishPermissions(): PublishPermissions { const localPermissions = useLocalParticipantPermissions(); const canPublishSource = (source: Track.Source) => { return ( !!localPermissions?.canPublish && (localPermissions.canPublishSources.length === 0 || localPermissions.canPublishSources.includes(trackSourceToProtocol(source))) ); }; return { camera: canPublishSource(Track.Source.Camera), microphone: canPublishSource(Track.Source.Microphone), screenShare: canPublishSource(Track.Source.ScreenShare), data: localPermissions?.canPublishData ?? false, }; } export interface UseInputControlsProps { saveUserChoices?: boolean; onDisconnect?: () => void; onDeviceError?: (error: { source: Track.Source; error: Error }) => void; } export interface UseInputControlsReturn { micTrackRef?: TrackReferenceOrPlaceholder; microphoneToggle: ReturnType>; cameraToggle: ReturnType>; screenShareToggle: ReturnType>; handleAudioDeviceChange: (deviceId: string) => void; handleVideoDeviceChange: (deviceId: string) => void; handleMicrophoneDeviceSelectError: (error: Error) => void; handleCameraDeviceSelectError: (error: Error) => void; } export function useInputControls({ saveUserChoices = true, onDeviceError, }: UseInputControlsProps = {}): UseInputControlsReturn { const microphoneToggle = useTrackToggle({ source: Track.Source.Microphone, onDeviceError: (error) => onDeviceError?.({ source: Track.Source.Microphone, error }), }); const cameraToggle = useTrackToggle({ source: Track.Source.Camera, onDeviceError: (error) => onDeviceError?.({ source: Track.Source.Camera, error }), }); const screenShareToggle = useTrackToggle({ source: Track.Source.ScreenShare, onDeviceError: (error) => onDeviceError?.({ source: Track.Source.ScreenShare, error }), }); const { microphoneTrack, localParticipant } = useLocalParticipant(); const micTrackRef = useMemo(() => { return localParticipant && microphoneTrack ? { participant: localParticipant, source: Track.Source.Microphone, publication: microphoneTrack, } : undefined; }, [localParticipant, microphoneTrack]); const { saveAudioInputEnabled, saveVideoInputEnabled, saveAudioInputDeviceId, saveVideoInputDeviceId, } = usePersistentUserChoices({ preventSave: !saveUserChoices }); const handleAudioDeviceChange = useCallback( (deviceId: string) => { saveAudioInputDeviceId(deviceId ?? 'default'); }, [saveAudioInputDeviceId] ); const handleVideoDeviceChange = useCallback( (deviceId: string) => { saveVideoInputDeviceId(deviceId ?? 'default'); }, [saveVideoInputDeviceId] ); const handleToggleCamera = useCallback( async (enabled?: boolean) => { if (screenShareToggle.enabled) { screenShareToggle.toggle(false); } await cameraToggle.toggle(enabled); // persist video input enabled preference saveVideoInputEnabled(!cameraToggle.enabled); }, [cameraToggle, screenShareToggle, saveVideoInputEnabled] ); const handleToggleMicrophone = useCallback( async (enabled?: boolean) => { await microphoneToggle.toggle(enabled); // persist audio input enabled preference saveAudioInputEnabled(!microphoneToggle.enabled); }, [microphoneToggle, saveAudioInputEnabled] ); const handleToggleScreenShare = useCallback( async (enabled?: boolean) => { if (cameraToggle.enabled) { cameraToggle.toggle(false); } await screenShareToggle.toggle(enabled); }, [cameraToggle, screenShareToggle] ); const handleMicrophoneDeviceSelectError = useCallback( (error: Error) => onDeviceError?.({ source: Track.Source.Microphone, error }), [onDeviceError] ); const handleCameraDeviceSelectError = useCallback( (error: Error) => onDeviceError?.({ source: Track.Source.Camera, error }), [onDeviceError] ); return { micTrackRef, cameraToggle: { ...cameraToggle, toggle: handleToggleCamera, }, microphoneToggle: { ...microphoneToggle, toggle: handleToggleMicrophone, }, screenShareToggle: { ...screenShareToggle, toggle: handleToggleScreenShare, }, handleAudioDeviceChange, handleVideoDeviceChange, handleMicrophoneDeviceSelectError, handleCameraDeviceSelectError, }; }