@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
126 lines (106 loc) • 4.15 kB
text/typescript
import { RoomEvent, type Room, type TextStreamInfo } from 'livekit-client';
import { from, scan, Subject, type Observable } from 'rxjs';
import { share, tap } from 'rxjs/operators';
import { ParticipantAgentAttributes } from '../helper';
export interface TextStreamData {
text: string;
participantInfo: { identity: string }; // Replace with the correct type from livekit-client
streamInfo: TextStreamInfo;
}
// Singleton getters for lazy initialization
let observableCacheInstance: Map<string, Observable<TextStreamData[]>> | null = null;
let roomInstanceMapInstance: WeakMap<Room, string> | null = null;
let nextRoomId = 0;
// Get or create the observable cache
function getObservableCache(): Map<string, Observable<TextStreamData[]>> {
if (!observableCacheInstance) {
observableCacheInstance = new Map<string, Observable<TextStreamData[]>>();
}
return observableCacheInstance;
}
// Get or create the room instance map
function getRoomInstanceMap(): WeakMap<Room, string> {
if (!roomInstanceMapInstance) {
roomInstanceMapInstance = new WeakMap<Room, string>();
}
return roomInstanceMapInstance;
}
// Helper to generate cache key
function getCacheKey(room: Room, topic: string): string {
const instanceMap = getRoomInstanceMap();
// Get or create a unique ID for this room instance
let roomId = instanceMap.get(room);
if (!roomId) {
roomId = `room_${nextRoomId++}`;
instanceMap.set(room, roomId);
}
return `${roomId}:${topic}`;
}
export function setupTextStream(room: Room, topic: string): Observable<TextStreamData[]> {
const cacheKey = getCacheKey(room, topic);
const observableCache = getObservableCache();
// Check if we already have an observable for this room and topic
const existingObservable = observableCache.get(cacheKey);
if (existingObservable) {
return existingObservable;
}
const textStreamsSubject = new Subject<TextStreamData[]>();
let textStreams: TextStreamData[] = [];
const segmentAttribute = ParticipantAgentAttributes.TranscriptionSegmentId;
// Create shared observable and store in cache
const sharedObservable = textStreamsSubject.pipe(
tap({
subscribe: () => {
room.registerTextStreamHandler(topic, async (reader, participantInfo) => {
// Create an observable from the reader
const streamObservable = from(reader).pipe(
scan((acc: string, chunk: string) => {
return acc + chunk;
}, ''),
);
const isTranscription = !!reader.info.attributes?.[segmentAttribute];
// Subscribe to the stream and update our array when new chunks arrive
streamObservable.subscribe((accumulatedText) => {
// Find and update the stream in our array
const index = textStreams.findIndex(
(stream) =>
stream.streamInfo.id === reader.info.id ||
(isTranscription &&
stream.streamInfo.attributes?.[segmentAttribute] ===
reader.info.attributes?.[segmentAttribute]),
);
if (index !== -1) {
textStreams[index] = {
...textStreams[index],
text: accumulatedText,
};
// Emit the updated array
textStreamsSubject.next([...textStreams]);
} else {
// Handle case where stream ID wasn't found (new stream)
textStreams.push({
text: accumulatedText,
participantInfo,
streamInfo: reader.info,
});
// Emit the updated array with the new stream
textStreamsSubject.next([...textStreams]);
}
});
});
},
finalize: () => {
room.unregisterTextStreamHandler(topic);
},
}),
share(),
);
observableCache.set(cacheKey, sharedObservable);
// Add cleanup when room is disconnected
room.on(RoomEvent.Disconnected, () => {
getObservableCache().delete(cacheKey);
textStreams = [];
textStreamsSubject.next([]);
});
return sharedObservable;
}