import type { AudioCaptureOptions, LocalParticipant, Room, ScreenShareCaptureOptions, TrackPublishOptions, VideoCaptureOptions, } from '@cc-livekit/livekit-client'; import { Track } from '@cc-livekit/livekit-client'; import type { Observable } from 'rxjs'; import { Subject, map, startWith } from 'rxjs'; import { observeParticipantMedia } from '../observables/participant'; import { prefixClass } from '../styles-interface'; export type CaptureOptionsBySource = T extends Track.Source.Camera ? VideoCaptureOptions : T extends Track.Source.Microphone ? AudioCaptureOptions : T extends Track.Source.ScreenShare ? ScreenShareCaptureOptions : never; export type MediaToggleType = { pendingObserver: Observable; toggle: ( forceState?: boolean, captureOptions?: CaptureOptionsBySource, ) => Promise; className: string; enabledObserver: Observable; }; export type ToggleSource = Exclude< Track.Source, Track.Source.ScreenShareAudio | Track.Source.Unknown >; export function setupMediaToggle( source: T, room: Room, options?: CaptureOptionsBySource, publishOptions?: TrackPublishOptions, onError?: (error: Error) => void, ): MediaToggleType { const { localParticipant } = room; const getSourceEnabled = (source: ToggleSource, localParticipant: LocalParticipant) => { let isEnabled = false; switch (source) { case Track.Source.Camera: isEnabled = localParticipant.isCameraEnabled; break; case Track.Source.Microphone: isEnabled = localParticipant.isMicrophoneEnabled; break; case Track.Source.ScreenShare: isEnabled = localParticipant.isScreenShareEnabled; break; default: break; } return isEnabled; }; const enabledObserver = observeParticipantMedia(localParticipant).pipe( map((media) => { return getSourceEnabled(source, media.participant as LocalParticipant); }), startWith(getSourceEnabled(source, localParticipant)), ); const pendingSubject = new Subject(); const toggle = async (forceState?: boolean, captureOptions?: CaptureOptionsBySource) => { try { captureOptions ??= options; // trigger observable update pendingSubject.next(true); switch (source) { case Track.Source.Camera: if (room.metadata) { const metadata = JSON.parse(room.metadata); if (metadata.canPublishVideo === false) { const localTrack = room.localParticipant.getTrackPublication(Track.Source.Camera); if (!localParticipant.isCameraEnabled && !localTrack) { throw new Error('RATE_LIMIT'); } } } await localParticipant.setCameraEnabled( forceState ?? !localParticipant.isCameraEnabled, captureOptions as VideoCaptureOptions, publishOptions, ); return localParticipant.isCameraEnabled; case Track.Source.Microphone: if (room.metadata) { const metadata = JSON.parse(room.metadata); if (metadata.canPublishAudio === false) { const localTrack = room.localParticipant.getTrackPublication(Track.Source.Microphone); if (!localParticipant.isMicrophoneEnabled && !localTrack) { throw new Error('RATE_LIMIT'); } } } await localParticipant.setMicrophoneEnabled( forceState ?? !localParticipant.isMicrophoneEnabled, captureOptions as AudioCaptureOptions, publishOptions, ); return localParticipant.isMicrophoneEnabled; case Track.Source.ScreenShare: // await localParticipant.setScreenShareEnabled( // forceState ?? !localParticipant.isScreenShareEnabled, // captureOptions as ScreenShareCaptureOptions, // publishOptions, // ); console.warn('do nothing, use custom screen share picker.'); return localParticipant.isScreenShareEnabled; default: throw new TypeError('Tried to toggle unsupported source'); } } catch (e) { if (onError && e instanceof Error) { onError?.(e); return undefined; } else { throw e; } } finally { pendingSubject.next(false); // trigger observable update } }; const className: string = prefixClass('button'); return { className, toggle, enabledObserver, pendingObserver: pendingSubject.asObservable(), }; } export function setupManualToggle() { let state = false; const enabledSubject = new Subject(); const pendingSubject = new Subject(); const toggle = async (forceState?: boolean) => { pendingSubject.next(true); state = forceState ?? !state; enabledSubject.next(state); pendingSubject.next(false); }; const className: string = prefixClass('button'); return { className, toggle, enabledObserver: enabledSubject.asObservable(), pendingObserver: pendingSubject.asObservable(), }; }