/* Copyright 2026 Element Creations Ltd. SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { ConnectionState as LivekitConnectionState, type RemoteParticipant, type Room as LivekitRoom, RoomEvent, } from "livekit-client"; import { distinctUntilChanged, filter, fromEvent, map, merge, type Observable, share, switchMap, } from "rxjs"; import { type Behavior } from "../../reactive/Behavior"; import { type Epoch, type ObservableScope, } from "../../reactive/ObservableScope"; import { type DataPacket } from "../api"; import { type Connection } from "./Connection"; import { type ConnectionMap } from "./ConnectionMap"; interface Props { scope: ObservableScope; connectionMap$: Behavior>; /** The local member's connection, which carries what we send. */ connection$: Behavior; } export interface DataChannel { data$: Observable; sendData: (topic: string, text: string) => Promise; } /** * A text channel beside the media: LiveKit's reliable data packets, received * on every connection the client holds and sent on the local member's. The * sender is reported by its identity; the packet's own claim of who sent it * never reaches the host. */ export function createDataChannel$({ scope, connectionMap$, connection$, }: Props): DataChannel { const decoder = new TextDecoder(); const encoder = new TextEncoder(); const rooms$ = connectionMap$.pipe( map(({ value }) => value.getConnections().map((c) => c.livekitRoom)), distinctUntilChanged( (a, b) => a.length === b.length && a.every((room, i) => room === b[i]), ), ); const data$ = rooms$.pipe( switchMap((rooms) => merge(...rooms.map(receivedOn))), map(({ payload, participant, topic }): DataPacket | null => // Only the server sends without a participant; a host has no use for it participant === undefined ? null : { senderId: participant.identity, topic: topic ?? "", text: decoder.decode(payload), }, ), filter((packet) => packet !== null), scope.bind(), share(), ); const sendData = async (topic: string, text: string): Promise => { const connection = connection$.value; const state = connection?.state$.value; if ( connection === null || state?.state !== "authenticated" || state.livekitState !== LivekitConnectionState.Connected ) throw new Error("Cannot send data: the local transport is not connected"); await connection.livekitRoom.localParticipant.publishData( encoder.encode(text), { reliable: true, topic }, ); }; return { data$, sendData }; } interface Received { payload: Uint8Array; participant: RemoteParticipant | undefined; topic: string | undefined; } function receivedOn(room: LivekitRoom): Observable { return fromEvent( room, RoomEvent.DataReceived, ( payload: Uint8Array, participant?: RemoteParticipant, _kind?: unknown, topic?: string, ): Received => ({ payload, participant, topic }), ); }