From 598ec42390d5af58f6a0951aac4b1c8bcc8efe4a Mon Sep 17 00:00:00 2001 From: "Timo K." Date: Thu, 8 Oct 2026 12:30:20 +0200 Subject: [PATCH] use updated state logic in the sdk --- sdk-target-based-on-call-view-model/main.ts | 9 ++- src/state/CallViewModel/CallViewModel.test.ts | 29 +++++----- src/state/CallViewModel/CallViewModel.ts | 58 +++++++++---------- .../CallViewModel/CallViewModelTestUtils.ts | 22 ++++--- src/utils/test.ts | 21 ++----- 5 files changed, 69 insertions(+), 70 deletions(-) diff --git a/sdk-target-based-on-call-view-model/main.ts b/sdk-target-based-on-call-view-model/main.ts index 1bb63f798..788f8575c 100644 --- a/sdk-target-based-on-call-view-model/main.ts +++ b/sdk-target-based-on-call-view-model/main.ts @@ -147,8 +147,8 @@ export async function createMatrixRTCSdk( vm.leave$ .pipe( switchMap(() => - participation.status$.pipe( - filter((status) => status === "left"), + participation.state$.pipe( + filter((state) => state.kind === "left"), take(1), ), ), @@ -164,7 +164,10 @@ export async function createMatrixRTCSdk( }, leave: (): void => vm?.leave(), stop: (): void => scope.end(), - connected$: over((p) => p.connected$, false), + connected$: over( + (p) => p.state$.pipe(map((state) => state.kind === "connected")), + false, + ), localMember$: over((p) => p.localMember$, null), remoteMembers$: over((p) => p.remoteMembers$, []), // Attested members only: the SDK has matched the sender diff --git a/src/state/CallViewModel/CallViewModel.test.ts b/src/state/CallViewModel/CallViewModel.test.ts index ce8d57c3f..aba231693 100644 --- a/src/state/CallViewModel/CallViewModel.test.ts +++ b/src/state/CallViewModel/CallViewModel.test.ts @@ -261,19 +261,22 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => { test("the selected audio output is handed to the client as it changes", () => { const { mediaDevices, selected$ } = withAudioOutput(); - withCallViewModel({ mediaDevices }, (_vm, _session, { rtcParticipation: participation }) => { - // Nothing selected yet, so the browser's choice stands - expect(participation.setAudioOutputDeviceId).not.toHaveBeenCalled(); - selected$.next({ id: "speaker", virtualEarpiece: false }); - expect(participation.setAudioOutputDeviceId).toHaveBeenCalledWith( - "speaker", - ); - selected$.next({ id: "headphones", virtualEarpiece: false }); - expect(participation.setAudioOutputDeviceId).toHaveBeenLastCalledWith( - "headphones", - ); - expect(participation.setAudioOutputDeviceId).toHaveBeenCalledTimes(2); - }); + withCallViewModel( + { mediaDevices }, + (_vm, _session, { rtcParticipation: participation }) => { + // Nothing selected yet, so the browser's choice stands + expect(participation.setAudioOutputDeviceId).not.toHaveBeenCalled(); + selected$.next({ id: "speaker", virtualEarpiece: false }); + expect(participation.setAudioOutputDeviceId).toHaveBeenCalledWith( + "speaker", + ); + selected$.next({ id: "headphones", virtualEarpiece: false }); + expect(participation.setAudioOutputDeviceId).toHaveBeenLastCalledWith( + "headphones", + ); + expect(participation.setAudioOutputDeviceId).toHaveBeenCalledTimes(2); + }, + ); }); test("a host that controls the audio devices keeps the output from the client", () => { diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 7cc2c7cf1..b940555fa 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -295,7 +295,7 @@ export interface CallViewModel { /** Call to initiate hangup. Use in conbination with reconnection state track the async hangup process. */ hangup: () => void; - /** Leaves the participation. The async leave can then be observed via connected$. */ + /** Leaves the participation. The leave can then be observed via `leave$`. */ leave: () => void; // screen sharing /** @@ -442,9 +442,6 @@ export interface CallViewModel { /** Connected once, and currently not: to the homeserver, the session or the media transport. */ reconnecting$: Behavior; - - /** Connected to the homeserver, the session and the media transport, all three. */ - connected$: Behavior; } /** @@ -627,8 +624,12 @@ export function createCallViewModel$( localUser: { userId, deviceId }, }); + const reconnecting$ = scope.behavior( + rtcParticipation.state$.pipe(map((state) => state.kind === "reconnecting")), + ); + const handsRaised$ = scope.behavior( - handsRaisedSubject$.pipe(pauseWhen(rtcParticipation.reconnecting$)), + handsRaisedSubject$.pipe(pauseWhen(reconnecting$)), ); const reactions$ = scope.behavior( @@ -641,7 +642,7 @@ export function createCallViewModel$( ]), ), ), - pauseWhen(rtcParticipation.reconnecting$), + pauseWhen(reconnecting$), ), ); @@ -677,7 +678,7 @@ export function createCallViewModel$( member.transport$.pipe(map((transport) => transport?.id)), ), mediaDevices, - pretendToBeDisconnected$: rtcParticipation.reconnecting$, + pretendToBeDisconnected$: reconnecting$, displayName$: member.displayName$, mxcAvatarUrl$: member.avatarUrl$, handRaised$: scope.behavior( @@ -1509,25 +1510,21 @@ export function createCallViewModel$( }; let reconnectStart: { time: number; reason: DisconnectReason } | null = null; - rtcParticipation.disconnectReason$ - .pipe(distinctUntilChanged(), pairwise(), scope.bind()) - .subscribe(([prev, reason]) => { - if (reason !== null) { - // Only the loss of a connection that existed counts as a reconnect, - // not the startup phase - if (prev === null) reconnectStart ??= { time: Date.now(), reason }; - } else if (reconnectStart !== null) { - const reason = - reconnectStart.reason === "media" ? "livekit" : reconnectStart.reason; - PosthogAnalytics.instance.eventCallReconnecting.track( - matrixRoom.roomId, - reason, - (Date.now() - reconnectStart.time) / 1000, - ); - PosthogAnalytics.instance.eventCallEnded.cacheReconnecting(reason); - reconnectStart = null; - } - }); + rtcParticipation.state$.pipe(scope.bind()).subscribe((state) => { + if (state.kind === "reconnecting") { + reconnectStart ??= { time: Date.now(), reason: state.reason }; + } else if (state.kind === "connected" && reconnectStart !== null) { + const reason = + reconnectStart.reason === "media" ? "livekit" : reconnectStart.reason; + PosthogAnalytics.instance.eventCallReconnecting.track( + matrixRoom.roomId, + reason, + (Date.now() - reconnectStart.time) / 1000, + ); + PosthogAnalytics.instance.eventCallEnded.cacheReconnecting(reason); + reconnectStart = null; + } + }); return { autoLeave$: autoLeave$, @@ -1545,8 +1542,10 @@ export function createCallViewModel$( unhoverScreen: (): void => screenUnhover$.next(), fatalError$: scope.behavior( - rtcParticipation.fatalError$.pipe( - map((error) => error && fromMatrixRTCError(error)), + rtcParticipation.state$.pipe( + map((state) => + state.kind === "failed" ? fromMatrixRTCError(state.error) : null, + ), ), ), participantCount$: participantCount$, @@ -1582,8 +1581,7 @@ export function createCallViewModel$( overflowing$, earpieceMode$: earpieceMode$, audioOutputSwitcher$: audioOutputSwitcher$, - reconnecting$: rtcParticipation.reconnecting$, - connected$: rtcParticipation.connected$, + reconnecting$, screenShareError$, dismissScreenShareError: (): void => screenShareError$.next(null), }; diff --git a/src/state/CallViewModel/CallViewModelTestUtils.ts b/src/state/CallViewModel/CallViewModelTestUtils.ts index d41119514..9d533a64f 100644 --- a/src/state/CallViewModel/CallViewModelTestUtils.ts +++ b/src/state/CallViewModel/CallViewModelTestUtils.ts @@ -11,13 +11,14 @@ import { constant, E2eeType, type LocalRTCMember, + type ParticipationState, type RTCParticipation, type MatrixRTCMode, type ObservableScope, type RemoteRTCMember, } from "@element-hq/matrixrtc-sdk"; import { SyncState } from "matrix-js-sdk/lib/sync"; -import { BehaviorSubject, combineLatest, map, of, switchMap } from "rxjs"; +import { BehaviorSubject, map, of, switchMap } from "rxjs"; import { ClientEvent, type RoomMember, type MatrixClient } from "matrix-js-sdk"; import EventEmitter from "events"; @@ -244,11 +245,13 @@ export function withCallViewModel(mode: MatrixRTCMode) { const rtcParticipation = mockRTCParticipation(scope, { localMember$, remoteMembers$, - connected$, - reconnecting$: scope.behavior( - combineLatest([connected$, of(initialSyncState)]).pipe( - switchMap(() => connected$), - map((connected) => !connected), + state$: scope.behavior( + connected$.pipe( + map((connected): ParticipationState => + connected + ? { kind: "connected" } + : { kind: "reconnecting", reason: "media" }, + ), ), ), }); @@ -279,6 +282,11 @@ export function withCallViewModel(mode: MatrixRTCMode) { ); void mode; - continuation(vm, rtcSession, { raisedHands$, rtcParticipation }, setSyncState); + continuation( + vm, + rtcSession, + { raisedHands$, rtcParticipation }, + setSyncState, + ); }; } diff --git a/src/utils/test.ts b/src/utils/test.ts index 2592351e5..7a6677008 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -63,7 +63,6 @@ import { type AudioMediaTrack, type Behavior, constant, - type DisconnectReason, type EncryptionError, type LocalAudioMediaTrack, type LocalRTCMember, @@ -71,7 +70,7 @@ import { type RTCMember, type RTCParticipation, type RTCSlot, - type MatrixRTCError, + type ParticipationState, type PublishRequest, type RemoteRTCMember, type TransportMetadata, @@ -494,10 +493,7 @@ export function mockRTCMember( export interface MockParticipationInputs { localMember$?: Behavior; remoteMembers$?: Behavior; - connected$?: Behavior; - reconnecting$?: Behavior; - disconnectReason$?: Behavior; - fatalError$?: Behavior; + state$?: Behavior; keyRotationSuppressed$?: Behavior; connectedTransports$?: Behavior; } @@ -511,10 +507,7 @@ export function mockRTCParticipation( { localMember$ = constant(null), remoteMembers$ = constant([]), - connected$ = constant(true), - reconnecting$ = constant(false), - disconnectReason$ = constant(null), - fatalError$ = constant(null), + state$ = constant({ kind: "connected" }), keyRotationSuppressed$ = constant(false), connectedTransports$ = constant([]), }: MockParticipationInputs = {}, @@ -537,13 +530,7 @@ export function mockRTCParticipation( const participation: RTCParticipation = { slot, leave: vi.fn(), - status$: scope.behavior( - connected$.pipe(map((c) => (c ? "connected" : "connecting"))), - ), - connected$, - reconnecting$, - disconnectReason$, - fatalError$, + state$, localMember$, remoteMembers$, publish: vi.fn(async (request: PublishRequest) =>