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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
39377fb6e1
commit
444af5f7ac
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user