mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
use updated state logic in the sdk
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
/** Connected to the homeserver, the session and the media transport, all three. */
|
||||
connected$: Behavior<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
+4
-17
@@ -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<LocalRTCMember | null>;
|
||||
remoteMembers$?: Behavior<RemoteRTCMember[]>;
|
||||
connected$?: Behavior<boolean>;
|
||||
reconnecting$?: Behavior<boolean>;
|
||||
disconnectReason$?: Behavior<DisconnectReason | null>;
|
||||
fatalError$?: Behavior<MatrixRTCError | null>;
|
||||
state$?: Behavior<ParticipationState>;
|
||||
keyRotationSuppressed$?: Behavior<boolean>;
|
||||
connectedTransports$?: Behavior<TransportMetadata[]>;
|
||||
}
|
||||
@@ -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<ParticipationState>({ 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) =>
|
||||
|
||||
Reference in New Issue
Block a user