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

292 lines (263 loc) 8.54 kB
import { LocalParticipant, Room, RoomEvent, Track, type Participant, type TrackPublication, type RoomEventCallbacks, } from 'livekit-client'; import type { Subscriber, Subscription } from 'rxjs'; import { concat, filter, map, Observable, startWith } from 'rxjs'; import { log } from '../logger'; export function observeRoomEvents(room: Room, ...events: RoomEvent[]): Observable<Room> { const observable = new Observable<Room>((subscribe) => { const onRoomUpdate = () => { subscribe.next(room); }; events.forEach((evt) => { room.on(evt, onRoomUpdate); }); const unsubscribe = () => { events.forEach((evt) => { room.off(evt, onRoomUpdate); }); }; return unsubscribe; }).pipe(startWith(room)); return observable; } export function roomEventSelector<T extends RoomEvent>(room: Room, event: T) { const observable = new Observable<Parameters<RoomEventCallbacks[T]>>((subscribe) => { const update = (...params: Parameters<RoomEventCallbacks[T]>) => { subscribe.next(params); }; room.on(event as keyof RoomEventCallbacks, update); const unsubscribe = () => { room.off(event as keyof RoomEventCallbacks, update); }; return unsubscribe; }); return observable; } export function roomObserver(room: Room) { const observable = observeRoomEvents( room, RoomEvent.ParticipantConnected, RoomEvent.ParticipantDisconnected, RoomEvent.ActiveSpeakersChanged, RoomEvent.TrackSubscribed, RoomEvent.TrackUnsubscribed, RoomEvent.LocalTrackPublished, RoomEvent.LocalTrackUnpublished, RoomEvent.AudioPlaybackStatusChanged, RoomEvent.ConnectionStateChanged, ); return observable; } export function connectionStateObserver(room: Room) { return roomEventSelector(room, RoomEvent.ConnectionStateChanged).pipe( map(([connectionState]) => connectionState), startWith(room.state), ); } export type ScreenShareTrackMap = Array<{ participant: Participant; tracks: Array<TrackPublication>; }>; export function screenShareObserver(room: Room) { let screenShareSubscriber: Subscriber<ScreenShareTrackMap>; const observers: Array<Subscription> = []; const observable = new Observable<ScreenShareTrackMap>((subscriber) => { screenShareSubscriber = subscriber; return () => { observers.forEach((observer) => { observer.unsubscribe(); }); }; }); const screenShareTracks: ScreenShareTrackMap = []; const handleSub = (publication: TrackPublication, participant: Participant) => { if ( publication.source !== Track.Source.ScreenShare && publication.source !== Track.Source.ScreenShareAudio ) { return; } let trackMap = screenShareTracks.find((tr) => tr.participant.identity === participant.identity); const getScreenShareTracks = (participant: Participant) => { return participant .getTrackPublications() .filter( (track) => (track.source === Track.Source.ScreenShare || track.source === Track.Source.ScreenShareAudio) && track.track, ); }; if (!trackMap) { trackMap = { participant, tracks: getScreenShareTracks(participant), }; } else { const index = screenShareTracks.indexOf(trackMap); screenShareTracks.splice(index, 1); trackMap.tracks = getScreenShareTracks(participant); } if (trackMap.tracks.length > 0) { screenShareTracks.push(trackMap); } screenShareSubscriber.next(screenShareTracks); }; observers.push( roomEventSelector(room, RoomEvent.TrackSubscribed).subscribe(([, ...args]) => handleSub(...args), ), ); observers.push( roomEventSelector(room, RoomEvent.TrackUnsubscribed).subscribe(([, ...args]) => handleSub(...args), ), ); observers.push( roomEventSelector(room, RoomEvent.LocalTrackPublished).subscribe((args) => handleSub(...args)), ); observers.push( roomEventSelector(room, RoomEvent.LocalTrackUnpublished).subscribe((args) => { handleSub(...args); }), ); observers.push( roomEventSelector(room, RoomEvent.TrackMuted).subscribe((args) => { handleSub(...args); }), ); observers.push( roomEventSelector(room, RoomEvent.TrackUnmuted).subscribe((args) => { handleSub(...args); }), ); setTimeout(() => { // TODO find way to avoid this timeout for (const p of room.remoteParticipants.values()) { p.getTrackPublications().forEach((track) => { handleSub(track, p); }); } }, 1); return observable; } export function roomInfoObserver(room: Room) { const observer = observeRoomEvents( room, RoomEvent.RoomMetadataChanged, RoomEvent.ConnectionStateChanged, ).pipe( map((r) => { return { name: r.name, metadata: r.metadata }; }), ); return observer; } export function activeSpeakerObserver(room: Room) { return roomEventSelector(room, RoomEvent.ActiveSpeakersChanged).pipe( map(([speakers]) => speakers), ); } export function createMediaDeviceObserver( kind?: MediaDeviceKind, onError?: (e: Error) => void, requestPermissions = true, ) { // Initial devices fetch observable const initialDevices$ = new Observable<MediaDeviceInfo[]>((subscriber) => { Room.getLocalDevices(kind, requestPermissions) .then((devices) => { subscriber.next(devices); subscriber.complete(); }) .catch((e) => { onError?.(e); subscriber.next([] as MediaDeviceInfo[]); subscriber.complete(); }); }); // Device change observable const deviceChanges$ = new Observable<MediaDeviceInfo[]>((subscriber) => { const onDeviceChange = async () => { try { const newDevices = await Room.getLocalDevices(kind, requestPermissions); subscriber.next(newDevices); } catch (e: any) { onError?.(e); } }; if (typeof window !== 'undefined') { if (!window.isSecureContext) { throw new Error( `Accessing media devices is available only in secure contexts (HTTPS and localhost), in some or all supporting browsers. See: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mediaDevices`, ); } navigator?.mediaDevices?.addEventListener('devicechange', onDeviceChange); } // Return unsubscribe function that cleans up when the observable is unsubscribed return () => { navigator?.mediaDevices?.removeEventListener('devicechange', onDeviceChange); }; }); // Combine both observables return concat(initialDevices$, deviceChanges$); } export function createDataObserver(room: Room) { return roomEventSelector(room, RoomEvent.DataReceived); } export function createChatObserver(room: Room) { return roomEventSelector(room, RoomEvent.ChatMessage); } export function roomAudioPlaybackAllowedObservable(room: Room) { const observable = observeRoomEvents(room, RoomEvent.AudioPlaybackStatusChanged).pipe( map((room) => { return { canPlayAudio: room.canPlaybackAudio }; }), ); return observable; } export function roomVideoPlaybackAllowedObservable(room: Room) { const observable = observeRoomEvents(room, RoomEvent.VideoPlaybackStatusChanged).pipe( map((room) => { return { canPlayVideo: room.canPlaybackVideo }; }), ); return observable; } export function createActiveDeviceObservable(room: Room, kind: MediaDeviceKind) { return roomEventSelector(room, RoomEvent.ActiveDeviceChanged).pipe( filter(([kindOfDevice]) => kindOfDevice === kind), map(([kind, deviceId]) => { log.debug('activeDeviceObservable | RoomEvent.ActiveDeviceChanged', { kind, deviceId }); return deviceId; }), ); } export function encryptionStatusObservable(room: Room, participant: Participant | undefined) { return roomEventSelector(room, RoomEvent.ParticipantEncryptionStatusChanged).pipe( filter( ([, p]) => participant?.identity === p?.identity || (!p && participant?.identity === room.localParticipant.identity), ), map(([encrypted]) => encrypted), startWith( participant?.isLocal ? (participant as LocalParticipant).isE2EEEnabled : !!participant?.isEncrypted, ), ); } export function recordingStatusObservable(room: Room) { return roomEventSelector(room, RoomEvent.RecordingStatusChanged).pipe( map(([recording]) => recording), startWith(room.isRecording), ); }