/* Copyright 2026 Lotus Guild SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ import { combineLatest, of, type Subscription } from "rxjs"; import { distinctUntilChanged, map, switchMap, throttleTime } from "rxjs/operators"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { LotusWidgetActions, lotusFlag, lotusSendToHost } from "./lotusWidget"; interface ParticipantState { /** EC media id (`${userId}:${deviceId}`), stable per participant device. */ id: string; /** Matrix user id this media belongs to. */ userId: string; speaking: boolean; audioEnabled: boolean; videoEnabled: boolean; } /** * Stream per-participant speaking / mute / camera state to the Lotus host * (cinny) over the widget API, so the host can drive speaking rings, mute * badges and PiP from real events instead of scraping Element Call's rendered * DOM (`useCallSpeakers.ts`). * * Opt-in: does nothing unless the host set `lotusCallState=1` on the widget * URL. Returns a teardown function. */ export function startLotusCallState(vm: CallViewModel): () => void { if (!lotusFlag("lotusCallState")) return () => undefined; const sub: Subscription = vm.userMedia$ .pipe( switchMap((members) => members.length === 0 ? of([] as ParticipantState[]) : combineLatest( members.map((m) => combineLatest([ m.speaking$, m.audioEnabled$, m.videoEnabled$, ]).pipe( map( ([speaking, audioEnabled, videoEnabled]): ParticipantState => ({ id: m.id, userId: m.userId, speaking, audioEnabled, videoEnabled, }), ), ), ), ), ), // `speaking` flips rapidly; cap the send rate and drop no-op repeats. // 250ms is plenty for speaking rings / mute badges and keeps the // request/response widget traffic modest. throttleTime(250, undefined, { leading: true, trailing: true }), distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), ) .subscribe((participants) => { lotusSendToHost(LotusWidgetActions.CallState, { participants }); }); return () => sub.unsubscribe(); }