@livekit/components-core
Version:
This package is a wrapper around the [livekit/client-sdk-js](https://github.com/livekit/client-sdk-js) package. It transforms the event based logic into simple to use observable state and component level APIs. This package is the core for all framework sp
146 lines (133 loc) • 4.36 kB
text/typescript
import type {
AudioCaptureOptions,
LocalParticipant,
Room,
ScreenShareCaptureOptions,
TrackPublishOptions,
VideoCaptureOptions,
} from 'livekit-client';
import { Track } from '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 ToggleSource> = T extends Track.Source.Camera
? VideoCaptureOptions
: T extends Track.Source.Microphone
? AudioCaptureOptions
: T extends Track.Source.ScreenShare
? ScreenShareCaptureOptions
: never;
export type MediaToggleType<T extends ToggleSource> = {
pendingObserver: Observable<boolean>;
toggle: (
forceState?: boolean,
captureOptions?: CaptureOptionsBySource<T>,
) => Promise<boolean | undefined>;
className: string;
enabledObserver: Observable<boolean>;
};
export type ToggleSource = Exclude<
Track.Source,
Track.Source.ScreenShareAudio | Track.Source.Unknown
>;
export function setupMediaToggle<T extends ToggleSource>(
source: T,
room: Room,
options?: CaptureOptionsBySource<T>,
publishOptions?: TrackPublishOptions,
onError?: (error: Error) => void,
): MediaToggleType<T> {
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<boolean>();
const toggle = async (forceState?: boolean, captureOptions?: CaptureOptionsBySource<T>) => {
try {
captureOptions ??= options;
// trigger observable update
pendingSubject.next(true);
switch (source) {
case Track.Source.Camera:
await localParticipant.setCameraEnabled(
forceState ?? !localParticipant.isCameraEnabled,
captureOptions as VideoCaptureOptions,
publishOptions,
);
return localParticipant.isCameraEnabled;
case Track.Source.Microphone:
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,
);
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<boolean>();
const pendingSubject = new Subject<boolean>();
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(),
};
}