Files
element-call/src/lotus/lotusCallState.ts
T
Lotus CIandClaude Opus 4.8 444af5f7ac
CI / Build embedded bundle (push) Successful in 1m45s
CI / Publish to Gitea npm registry (push) Has been skipped
lotus(#2): stream per-participant call state over widget API
Opt-in (lotusCallState=1) bridge that emits io.lotus.call_state with each
participant's speaking/audio/video state, so the Lotus host can drive
speaking rings / mute badges / PiP from real events instead of scraping
EC's rendered DOM. Exposes vm.userMedia$ on the public CallViewModel.
Additive: no-op without the flag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 23:05:20 -04:00

71 lines
2.3 KiB
TypeScript

/*
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 {
/** Opaque media id (stable per tile). */
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.
throttleTime(150, undefined, { leading: true, trailing: true }),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
)
.subscribe((participants) => {
lotusSendToHost(LotusWidgetActions.CallState, { participants });
});
return () => sub.unsubscribe();
}