@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
text/typescript
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),
);
}