This commit is contained in:
Timo K
2025-12-05 19:48:02 +01:00
parent 88721be952
commit 7c40b0e177
2 changed files with 51 additions and 39 deletions

View File

@@ -452,14 +452,18 @@ export function createCallViewModel$(
const localMembership = createLocalMembership$({ const localMembership = createLocalMembership$({
scope: scope, scope: scope,
homeserverConnected: createHomeserverConnected$( homeserverConnected$: createHomeserverConnected$(
scope, scope,
client, client,
matrixRTCSession, matrixRTCSession,
), ),
muteStates: muteStates, muteStates: muteStates,
joinMatrixRTC: (transport: LivekitTransport) => { joinMatrixRTC: async (transport: LivekitTransport) => {
enterRTCSession(matrixRTCSession, transport, connectOptions$.value); return enterRTCSession(
matrixRTCSession,
transport,
connectOptions$.value,
);
}, },
createPublisherFactory: (connection: Connection) => { createPublisherFactory: (connection: Connection) => {
return new Publisher( 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( const audioParticipants$ = scope.behavior(
matrixLivekitMembers$.pipe( matrixLivekitMembers$.pipe(
switchMap((membersWithEpoch) => { switchMap((membersWithEpoch) => {
@@ -616,7 +631,7 @@ export function createCallViewModel$(
); );
const handsRaised$ = scope.behavior( const handsRaised$ = scope.behavior(
handsRaisedSubject$.pipe(pauseWhen(localMembership.reconnecting$)), handsRaisedSubject$.pipe(pauseWhen(reconnecting$)),
); );
const reactions$ = scope.behavior( const reactions$ = scope.behavior(
@@ -629,7 +644,7 @@ export function createCallViewModel$(
]), ]),
), ),
), ),
pauseWhen(localMembership.reconnecting$), pauseWhen(reconnecting$),
), ),
); );
@@ -720,7 +735,7 @@ export function createCallViewModel$(
livekitRoom$, livekitRoom$,
focusUrl$, focusUrl$,
mediaDevices, mediaDevices,
localMembership.reconnecting$, reconnecting$,
displayName$, displayName$,
matrixMemberMetadataStore.createAvatarUrlBehavior$(userId), matrixMemberMetadataStore.createAvatarUrlBehavior$(userId),
handsRaised$.pipe(map((v) => v[participantId]?.time ?? null)), handsRaised$.pipe(map((v) => v[participantId]?.time ?? null)),
@@ -812,17 +827,11 @@ export function createCallViewModel$(
}), }),
); );
const shouldLeave$: Observable< const leave$: Observable<"user" | "timeout" | "decline" | "allOthersLeft"> =
"user" | "timeout" | "decline" | "allOthersLeft" merge(
> = merge( autoLeave$,
autoLeave$, merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)),
merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)), ).pipe(scope.share);
).pipe(scope.share);
shouldLeave$.pipe(scope.bind()).subscribe((reason) => {
logger.info(`Call left due to ${reason}`);
localMembership.requestDisconnect();
});
const spotlightSpeaker$ = scope.behavior<UserMediaViewModel | null>( const spotlightSpeaker$ = scope.behavior<UserMediaViewModel | null>(
userMedia$.pipe( userMedia$.pipe(
@@ -1444,7 +1453,7 @@ export function createCallViewModel$(
autoLeave$: autoLeave$, autoLeave$: autoLeave$,
callPickupState$: callPickupState$, callPickupState$: callPickupState$,
ringOverlay$: ringOverlay$, ringOverlay$: ringOverlay$,
leave$: shouldLeave$, leave$: leave$,
hangup: (): void => userHangup$.next(), hangup: (): void => userHangup$.next(),
join: localMembership.requestConnect, join: localMembership.requestConnect,
toggleScreenSharing: toggleScreenSharing, toggleScreenSharing: toggleScreenSharing,
@@ -1491,7 +1500,7 @@ export function createCallViewModel$(
showFooter$: showFooter$, showFooter$: showFooter$,
earpieceMode$: earpieceMode$, earpieceMode$: earpieceMode$,
audioOutputSwitcher$: audioOutputSwitcher$, audioOutputSwitcher$: audioOutputSwitcher$,
reconnecting$: localMembership.reconnecting$, reconnecting$: reconnecting$,
}; };
} }

View File

@@ -12,7 +12,7 @@ import {
} from "@livekit/components-core"; } from "@livekit/components-core";
import { import {
ConnectionError, ConnectionError,
type ConnectionState as LivekitConnectionState, ConnectionState as LivekitConnectionState,
type Room as LivekitRoom, type Room as LivekitRoom,
type LocalParticipant, type LocalParticipant,
type RemoteParticipant, type RemoteParticipant,
@@ -47,17 +47,24 @@ export interface ConnectionOpts {
/** Optional factory to create the LiveKit room, mainly for testing purposes. */ /** Optional factory to create the LiveKit room, mainly for testing purposes. */
livekitRoomFactory: () => LivekitRoom; livekitRoomFactory: () => LivekitRoom;
} }
export enum ConnectionAdditionalState { export class FailedToStartError extends Error {
public constructor(message: string) {
super(message);
this.name = "FailedToStartError";
}
}
export enum ConnectionState {
Initialized = "Initialized", Initialized = "Initialized",
FetchingConfig = "FetchingConfig", FetchingConfig = "FetchingConfig",
// FailedToStart = "FailedToStart",
Stopped = "Stopped", Stopped = "Stopped",
ConnectingToLkRoom = "ConnectingToLkRoom", 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. * A connection to a Matrix RTC LiveKit backend.
@@ -66,14 +73,15 @@ export type ConnectionState =
*/ */
export class Connection { export class Connection {
// Private Behavior // Private Behavior
private readonly _state$ = new BehaviorSubject<ConnectionState>({ private readonly _state$ = new BehaviorSubject<
state: ConnectionAdditionalState.Initialized, ConnectionState | FailedToStartError
}); >(ConnectionState.Initialized);
/** /**
* The current state of the connection to the media transport. * The current state of the connection to the media transport.
*/ */
public readonly state$: Behavior<ConnectionState> = this._state$; public readonly state$: Behavior<ConnectionState | FailedToStartError> =
this._state$;
/** /**
* The media transport to connect to. * The media transport to connect to.
@@ -117,16 +125,12 @@ export class Connection {
this.logger.debug("Starting Connection"); this.logger.debug("Starting Connection");
this.stopped = false; this.stopped = false;
try { try {
this._state$.next({ this._state$.next(ConnectionState.FetchingConfig);
state: ConnectionAdditionalState.FetchingConfig,
});
const { url, jwt } = await this.getSFUConfigWithOpenID(); const { url, jwt } = await this.getSFUConfigWithOpenID();
// If we were stopped while fetching the config, don't proceed to connect // If we were stopped while fetching the config, don't proceed to connect
if (this.stopped) return; if (this.stopped) return;
this._state$.next({ this._state$.next(ConnectionState.ConnectingToLkRoom);
state: ConnectionAdditionalState.ConnectingToLkRoom,
});
try { try {
await this.livekitRoom.connect(url, jwt); await this.livekitRoom.connect(url, jwt);
} catch (e) { } catch (e) {
@@ -157,9 +161,8 @@ export class Connection {
connectionStateObserver(this.livekitRoom) connectionStateObserver(this.livekitRoom)
.pipe(this.scope.bind()) .pipe(this.scope.bind())
.subscribe((lkState) => { .subscribe((lkState) => {
this._state$.next({ // It si save to cast lkState to ConnectionState as they are fully overlapping.
state: lkState, this._state$.next(lkState as unknown as ConnectionState);
});
}); });
} catch (error) { } catch (error) {
this.logger.debug(`Failed to connect to LiveKit room: ${error}`); this.logger.debug(`Failed to connect to LiveKit room: ${error}`);