2025-11-04 20:24:15 +01:00
/*
2025-11-18 10:13:10 +01:00
Copyright 2025 Element Creations Ltd.
2025-11-04 20:24:15 +01:00
SPDX-License-IdFentifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
2025-11-07 17:36:16 -05:00
import {
type LocalTrack ,
type Participant ,
ParticipantEvent ,
2025-11-12 12:09:31 +01:00
type LocalParticipant ,
2025-11-07 17:36:16 -05:00
} from "livekit-client" ;
import { observeParticipantEvents } from "@livekit/components-core" ;
2025-11-04 20:24:15 +01:00
import {
MembershipManagerEvent ,
Status ,
2025-11-20 14:42:12 +01:00
type LivekitTransport ,
type MatrixRTCSession ,
2025-11-04 20:24:15 +01:00
} from "matrix-js-sdk/lib/matrixrtc" ;
import {
BehaviorSubject ,
2025-11-20 14:42:12 +01:00
catchError ,
2025-11-04 20:24:15 +01:00
combineLatest ,
2025-11-17 18:22:37 +01:00
distinctUntilChanged ,
2025-11-04 20:24:15 +01:00
fromEvent ,
map ,
type Observable ,
of ,
2025-11-05 12:56:58 +01:00
scan ,
2025-11-04 20:24:15 +01:00
startWith ,
switchMap ,
2025-11-14 16:18:31 +01:00
tap ,
2025-11-04 20:24:15 +01:00
} from "rxjs" ;
2025-11-20 14:42:12 +01:00
import { logger , type Logger } from "matrix-js-sdk/lib/logger" ;
import { ClientEvent , type Room , SyncState } from "matrix-js-sdk" ;
2025-11-04 20:24:15 +01:00
2025-11-07 08:44:44 +01:00
import { type Behavior } from "../../Behavior" ;
2025-11-06 21:54:34 +01:00
import { type IConnectionManager } from "../remoteMembers/ConnectionManager" ;
2025-11-07 08:44:44 +01:00
import { ObservableScope } from "../../ObservableScope" ;
2025-11-20 14:42:12 +01:00
import { type Publisher } from "./Publisher" ;
2025-11-07 08:44:44 +01:00
import { type MuteStates } from "../../MuteStates" ;
import { and$ } from "../../../utils/observable" ;
2025-11-17 18:22:37 +01:00
import { ElementCallError , UnknownCallError } from "../../../utils/errors" ;
2025-11-20 14:42:12 +01:00
import { ElementWidgetActions , widget } from "../../../widget" ;
2025-11-07 08:44:44 +01:00
import { getUrlParams } from "../../../UrlParams.ts" ;
2025-11-11 15:51:48 +01:00
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics.ts" ;
import { MatrixRTCMode } from "../../../settings/settings.ts" ;
import { Config } from "../../../config/Config.ts" ;
2025-11-17 18:22:37 +01:00
import {
type Connection ,
type ConnectionState ,
} from "../remoteMembers/Connection.ts" ;
2025-11-14 16:18:31 +01:00
2025-11-07 08:44:44 +01:00
export enum LivekitState {
Uninitialized = "uninitialized" ,
Connecting = "connecting" ,
Connected = "connected" ,
Error = "error" ,
Disconnected = "disconnected" ,
Disconnecting = "disconnecting" ,
2025-11-04 20:24:15 +01:00
}
2025-11-14 16:18:31 +01:00
2025-11-04 20:24:15 +01:00
type LocalMemberLivekitState =
2025-11-20 14:42:12 +01:00
| { state : LivekitState.Error ; error : ElementCallError }
2025-11-07 08:44:44 +01:00
| { state : LivekitState.Connected }
| { state : LivekitState.Connecting }
| { state : LivekitState.Uninitialized }
| { state : LivekitState.Disconnected }
| { state : LivekitState.Disconnecting };
2025-11-04 20:24:15 +01:00
2025-11-07 08:44:44 +01:00
export enum MatrixState {
Connected = "connected" ,
Disconnected = "disconnected" ,
Connecting = "connecting" ,
2025-11-20 14:42:12 +01:00
Error = "Error" ,
2025-11-04 20:24:15 +01:00
}
2025-11-14 16:18:31 +01:00
2025-11-04 20:24:15 +01:00
type LocalMemberMatrixState =
2025-11-07 08:44:44 +01:00
| { state : MatrixState.Connected }
| { state : MatrixState.Connecting }
2025-11-20 14:42:12 +01:00
| { state : MatrixState.Disconnected }
| { state : MatrixState.Error ; error : Error };
2025-11-04 20:24:15 +01:00
2025-11-07 08:44:44 +01:00
export interface LocalMemberConnectionState {
2025-11-12 12:09:31 +01:00
livekit$ : Behavior < LocalMemberLivekitState >;
matrix$ : Behavior < LocalMemberMatrixState >;
2025-11-04 20:24:15 +01:00
}
2025-11-05 12:56:58 +01:00
/*
* - get well known
* - get oldest membership
* - get transport to use
* - get openId + jwt token
* - wait for createTrack() call
* - create tracks
* - wait for join() call
* - Publisher.publishTracks()
* - send join state/sticky event
*/
2025-11-20 14:42:12 +01:00
2025-11-05 12:56:58 +01:00
interface Props {
2025-11-20 14:42:12 +01:00
// TODO add a comment into some code style readme or file header callviewmodel
// that the inputs for those createSomething$() functions should NOT contain any js-sdk objectes
2025-11-05 12:56:58 +01:00
scope : ObservableScope ;
muteStates : MuteStates ;
2025-11-06 21:54:34 +01:00
connectionManager : IConnectionManager ;
2025-11-20 14:42:12 +01:00
createPublisherFactory : ( connection : Connection ) => Publisher ;
joinMatrixRTC : ( trasnport : LivekitTransport ) => Promise < void >;
homeserverConnected$ : Behavior < boolean >;
2025-11-07 19:07:45 +01:00
localTransport$ : Behavior < LivekitTransport | null >;
2025-11-20 14:42:12 +01:00
matrixRTCSession : Pick <
MatrixRTCSession ,
"updateCallIntent" | "leaveRoomSession"
> ;
2025-11-14 16:18:31 +01:00
logger : Logger ;
2025-11-05 12:56:58 +01:00
}
2025-11-04 20:24:15 +01:00
/**
* This class is responsible for managing the own membership in a room.
* We want
* - a publisher
* -
* @param param0
* @returns
* - publisher: The handle to create tracks and publish them to the room.
2025-11-07 08:44:44 +01:00
* - 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)
2025-11-04 20:24:15 +01:00
* - transport$: the transport object the ownMembership$ ended up using.
2025-11-07 08:44:44 +01:00
* - 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.
2025-11-04 20:24:15 +01:00
*/
2025-11-05 18:57:24 +01:00
export const createLocalMembership$ = ({
2025-11-04 20:24:15 +01:00
scope ,
connectionManager ,
2025-11-20 14:42:12 +01:00
localTransport$ : localTransportCanThrow$ ,
homeserverConnected$ ,
createPublisherFactory ,
joinMatrixRTC ,
2025-11-14 16:18:31 +01:00
logger : parentLogger ,
2025-11-20 14:42:12 +01:00
muteStates ,
matrixRTCSession ,
2025-11-04 20:24:15 +01:00
} : Props ) : {
2025-11-07 08:44:44 +01:00
requestConnect : () => LocalMemberConnectionState ;
2025-11-04 20:24:15 +01:00
startTracks : () => Behavior < LocalTrack [] >;
requestDisconnect : () => Observable < LocalMemberLivekitState > | null ;
2025-11-07 08:44:44 +01:00
connectionState : LocalMemberConnectionState ;
2025-11-12 15:02:19 -05:00
sharingScreen$ : Behavior < boolean >;
/**
* Callback to toggle screen sharing. If null, screen sharing is not possible.
*/
2025-11-07 08:44:44 +01:00
toggleScreenSharing : (() => void ) | null ;
2025-11-12 12:09:31 +01:00
participant$ : Behavior < LocalParticipant | null >;
connection$ : Behavior < Connection | null >;
2025-11-04 20:24:15 +01:00
homeserverConnected$ : Behavior < boolean >;
2025-11-20 14:42:12 +01:00
// deprecated fields
2025-11-07 08:44:44 +01:00
/** @deprecated use state instead*/
2025-11-04 20:24:15 +01:00
connected$ : Behavior < boolean >;
2025-11-07 08:44:44 +01:00
// this needs to be discussed
/** @deprecated use state instead*/
2025-11-05 12:56:58 +01:00
reconnecting$ : Behavior < boolean >;
2025-11-04 20:24:15 +01:00
} => {
2025-11-14 16:18:31 +01:00
const logger = parentLogger . getChild ( "[LocalMembership]" );
logger . debug ( `Creating local membership..` );
2025-11-04 20:24:15 +01:00
const state = {
livekit$ : new BehaviorSubject < LocalMemberLivekitState >({
2025-11-07 08:44:44 +01:00
state : LivekitState.Uninitialized ,
2025-11-04 20:24:15 +01:00
}),
matrix$ : new BehaviorSubject < LocalMemberMatrixState >({
2025-11-07 08:44:44 +01:00
state : MatrixState.Disconnected ,
2025-11-04 20:24:15 +01:00
}),
};
// This should be used in a combineLatest with publisher$ to connect.
// to make it possible to call startTracks before the preferredTransport$ has resolved.
2025-11-07 19:07:45 +01:00
const trackStartRequested$ = new BehaviorSubject ( false );
// This should be used in a combineLatest with publisher$ to connect.
// to make it possible to call startTracks before the preferredTransport$ has resolved.
const connectRequested$ = new BehaviorSubject ( false );
2025-11-04 20:24:15 +01:00
// This should be used in a combineLatest with publisher$ to connect.
const tracks$ = new BehaviorSubject < LocalTrack [] >([]);
2025-11-20 14:42:12 +01:00
// unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
const localTransport$ = scope . behavior (
localTransportCanThrow$ . pipe (
catchError (( e : unknown ) => {
if ( e instanceof ElementCallError ) {
state . livekit$ . next ({ state : LivekitState.Error , error : e });
} else {
logger . error ( "Unknown error from localTransport$" , e );
}
return of ( null );
}),
),
);
2025-11-06 21:54:34 +01:00
// Drop Epoch data here since we will not combine this anymore
2025-11-14 16:18:31 +01:00
const localConnection$ = scope . behavior (
2025-11-20 14:42:12 +01:00
combineLatest ([
connectionManager . connectionManagerData$ ,
localTransport$ ,
]). pipe (
map (([ connectionData , localTransport ]) => {
2025-11-14 16:18:31 +01:00
if ( localTransport === null ) {
return null ;
}
2025-11-20 14:42:12 +01:00
return connectionData . value . getConnectionForTransport ( localTransport );
2025-11-14 16:18:31 +01:00
}),
tap (( connection ) => {
logger . info (
`Local connection updated: ${ connection ? . transport ? . livekit_service_url } ` ,
);
}),
2025-11-04 20:24:15 +01:00
),
);
// /**
// * Whether we are "fully" connected to the call. Accounts for both the
// * connection to the MatrixRTC session and the LiveKit publish connection.
// */
// // TODO use this in combination with the MemberState.
const connected$ = scope . behavior (
and$ (
homeserverConnected$ ,
2025-11-14 16:18:31 +01:00
localConnection$ . pipe (
2025-11-04 20:24:15 +01:00
switchMap (( c ) =>
c
? c . state$ . pipe ( map (( state ) => state . state === "ConnectedToLkRoom" ))
: of ( false ),
),
),
),
);
2025-11-07 19:07:45 +01:00
const publisher$ = new BehaviorSubject < Publisher | null >( null );
2025-11-18 12:14:17 +01:00
localConnection$ . pipe ( scope . bind ()). subscribe (( connection ) => {
2025-11-07 19:07:45 +01:00
if ( connection !== null && publisher$ . value === null ) {
2025-11-14 16:18:31 +01:00
// TODO looks strange to not change publisher if connection changes.
2025-11-20 14:42:12 +01:00
// @valere will take care of this!
publisher$ . next ( createPublisherFactory ( connection ));
2025-11-07 19:07:45 +01:00
}
});
2025-11-04 20:24:15 +01:00
2025-11-20 14:42:12 +01:00
// const mutestate= publisher$.pipe(switchMap((publisher) => {
// return publisher.muteState$
// });
2025-11-07 19:07:45 +01:00
combineLatest ([ publisher$ , trackStartRequested$ ]). subscribe (
([ publisher , shouldStartTracks ]) => {
2025-11-04 20:24:15 +01:00
if ( publisher && shouldStartTracks ) {
publisher
. createAndSetupTracks ()
. then (( tracks ) => {
tracks$ . next ( tracks );
})
. catch (( error ) => {
logger . error ( "Error creating tracks:" , error );
});
}
},
);
// MATRIX RELATED
// /**
// * Whether we should tell the user that we're reconnecting to the call.
// */
2025-11-05 12:56:58 +01:00
// DISCUSSION is there a better way to do this?
// sth that is more deriectly implied from the membership manager of the js sdk. (fromEvent(matrixRTCSession, Reconnecting)) ??? or similar
const reconnecting$ = scope . behavior (
connected$ . pipe (
// We are reconnecting if we previously had some successful initial
// connection but are now disconnected
scan (
({ connectedPreviously }, connectedNow ) => ({
connectedPreviously : connectedPreviously || connectedNow ,
reconnecting : connectedPreviously && ! connectedNow ,
}),
{ connectedPreviously : false , reconnecting : false },
),
map (({ reconnecting }) => reconnecting ),
),
);
2025-11-04 20:24:15 +01:00
const startTracks = () : Behavior < LocalTrack [] > => {
2025-11-07 19:07:45 +01:00
trackStartRequested$ . next ( true );
2025-11-04 20:24:15 +01:00
return tracks$ ;
};
2025-11-07 19:07:45 +01:00
combineLatest ([ publisher$ , tracks$ ]). subscribe (([ publisher , tracks ]) => {
if (
tracks . length === 0 ||
// change this to !== Publishing
state . livekit$ . value . state !== LivekitState . Uninitialized
) {
return ;
2025-11-04 20:24:15 +01:00
}
2025-11-07 19:07:45 +01:00
state . livekit$ . next ({ state : LivekitState.Connecting });
publisher
? . startPublishing ()
. then (() => {
state . livekit$ . next ({ state : LivekitState.Connected });
})
. catch (( error ) => {
state . livekit$ . next ({ state : LivekitState.Error , error });
});
});
2025-11-12 12:09:31 +01:00
2025-11-07 19:07:45 +01:00
combineLatest ([ localTransport$ , connectRequested$ ]). subscribe (
2025-11-18 12:14:17 +01:00
// TODO reconnect when transport changes => create test.
2025-11-07 19:07:45 +01:00
([ transport , connectRequested ]) => {
if (
transport === null ||
! connectRequested ||
state . matrix$ . value . state !== MatrixState . Disconnected
) {
2025-11-14 10:44:16 +01:00
logger . info (
"Not yet connecting because: " ,
"transport === null:" ,
transport === null ,
"!connectRequested:" ,
! connectRequested ,
"state.matrix$.value.state !== MatrixState.Disconnected:" ,
state . matrix$ . value . state !== MatrixState . Disconnected ,
);
2025-11-07 19:07:45 +01:00
return ;
}
2025-11-07 08:44:44 +01:00
state . matrix$ . next ({ state : MatrixState.Connecting });
2025-11-14 10:44:16 +01:00
logger . info ( "Matrix State connecting" );
2025-11-20 14:42:12 +01:00
joinMatrixRTC ( transport ). catch (( error ) => {
logger . error ( error );
state . matrix$ . next ({ state : MatrixState.Error , error });
});
2025-11-07 19:07:45 +01:00
},
);
2025-11-20 14:42:12 +01:00
// TODO add this and update `state.matrix$` based on it.
// useTypedEventEmitter(
// rtcSession,
// MatrixRTCSessionEvent.MembershipManagerError,
// (error) => setExternalError(new ConnectionLostError()),
// );
2025-11-07 19:07:45 +01:00
const requestConnect = () : LocalMemberConnectionState => {
trackStartRequested$ . next ( true );
connectRequested$ . next ( true );
2025-11-04 20:24:15 +01:00
return state ;
};
const requestDisconnect = () : Behavior < LocalMemberLivekitState > | null => {
2025-11-07 08:44:44 +01:00
if ( state . livekit$ . value . state !== LivekitState . Connected ) return null ;
state . livekit$ . next ({ state : LivekitState.Disconnecting });
2025-11-04 20:24:15 +01:00
combineLatest ([ publisher$ , tracks$ ], ( publisher , tracks ) => {
publisher
? . stopPublishing ()
. then (() => {
tracks . forEach (( track ) => track . stop ());
2025-11-07 08:44:44 +01:00
state . livekit$ . next ({ state : LivekitState.Disconnected });
2025-11-04 20:24:15 +01:00
})
. catch (( error ) => {
2025-11-07 08:44:44 +01:00
state . livekit$ . next ({ state : LivekitState.Error , error });
2025-11-04 20:24:15 +01:00
});
});
return state . livekit$ ;
};
2025-11-05 12:56:58 +01:00
// Pause upstream of all local media tracks when we're disconnected from
// MatrixRTC, because it can be an unpleasant surprise for the app to say
// 'reconnecting' and yet still be transmitting your media to others.
// We use matrixConnected$ rather than reconnecting$ because we want to
// pause tracks during the initial joining sequence too until we're sure
// that our own media is displayed on screen.
2025-11-14 16:18:31 +01:00
combineLatest ([ localConnection$ , homeserverConnected$ ])
2025-11-05 12:56:58 +01:00
. pipe ( scope . bind ())
. subscribe (([ connection , connected ]) => {
if ( connection ? . state$ . value . state !== "ConnectedToLkRoom" ) return ;
const publications =
connection . livekitRoom . localParticipant . trackPublications . values ();
if ( connected ) {
for ( const p of publications ) {
if ( p . track ? . isUpstreamPaused === true ) {
const kind = p . track . kind ;
2025-11-14 10:44:16 +01:00
logger . info (
`Resuming ${ kind } track (MatrixRTC connection present)` ,
);
2025-11-05 12:56:58 +01:00
p . track
. resumeUpstream ()
. catch (( e ) =>
logger . error (
`Failed to resume ${ kind } track after MatrixRTC reconnection` ,
e ,
),
);
}
}
} else {
for ( const p of publications ) {
if ( p . track ? . isUpstreamPaused === false ) {
const kind = p . track . kind ;
2025-11-14 10:44:16 +01:00
logger . info (
2025-11-05 12:56:58 +01:00
`Pausing ${ kind } track (uncertain MatrixRTC connection)` ,
);
p . track
. pauseUpstream ()
. catch (( e ) =>
logger . error (
`Failed to pause ${ kind } track after entering uncertain MatrixRTC connection` ,
e ,
),
);
}
}
}
});
2025-11-20 14:42:12 +01:00
// TODO: Refactor updateCallIntent to sth like this:
// combineLatest([muteStates.video.enabled$,localTransport$, state.matrix$]).pipe(map(()=>{
// matrixRTCSession.updateCallIntent(videoEnabled ? "video" : "audio"),
// }))
//
2025-11-05 12:56:58 +01:00
// TODO I do not fully understand what this does.
// Is it needed?
// Is this at the right place?
// Can this be simplified?
// Start and stop session membership as needed
2025-11-20 14:42:12 +01:00
// Discussed in statndup -> It seems we can remove this (there is another call to enterRTCSession in this file)
// MAKE SURE TO UNDERSTAND why reconcile is needed and what is potentially missing from the alternative enterRTCSession block.
// @toger5 will try to take care of this.
scope . reconcile ( localTransport$ , async ( transport ) => {
if ( transport !== null && transport !== undefined ) {
2025-11-05 12:56:58 +01:00
try {
2025-11-20 14:42:12 +01:00
state . matrix$ . next ({ state : MatrixState.Connecting });
await joinMatrixRTC ( transport );
2025-11-05 12:56:58 +01:00
} catch ( e ) {
logger . error ( "Error entering RTC session" , e );
}
// Update our member event when our mute state changes.
const intentScope = new ObservableScope ();
intentScope . reconcile ( muteStates . video . enabled$ , async ( videoEnabled ) =>
matrixRTCSession . updateCallIntent ( videoEnabled ? "video" : "audio" ),
);
return async () : Promise < void > => {
intentScope . end ();
// Only sends Matrix leave event. The LiveKit session will disconnect
// as soon as either the stopConnection$ handler above gets to it or
// the view model is destroyed.
try {
await matrixRTCSession . leaveRoomSession ();
} catch ( e ) {
logger . error ( "Error leaving RTC session" , e );
}
try {
await widget ? . api . transport . send ( ElementWidgetActions . HangupCall , {});
} catch ( e ) {
logger . error ( "Failed to send hangup action" , e );
}
};
}
});
2025-11-17 18:22:37 +01:00
localConnection$
. pipe (
distinctUntilChanged (),
switchMap (( c ) =>
c === null ? of ({ state : "Initialized" } as ConnectionState ) : c . state$ ,
),
map (( s ) => {
logger . trace ( `Local connection state update: ${ s . state } ` );
if ( s . state == "FailedToStart" ) {
return s . error instanceof ElementCallError
? s.error
: new UnknownCallError ( s . error );
}
}),
scope . bind (),
)
2025-11-20 14:42:12 +01:00
. subscribe (( error ) => {
if ( error !== undefined )
state . livekit$ . next ({ state : LivekitState.Error , error });
2025-11-17 18:22:37 +01:00
});
2025-11-07 08:44:44 +01:00
/**
2025-11-12 15:02:19 -05:00
* Whether the user is currently sharing their screen.
2025-11-07 08:44:44 +01:00
*/
const sharingScreen$ = scope . behavior (
2025-11-14 16:18:31 +01:00
localConnection$ . pipe (
2025-11-12 15:02:19 -05:00
switchMap (( c ) =>
2025-11-20 14:42:12 +01:00
c !== null && c . livekitRoom
? observeSharingScreen$ ( c . livekitRoom . localParticipant )
: of ( false ),
2025-11-12 15:02:19 -05:00
),
2025-11-07 08:44:44 +01:00
),
);
const toggleScreenSharing =
"getDisplayMedia" in ( navigator . mediaDevices ?? {}) &&
! getUrlParams (). hideScreensharing
? () : void =>
2025-11-12 15:02:19 -05:00
// If a connection is ready, toggle screen sharing.
// We deliberately do nothing in the case of a null connection because
// it looks nice for the call control buttons to all become available
// at once upon joining the call, rather than introducing a disabled
// state. The user can just click again.
// We also allow screen sharing to be toggled even if the connection
// is still initializing or publishing tracks, because there's no
// technical reason to disallow this. LiveKit will publish if it can.
2025-11-14 16:18:31 +01:00
void localConnection$ . value ? . livekitRoom . localParticipant
2025-11-12 15:02:19 -05:00
. setScreenShareEnabled ( ! sharingScreen$ . value , {
audio : true ,
selfBrowserSurface : "include" ,
surfaceSwitching : "include" ,
systemAudio : "include" ,
})
. catch ( logger . error )
2025-11-07 08:44:44 +01:00
: null ;
2025-11-12 12:09:31 +01:00
const participant$ = scope . behavior (
2025-11-20 14:42:12 +01:00
localConnection$ . pipe ( map (( c ) => c ? . livekitRoom ? . localParticipant ?? null )),
2025-11-12 12:09:31 +01:00
);
2025-11-04 20:24:15 +01:00
return {
startTracks ,
requestConnect ,
requestDisconnect ,
2025-11-07 08:44:44 +01:00
connectionState : state ,
2025-11-04 20:24:15 +01:00
homeserverConnected$ ,
connected$ ,
2025-11-05 12:56:58 +01:00
reconnecting$ ,
2025-11-20 14:42:12 +01:00
2025-11-07 08:44:44 +01:00
sharingScreen$ ,
toggleScreenSharing ,
2025-11-12 12:09:31 +01:00
participant$ ,
2025-11-14 16:18:31 +01:00
connection$ : localConnection$ ,
2025-11-04 20:24:15 +01:00
};
};
2025-11-07 17:36:16 -05:00
export function observeSharingScreen$ ( p : Participant ) : Observable < boolean > {
return observeParticipantEvents (
p ,
ParticipantEvent . TrackPublished ,
ParticipantEvent . TrackUnpublished ,
ParticipantEvent . LocalTrackPublished ,
ParticipantEvent . LocalTrackUnpublished ,
). pipe ( map (( p ) => p . isScreenShareEnabled ));
}
2025-11-11 15:51:48 +01:00
interface EnterRTCSessionOptions {
encryptMedia : boolean ;
matrixRTCMode : MatrixRTCMode ;
}
/**
2025-11-18 12:14:17 +01:00
* Does the necessary steps to enter the RTC session on the matrix side:
* - Preparing the membership info (FOCUS to use, options)
* - Sends the matrix event to join the call, and starts the membership manager:
* - Delay events management
* - Handles retries (fails only after several attempts)
*
2025-11-11 15:51:48 +01:00
* @param rtcSession
* @param transport
* @param options
2025-11-14 16:18:31 +01:00
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
2025-11-11 15:51:48 +01:00
*/
2025-11-14 10:48:24 -05:00
// Exported for unit testing
export async function enterRTCSession (
2025-11-11 15:51:48 +01:00
rtcSession : MatrixRTCSession ,
transport : LivekitTransport ,
{ encryptMedia , matrixRTCMode } : EnterRTCSessionOptions ,
) : Promise < void > {
PosthogAnalytics . instance . eventCallEnded . cacheStartCall ( new Date ());
PosthogAnalytics . instance . eventCallStarted . track ( rtcSession . room . roomId );
// This must be called before we start trying to join the call, as we need to
// have started tracking by the time calls start getting created.
// groupCallOTelMembership?.onJoinCall();
const { features , matrix_rtc_session : matrixRtcSessionConfig } = Config . get ();
const useDeviceSessionMemberEvents =
features ? . feature_use_device_session_member_events ;
const { sendNotificationType : notificationType , callIntent } = getUrlParams ();
const multiSFU = matrixRTCMode !== MatrixRTCMode . Legacy ;
// Multi-sfu does not need a preferred foci list. just the focus that is actually used.
2025-11-20 14:42:12 +01:00
// TODO where/how do we track errors originating from the ongoing rtcSession?
2025-11-11 15:51:48 +01:00
rtcSession . joinRoomSession (
multiSFU ? [] : [ transport ],
multiSFU ? transport : undefined ,
{
notificationType ,
callIntent ,
manageMediaKeys : encryptMedia ,
...( useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents : ! useDeviceSessionMemberEvents ,
}),
delayedLeaveEventRestartMs :
matrixRtcSessionConfig?.delayed_leave_event_restart_ms ,
delayedLeaveEventDelayMs :
matrixRtcSessionConfig?.delayed_leave_event_delay_ms ,
delayedLeaveEventRestartLocalTimeoutMs :
matrixRtcSessionConfig?.delayed_leave_event_restart_local_timeout_ms ,
networkErrorRetryMs : matrixRtcSessionConfig?.network_error_retry_ms ,
makeKeyDelay : matrixRtcSessionConfig?.wait_for_key_rotation_ms ,
membershipEventExpiryMs :
matrixRtcSessionConfig?.membership_event_expiry_ms ,
useExperimentalToDeviceTransport : true ,
unstableSendStickyEvents : matrixRTCMode === MatrixRTCMode . Matrix_2_0 ,
},
);
if ( widget ) {
2025-11-14 16:18:31 +01:00
await widget . api . transport . send ( ElementWidgetActions . JoinCall , {});
2025-11-11 15:51:48 +01:00
}
}
2025-11-20 14:42:12 +01:00
/**
* Whether we are connected to the MatrixRTC session.
*/
export function createHomeserverConnected$ (
scope : ObservableScope ,
matrixRoom : Room ,
matrixRTCSession : MatrixRTCSession ,
) : Behavior < boolean > {
return scope . behavior (
// To consider ourselves connected to MatrixRTC, we check the following:
and$ (
// The client is connected to the sync loop
(
fromEvent ( matrixRoom . client , ClientEvent . Sync ) as Observable <
[ SyncState ]
>
). pipe (
startWith ([ matrixRoom . client . getSyncState ()]),
map (([ state ]) => state === SyncState . Syncing ),
),
// Room state observed by session says we're connected
fromEvent ( matrixRTCSession , MembershipManagerEvent . StatusChanged ). pipe (
startWith ( null ),
map (() => matrixRTCSession . membershipStatus === Status . Connected ),
),
// Also watch out for warnings that we've likely hit a timeout and our
// delayed leave event is being sent (this condition is here because it
// provides an earlier warning than the sync loop timeout, and we wouldn't
// see the actual leave event until we reconnect to the sync loop)
fromEvent ( matrixRTCSession , MembershipManagerEvent . ProbablyLeft ). pipe (
startWith ( null ),
map (() => matrixRTCSession . probablyLeft !== true ),
),
). pipe (
tap (( connected ) => {
logger . info ( `Homeserver connected update: ${ connected } ` );
}),
),
);
}