pull out all screen share related logic.

This commit is contained in:
Timo K
2025-11-07 08:44:44 +01:00
parent 7c41aef801
commit 92fdce33ea
17 changed files with 461 additions and 310 deletions

View File

@@ -58,7 +58,10 @@ import { type MuteStates } from "../state/MuteStates";
import { type MatrixInfo } from "./VideoPreview"; import { type MatrixInfo } from "./VideoPreview";
import { InviteButton } from "../button/InviteButton"; import { InviteButton } from "../button/InviteButton";
import { LayoutToggle } from "./LayoutToggle"; import { LayoutToggle } from "./LayoutToggle";
import { CallViewModel, type GridMode } from "../state/CallViewModel"; import {
CallViewModel,
type GridMode,
} from "../state/CallViewModel/CallViewModel.ts";
import { Grid, type TileProps } from "../grid/Grid"; import { Grid, type TileProps } from "../grid/Grid";
import { useInitial } from "../useInitial"; import { useInitial } from "../useInitial";
import { SpotlightTile } from "../tile/SpotlightTile"; import { SpotlightTile } from "../tile/SpotlightTile";

View File

@@ -0,0 +1,210 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import {
type CallMembership,
type MatrixRTCSession,
MatrixRTCSessionEvent,
type MatrixRTCSessionEventHandlerMap,
} from "matrix-js-sdk/lib/matrixrtc";
import {
combineLatest,
concat,
endWith,
filter,
fromEvent,
ignoreElements,
map,
merge,
NEVER,
type Observable,
of,
pairwise,
startWith,
switchMap,
takeUntil,
timer,
} from "rxjs";
import {
type EventTimelineSetHandlerMap,
EventType,
type Room as MatrixRoom,
RoomEvent,
} from "matrix-js-sdk";
import { type Behavior } from "../Behavior";
import { type Epoch, mapEpoch, type ObservableScope } from "../ObservableScope";
export type AutoLeaveReason = "allOthersLeft" | "timeout" | "decline";
export type CallPickupState =
| "unknown"
| "ringing"
| "timeout"
| "decline"
| "success"
| null;
export type CallNotificationWrapper = Parameters<
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
>;
export function createSentCallNotification$(
scope: ObservableScope,
matrixRTCSession: MatrixRTCSession,
): Behavior<CallNotificationWrapper | null> {
const sentCallNotification$ = scope.behavior(
fromEvent(matrixRTCSession, MatrixRTCSessionEvent.DidSendCallNotification),
null,
) as Behavior<CallNotificationWrapper | null>;
return sentCallNotification$;
}
export function createReceivedDecline$(
matrixRoom: MatrixRoom,
): Observable<Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>> {
return (
fromEvent(matrixRoom, RoomEvent.Timeline) as Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>
).pipe(filter(([event]) => event.getType() === EventType.RTCDecline));
}
interface Props {
scope: ObservableScope;
memberships$: Behavior<Epoch<CallMembership[]>>;
sentCallNotification$: Observable<CallNotificationWrapper | null>;
receivedDecline$: Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>;
options: { waitForCallPickup?: boolean; autoLeaveWhenOthersLeft?: boolean };
localUser: { deviceId: string; userId: string };
}
/**
* @returns {callPickupState$, autoLeave$}
* `callPickupState$` The current call pickup state of the call.
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
* Then we can conclude if we were the first one to join or not.
* This may also be set if we are disconnected.
* - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening).
* - "timeout": No-one picked up in the defined time this call should be ringing on others devices.
* The call failed. If desired this can be used as a trigger to exit the call.
* - "success": Someone else joined. The call is in a normal state. No audiovisual feedback.
* - null: EC is configured to never show any waiting for answer state.
*
* `autoLeave$` An observable that emits (null) when the call should be automatically left.
* - if options.autoLeaveWhenOthersLeft is set to true it emits when all others left.
* - if options.waitForCallPickup is set to true it emits if noone picked up the ring or if the ring got declined.
* - if options.autoLeaveWhenOthersLeft && options.waitForCallPickup is false it will never emit.
*
*/
export function createCallNotificationLifecycle$({
scope,
memberships$,
sentCallNotification$,
receivedDecline$,
options,
localUser,
}: Props): {
callPickupState$: Behavior<CallPickupState>;
autoLeave$: Observable<AutoLeaveReason>;
} {
// TODO convert all ring and all others left logic into one callLifecycleTracker$(didSendCallNotification$,matrixLivekitItem$): {autoLeave$,callPickupState$}
const allOthersLeft$ = memberships$.pipe(
pairwise(),
filter(
([{ value: prev }, { value: current }]) =>
current.every((m) => m.userId === localUser.userId) &&
prev.some((m) => m.userId !== localUser.userId),
),
map(() => {}),
);
/**
* Whether some Matrix user other than ourself is joined to the call.
*/
const someoneElseJoined$ = memberships$.pipe(
mapEpoch((ms) => ms.some((m) => m.userId !== localUser.userId)),
) as Behavior<Epoch<boolean>>;
/**
* Whenever the RTC session tells us that it intends to ring the remote
* participant's devices, this emits an Observable tracking the current state of
* that ringing process.
*/
// This is a behavior since we need to store the latest state for when we subscribe to this after `didSendCallNotification$`
// has already emitted but we still need the latest observable with a timeout timer that only gets created on after receiving `notificationEvent`.
// A behavior will emit the latest observable with the running timer to new subscribers.
// see also: callPickupState$ and in particular the line: `return this.ring$.pipe(mergeAll());` here we otherwise might get an EMPTY observable if
// `ring$` would not be a behavior.
const remoteRingState$: Behavior<"ringing" | "timeout" | "decline" | null> =
scope.behavior(
sentCallNotification$.pipe(
filter(
(newAndLegacyEvents) =>
// only care about new events (legacy do not have decline pattern)
newAndLegacyEvents?.[0].notification_type === "ring",
),
map((e) => e as CallNotificationWrapper),
switchMap(([notificastionEvent]) => {
const lifetimeMs = notificationEvent?.lifetime ?? 0;
return concat(
lifetimeMs === 0
? // If no lifetime, skip the ring state
of(null)
: // Ring until lifetime ms have passed
timer(lifetimeMs).pipe(
ignoreElements(),
startWith("ringing" as const),
),
// The notification lifetime has timed out, meaning ringing has likely
// stopped on all receiving clients.
of("timeout" as const),
// This makes sure we will not drop into the `endWith("decline" as const)` state
NEVER,
).pipe(
takeUntil(
receivedDecline$.pipe(
filter(
([event]) =>
event.getRelation()?.rel_type === "m.reference" &&
event.getRelation()?.event_id ===
notificationEvent.event_id &&
event.getSender() !== localUser.userId,
),
),
),
endWith("decline" as const),
);
}),
),
null,
);
const callPickupState$ = scope.behavior(
options.waitForCallPickup === true
? combineLatest(
[someoneElseJoined$, remoteRingState$],
(someoneElseJoined, ring) => {
if (someoneElseJoined) {
return "success" as const;
}
// Show the ringing state of the most recent ringing attempt.
// as long as we have not yet sent an RTC notification event, ring will be null -> callPickupState$ = unknown.
return ring ?? ("unknown" as const);
},
)
: NEVER,
null,
);
const autoLeave$ = merge(
options.autoLeaveWhenOthersLeft === true
? allOthersLeft$.pipe(map(() => "allOthersLeft" as const))
: NEVER,
callPickupState$.pipe(
filter((state) => state === "timeout" || state === "decline"),
),
);
return { autoLeave$, callPickupState$ };
}

View File

@@ -7,29 +7,20 @@ Please see LICENSE in the repository root for full details.
import { import {
type BaseKeyProvider, type BaseKeyProvider,
ConnectionState, type ConnectionState,
type E2EEOptions, type E2EEOptions,
ExternalE2EEKeyProvider, ExternalE2EEKeyProvider,
type Room as LivekitRoom, type Room as LivekitRoom,
type RoomOptions, type RoomOptions,
} from "livekit-client"; } from "livekit-client";
import E2EEWorker from "livekit-client/e2ee-worker?worker"; import E2EEWorker from "livekit-client/e2ee-worker?worker";
import { import { type Room as MatrixRoom } from "matrix-js-sdk";
type EventTimelineSetHandlerMap,
EventType,
type Room as MatrixRoom,
RoomEvent,
} from "matrix-js-sdk";
import { import {
combineLatest, combineLatest,
concat,
distinctUntilChanged, distinctUntilChanged,
EMPTY, EMPTY,
endWith,
filter, filter,
from,
fromEvent, fromEvent,
ignoreElements,
map, map,
merge, merge,
NEVER, NEVER,
@@ -46,18 +37,12 @@ import {
switchMap, switchMap,
switchScan, switchScan,
take, take,
takeUntil,
takeWhile,
tap, tap,
throttleTime, throttleTime,
timer, timer,
} from "rxjs"; } from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
type MatrixRTCSession,
MatrixRTCSessionEvent,
type MatrixRTCSessionEventHandlerMap,
} from "matrix-js-sdk/lib/matrixrtc";
import { type IWidgetApiRequest } from "matrix-widget-api"; import { type IWidgetApiRequest } from "matrix-widget-api";
import { import {
@@ -66,39 +51,39 @@ import {
type RemoteUserMediaViewModel, type RemoteUserMediaViewModel,
ScreenShareViewModel, ScreenShareViewModel,
type UserMediaViewModel, type UserMediaViewModel,
} from "./MediaViewModel"; } from "../MediaViewModel";
import { accumulate, generateKeyed$, pauseWhen } from "../utils/observable"; import { accumulate, generateKeyed$, pauseWhen } from "../../utils/observable";
import { import {
duplicateTiles, duplicateTiles,
MatrixRTCMode, MatrixRTCMode,
matrixRTCMode, matrixRTCMode,
playReactionsSound, playReactionsSound,
showReactions, showReactions,
} from "../settings/settings"; } from "../../settings/settings";
import { isFirefox } from "../Platform"; import { isFirefox } from "../../Platform";
import { setPipEnabled$ } from "../controls"; import { setPipEnabled$ } from "../../controls";
import { TileStore } from "./TileStore"; import { TileStore } from "../TileStore";
import { gridLikeLayout } from "./GridLikeLayout"; import { gridLikeLayout } from "../GridLikeLayout";
import { spotlightExpandedLayout } from "./SpotlightExpandedLayout"; import { spotlightExpandedLayout } from "../SpotlightExpandedLayout";
import { oneOnOneLayout } from "./OneOnOneLayout"; import { oneOnOneLayout } from "../OneOnOneLayout";
import { pipLayout } from "./PipLayout"; import { pipLayout } from "../PipLayout";
import { type EncryptionSystem } from "../e2ee/sharedKeyManagement"; import { type EncryptionSystem } from "../../e2ee/sharedKeyManagement";
import { import {
type RaisedHandInfo, type RaisedHandInfo,
type ReactionInfo, type ReactionInfo,
type ReactionOption, type ReactionOption,
} from "../reactions"; } from "../../reactions";
import { shallowEquals } from "../utils/array"; import { shallowEquals } from "../../utils/array";
import { type MediaDevices } from "./MediaDevices"; import { type MediaDevices } from "../MediaDevices";
import { type Behavior, constant } from "./Behavior"; import { type Behavior, constant } from "../Behavior";
import { E2eeType } from "../e2ee/e2eeType"; import { E2eeType } from "../../e2ee/e2eeType";
import { MatrixKeyProvider } from "../e2ee/matrixKeyProvider"; import { MatrixKeyProvider } from "../../e2ee/matrixKeyProvider";
import { type MuteStates } from "./MuteStates"; import { type MuteStates } from "../MuteStates";
import { getUrlParams } from "../UrlParams"; import { getUrlParams } from "../../UrlParams";
import { type ProcessorState } from "../livekit/TrackProcessorContext"; import { type ProcessorState } from "../../livekit/TrackProcessorContext";
import { ElementWidgetActions, widget } from "../widget"; import { ElementWidgetActions, widget } from "../../widget";
import { sharingScreen$, UserMedia } from "./UserMedia.ts"; import { UserMedia } from "../UserMedia.ts";
import { ScreenShare } from "./ScreenShare.ts"; import { ScreenShare } from "../ScreenShare.ts";
import { import {
type GridLayoutMedia, type GridLayoutMedia,
type Layout, type Layout,
@@ -107,18 +92,23 @@ import {
type SpotlightExpandedLayoutMedia, type SpotlightExpandedLayoutMedia,
type SpotlightLandscapeLayoutMedia, type SpotlightLandscapeLayoutMedia,
type SpotlightPortraitLayoutMedia, type SpotlightPortraitLayoutMedia,
} from "./layout-types.ts"; } from "../layout-types.ts";
import { type ElementCallError } from "../utils/errors.ts"; import { type ElementCallError } from "../../utils/errors.ts";
import { type ObservableScope } from "./ObservableScope.ts"; import { type ObservableScope } from "../ObservableScope.ts";
import { createLocalMembership$ } from "./localMember/LocalMembership.ts"; import { createLocalMembership$ } from "./localMember/LocalMembership.ts";
import { createLocalTransport$ } from "./localMember/LocalTransport.ts"; import { createLocalTransport$ } from "./localMember/LocalTransport.ts";
import { import {
createMemberships$, createMemberships$,
membershipsAndTransports$, membershipsAndTransports$,
} from "./SessionBehaviors.ts"; } from "../SessionBehaviors.ts";
import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts"; import { ECConnectionFactory } from "./remoteMembers/ConnectionFactory.ts";
import { createConnectionManager$ } from "./remoteMembers/ConnectionManager.ts"; import { createConnectionManager$ } from "./remoteMembers/ConnectionManager.ts";
import { createMatrixLivekitMembers$ } from "./remoteMembers/MatrixLivekitMembers.ts"; import { createMatrixLivekitMembers$ } from "./remoteMembers/MatrixLivekitMembers.ts";
import {
createCallNotificationLifecycle$,
createReceivedDecline$,
createSentCallNotification$,
} from "./CallNotificationLifecycle.ts";
const logger = rootLogger.getChild("[CallViewModel]"); const logger = rootLogger.getChild("[CallViewModel]");
//TODO //TODO
@@ -274,6 +264,23 @@ export class CallViewModel {
options: this.connectOptions$, options: this.connectOptions$,
}); });
// ------------------------------------------------------------------------
// CallNotificationLifecycle
private sentCallNotification$ = createSentCallNotification$(
this.scope,
this.matrixRTCSession,
);
private receivedDecline$ = createReceivedDecline$(this.matrixRoom);
private callLifecycle = createCallNotificationLifecycle$({
scope: this.scope,
memberships$: this.memberships$,
sentCallNotification$: this.sentCallNotification$,
receivedDecline$: this.receivedDecline$,
options: this.options,
localUser: { userId: this.userId, deviceId: this.deviceId },
});
/** /**
* If there is a configuration error with the call (e.g. misconfigured E2EE). * If there is a configuration error with the call (e.g. misconfigured E2EE).
* This is a fatal error that prevents the call from being created/joined. * This is a fatal error that prevents the call from being created/joined.
@@ -315,7 +322,7 @@ export class CallViewModel {
public readonly audioParticipants$ = this.scope.behavior( public readonly audioParticipants$ = this.scope.behavior(
this.matrixLivekitMembers$.pipe( this.matrixLivekitMembers$.pipe(
map((members) => members.map((m) => m.participant)), map((members) => members.value.map((m) => m.participant)),
), ),
); );
@@ -350,7 +357,7 @@ export class CallViewModel {
// Generate a collection of MediaItems from the list of expected (whether // Generate a collection of MediaItems from the list of expected (whether
// present or missing) LiveKit participants. // present or missing) LiveKit participants.
combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]), combineLatest([this.matrixLivekitMembers$, duplicateTiles.value$]),
([matrixLivekitMembers, duplicateTiles], createOrGet) => { ([{ value: matrixLivekitMembers }, duplicateTiles], createOrGet) => {
const items: MediaItem[] = []; const items: MediaItem[] = [];
for (const { for (const {
@@ -455,129 +462,11 @@ export class CallViewModel {
*/ */
// TODO KEEP THIS!! and adapt it to what our membershipManger returns // TODO KEEP THIS!! and adapt it to what our membershipManger returns
public readonly participantCount$ = this.scope.behavior( public readonly participantCount$ = this.scope.behavior(
this.memberships$.pipe(map((ms) => ms.length)), this.memberships$.pipe(map((ms) => ms.value.length)),
); );
// TODO convert all ring and all others left logic into one callLifecycleTracker$(didSendCallNotification$,matrixLivekitItem$): {autoLeave$,callPickupState$}
private readonly allOthersLeft$ = this.memberships$.pipe(
pairwise(),
filter(
([prev, current]) =>
current.every((m) => m.userId === this.userId) &&
prev.some((m) => m.userId !== this.userId),
),
map(() => {}),
);
private readonly didSendCallNotification$ = fromEvent(
this.matrixRTCSession,
MatrixRTCSessionEvent.DidSendCallNotification,
) as Observable<
Parameters<
MatrixRTCSessionEventHandlerMap[MatrixRTCSessionEvent.DidSendCallNotification]
>
>;
/**
* Whenever the RTC session tells us that it intends to ring the remote
* participant's devices, this emits an Observable tracking the current state of
* that ringing process.
*/
// This is a behavior since we need to store the latest state for when we subscribe to this after `didSendCallNotification$`
// has already emitted but we still need the latest observable with a timeout timer that only gets created on after receiving `notificationEvent`.
// A behavior will emit the latest observable with the running timer to new subscribers.
// see also: callPickupState$ and in particular the line: `return this.ring$.pipe(mergeAll());` here we otherwise might get an EMPTY observable if
// `ring$` would not be a behavior.
private readonly ring$: Behavior<"ringing" | "timeout" | "decline" | null> =
this.scope.behavior(
this.didSendCallNotification$.pipe(
filter(
([notificationEvent]) =>
notificationEvent.notification_type === "ring",
),
switchMap(([notificationEvent]) => {
const lifetimeMs = notificationEvent?.lifetime ?? 0;
return concat(
lifetimeMs === 0
? // If no lifetime, skip the ring state
of(null)
: // Ring until lifetime ms have passed
timer(lifetimeMs).pipe(
ignoreElements(),
startWith("ringing" as const),
),
// The notification lifetime has timed out, meaning ringing has likely
// stopped on all receiving clients.
of("timeout" as const),
// This makes sure we will not drop into the `endWith("decline" as const)` state
NEVER,
).pipe(
takeUntil(
(
fromEvent(this.matrixRoom, RoomEvent.Timeline) as Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>
).pipe(
filter(
([event]) =>
event.getType() === EventType.RTCDecline &&
event.getRelation()?.rel_type === "m.reference" &&
event.getRelation()?.event_id ===
notificationEvent.event_id &&
event.getSender() !== this.userId,
),
),
),
endWith("decline" as const),
);
}),
),
null,
);
/**
* Whether some Matrix user other than ourself is joined to the call.
*/
private readonly someoneElseJoined$ = this.memberships$.pipe(
map((ms) => ms.some((m) => m.userId !== this.userId)),
) as Behavior<boolean>;
/**
* The current call pickup state of the call.
* - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership.
* Then we can conclude if we were the first one to join or not.
* This may also be set if we are disconnected.
* - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening).
* - "timeout": No-one picked up in the defined time this call should be ringing on others devices.
* The call failed. If desired this can be used as a trigger to exit the call.
* - "success": Someone else joined. The call is in a normal state. No audiovisual feedback.
* - null: EC is configured to never show any waiting for answer state.
*/
public readonly callPickupState$: Behavior<
"unknown" | "ringing" | "timeout" | "decline" | "success" | null
> = this.options.waitForCallPickup
? this.scope.behavior<
"unknown" | "ringing" | "timeout" | "decline" | "success"
>(
combineLatest(
[this.livekitConnectionState$, this.someoneElseJoined$, this.ring$],
(livekitConnectionState, someoneElseJoined, ring) => {
if (livekitConnectionState === ConnectionState.Disconnected) {
// Do not ring until we're connected.
return "unknown" as const;
} else if (someoneElseJoined) {
return "success" as const;
}
// Show the ringing state of the most recent ringing attempt.
// as long as we have not yet sent an RTC notification event, ring will be null -> callPickupState$ = unknown.
return ring ?? ("unknown" as const);
},
),
)
: constant(null);
public readonly leaveSoundEffect$ = combineLatest([ public readonly leaveSoundEffect$ = combineLatest([
this.callPickupState$, this.callLifecycle.callPickupState$,
this.userMedia$, this.userMedia$,
]).pipe( ]).pipe(
// Until the call is successful, do not play a leave sound. // Until the call is successful, do not play a leave sound.
@@ -594,16 +483,6 @@ export class CallViewModel {
throttleTime(THROTTLE_SOUND_EFFECT_MS), throttleTime(THROTTLE_SOUND_EFFECT_MS),
); );
// Public for testing
public readonly autoLeave$ = merge(
this.options.autoLeaveWhenOthersLeft
? this.allOthersLeft$.pipe(map(() => "allOthersLeft" as const))
: NEVER,
this.callPickupState$.pipe(
filter((state) => state === "timeout" || state === "decline"),
),
);
private readonly userHangup$ = new Subject<void>(); private readonly userHangup$ = new Subject<void>();
public hangup(): void { public hangup(): void {
this.userHangup$.next(); this.userHangup$.next();
@@ -626,7 +505,7 @@ export class CallViewModel {
public readonly leave$: Observable< public readonly leave$: Observable<
"user" | "timeout" | "decline" | "allOthersLeft" "user" | "timeout" | "decline" | "allOthersLeft"
> = merge( > = merge(
this.autoLeave$, this.callLifecycle.autoLeave$,
merge(this.userHangup$, this.widgetHangup$).pipe( merge(this.userHangup$, this.widgetHangup$).pipe(
map(() => "user" as const), map(() => "user" as const),
), ),
@@ -717,6 +596,7 @@ export class CallViewModel {
private readonly pip$ = this.scope.behavior<UserMediaViewModel | null>( private readonly pip$ = this.scope.behavior<UserMediaViewModel | null>(
combineLatest([ combineLatest([
// TODO This also needs epoch logic to dedupe the screenshares and mediaItems emits
this.screenShares$, this.screenShares$,
this.spotlightSpeaker$, this.spotlightSpeaker$,
this.mediaItems$, this.mediaItems$,
@@ -1298,47 +1178,16 @@ export class CallViewModel {
/** /**
* Whether we are sharing our screen. * Whether we are sharing our screen.
*/ */
// TODO move to LocalMembership // reassigned here to make it publicly accessible
public readonly sharingScreen$ = this.scope.behavior( public readonly sharingScreen$ = this.localMembership.sharingScreen$;
from(this.localConnection$).pipe(
switchMap((c) =>
c?.state === "ready"
? sharingScreen$(c.value.livekitRoom.localParticipant)
: of(false),
),
),
);
/** /**
* Callback for toggling screen sharing. If null, screen sharing is not * Callback for toggling screen sharing. If null, screen sharing is not
* available. * available.
*/ */
// TODO move to LocalMembership // reassigned here to make it publicly accessible
public readonly toggleScreenSharing = public readonly toggleScreenSharing =
"getDisplayMedia" in (navigator.mediaDevices ?? {}) && this.localMembership.toggleScreenSharing;
!this.urlParams.hideScreensharing
? (): void =>
// Once a connection is ready...
void this.localConnection$
.pipe(
takeWhile((c) => c !== null && c.state !== "error"),
switchMap((c) => (c.state === "ready" ? of(c.value) : NEVER)),
take(1),
this.scope.bind(),
)
// ...toggle screen sharing.
.subscribe(
(c) =>
void c.livekitRoom.localParticipant
.setScreenShareEnabled(!this.sharingScreen$.value, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
})
.catch(logger.error),
)
: null;
public constructor( public constructor(
private readonly scope: ObservableScope, private readonly scope: ObservableScope,

View File

@@ -18,58 +18,63 @@ import {
combineLatest, combineLatest,
fromEvent, fromEvent,
map, map,
NEVER,
type Observable, type Observable,
of, of,
scan, scan,
startWith, startWith,
switchMap, switchMap,
take,
takeWhile,
tap, tap,
} from "rxjs"; } from "rxjs";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../Behavior"; import { sharingScreen$ as observeSharingScreen$ } from "../../UserMedia.ts";
import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "../remoteMembers/ConnectionManager"; import { type IConnectionManager } from "../remoteMembers/ConnectionManager";
import { ObservableScope } from "../ObservableScope"; import { ObservableScope } from "../../ObservableScope";
import { Publisher } from "./Publisher"; import { Publisher } from "./Publisher";
import { type MuteStates } from "../MuteStates"; import { type MuteStates } from "../../MuteStates";
import { type ProcessorState } from "../../livekit/TrackProcessorContext"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext";
import { type MediaDevices } from "../MediaDevices"; import { type MediaDevices } from "../../MediaDevices";
import { and$ } from "../../utils/observable"; import { and$ } from "../../../utils/observable";
import { import {
enterRTCSession, enterRTCSession,
type EnterRTCSessionOptions, type EnterRTCSessionOptions,
} from "../../rtcSessionHelpers"; } from "../../../rtcSessionHelpers";
import { type ElementCallError } from "../../utils/errors"; import { type ElementCallError } from "../../../utils/errors";
import { ElementWidgetActions, type WidgetHelpers } from "../../widget"; import { ElementWidgetActions, type WidgetHelpers } from "../../../widget";
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers"; import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers";
import { getUrlParams } from "../../../UrlParams.ts";
enum LivekitState { export enum LivekitState {
UNINITIALIZED = "uninitialized", Uninitialized = "uninitialized",
CONNECTING = "connecting", Connecting = "connecting",
CONNECTED = "connected", Connected = "connected",
ERROR = "error", Error = "error",
DISCONNECTED = "disconnected", Disconnected = "disconnected",
DISCONNECTING = "disconnecting", Disconnecting = "disconnecting",
} }
type LocalMemberLivekitState = type LocalMemberLivekitState =
| { state: LivekitState.ERROR; error: string } | { state: LivekitState.Error; error: string }
| { state: LivekitState.CONNECTED } | { state: LivekitState.Connected }
| { state: LivekitState.CONNECTING } | { state: LivekitState.Connecting }
| { state: LivekitState.UNINITIALIZED } | { state: LivekitState.Uninitialized }
| { state: LivekitState.DISCONNECTED } | { state: LivekitState.Disconnected }
| { state: LivekitState.DISCONNECTING }; | { state: LivekitState.Disconnecting };
enum MatrixState { export enum MatrixState {
CONNECTED = "connected", Connected = "connected",
DISCONNECTED = "disconnected", Disconnected = "disconnected",
CONNECTING = "connecting", Connecting = "connecting",
} }
type LocalMemberMatrixState = type LocalMemberMatrixState =
| { state: MatrixState.CONNECTED } | { state: MatrixState.Connected }
| { state: MatrixState.CONNECTING } | { state: MatrixState.Connecting }
| { state: MatrixState.DISCONNECTED }; | { state: MatrixState.Disconnected };
export interface LocalMemberState { export interface LocalMemberConnectionState {
livekit$: BehaviorSubject<LocalMemberLivekitState>; livekit$: BehaviorSubject<LocalMemberLivekitState>;
matrix$: BehaviorSubject<LocalMemberMatrixState>; matrix$: BehaviorSubject<LocalMemberMatrixState>;
} }
@@ -107,9 +112,10 @@ interface Props {
* @param param0 * @param param0
* @returns * @returns
* - publisher: The handle to create tracks and publish them to the room. * - publisher: The handle to create tracks and publish them to the room.
* - connected$: the current connection state. Including matrix server and livekit server connection. (only the livekit server relevant for our own participation) * - connected$: the current connection state. Including matrix server and livekit server connection. (only considering the livekit server we are using for our own media publication)
* - transport$: the transport object the ownMembership$ ended up using. * - transport$: the transport object the ownMembership$ ended up using.
* * - connectionState: the current connection state. Including matrix server and livekit server connection.
* - sharingScreen$: Whether we are sharing our screen. `undefined` if we cannot share the screen.
*/ */
export const createLocalMembership$ = ({ export const createLocalMembership$ = ({
scope, scope,
@@ -125,21 +131,31 @@ export const createLocalMembership$ = ({
widget, widget,
}: Props): { }: Props): {
// publisher: Publisher // publisher: Publisher
requestConnect: () => LocalMemberState; requestConnect: () => LocalMemberConnectionState;
startTracks: () => Behavior<LocalTrack[]>; startTracks: () => Behavior<LocalTrack[]>;
requestDisconnect: () => Observable<LocalMemberLivekitState> | null; requestDisconnect: () => Observable<LocalMemberLivekitState> | null;
state: LocalMemberState; // TODO this is probably superseeded by joinState$ connectionState: LocalMemberConnectionState;
sharingScreen$: Behavior<boolean | undefined>;
toggleScreenSharing: (() => void) | null;
// deprecated fields
/** @deprecated use state instead*/
homeserverConnected$: Behavior<boolean>; homeserverConnected$: Behavior<boolean>;
/** @deprecated use state instead*/
connected$: Behavior<boolean>; connected$: Behavior<boolean>;
// this needs to be discussed
/** @deprecated use state instead*/
reconnecting$: Behavior<boolean>; reconnecting$: Behavior<boolean>;
// also needs to be disccues
/** @deprecated use state instead*/
configError$: Behavior<ElementCallError | null>; configError$: Behavior<ElementCallError | null>;
} => { } => {
const state = { const state = {
livekit$: new BehaviorSubject<LocalMemberLivekitState>({ livekit$: new BehaviorSubject<LocalMemberLivekitState>({
state: LivekitState.UNINITIALIZED, state: LivekitState.Uninitialized,
}), }),
matrix$: new BehaviorSubject<LocalMemberMatrixState>({ matrix$: new BehaviorSubject<LocalMemberMatrixState>({
state: MatrixState.DISCONNECTED, state: MatrixState.Disconnected,
}), }),
}; };
@@ -271,23 +287,23 @@ export const createLocalMembership$ = ({
return tracks$; return tracks$;
}; };
const requestConnect = (): LocalMemberState => { const requestConnect = (): LocalMemberConnectionState => {
if (state.livekit$.value === null) { if (state.livekit$.value === null) {
startTracks(); startTracks();
state.livekit$.next({ state: LivekitState.CONNECTING }); state.livekit$.next({ state: LivekitState.Connecting });
combineLatest([publisher$, tracks$], (publisher, tracks) => { combineLatest([publisher$, tracks$], (publisher, tracks) => {
publisher publisher
?.startPublishing() ?.startPublishing()
.then(() => { .then(() => {
state.livekit$.next({ state: LivekitState.CONNECTED }); state.livekit$.next({ state: LivekitState.Connected });
}) })
.catch((error) => { .catch((error) => {
state.livekit$.next({ state: LivekitState.ERROR, error }); state.livekit$.next({ state: LivekitState.Error, error });
}); });
}); });
} }
if (state.matrix$.value.state !== MatrixState.DISCONNECTED) { if (state.matrix$.value.state !== MatrixState.Disconnected) {
state.matrix$.next({ state: MatrixState.CONNECTING }); state.matrix$.next({ state: MatrixState.Connecting });
localTransport$.pipe( localTransport$.pipe(
tap((transport) => { tap((transport) => {
if (transport !== undefined) { if (transport !== undefined) {
@@ -306,17 +322,17 @@ export const createLocalMembership$ = ({
}; };
const requestDisconnect = (): Behavior<LocalMemberLivekitState> | null => { const requestDisconnect = (): Behavior<LocalMemberLivekitState> | null => {
if (state.livekit$.value.state !== LivekitState.CONNECTED) return null; if (state.livekit$.value.state !== LivekitState.Connected) return null;
state.livekit$.next({ state: LivekitState.DISCONNECTING }); state.livekit$.next({ state: LivekitState.Disconnecting });
combineLatest([publisher$, tracks$], (publisher, tracks) => { combineLatest([publisher$, tracks$], (publisher, tracks) => {
publisher publisher
?.stopPublishing() ?.stopPublishing()
.then(() => { .then(() => {
tracks.forEach((track) => track.stop()); tracks.forEach((track) => track.stop());
state.livekit$.next({ state: LivekitState.DISCONNECTED }); state.livekit$.next({ state: LivekitState.Disconnected });
}) })
.catch((error) => { .catch((error) => {
state.livekit$.next({ state: LivekitState.ERROR, error }); state.livekit$.next({ state: LivekitState.Error, error });
}); });
}); });
@@ -410,14 +426,83 @@ export const createLocalMembership$ = ({
} }
}); });
/**
* Returns undefined if scrennSharing is not yet ready.
*/
const sharingScreen$ = scope.behavior(
connection$.pipe(
switchMap((c) => {
if (!c) return of(undefined);
if (c.state$.value.state === "ConnectedToLkRoom")
return observeSharingScreen$(c.livekitRoom.localParticipant);
return of(false);
}),
),
);
const toggleScreenSharing =
"getDisplayMedia" in (navigator.mediaDevices ?? {}) &&
!getUrlParams().hideScreensharing
? (): void =>
// If a connection is ready...
void connection$
.pipe(
// I dont see why we need this. isnt the check later on superseeding it?
takeWhile(
(c) =>
c !== undefined && c.state$.value.state !== "FailedToStart",
),
switchMap((c) =>
c?.state$.value.state === "ConnectedToLkRoom" ? of(c) : NEVER,
),
take(1),
scope.bind(),
)
// ...toggle screen sharing.
.subscribe(
(c) =>
void c.livekitRoom.localParticipant
.setScreenShareEnabled(!sharingScreen$.value, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
})
.catch(logger.error),
)
: null;
// we do not need all the auto waiting since we can just check via sharingScreen$.value !== undefined
let alternativeScreenshareToggle: (() => void) | null = null;
if (
"getDisplayMedia" in (navigator.mediaDevices ?? {}) &&
!getUrlParams().hideScreensharing
) {
alternativeScreenshareToggle = (): void =>
void connection$.value?.livekitRoom.localParticipant
.setScreenShareEnabled(!sharingScreen$.value, {
audio: true,
selfBrowserSurface: "include",
surfaceSwitching: "include",
systemAudio: "include",
})
.catch(logger.error);
}
logger.log(
"alternativeScreenshareToggle so that it is used",
alternativeScreenshareToggle,
);
return { return {
startTracks, startTracks,
requestConnect, requestConnect,
requestDisconnect, requestDisconnect,
state, connectionState: state,
homeserverConnected$, homeserverConnected$,
connected$, connected$,
reconnecting$, reconnecting$,
configError$, configError$,
sharingScreen$,
toggleScreenSharing,
}; };
}; };

View File

@@ -18,15 +18,15 @@ import { logger } from "matrix-js-sdk/lib/logger";
import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery"; import { AutoDiscovery } from "matrix-js-sdk/lib/autodiscovery";
import { deepCompare } from "matrix-js-sdk/lib/utils"; import { deepCompare } from "matrix-js-sdk/lib/utils";
import { type Behavior } from "../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
import { import {
type Epoch, type Epoch,
mapEpoch, mapEpoch,
type ObservableScope, type ObservableScope,
} from "../ObservableScope.ts"; } from "../../ObservableScope.ts";
import { Config } from "../../config/Config.ts"; import { Config } from "../../../config/Config.ts";
import { MatrixRTCTransportMissingError } from "../../utils/errors.ts"; import { MatrixRTCTransportMissingError } from "../../../utils/errors.ts";
import { getSFUConfigWithOpenID } from "../../livekit/openIDSFU.ts"; import { getSFUConfigWithOpenID } from "../../../livekit/openIDSFU.ts";
/* /*
* - get well known * - get well known

View File

@@ -22,17 +22,17 @@ import {
} from "rxjs"; } from "rxjs";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
import type { Behavior } from "../Behavior.ts"; import type { Behavior } from "../../Behavior.ts";
import type { MediaDevices, SelectedDevice } from "../MediaDevices.ts"; import type { MediaDevices, SelectedDevice } from "../../MediaDevices.ts";
import type { MuteStates } from "../MuteStates.ts"; import type { MuteStates } from "../../MuteStates.ts";
import { import {
type ProcessorState, type ProcessorState,
trackProcessorSync, trackProcessorSync,
} from "../../livekit/TrackProcessorContext.tsx"; } from "../../../livekit/TrackProcessorContext.tsx";
import { getUrlParams } from "../../UrlParams.ts"; import { getUrlParams } from "../../../UrlParams.ts";
import { observeTrackReference$ } from "../MediaViewModel.ts"; import { observeTrackReference$ } from "../../MediaViewModel.ts";
import { type Connection } from "../remoteMembers/Connection.ts"; import { type Connection } from "../CallViewModel/remoteMembers/Connection.ts";
import { type ObservableScope } from "../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
/** /**
* A wrapper for a Connection object. * A wrapper for a Connection object.

View File

@@ -36,12 +36,12 @@ import {
type ConnectionState, type ConnectionState,
type PublishingParticipant, type PublishingParticipant,
} from "./Connection.ts"; } from "./Connection.ts";
import { ObservableScope } from "../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import { type OpenIDClientParts } from "../../livekit/openIDSFU.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
import { FailToGetOpenIdToken } from "../../utils/errors.ts"; import { FailToGetOpenIdToken } from "../../../utils/errors.ts";
import { mockMediaDevices, mockMuteStates } from "../../utils/test.ts"; import { mockMediaDevices, mockMuteStates } from "../../../utils/test.ts";
import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { type MuteStates } from "../MuteStates.ts"; import { type MuteStates } from "../../MuteStates.ts";
let testScope: ObservableScope; let testScope: ObservableScope;

View File

@@ -25,13 +25,13 @@ import {
getSFUConfigWithOpenID, getSFUConfigWithOpenID,
type OpenIDClientParts, type OpenIDClientParts,
type SFUConfig, type SFUConfig,
} from "../../livekit/openIDSFU.ts"; } from "../../../livekit/openIDSFU.ts";
import { type Behavior } from "../Behavior.ts"; import { type Behavior } from "../../Behavior.ts";
import { type ObservableScope } from "../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
import { import {
InsufficientCapacityError, InsufficientCapacityError,
SFURoomCreationRestrictedError, SFURoomCreationRestrictedError,
} from "../../utils/errors.ts"; } from "../../../utils/errors.ts";
export type PublishingParticipant = LocalParticipant | RemoteParticipant; export type PublishingParticipant = LocalParticipant | RemoteParticipant;

View File

@@ -13,13 +13,13 @@ import {
} from "livekit-client"; } from "livekit-client";
import { type Logger } from "matrix-js-sdk/lib/logger"; import { type Logger } from "matrix-js-sdk/lib/logger";
import { type ObservableScope } from "../ObservableScope.ts"; import { type ObservableScope } from "../../ObservableScope.ts";
import { Connection } from "./Connection.ts"; import { Connection } from "./Connection.ts";
import type { OpenIDClientParts } from "../../livekit/openIDSFU.ts"; import type { OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
import type { MediaDevices } from "../MediaDevices.ts"; import type { MediaDevices } from "../../MediaDevices.ts";
import type { Behavior } from "../Behavior.ts"; import type { Behavior } from "../../Behavior.ts";
import type { ProcessorState } from "../../livekit/TrackProcessorContext.tsx"; import type { ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { defaultLiveKitOptions } from "../../livekit/options.ts"; import { defaultLiveKitOptions } from "../../../livekit/options.ts";
export interface ConnectionFactory { export interface ConnectionFactory {
createConnection( createConnection(

View File

@@ -10,14 +10,14 @@ import { BehaviorSubject } from "rxjs";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type Participant as LivekitParticipant } from "livekit-client"; import { type Participant as LivekitParticipant } from "livekit-client";
import { ObservableScope } from "../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import { import {
type IConnectionManager, type IConnectionManager,
createConnectionManager$, createConnectionManager$,
} from "./ConnectionManager.ts"; } from "./ConnectionManager.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
import { flushPromises, withTestScheduler } from "../../utils/test.ts"; import { flushPromises, withTestScheduler } from "../../../utils/test.ts";
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
// Some test constants // Some test constants

View File

@@ -17,10 +17,10 @@ import { BehaviorSubject, combineLatest, map, switchMap } from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { type LocalParticipant, type RemoteParticipant } from "livekit-client"; import { type LocalParticipant, type RemoteParticipant } from "livekit-client";
import { type Behavior } from "../Behavior"; import { type Behavior } from "../../Behavior.ts";
import { type Connection } from "./Connection"; import { type Connection } from "./Connection.ts";
import { Epoch, type ObservableScope } from "../ObservableScope"; import { Epoch, type ObservableScope } from "../../ObservableScope.ts";
import { generateKeyed$ } from "../../utils/observable"; import { generateKeyed$ } from "../../../utils/observable.ts";
import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts"; import { areLivekitTransportsEqual } from "./MatrixLivekitMembers.ts";
import { type ConnectionFactory } from "./ConnectionFactory.ts"; import { type ConnectionFactory } from "./ConnectionFactory.ts";

View File

@@ -19,14 +19,14 @@ import {
createMatrixLivekitMembers$, createMatrixLivekitMembers$,
areLivekitTransportsEqual, areLivekitTransportsEqual,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { ObservableScope } from "../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import { ConnectionManagerData } from "./ConnectionManager.ts"; import { ConnectionManagerData } from "./ConnectionManager.ts";
import { import {
mockCallMembership, mockCallMembership,
mockRemoteParticipant, mockRemoteParticipant,
type OurRunHelpers, type OurRunHelpers,
withTestScheduler, withTestScheduler,
} from "../../utils/test.ts"; } from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
let testScope: ObservableScope; let testScope: ObservableScope;

View File

@@ -19,9 +19,9 @@ import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk"; import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type Behavior } from "../Behavior"; import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "./ConnectionManager"; import { type IConnectionManager } from "./ConnectionManager";
import { Epoch, mapEpoch, type ObservableScope } from "../ObservableScope"; import { Epoch, mapEpoch, type ObservableScope } from "../../ObservableScope";
import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname"; import { getRoomMemberFromRtcMember, memberDisplaynames$ } from "./displayname";
import { type Connection } from "./Connection"; import { type Connection } from "./Connection";

View File

@@ -14,9 +14,9 @@ import {
} from "matrix-js-sdk"; } from "matrix-js-sdk";
import EventEmitter from "events"; import EventEmitter from "events";
import { ObservableScope } from "../ObservableScope.ts"; import { ObservableScope } from "../../ObservableScope.ts";
import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room"; import type { Room as MatrixRoom } from "matrix-js-sdk/lib/models/room";
import { mockCallMembership, withTestScheduler } from "../../utils/test.ts"; import { mockCallMembership, withTestScheduler } from "../../../utils/test.ts";
import { memberDisplaynames$ } from "./displayname.ts"; import { memberDisplaynames$ } from "./displayname.ts";
let testScope: ObservableScope; let testScope: ObservableScope;

View File

@@ -19,12 +19,12 @@ import { type Room as MatrixRoom } from "matrix-js-sdk/lib/matrix";
// eslint-disable-next-line rxjs/no-internal // eslint-disable-next-line rxjs/no-internal
import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent"; import { type NodeStyleEventEmitter } from "rxjs/internal/observable/fromEvent";
import { Epoch, type ObservableScope } from "../ObservableScope"; import { Epoch, type ObservableScope } from "../../ObservableScope";
import { import {
calculateDisplayName, calculateDisplayName,
shouldDisambiguate, shouldDisambiguate,
} from "../../utils/displayname"; } from "../../../utils/displayname";
import { type Behavior } from "../Behavior"; import { type Behavior } from "../../Behavior";
/** /**
* Displayname for each member of the call. This will disambiguate * Displayname for each member of the call. This will disambiguate

View File

@@ -14,22 +14,26 @@ import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk"; import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { type Epoch, ObservableScope, trackEpoch } from "../ObservableScope.ts"; import {
type Epoch,
ObservableScope,
trackEpoch,
} from "../../ObservableScope.ts";
import { ECConnectionFactory } from "./ConnectionFactory.ts"; import { ECConnectionFactory } from "./ConnectionFactory.ts";
import { type OpenIDClientParts } from "../../livekit/openIDSFU.ts"; import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
import { import {
mockCallMembership, mockCallMembership,
mockMediaDevices, mockMediaDevices,
withTestScheduler, withTestScheduler,
} from "../../utils/test"; } from "../../../utils/test.ts";
import { type ProcessorState } from "../../livekit/TrackProcessorContext.tsx"; import { type ProcessorState } from "../../../livekit/TrackProcessorContext.tsx";
import { import {
areLivekitTransportsEqual, areLivekitTransportsEqual,
createMatrixLivekitMembers$, createMatrixLivekitMembers$,
type MatrixLivekitMember, type MatrixLivekitMember,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { createConnectionManager$ } from "./ConnectionManager.ts"; import { createConnectionManager$ } from "./ConnectionManager.ts";
import { membershipsAndTransports$ } from "../SessionBehaviors.ts"; import { membershipsAndTransports$ } from "../../SessionBehaviors.ts";
// Test the integration of ConnectionManager and MatrixLivekitMerger // Test the integration of ConnectionManager and MatrixLivekitMerger