From 7c40b0e177fbbfbf14c7c28b71a22b58f6df94e4 Mon Sep 17 00:00:00 2001 From: Timo K Date: Fri, 5 Dec 2025 19:48:02 +0100 Subject: [PATCH] ideas --- src/state/CallViewModel/CallViewModel.ts | 47 +++++++++++-------- .../CallViewModel/remoteMembers/Connection.ts | 43 +++++++++-------- 2 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 9bfa979c..3c15958a 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -452,14 +452,18 @@ export function createCallViewModel$( const localMembership = createLocalMembership$({ scope: scope, - homeserverConnected: createHomeserverConnected$( + homeserverConnected$: createHomeserverConnected$( scope, client, matrixRTCSession, ), muteStates: muteStates, - joinMatrixRTC: (transport: LivekitTransport) => { - enterRTCSession(matrixRTCSession, transport, connectOptions$.value); + joinMatrixRTC: async (transport: LivekitTransport) => { + return enterRTCSession( + matrixRTCSession, + transport, + connectOptions$.value, + ); }, createPublisherFactory: (connection: Connection) => { return new Publisher( @@ -569,6 +573,17 @@ export function createCallViewModel$( ), ); + /** + * Whether various media/event sources should pretend to be disconnected from + * all network input, even if their connection still technically works. + */ + // We do this when the app is in the 'reconnecting' state, because it might be + // that the LiveKit connection is still functional while the homeserver is + // down, for example, and we want to avoid making people worry that the app is + // in a split-brained state. + // DISCUSSION own membership manager ALSO this probably can be simplifis + const reconnecting$ = localMembership.reconnecting$; + const audioParticipants$ = scope.behavior( matrixLivekitMembers$.pipe( switchMap((membersWithEpoch) => { @@ -616,7 +631,7 @@ export function createCallViewModel$( ); const handsRaised$ = scope.behavior( - handsRaisedSubject$.pipe(pauseWhen(localMembership.reconnecting$)), + handsRaisedSubject$.pipe(pauseWhen(reconnecting$)), ); const reactions$ = scope.behavior( @@ -629,7 +644,7 @@ export function createCallViewModel$( ]), ), ), - pauseWhen(localMembership.reconnecting$), + pauseWhen(reconnecting$), ), ); @@ -720,7 +735,7 @@ export function createCallViewModel$( livekitRoom$, focusUrl$, mediaDevices, - localMembership.reconnecting$, + reconnecting$, displayName$, matrixMemberMetadataStore.createAvatarUrlBehavior$(userId), handsRaised$.pipe(map((v) => v[participantId]?.time ?? null)), @@ -812,17 +827,11 @@ export function createCallViewModel$( }), ); - const shouldLeave$: Observable< - "user" | "timeout" | "decline" | "allOthersLeft" - > = merge( - autoLeave$, - merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)), - ).pipe(scope.share); - - shouldLeave$.pipe(scope.bind()).subscribe((reason) => { - logger.info(`Call left due to ${reason}`); - localMembership.requestDisconnect(); - }); + const leave$: Observable<"user" | "timeout" | "decline" | "allOthersLeft"> = + merge( + autoLeave$, + merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)), + ).pipe(scope.share); const spotlightSpeaker$ = scope.behavior( userMedia$.pipe( @@ -1444,7 +1453,7 @@ export function createCallViewModel$( autoLeave$: autoLeave$, callPickupState$: callPickupState$, ringOverlay$: ringOverlay$, - leave$: shouldLeave$, + leave$: leave$, hangup: (): void => userHangup$.next(), join: localMembership.requestConnect, toggleScreenSharing: toggleScreenSharing, @@ -1491,7 +1500,7 @@ export function createCallViewModel$( showFooter$: showFooter$, earpieceMode$: earpieceMode$, audioOutputSwitcher$: audioOutputSwitcher$, - reconnecting$: localMembership.reconnecting$, + reconnecting$: reconnecting$, }; } diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index 962f56d9..549777f9 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -12,7 +12,7 @@ import { } from "@livekit/components-core"; import { ConnectionError, - type ConnectionState as LivekitConnectionState, + ConnectionState as LivekitConnectionState, type Room as LivekitRoom, type LocalParticipant, type RemoteParticipant, @@ -47,17 +47,24 @@ export interface ConnectionOpts { /** Optional factory to create the LiveKit room, mainly for testing purposes. */ livekitRoomFactory: () => LivekitRoom; } -export enum ConnectionAdditionalState { +export class FailedToStartError extends Error { + public constructor(message: string) { + super(message); + this.name = "FailedToStartError"; + } +} + +export enum ConnectionState { Initialized = "Initialized", FetchingConfig = "FetchingConfig", - // FailedToStart = "FailedToStart", Stopped = "Stopped", ConnectingToLkRoom = "ConnectingToLkRoom", + LivekitDisconnected = "disconnected", + LivekitConnecting = "connecting", + LivekitConnected = "connected", + LivekitReconnecting = "reconnecting", + LivekitSignalReconnecting = "signalReconnecting", } -export type ConnectionState = - | { state: ConnectionAdditionalState } - | { state: LivekitConnectionState } - | { state: "FailedToStart"; error: Error }; /** * A connection to a Matrix RTC LiveKit backend. @@ -66,14 +73,15 @@ export type ConnectionState = */ export class Connection { // Private Behavior - private readonly _state$ = new BehaviorSubject({ - state: ConnectionAdditionalState.Initialized, - }); + private readonly _state$ = new BehaviorSubject< + ConnectionState | FailedToStartError + >(ConnectionState.Initialized); /** * The current state of the connection to the media transport. */ - public readonly state$: Behavior = this._state$; + public readonly state$: Behavior = + this._state$; /** * The media transport to connect to. @@ -117,16 +125,12 @@ export class Connection { this.logger.debug("Starting Connection"); this.stopped = false; try { - this._state$.next({ - state: ConnectionAdditionalState.FetchingConfig, - }); + this._state$.next(ConnectionState.FetchingConfig); const { url, jwt } = await this.getSFUConfigWithOpenID(); // If we were stopped while fetching the config, don't proceed to connect if (this.stopped) return; - this._state$.next({ - state: ConnectionAdditionalState.ConnectingToLkRoom, - }); + this._state$.next(ConnectionState.ConnectingToLkRoom); try { await this.livekitRoom.connect(url, jwt); } catch (e) { @@ -157,9 +161,8 @@ export class Connection { connectionStateObserver(this.livekitRoom) .pipe(this.scope.bind()) .subscribe((lkState) => { - this._state$.next({ - state: lkState, - }); + // It si save to cast lkState to ConnectionState as they are fully overlapping. + this._state$.next(lkState as unknown as ConnectionState); }); } catch (error) { this.logger.debug(`Failed to connect to LiveKit room: ${error}`);