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-21 16:14:12 +01:00
type ScreenShareCaptureOptions ,
2025-12-01 14:42:15 +01:00
ConnectionState ,
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 {
type LivekitTransport ,
type MatrixRTCSession ,
} 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-27 14:42:23 +01:00
from ,
2025-11-04 20:24:15 +01:00
map ,
type Observable ,
of ,
2025-11-05 12:56:58 +01:00
scan ,
2025-11-27 14:42:23 +01:00
startWith ,
2025-11-04 20:24:15 +01:00
switchMap ,
2025-11-14 16:18:31 +01:00
tap ,
2025-11-04 20:24:15 +01:00
} from "rxjs" ;
2025-11-14 16:18:31 +01:00
import { type Logger } from "matrix-js-sdk/lib/logger" ;
2025-11-25 20:18:34 +01:00
import { deepCompare } from "matrix-js-sdk/lib/utils" ;
2025-11-04 20:24:15 +01:00
2025-11-25 20:18:34 +01:00
import { constant , 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-25 20:18:34 +01:00
import {
ElementCallError ,
MembershipManagerError ,
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-25 20:18:34 +01:00
import { type Connection } from "../remoteMembers/Connection.ts" ;
2025-11-14 16:18:31 +01:00
2025-12-01 14:42:15 +01:00
export enum RTCBackendState {
2025-11-25 20:18:34 +01:00
Error = "error" ,
/** Not even a transport is available to the LocalMembership */
WaitingForTransport = "waiting_for_transport" ,
2025-11-27 14:42:23 +01:00
/** A connection appeared so we can initialise the publisher */
WaitingForConnection = "waiting_for_connection" ,
/** Connection and transport arrived, publisher Initialized */
2025-11-25 20:18:34 +01:00
Initialized = "Initialized" ,
CreatingTracks = "creating_tracks" ,
ReadyToPublish = "ready_to_publish" ,
2025-11-27 14:42:23 +01:00
WaitingToPublish = "waiting_to_publish" ,
2025-11-07 08:44:44 +01:00
Connected = "connected" ,
Disconnected = "disconnected" ,
Disconnecting = "disconnecting" ,
2025-11-04 20:24:15 +01:00
}
2025-11-14 16:18:31 +01:00
2025-12-01 14:42:15 +01:00
type LocalMemberRtcBackendState =
| { state : RTCBackendState.Error ; error : ElementCallError }
| { state : RTCBackendState.WaitingForTransport }
| { state : RTCBackendState.WaitingForConnection }
| { state : RTCBackendState.Initialized }
| { state : RTCBackendState.CreatingTracks }
| { state : RTCBackendState.ReadyToPublish }
| { state : RTCBackendState.WaitingToPublish }
| { state : RTCBackendState.Connected }
| { state : RTCBackendState.Disconnected }
| { state : RTCBackendState.Disconnecting };
2025-11-04 20:24:15 +01:00
2025-11-07 08:44:44 +01:00
export enum MatrixState {
2025-11-25 20:18:34 +01:00
WaitingForTransport = "waiting_for_transport" ,
Ready = "ready" ,
Connecting = "connecting" ,
2025-11-07 08:44:44 +01:00
Connected = "connected" ,
Disconnected = "disconnected" ,
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 }
2025-11-25 20:18:34 +01:00
| { state : MatrixState.WaitingForTransport }
| { state : MatrixState.Ready }
2025-11-07 08:44:44 +01:00
| { 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-12-01 14:42:15 +01:00
livekit$ : Behavior < LocalMemberRtcBackendState >;
2025-11-12 12:09:31 +01:00
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 ;
2025-11-25 20:18:34 +01:00
joinMatrixRTC : ( transport : LivekitTransport ) => Promise < void >;
2025-11-20 14:42:12 +01:00
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-12-01 14:42:15 +01:00
/**
* This starts audio and video tracks. They will be reused when calling `requestConnect`.
*/
2025-11-04 20:24:15 +01:00
startTracks : () => Behavior < LocalTrack [] >;
2025-12-01 14:42:15 +01:00
/**
* This sets a inner state (shouldConnect) to true and instructs the js-sdk and livekit to keep the user
* connected to matrix and livekit.
*/
requestConnect : () => void ;
2025-11-25 20:18:34 +01:00
requestDisconnect : () => void ;
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-27 14:42:23 +01:00
tracks$ : Behavior < LocalTrack [] >;
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-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
2025-11-25 20:18:34 +01:00
// Unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
2025-11-20 14:42:12 +01:00
const localTransport$ = scope . behavior (
localTransportCanThrow$ . pipe (
catchError (( e : unknown ) => {
2025-11-21 13:04:28 +01:00
let error : ElementCallError ;
2025-11-20 14:42:12 +01:00
if ( e instanceof ElementCallError ) {
2025-11-21 13:04:28 +01:00
error = e ;
2025-11-20 14:42:12 +01:00
} else {
2025-11-21 13:04:28 +01:00
error = new UnknownCallError (
e instanceof Error
? e
: new Error ( "Unknown error from localTransport" ),
);
2025-11-20 14:42:12 +01:00
}
2025-11-25 20:18:34 +01:00
setLivekitError ( error );
2025-11-20 14:42:12 +01:00
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 (
2025-11-25 20:18:34 +01:00
map (([{ value : connectionData }, localTransport ]) => {
2025-11-14 16:18:31 +01:00
if ( localTransport === null ) {
return null ;
}
2025-11-20 14:42:12 +01:00
2025-11-25 20:18:34 +01:00
return connectionData . 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
),
);
2025-11-27 14:42:23 +01:00
const localConnectionState$ = localConnection$ . pipe (
switchMap (( connection ) => ( connection ? connection.state$ : of ( null ))),
);
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.
// */
const connected$ = scope . behavior (
and$ (
2025-12-01 17:29:21 +01:00
homeserverConnected$ . pipe (
2025-12-01 17:34:37 +01:00
tap (( v ) => logger . debug ( "matrix: Connected state changed" , v )),
2025-12-01 17:29:21 +01:00
),
2025-11-27 14:42:23 +01:00
localConnectionState$ . pipe (
2025-12-01 14:42:15 +01:00
switchMap (( state ) => {
2025-12-01 17:34:37 +01:00
logger . debug ( "livekit: Connected state changed" , state );
2025-12-01 14:42:15 +01:00
if ( ! state ) return of ( false );
if ( state . state === "ConnectedToLkRoom" ) {
2025-12-01 17:34:37 +01:00
logger . debug (
2025-12-01 17:29:21 +01:00
"livekit: Connected state changed (inner livekitConnectionState$)" ,
state . livekitConnectionState$ . value ,
);
return state . livekitConnectionState$ . pipe (
2025-12-01 14:42:15 +01:00
map (( lkState ) => lkState === ConnectionState . Connected ),
);
}
return of ( false );
}),
2025-11-04 20:24:15 +01:00
),
2025-12-01 17:34:37 +01:00
). pipe ( tap (( v ) => logger . debug ( "combined: Connected state changed" , v ))),
2025-11-04 20:24:15 +01:00
);
// 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
2025-11-25 20:18:34 +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-27 14:42:23 +01:00
const trackStartRequested = Promise . withResolvers < void >();
2025-11-25 20:18:34 +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.
const connectRequested$ = new BehaviorSubject ( false );
/**
* The publisher is stored in here an abstracts creating and publishing tracks.
*/
const publisher$ = new BehaviorSubject < Publisher | null >( null );
/**
* Extract the tracks from the published. Also reacts to changing publishers.
*/
const tracks$ = scope . behavior (
2025-11-27 14:42:23 +01:00
publisher$ . pipe ( switchMap (( p ) => ( p ? . tracks$ ? p.tracks$ : constant ([])))),
2025-11-25 20:18:34 +01:00
);
const publishing$ = scope . behavior (
2025-12-01 14:42:15 +01:00
publisher$ . pipe ( switchMap (( p ) => p ? . publishing$ ?? constant ( false ))),
2025-11-25 20:18:34 +01:00
);
2025-11-04 20:24:15 +01:00
const startTracks = () : Behavior < LocalTrack [] > => {
2025-11-27 14:42:23 +01:00
trackStartRequested . resolve ();
2025-11-04 20:24:15 +01:00
return tracks$ ;
};
2025-11-25 20:18:34 +01:00
const requestConnect = () : void => {
2025-11-27 14:42:23 +01:00
trackStartRequested . resolve ();
2025-11-25 20:18:34 +01:00
connectRequested$ . next ( true );
};
const requestDisconnect = () : void => {
connectRequested$ . next ( false );
};
// Take care of the publisher$
// create a new one as soon as a local Connection is available
//
// Recreate a new one once the local connection changes
// - stop publishing
// - destruct all current streams
// - overwrite current publisher
scope . reconcile ( localConnection$ , async ( connection ) => {
if ( connection !== null ) {
2025-12-08 23:33:41 -05:00
const publisher = createPublisherFactory ( connection );
publisher$ . next ( publisher );
// Clean-up callback
return Promise . resolve ( async () : Promise < void > => {
await publisher . stopPublishing ();
publisher . stopTracks ();
});
2025-11-04 20:24:15 +01:00
}
2025-11-07 19:07:45 +01:00
});
2025-11-12 12:09:31 +01:00
2025-11-25 20:18:34 +01:00
// Use reconcile here to not run concurrent createAndSetupTracks calls
// `tracks$` will update once they are ready.
scope . reconcile (
2025-11-27 14:42:23 +01:00
scope . behavior (
combineLatest ([ publisher$ , tracks$ , from ( trackStartRequested . promise )]),
null ,
),
async ( valueIfReady ) => {
if ( ! valueIfReady ) return ;
const [ publisher , tracks ] = valueIfReady ;
if ( publisher && tracks . length === 0 ) {
2025-11-25 20:18:34 +01:00
await publisher . createAndSetupTracks (). catch (( e ) => logger . error ( e ));
}
2025-11-07 19:07:45 +01:00
},
);
2025-11-25 20:18:34 +01:00
// Based on `connectRequested$` we start publishing tracks. (once they are there!)
scope . reconcile (
scope . behavior ( combineLatest ([ publisher$ , tracks$ , connectRequested$ ])),
async ([ publisher , tracks , shouldConnect ]) => {
2025-11-27 14:42:23 +01:00
if ( shouldConnect === publisher ? . publishing$ . value ) return ;
2025-11-25 20:18:34 +01:00
if ( tracks . length !== 0 && shouldConnect ) {
try {
await publisher ? . startPublishing ();
} catch ( error ) {
setLivekitError ( error as ElementCallError );
}
} else if ( tracks . length !== 0 && ! shouldConnect ) {
try {
await publisher ? . stopPublishing ();
} catch ( error ) {
setLivekitError ( new UnknownCallError ( error as Error ));
}
}
},
);
2025-11-20 14:42:12 +01:00
2025-11-25 20:18:34 +01:00
const fatalLivekitError$ = new BehaviorSubject < ElementCallError | null >( null );
const setLivekitError = ( e : ElementCallError ) : void => {
if ( fatalLivekitError$ . value !== null )
logger . error ( "Multiple Livkit Errors:" , e );
else fatalLivekitError$ . next ( e );
};
2025-12-01 14:42:15 +01:00
const livekitState$ : Behavior < LocalMemberRtcBackendState > = scope . behavior (
2025-11-27 14:42:23 +01:00
combineLatest ([
publisher$ ,
localTransport$ ,
tracks$ . pipe (
tap (( t ) => {
logger . info ( "tracks$: " , t );
}),
),
publishing$ ,
connectRequested$ ,
from ( trackStartRequested . promise ). pipe (
map (() => true ),
startWith ( false ),
),
fatalLivekitError$ ,
]). pipe (
map (
([
publisher ,
localTransport ,
tracks ,
publishing ,
shouldConnect ,
shouldStartTracks ,
error ,
]) => {
// read this:
// if(!<A>) return {state: ...}
// if(!<B>) return {state: <MyState>}
//
// as:
// We do have <A> but not yet <B> so we are in <MyState>
2025-12-01 14:42:15 +01:00
if ( error !== null ) return { state : RTCBackendState.Error , error };
2025-11-27 14:42:23 +01:00
const hasTracks = tracks . length > 0 ;
if ( ! localTransport )
2025-12-01 14:42:15 +01:00
return { state : RTCBackendState.WaitingForTransport };
if ( ! publisher )
return { state : RTCBackendState.WaitingForConnection };
if ( ! shouldStartTracks ) return { state : RTCBackendState.Initialized };
if ( ! hasTracks ) return { state : RTCBackendState.CreatingTracks };
if ( ! shouldConnect ) return { state : RTCBackendState.ReadyToPublish };
if ( ! publishing ) return { state : RTCBackendState.WaitingToPublish };
return { state : RTCBackendState.Connected };
2025-11-27 14:42:23 +01:00
},
),
distinctUntilChanged ( deepCompare ),
2025-11-25 20:18:34 +01:00
),
);
2025-11-07 19:07:45 +01:00
2025-11-25 20:18:34 +01:00
const fatalMatrixError$ = new BehaviorSubject < ElementCallError | null >( null );
const setMatrixError = ( e : ElementCallError ) : void => {
if ( fatalMatrixError$ . value !== null )
logger . error ( "Multiple Matrix Errors:" , e );
else fatalMatrixError$ . next ( e );
2025-11-04 20:24:15 +01:00
};
2025-11-25 20:18:34 +01:00
const matrixState$ : Behavior < LocalMemberMatrixState > = scope . behavior (
combineLatest ([
localTransport$ ,
connectRequested$ ,
homeserverConnected$ ,
]). pipe (
map (([ localTransport , connectRequested , homeserverConnected ]) => {
if ( ! localTransport ) return { state : MatrixState.WaitingForTransport };
if ( ! connectRequested ) return { state : MatrixState.Ready };
if ( ! homeserverConnected ) return { state : MatrixState.Connecting };
return { state : MatrixState.Connected };
}),
),
);
2025-11-04 20:24:15 +01:00
2025-11-25 20:18:34 +01:00
// Keep matrix rtc session in sync with localTransport$, connectRequested$ and muteStates.video.enabled$
scope . reconcile (
scope . behavior ( combineLatest ([ localTransport$ , connectRequested$ ])),
async ([ transport , shouldConnect ]) => {
if ( ! shouldConnect ) return ;
2025-11-04 20:24:15 +01:00
2025-11-25 20:18:34 +01:00
if ( ! transport ) return ;
try {
await joinMatrixRTC ( transport );
} catch ( error ) {
logger . error ( "Error entering RTC session" , error );
if ( error instanceof Error )
setMatrixError ( new MembershipManagerError ( error ));
}
// Update our member event when our mute state changes.
const callIntentScope = new ObservableScope ();
// because this uses its own scope, we can start another reconciliation for the duration of one connection.
callIntentScope . reconcile (
muteStates . video . enabled$ ,
async ( videoEnabled ) =>
matrixRTCSession . updateCallIntent ( videoEnabled ? "video" : "audio" ),
);
return async () : Promise < void > => {
callIntentScope . end ();
try {
// Update matrixRTCSession to allow udpating the transport without leaving the session!
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 );
}
};
},
);
const participant$ = scope . behavior (
localConnection$ . pipe ( map (( c ) => c ? . livekitRoom ? . localParticipant ?? null )),
);
2025-11-04 20:24:15 +01:00
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-25 20:18:34 +01:00
// TODO refactor this based no livekitState$
combineLatest ([ participant$ , homeserverConnected$ ])
2025-11-05 12:56:58 +01:00
. pipe ( scope . bind ())
2025-11-25 20:18:34 +01:00
. subscribe (([ participant , connected ]) => {
if ( ! participant ) return ;
const publications = participant . trackPublications . values ();
2025-11-05 12:56:58 +01:00
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-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-25 20:18:34 +01:00
participant$ . pipe (
switchMap (( p ) => ( p !== null ? observeSharingScreen$ ( p ) : of ( false ))),
2025-11-07 08:44:44 +01:00
),
);
2025-12-01 14:42:15 +01:00
let toggleScreenSharing : (() => void ) | null = null ;
2025-11-21 16:14:12 +01:00
if (
2025-11-07 08:44:44 +01:00
"getDisplayMedia" in ( navigator . mediaDevices ?? {}) &&
! getUrlParams (). hideScreensharing
2025-11-21 16:14:12 +01:00
) {
toggleScreenSharing = () : void => {
const screenshareSettings : ScreenShareCaptureOptions = {
audio : true ,
selfBrowserSurface : "include" ,
surfaceSwitching : "include" ,
systemAudio : "include" ,
};
const targetScreenshareState = ! sharingScreen$ . value ;
logger . info (
`toggleScreenSharing called. Switching ${
targetScreenshareState ? "On" : "Off"
} ` ,
);
// 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-25 20:18:34 +01:00
participant$ . value
? . setScreenShareEnabled ( targetScreenshareState , screenshareSettings )
2025-11-21 16:14:12 +01:00
. catch ( logger . error );
};
}
2025-11-07 08:44:44 +01:00
2025-11-04 20:24:15 +01:00
return {
startTracks ,
requestConnect ,
requestDisconnect ,
2025-11-25 20:18:34 +01:00
connectionState : {
2025-11-27 14:42:23 +01:00
livekit$ : livekitState$ ,
2025-11-25 20:18:34 +01:00
matrix$ : matrixState$ ,
},
2025-11-27 14:42:23 +01:00
tracks$ ,
participant$ ,
2025-11-04 20:24:15 +01:00
homeserverConnected$ ,
2025-11-05 12:56:58 +01:00
reconnecting$ ,
2025-11-07 08:44:44 +01:00
sharingScreen$ ,
toggleScreenSharing ,
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
}
}