UNPKG

@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

322 lines (293 loc) 10 kB
import type { ParticipantPermission } from '@livekit/protocol'; import { Participant, RemoteParticipant, Room, TrackPublication, ParticipantEvent, RoomEvent, Track, type ParticipantEventCallbacks, } from 'livekit-client'; import type { Subscriber } from 'rxjs'; import { Observable, map, startWith, switchMap } from 'rxjs'; import { getTrackByIdentifier } from '../components/mediaTrack'; import { allParticipantEvents, allParticipantRoomEvents } from '../helper/eventGroups'; import type { TrackReferenceOrPlaceholder } from '../track-reference'; import type { ParticipantIdentifier, TrackIdentifier } from '../types'; import { observeRoomEvents } from './room'; export function observeParticipantEvents<T extends Participant>( participant: T, ...events: ParticipantEvent[] ) { const observable = new Observable<T>((subscribe) => { const onParticipantUpdate = () => { subscribe.next(participant); }; events.forEach((evt) => { participant.on(evt as keyof ParticipantEventCallbacks, onParticipantUpdate); }); const unsubscribe = () => { events.forEach((evt) => { participant.off(evt as keyof ParticipantEventCallbacks, onParticipantUpdate); }); }; return unsubscribe; }).pipe(startWith(participant)); return observable; } export interface ParticipantMedia<T extends Participant = Participant> { isCameraEnabled: boolean; isMicrophoneEnabled: boolean; isScreenShareEnabled: boolean; microphoneTrack?: TrackPublication; cameraTrack?: TrackPublication; participant: T; } export function observeParticipantMedia<T extends Participant>(participant: T) { const participantObserver = observeParticipantEvents( participant, ParticipantEvent.TrackMuted, ParticipantEvent.TrackUnmuted, ParticipantEvent.ParticipantPermissionsChanged, // ParticipantEvent.IsSpeakingChanged, ParticipantEvent.TrackPublished, ParticipantEvent.TrackUnpublished, ParticipantEvent.LocalTrackPublished, ParticipantEvent.LocalTrackUnpublished, ParticipantEvent.MediaDevicesError, ParticipantEvent.TrackSubscriptionStatusChanged, // ParticipantEvent.ConnectionQualityChanged, ).pipe( map((p) => { const { isMicrophoneEnabled, isCameraEnabled, isScreenShareEnabled } = p; const microphoneTrack = p.getTrackPublication(Track.Source.Microphone); const cameraTrack = p.getTrackPublication(Track.Source.Camera); const participantMedia: ParticipantMedia<T> = { isCameraEnabled, isMicrophoneEnabled, isScreenShareEnabled, cameraTrack, microphoneTrack, participant: p, }; return participantMedia; }), ); return participantObserver; } export function createTrackObserver(participant: Participant, options: TrackIdentifier) { return observeParticipantMedia(participant).pipe( map(() => { return { publication: getTrackByIdentifier(options) }; }), ); } export function participantInfoObserver(participant?: Participant) { if (!participant) { return undefined; } const observer = observeParticipantEvents( participant, ParticipantEvent.ParticipantMetadataChanged, ParticipantEvent.ParticipantNameChanged, ).pipe( map(({ name, identity, metadata }) => { return { name, identity, metadata, }; }), startWith({ name: participant.name, identity: participant.identity, metadata: participant.metadata, }), ); return observer; } export function createConnectionQualityObserver(participant: Participant) { const observer = participantEventSelector( participant, ParticipantEvent.ConnectionQualityChanged, ).pipe( map(([quality]) => quality), startWith(participant.connectionQuality), ); return observer; } export function participantEventSelector<T extends ParticipantEvent>( participant: Participant, event: T, ) { const observable = new Observable< Parameters<ParticipantEventCallbacks[Extract<T, keyof ParticipantEventCallbacks>]> >((subscribe) => { const update = ( ...params: Parameters<ParticipantEventCallbacks[Extract<T, keyof ParticipantEventCallbacks>]> ) => { subscribe.next(params); }; // @ts-expect-error not a perfect overlap between ParticipantEvent and keyof ParticipantEventCallbacks participant.on(event, update); const unsubscribe = () => { // @ts-expect-error not a perfect overlap between ParticipantEvent and keyof ParticipantEventCallbacks participant.off(event, update); }; return unsubscribe; }); return observable; } export function mutedObserver(trackRef: TrackReferenceOrPlaceholder) { return observeParticipantEvents( trackRef.participant, ParticipantEvent.TrackMuted, ParticipantEvent.TrackUnmuted, ParticipantEvent.TrackSubscribed, ParticipantEvent.TrackUnsubscribed, ParticipantEvent.LocalTrackPublished, ParticipantEvent.LocalTrackUnpublished, ).pipe( map((participant) => { const pub = trackRef.publication ?? participant.getTrackPublication(trackRef.source); return pub?.isMuted ?? true; }), startWith( trackRef.publication?.isMuted ?? trackRef.participant.getTrackPublication(trackRef.source)?.isMuted ?? true, ), ); } export function createIsSpeakingObserver(participant: Participant) { return participantEventSelector(participant, ParticipantEvent.IsSpeakingChanged).pipe( map(([isSpeaking]) => isSpeaking), ); } type ConnectedParticipantsObserverOptions = { additionalRoomEvents?: RoomEvent[]; }; export function connectedParticipantsObserver( room: Room, options: ConnectedParticipantsObserverOptions = {}, ) { let subscriber: Subscriber<RemoteParticipant[]> | undefined; const observable = new Observable<RemoteParticipant[]>((sub) => { subscriber = sub; return () => listener.unsubscribe(); }).pipe(startWith(Array.from(room.remoteParticipants.values()))); const additionalRoomEvents = options.additionalRoomEvents ?? allParticipantRoomEvents; const roomEvents = Array.from( new Set([ RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ConnectionStateChanged, ...additionalRoomEvents, ]), ); const listener = observeRoomEvents(room, ...roomEvents).subscribe((r) => subscriber?.next(Array.from(r.remoteParticipants.values())), ); if (room.remoteParticipants.size > 0) { subscriber?.next(Array.from(room.remoteParticipants.values())); } return observable; } export type ConnectedParticipantObserverOptions = { additionalEvents?: ParticipantEvent[]; }; export function connectedParticipantObserver( room: Room, identity: string, options: ConnectedParticipantObserverOptions = {}, ) { const additionalEvents = options.additionalEvents ?? allParticipantEvents; const observable = observeRoomEvents( room, RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ConnectionStateChanged, ).pipe( switchMap((r) => { const participant = r.getParticipantByIdentity(identity) as RemoteParticipant | undefined; if (participant) { return observeParticipantEvents(participant, ...additionalEvents); } else { return new Observable<undefined>((subscribe) => subscribe.next(undefined)); } }), startWith(room.getParticipantByIdentity(identity) as RemoteParticipant | undefined), ); return observable; } export function participantPermissionObserver( participant: Participant, ): Observable<ParticipantPermission | undefined> { const observer = participantEventSelector( participant, ParticipantEvent.ParticipantPermissionsChanged, ).pipe( map(() => participant.permissions), startWith(participant.permissions), ); return observer; } export function participantByIdentifierObserver( room: Room, { kind, identity }: ParticipantIdentifier, options: ConnectedParticipantObserverOptions = {}, ): Observable<RemoteParticipant | undefined> { const additionalEvents = options.additionalEvents ?? allParticipantEvents; const matchesIdentifier = (participant: RemoteParticipant) => { let isMatch = true; if (kind) { isMatch = isMatch && participant.kind === kind; } if (identity) { isMatch = isMatch && participant.identity === identity; } return isMatch; }; const observable = observeRoomEvents( room, RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ConnectionStateChanged, ).pipe( switchMap((r) => { const participant = Array.from(r.remoteParticipants.values()).find((p) => matchesIdentifier(p), ); if (participant) { return observeParticipantEvents(participant, ...additionalEvents); } else { return new Observable<undefined>((subscribe) => subscribe.next(undefined)); } }), startWith(Array.from(room.remoteParticipants.values()).find((p) => matchesIdentifier(p))), ); return observable; } export function participantAttributesObserver(participant: Participant): Observable<{ changed: Readonly<Record<string, string>>; attributes: Readonly<Record<string, string>>; }>; export function participantAttributesObserver(participant: undefined): Observable<{ changed: undefined; attributes: undefined; }>; export function participantAttributesObserver(participant: Participant | undefined) { if (typeof participant === 'undefined') { return new Observable<{ changed: undefined; attributes: undefined }>(); } return participantEventSelector(participant, ParticipantEvent.AttributesChanged).pipe( map(([changedAttributes]) => { return { changed: changedAttributes as Readonly<Record<string, string>>, attributes: participant.attributes, }; }), startWith({ changed: participant.attributes, attributes: participant.attributes }), ); }