mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
use sdk client -> participation/slot seperation
This commit is contained in:
@@ -26,7 +26,7 @@ import {
|
|||||||
mockMuteStates,
|
mockMuteStates,
|
||||||
mockRtcMembership,
|
mockRtcMembership,
|
||||||
type MockRTCSession,
|
type MockRTCSession,
|
||||||
mockMatrixRTCClient,
|
mockRTCSlot,
|
||||||
mockRTCMember,
|
mockRTCMember,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
@@ -69,11 +69,11 @@ vi.mock("../tile/GridTile");
|
|||||||
vi.mock("../tile/SpotlightTile");
|
vi.mock("../tile/SpotlightTile");
|
||||||
vi.mock("../e2ee/sharedKeyManagement");
|
vi.mock("../e2ee/sharedKeyManagement");
|
||||||
vi.mock("../tracks/MemberAudioRenderer");
|
vi.mock("../tracks/MemberAudioRenderer");
|
||||||
// ActiveCall builds the client itself; the view is what these tests are about
|
// ActiveCall builds the slot itself; the view is what these tests are about
|
||||||
const createMatrixRTCClientMock = vi.hoisted(() => vi.fn());
|
const createRTCSlotMock = vi.hoisted(() => vi.fn());
|
||||||
vi.mock("@element-hq/matrixrtc-sdk", async (importOriginal) => ({
|
vi.mock("@element-hq/matrixrtc-sdk", async (importOriginal) => ({
|
||||||
...(await importOriginal<typeof MatrixRTCSdk>()),
|
...(await importOriginal<typeof MatrixRTCSdk>()),
|
||||||
createMatrixRTCClient: createMatrixRTCClientMock,
|
createRTCSlot: createRTCSlotMock,
|
||||||
}));
|
}));
|
||||||
vi.mock("react-use-measure", () => ({
|
vi.mock("react-use-measure", () => ({
|
||||||
default: (): [() => void, object] => [(): void => {}, {}],
|
default: (): [() => void, object] => [(): void => {}, {}],
|
||||||
@@ -97,8 +97,8 @@ let useRoomEncryptionSystemMock: MockedFunction<typeof useRoomEncryptionSystem>;
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|
||||||
createMatrixRTCClientMock.mockImplementation((scope: ObservableScope) =>
|
createRTCSlotMock.mockImplementation((scope: ObservableScope) =>
|
||||||
mockMatrixRTCClient(scope, {
|
mockRTCSlot(scope, {
|
||||||
localMember$: constant(
|
localMember$: constant(
|
||||||
mockRTCMember(true, {
|
mockRTCMember(true, {
|
||||||
membership: localRtcMember,
|
membership: localRtcMember,
|
||||||
|
|||||||
+23
-20
@@ -69,8 +69,9 @@ import { ReactionsAudioRenderer } from "./ReactionAudioRenderer";
|
|||||||
import { ReactionsOverlay } from "./ReactionsOverlay";
|
import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||||
import {
|
import {
|
||||||
createMatrixRTCClient,
|
createRTCSlot,
|
||||||
type MatrixRTCClientOptions,
|
type RTCParticipationOptions,
|
||||||
|
type RTCSlotOptions,
|
||||||
constant,
|
constant,
|
||||||
} from "@element-hq/matrixrtc-sdk";
|
} from "@element-hq/matrixrtc-sdk";
|
||||||
import { map } from "rxjs";
|
import { map } from "rxjs";
|
||||||
@@ -161,22 +162,20 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
urlParams;
|
urlParams;
|
||||||
|
|
||||||
const capture = captureSettings();
|
const capture = captureSettings();
|
||||||
const rtcClient = createMatrixRTCClient(
|
const slot = createRTCSlot(
|
||||||
scope,
|
scope,
|
||||||
props.client,
|
props.client,
|
||||||
props.matrixRoom,
|
props.matrixRoom,
|
||||||
{
|
rtcSlotOptions(props.e2eeSystem),
|
||||||
...matrixRTCClientOptions(urlParams, props.e2eeSystem),
|
|
||||||
publish: initialPublishRequests(
|
|
||||||
props.muteStates,
|
|
||||||
mediaDevices,
|
|
||||||
capture,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
const rtcParticipation = slot.join({
|
||||||
|
...rtcParticipationOptions(urlParams),
|
||||||
|
publish: initialPublishRequests(props.muteStates, mediaDevices, capture),
|
||||||
|
});
|
||||||
|
|
||||||
const vm = createCallViewModel$(
|
const vm = createCallViewModel$(
|
||||||
scope,
|
scope,
|
||||||
rtcClient,
|
rtcParticipation,
|
||||||
props.matrixRoom,
|
props.matrixRoom,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
props.muteStates,
|
props.muteStates,
|
||||||
@@ -199,8 +198,6 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
);
|
);
|
||||||
// TODO move this somewhere else once we use the callViewModel in the lobby as well!
|
|
||||||
vm.join();
|
|
||||||
setVm(vm);
|
setVm(vm);
|
||||||
|
|
||||||
vm.leave$.pipe(scope.bind()).subscribe(props.onLeft);
|
vm.leave$.pipe(scope.bind()).subscribe(props.onLeft);
|
||||||
@@ -793,20 +790,26 @@ export function captureSettings(): CaptureSettings {
|
|||||||
* switches and the devices, so the caller adds that. Shared with the legacy
|
* switches and the devices, so the caller adds that. Shared with the legacy
|
||||||
* SDK bundle so that a widget built from it behaves like the app.
|
* SDK bundle so that a widget built from it behaves like the app.
|
||||||
*/
|
*/
|
||||||
export function matrixRTCClientOptions(
|
export function rtcSlotOptions(
|
||||||
urlParams: ReturnType<typeof useUrlParams>,
|
|
||||||
encryptionSystem: EncryptionSystem,
|
encryptionSystem: EncryptionSystem,
|
||||||
): Omit<MatrixRTCClientOptions, "publish"> {
|
): RTCSlotOptions {
|
||||||
const config = Config.get();
|
|
||||||
const session = config.matrix_rtc_session;
|
|
||||||
return {
|
return {
|
||||||
encryptionSystem,
|
encryptionSystem,
|
||||||
// matrix_rtc_mode in config.json overrides the user's Developer Settings
|
// matrix_rtc_mode in config.json overrides the user's Developer Settings
|
||||||
// choice. We merely sample the current mode here, so the user would need
|
// choice. We merely sample the current mode here, so the user would need
|
||||||
// to manually rejoin to switch to a different one.
|
// to manually rejoin to switch to a different one.
|
||||||
matrixRTCMode:
|
matrixRTCMode:
|
||||||
(config.matrix_rtc_mode as MatrixRTCMode | undefined) ??
|
(Config.get().matrix_rtc_mode as MatrixRTCMode | undefined) ??
|
||||||
matrixRTCModeSetting.value$.value,
|
matrixRTCModeSetting.value$.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rtcParticipationOptions(
|
||||||
|
urlParams: ReturnType<typeof useUrlParams>,
|
||||||
|
): Omit<RTCParticipationOptions, "publish"> {
|
||||||
|
const config = Config.get();
|
||||||
|
const session = config.matrix_rtc_session;
|
||||||
|
return {
|
||||||
sendNotificationType: urlParams.sendNotificationType,
|
sendNotificationType: urlParams.sendNotificationType,
|
||||||
applicationData: urlParams.callIntent
|
applicationData: urlParams.callIntent
|
||||||
? { "m.call.intent": urlParams.callIntent }
|
? { "m.call.intent": urlParams.callIntent }
|
||||||
|
|||||||
@@ -261,16 +261,18 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
|||||||
|
|
||||||
test("the selected audio output is handed to the client as it changes", () => {
|
test("the selected audio output is handed to the client as it changes", () => {
|
||||||
const { mediaDevices, selected$ } = withAudioOutput();
|
const { mediaDevices, selected$ } = withAudioOutput();
|
||||||
withCallViewModel({ mediaDevices }, (_vm, _session, { rtcClient }) => {
|
withCallViewModel({ mediaDevices }, (_vm, _session, { rtcParticipation: participation }) => {
|
||||||
// Nothing selected yet, so the browser's choice stands
|
// Nothing selected yet, so the browser's choice stands
|
||||||
expect(rtcClient.setAudioOutputDeviceId).not.toHaveBeenCalled();
|
expect(participation.setAudioOutputDeviceId).not.toHaveBeenCalled();
|
||||||
selected$.next({ id: "speaker", virtualEarpiece: false });
|
selected$.next({ id: "speaker", virtualEarpiece: false });
|
||||||
expect(rtcClient.setAudioOutputDeviceId).toHaveBeenCalledWith("speaker");
|
expect(participation.setAudioOutputDeviceId).toHaveBeenCalledWith(
|
||||||
|
"speaker",
|
||||||
|
);
|
||||||
selected$.next({ id: "headphones", virtualEarpiece: false });
|
selected$.next({ id: "headphones", virtualEarpiece: false });
|
||||||
expect(rtcClient.setAudioOutputDeviceId).toHaveBeenLastCalledWith(
|
expect(participation.setAudioOutputDeviceId).toHaveBeenLastCalledWith(
|
||||||
"headphones",
|
"headphones",
|
||||||
);
|
);
|
||||||
expect(rtcClient.setAudioOutputDeviceId).toHaveBeenCalledTimes(2);
|
expect(participation.setAudioOutputDeviceId).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,9 +280,9 @@ describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
|||||||
const { mediaDevices, selected$ } = withAudioOutput();
|
const { mediaDevices, selected$ } = withAudioOutput();
|
||||||
withCallViewModel(
|
withCallViewModel(
|
||||||
{ mediaDevices },
|
{ mediaDevices },
|
||||||
(_vm, _session, { rtcClient }) => {
|
(_vm, _session, { rtcParticipation: participation }) => {
|
||||||
selected$.next({ id: "earpiece", virtualEarpiece: true });
|
selected$.next({ id: "earpiece", virtualEarpiece: true });
|
||||||
expect(rtcClient.setAudioOutputDeviceId).not.toHaveBeenCalled();
|
expect(participation.setAudioOutputDeviceId).not.toHaveBeenCalled();
|
||||||
},
|
},
|
||||||
{ controlledAudioDevices: true },
|
{ controlledAudioDevices: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
type LocalMediaTrack,
|
type LocalMediaTrack,
|
||||||
type LocalRTCMember,
|
type LocalRTCMember,
|
||||||
type LocalVideoMediaTrack,
|
type LocalVideoMediaTrack,
|
||||||
type MatrixRTCClient,
|
type RTCParticipation,
|
||||||
type ObservableScope,
|
type ObservableScope,
|
||||||
pauseWhen,
|
pauseWhen,
|
||||||
type PublishRequest,
|
type PublishRequest,
|
||||||
@@ -158,7 +158,7 @@ export function publishRequest(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** What the client publishes at the join: the sources whose mute switch is on. */
|
/** What the participation publishes at the join: the sources whose mute switch is on. */
|
||||||
export function initialPublishRequests(
|
export function initialPublishRequests(
|
||||||
muteStates: MuteStates,
|
muteStates: MuteStates,
|
||||||
mediaDevices: MediaDevices,
|
mediaDevices: MediaDevices,
|
||||||
@@ -295,12 +295,7 @@ export interface CallViewModel {
|
|||||||
/** Call to initiate hangup. Use in conbination with reconnection state track the async hangup process. */
|
/** Call to initiate hangup. Use in conbination with reconnection state track the async hangup process. */
|
||||||
hangup: () => void;
|
hangup: () => void;
|
||||||
|
|
||||||
// joining
|
/** Leaves the participation. The async leave can then be observed via connected$. */
|
||||||
join: () => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* calls requestDisconnect. The async leave state can than be observed via connected$
|
|
||||||
*/
|
|
||||||
leave: () => void;
|
leave: () => void;
|
||||||
// screen sharing
|
// screen sharing
|
||||||
/**
|
/**
|
||||||
@@ -457,12 +452,12 @@ export interface CallViewModel {
|
|||||||
* UI (may eventually be expanded to cover the lobby and feedback screens in the
|
* UI (may eventually be expanded to cover the lobby and feedback screens in the
|
||||||
* future).
|
* future).
|
||||||
*
|
*
|
||||||
* The session itself — memberships, transports, media — is the client's; this
|
* The slot and the participation — memberships, transports, media — are the
|
||||||
* adds everything that makes it a call.
|
* SDK's; this adds everything that makes it a call.
|
||||||
*/
|
*/
|
||||||
export function createCallViewModel$(
|
export function createCallViewModel$(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
rtcClient: MatrixRTCClient,
|
rtcParticipation: RTCParticipation,
|
||||||
// A call is permanently tied to a single Matrix room
|
// A call is permanently tied to a single Matrix room
|
||||||
matrixRoom: MatrixRoom,
|
matrixRoom: MatrixRoom,
|
||||||
mediaDevices: MediaDevices,
|
mediaDevices: MediaDevices,
|
||||||
@@ -492,7 +487,7 @@ export function createCallViewModel$(
|
|||||||
const localTracks$ = scope.behavior<
|
const localTracks$ = scope.behavior<
|
||||||
(LocalAudioMediaTrack | LocalVideoMediaTrack)[]
|
(LocalAudioMediaTrack | LocalVideoMediaTrack)[]
|
||||||
>(
|
>(
|
||||||
rtcClient.localMember$.pipe(
|
rtcParticipation.localMember$.pipe(
|
||||||
switchMap((member) => member?.tracks$ ?? of(null)),
|
switchMap((member) => member?.tracks$ ?? of(null)),
|
||||||
map((tracks) => tracks ?? []),
|
map((tracks) => tracks ?? []),
|
||||||
),
|
),
|
||||||
@@ -534,10 +529,10 @@ export function createCallViewModel$(
|
|||||||
return desired;
|
return desired;
|
||||||
}
|
}
|
||||||
if (!enabled) return false;
|
if (!enabled) return false;
|
||||||
const member = rtcClient.localMember$.value;
|
|
||||||
if (member === null) return false;
|
|
||||||
try {
|
try {
|
||||||
await member.publish(publishRequest(source, mediaDevices, capture));
|
await rtcParticipation.publish(
|
||||||
|
publishRequest(source, mediaDevices, capture),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Failed to publish the ${source}`, e);
|
logger.error(`Failed to publish the ${source}`, e);
|
||||||
@@ -587,7 +582,7 @@ export function createCallViewModel$(
|
|||||||
.pipe(scope.bind())
|
.pipe(scope.bind())
|
||||||
.subscribe((selected) => {
|
.subscribe((selected) => {
|
||||||
if (selected === undefined) return;
|
if (selected === undefined) return;
|
||||||
rtcClient.setAudioOutputDeviceId(selected.id).catch((e) => {
|
rtcParticipation.setAudioOutputDeviceId(selected.id).catch((e) => {
|
||||||
logger.error("Failed to switch the audio output device", e);
|
logger.error("Failed to switch the audio output device", e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -604,7 +599,7 @@ export function createCallViewModel$(
|
|||||||
|
|
||||||
const members$ = scope.behavior<(LocalRTCMember | RemoteRTCMember)[]>(
|
const members$ = scope.behavior<(LocalRTCMember | RemoteRTCMember)[]>(
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[rtcClient.localMember$, rtcClient.remoteMembers$],
|
[rtcParticipation.localMember$, rtcParticipation.remoteMembers$],
|
||||||
(local, remote) => (local === null ? remote : [local, ...remote]),
|
(local, remote) => (local === null ? remote : [local, ...remote]),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -633,7 +628,7 @@ export function createCallViewModel$(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handsRaised$ = scope.behavior(
|
const handsRaised$ = scope.behavior(
|
||||||
handsRaisedSubject$.pipe(pauseWhen(rtcClient.reconnecting$)),
|
handsRaisedSubject$.pipe(pauseWhen(rtcParticipation.reconnecting$)),
|
||||||
);
|
);
|
||||||
|
|
||||||
const reactions$ = scope.behavior(
|
const reactions$ = scope.behavior(
|
||||||
@@ -646,7 +641,7 @@ export function createCallViewModel$(
|
|||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
pauseWhen(rtcClient.reconnecting$),
|
pauseWhen(rtcParticipation.reconnecting$),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -663,7 +658,10 @@ export function createCallViewModel$(
|
|||||||
for (const member of members) {
|
for (const member of members) {
|
||||||
const mediaId = `${member.userId}:${member.deviceId}`;
|
const mediaId = `${member.userId}:${member.deviceId}`;
|
||||||
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
for (let dup = 0; dup < 1 + duplicateTiles; dup++) {
|
||||||
yield { keys: [dup, mediaId, member.id], data: member };
|
yield {
|
||||||
|
keys: [dup, mediaId, member.rtcBackendIdentity],
|
||||||
|
data: member,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -672,14 +670,14 @@ export function createCallViewModel$(
|
|||||||
return createWrappedUserMedia(scope, {
|
return createWrappedUserMedia(scope, {
|
||||||
id: `${mediaId}:${dup}`,
|
id: `${mediaId}:${dup}`,
|
||||||
userId: member.userId,
|
userId: member.userId,
|
||||||
rtcBackendIdentity: member.id,
|
rtcBackendIdentity: member.rtcBackendIdentity,
|
||||||
member,
|
member,
|
||||||
encryptionSystem: options.encryptionSystem,
|
encryptionSystem: options.encryptionSystem,
|
||||||
focusUrl$: scope.behavior(
|
focusUrl$: scope.behavior(
|
||||||
member.transport$.pipe(map((transport) => transport?.id)),
|
member.transport$.pipe(map((transport) => transport?.id)),
|
||||||
),
|
),
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
pretendToBeDisconnected$: rtcClient.reconnecting$,
|
pretendToBeDisconnected$: rtcParticipation.reconnecting$,
|
||||||
displayName$: member.displayName$,
|
displayName$: member.displayName$,
|
||||||
mxcAvatarUrl$: member.avatarUrl$,
|
mxcAvatarUrl$: member.avatarUrl$,
|
||||||
handRaised$: scope.behavior(
|
handRaised$: scope.behavior(
|
||||||
@@ -761,9 +759,12 @@ export function createCallViewModel$(
|
|||||||
* - There can be multiple participants for one Matrix user if they join from
|
* - There can be multiple participants for one Matrix user if they join from
|
||||||
* multiple devices.
|
* multiple devices.
|
||||||
*/
|
*/
|
||||||
const participantCount$ = rtcClient.memberCount$;
|
// One per membership, so one user may count several times
|
||||||
|
const participantCount$ = scope.behavior(
|
||||||
|
rtcParticipation.slot.members$.pipe(map((members) => members.length)),
|
||||||
|
);
|
||||||
|
|
||||||
const keyRotationSuppressed$ = rtcClient.keyRotationSuppressed$;
|
const keyRotationSuppressed$ = rtcParticipation.keyRotationSuppressed$;
|
||||||
|
|
||||||
const leaveSoundEffect$ = userMedia$.pipe(
|
const leaveSoundEffect$ = userMedia$.pipe(
|
||||||
pairwise(),
|
pairwise(),
|
||||||
@@ -1473,15 +1474,13 @@ export function createCallViewModel$(
|
|||||||
(hideScreensharing || !("getDisplayMedia" in (navigator.mediaDevices ?? {}))
|
(hideScreensharing || !("getDisplayMedia" in (navigator.mediaDevices ?? {}))
|
||||||
? null
|
? null
|
||||||
: (): void => {
|
: (): void => {
|
||||||
const member = rtcClient.localMember$.value;
|
|
||||||
if (member === null) return;
|
|
||||||
const track = screenShare$.value;
|
const track = screenShare$.value;
|
||||||
(track === undefined
|
(track === undefined
|
||||||
? member.publish({
|
? rtcParticipation.publish({
|
||||||
source: "screenShare",
|
source: "screenShare",
|
||||||
capture: capture.screenShare,
|
capture: capture.screenShare,
|
||||||
})
|
})
|
||||||
: member.unpublish(track.id)
|
: rtcParticipation.unpublish("screenShare")
|
||||||
).catch((e: unknown) => {
|
).catch((e: unknown) => {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Screen share ${track === undefined ? "start" : "stop"} failed`,
|
`Screen share ${track === undefined ? "start" : "stop"} failed`,
|
||||||
@@ -1495,24 +1494,22 @@ export function createCallViewModel$(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tell the host and the analytics about the user's joins and leaves
|
// Tell the host and the analytics about the user's joins and leaves. The
|
||||||
const join = (): void => {
|
// participation is joined by the time the view model is built on it
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(matrixRoom.roomId);
|
PosthogAnalytics.instance.eventCallStarted.track(matrixRoom.roomId);
|
||||||
rtcClient.join();
|
|
||||||
hostBridge.notifyJoined().catch((e) => {
|
hostBridge.notifyJoined().catch((e) => {
|
||||||
logger.error("Failed to notify the host that we joined", e);
|
logger.error("Failed to notify the host that we joined", e);
|
||||||
});
|
});
|
||||||
};
|
|
||||||
const leave = (): void => {
|
const leave = (): void => {
|
||||||
rtcClient.leave();
|
rtcParticipation.leave();
|
||||||
hostBridge.notifyHungUp().catch((e) => {
|
hostBridge.notifyHungUp().catch((e) => {
|
||||||
logger.error("Failed to notify the host that we hung up", e);
|
logger.error("Failed to notify the host that we hung up", e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let reconnectStart: { time: number; reason: DisconnectReason } | null = null;
|
let reconnectStart: { time: number; reason: DisconnectReason } | null = null;
|
||||||
rtcClient.disconnectReason$
|
rtcParticipation.disconnectReason$
|
||||||
.pipe(distinctUntilChanged(), pairwise(), scope.bind())
|
.pipe(distinctUntilChanged(), pairwise(), scope.bind())
|
||||||
.subscribe(([prev, reason]) => {
|
.subscribe(([prev, reason]) => {
|
||||||
if (reason !== null) {
|
if (reason !== null) {
|
||||||
@@ -1538,7 +1535,6 @@ export function createCallViewModel$(
|
|||||||
ringingStatusLocation: header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
ringingStatusLocation: header === HeaderStyle.AppBar ? "app_bar" : "tile",
|
||||||
leave$: leave$,
|
leave$: leave$,
|
||||||
hangup: (): void => userHangup$.next(),
|
hangup: (): void => userHangup$.next(),
|
||||||
join,
|
|
||||||
leave,
|
leave,
|
||||||
toggleScreenSharing: toggleScreenSharing,
|
toggleScreenSharing: toggleScreenSharing,
|
||||||
sharingScreen$: sharingScreen$,
|
sharingScreen$: sharingScreen$,
|
||||||
@@ -1549,15 +1545,15 @@ export function createCallViewModel$(
|
|||||||
unhoverScreen: (): void => screenUnhover$.next(),
|
unhoverScreen: (): void => screenUnhover$.next(),
|
||||||
|
|
||||||
fatalError$: scope.behavior(
|
fatalError$: scope.behavior(
|
||||||
rtcClient.fatalError$.pipe(
|
rtcParticipation.fatalError$.pipe(
|
||||||
map((error) => error && fromMatrixRTCError(error)),
|
map((error) => error && fromMatrixRTCError(error)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
participantCount$: participantCount$,
|
participantCount$: participantCount$,
|
||||||
keyRotationSuppressed$: keyRotationSuppressed$,
|
keyRotationSuppressed$: keyRotationSuppressed$,
|
||||||
localMember$: rtcClient.localMember$,
|
localMember$: rtcParticipation.localMember$,
|
||||||
remoteMembers$: rtcClient.remoteMembers$,
|
remoteMembers$: rtcParticipation.remoteMembers$,
|
||||||
connectedTransports$: rtcClient.connectedTransports$,
|
connectedTransports$: rtcParticipation.connectedTransports$,
|
||||||
handsRaised$: handsRaised$,
|
handsRaised$: handsRaised$,
|
||||||
reactions$: reactions$,
|
reactions$: reactions$,
|
||||||
joinSoundEffect$: joinSoundEffect$,
|
joinSoundEffect$: joinSoundEffect$,
|
||||||
@@ -1586,8 +1582,8 @@ export function createCallViewModel$(
|
|||||||
overflowing$,
|
overflowing$,
|
||||||
earpieceMode$: earpieceMode$,
|
earpieceMode$: earpieceMode$,
|
||||||
audioOutputSwitcher$: audioOutputSwitcher$,
|
audioOutputSwitcher$: audioOutputSwitcher$,
|
||||||
reconnecting$: rtcClient.reconnecting$,
|
reconnecting$: rtcParticipation.reconnecting$,
|
||||||
connected$: rtcClient.connected$,
|
connected$: rtcParticipation.connected$,
|
||||||
screenShareError$,
|
screenShareError$,
|
||||||
dismissScreenShareError: (): void => screenShareError$.next(null),
|
dismissScreenShareError: (): void => screenShareError$.next(null),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
constant,
|
constant,
|
||||||
E2eeType,
|
E2eeType,
|
||||||
type LocalRTCMember,
|
type LocalRTCMember,
|
||||||
type MatrixRTCClient,
|
type RTCParticipation,
|
||||||
type MatrixRTCMode,
|
type MatrixRTCMode,
|
||||||
type ObservableScope,
|
type ObservableScope,
|
||||||
type RemoteRTCMember,
|
type RemoteRTCMember,
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
mockConfig,
|
mockConfig,
|
||||||
mockMatrixRoom,
|
mockMatrixRoom,
|
||||||
mockMatrixRoomMember,
|
mockMatrixRoomMember,
|
||||||
mockMatrixRTCClient,
|
mockRTCParticipation,
|
||||||
mockAudioTrack,
|
mockAudioTrack,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockVideoTrack,
|
mockVideoTrack,
|
||||||
@@ -112,8 +112,8 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
|||||||
rtcSession: MockRTCSession,
|
rtcSession: MockRTCSession,
|
||||||
subjects: {
|
subjects: {
|
||||||
raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
raisedHands$: BehaviorSubject<Record<string, RaisedHandInfo>>;
|
||||||
/** The client the view model was built on, with spies for its calls. */
|
/** The participation the view model was built on, with spies for its calls. */
|
||||||
rtcClient: MatrixRTCClient;
|
rtcParticipation: RTCParticipation;
|
||||||
},
|
},
|
||||||
setSyncState: (value: SyncState) => void,
|
setSyncState: (value: SyncState) => void,
|
||||||
) => void,
|
) => void,
|
||||||
@@ -241,7 +241,7 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
const rtcClient = mockMatrixRTCClient(scope, {
|
const rtcParticipation = mockRTCParticipation(scope, {
|
||||||
localMember$,
|
localMember$,
|
||||||
remoteMembers$,
|
remoteMembers$,
|
||||||
connected$,
|
connected$,
|
||||||
@@ -260,7 +260,7 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
|||||||
|
|
||||||
const vm = createCallViewModel$(
|
const vm = createCallViewModel$(
|
||||||
scope,
|
scope,
|
||||||
rtcClient,
|
rtcParticipation,
|
||||||
room,
|
room,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
@@ -279,6 +279,6 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
|||||||
);
|
);
|
||||||
void mode;
|
void mode;
|
||||||
|
|
||||||
continuation(vm, rtcSession, { raisedHands$, rtcClient }, setSyncState);
|
continuation(vm, rtcSession, { raisedHands$, rtcParticipation }, setSyncState);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
type EncryptionSystem,
|
type EncryptionSystem,
|
||||||
type MediaTrack,
|
type MediaTrack,
|
||||||
type ObservableScope,
|
type ObservableScope,
|
||||||
|
type MemberMedia,
|
||||||
type RTCMember,
|
type RTCMember,
|
||||||
trackBySource$,
|
trackBySource$,
|
||||||
type VideoMediaTrack,
|
type VideoMediaTrack,
|
||||||
@@ -64,10 +65,7 @@ export interface BaseMemberMediaViewModel extends BaseMediaViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The member whose media this is, as far as the media view models read it. */
|
/** The member whose media this is, as far as the media view models read it. */
|
||||||
export type MediaMember = Pick<
|
export type MediaMember = Pick<RTCMember, "local"> & MemberMedia;
|
||||||
RTCMember,
|
|
||||||
"local" | "tracks$" | "encryptionError$"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface MemberMediaInputs extends BaseMediaViewModel {
|
export interface MemberMediaInputs extends BaseMediaViewModel {
|
||||||
member: MediaMember;
|
member: MediaMember;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const MemberAudioRenderer: FC<MemberAudioRendererProps> = ({
|
|||||||
<div style={{ display: "none" }}>
|
<div style={{ display: "none" }}>
|
||||||
{members.map((member) => (
|
{members.map((member) => (
|
||||||
<MemberAudio
|
<MemberAudio
|
||||||
key={member.id}
|
key={member.rtcBackendIdentity}
|
||||||
tracks$={member.tracks$}
|
tracks$={member.tracks$}
|
||||||
muted={muted}
|
muted={muted}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
mockConfig,
|
mockConfig,
|
||||||
mockMatrixRoom,
|
mockMatrixRoom,
|
||||||
mockMatrixRTCClient,
|
mockRTCParticipation,
|
||||||
mockMediaDevices,
|
mockMediaDevices,
|
||||||
mockMuteStates,
|
mockMuteStates,
|
||||||
MockRTCSession,
|
MockRTCSession,
|
||||||
@@ -160,7 +160,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
const muteStates = mockMuteStates();
|
const muteStates = mockMuteStates();
|
||||||
const mediaDevices = mediaDevicesOverride ?? mockMediaDevices({});
|
const mediaDevices = mediaDevicesOverride ?? mockMediaDevices({});
|
||||||
// Every membership is a member whose media has arrived
|
// Every membership is a member whose media has arrived
|
||||||
const rtcClient = mockMatrixRTCClient(scope, {
|
const rtcParticipation = mockRTCParticipation(scope, {
|
||||||
localMember$: scope.behavior(
|
localMember$: scope.behavior(
|
||||||
rtcMemberships$.pipe(
|
rtcMemberships$.pipe(
|
||||||
map((memberships) =>
|
map((memberships) =>
|
||||||
@@ -196,7 +196,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
});
|
});
|
||||||
const vm = createCallViewModel$(
|
const vm = createCallViewModel$(
|
||||||
scope,
|
scope,
|
||||||
rtcClient,
|
rtcParticipation,
|
||||||
matrixRoom,
|
matrixRoom,
|
||||||
mediaDevices,
|
mediaDevices,
|
||||||
muteStates,
|
muteStates,
|
||||||
|
|||||||
+48
-22
@@ -5,6 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
map,
|
map,
|
||||||
NEVER,
|
NEVER,
|
||||||
@@ -67,7 +68,9 @@ import {
|
|||||||
type LocalAudioMediaTrack,
|
type LocalAudioMediaTrack,
|
||||||
type LocalRTCMember,
|
type LocalRTCMember,
|
||||||
type LocalVideoMediaTrack,
|
type LocalVideoMediaTrack,
|
||||||
type MatrixRTCClient,
|
type RTCMember,
|
||||||
|
type RTCParticipation,
|
||||||
|
type RTCSlot,
|
||||||
type MatrixRTCError,
|
type MatrixRTCError,
|
||||||
type PublishRequest,
|
type PublishRequest,
|
||||||
type RemoteRTCMember,
|
type RemoteRTCMember,
|
||||||
@@ -467,13 +470,14 @@ export function mockRTCMember(
|
|||||||
resolved$: constant(undefined),
|
resolved$: constant(undefined),
|
||||||
};
|
};
|
||||||
const base = {
|
const base = {
|
||||||
id: membership.rtcBackendIdentity,
|
rtcBackendIdentity: membership.rtcBackendIdentity,
|
||||||
userId: membership.userId,
|
userId: membership.userId,
|
||||||
deviceId: membership.deviceId,
|
deviceId: membership.deviceId,
|
||||||
membership$: constant(membership),
|
memberId: membership.memberId,
|
||||||
displayName$: constant(member.rawDisplayName ?? membership.userId),
|
displayName$: constant(member.rawDisplayName ?? membership.userId),
|
||||||
avatarUrl$: constant(member.getMxcAvatarUrl()),
|
avatarUrl$: constant(member.getMxcAvatarUrl()),
|
||||||
transport$: constant(transport),
|
transport$: constant(transport),
|
||||||
|
applicationData$: constant(membership.applicationData),
|
||||||
encryptionError$,
|
encryptionError$,
|
||||||
};
|
};
|
||||||
if (!local) return { ...base, local: false, tracks$ };
|
if (!local) return { ...base, local: false, tracks$ };
|
||||||
@@ -484,18 +488,10 @@ export function mockRTCMember(
|
|||||||
tracks$: tracks$ as Behavior<
|
tracks$: tracks$ as Behavior<
|
||||||
(LocalAudioMediaTrack | LocalVideoMediaTrack)[] | null
|
(LocalAudioMediaTrack | LocalVideoMediaTrack)[] | null
|
||||||
>,
|
>,
|
||||||
publish: vi.fn(async (request: PublishRequest) =>
|
|
||||||
Promise.resolve(
|
|
||||||
request.source === "microphone"
|
|
||||||
? mockAudioTrack()
|
|
||||||
: mockVideoTrack({ source: request.source }),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
unpublish: vi.fn(async () => Promise.resolve()),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MockClientInputs {
|
export interface MockParticipationInputs {
|
||||||
localMember$?: Behavior<LocalRTCMember | null>;
|
localMember$?: Behavior<LocalRTCMember | null>;
|
||||||
remoteMembers$?: Behavior<RemoteRTCMember[]>;
|
remoteMembers$?: Behavior<RemoteRTCMember[]>;
|
||||||
connected$?: Behavior<boolean>;
|
connected$?: Behavior<boolean>;
|
||||||
@@ -506,8 +502,11 @@ export interface MockClientInputs {
|
|||||||
connectedTransports$?: Behavior<TransportMetadata[]>;
|
connectedTransports$?: Behavior<TransportMetadata[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A client that does nothing but hold the state a test hands it. */
|
/**
|
||||||
export function mockMatrixRTCClient(
|
* A participation that does nothing but hold the state a test hands it, in a
|
||||||
|
* slot whose members are the same ones without their media.
|
||||||
|
*/
|
||||||
|
export function mockRTCParticipation(
|
||||||
scope: ObservableScope,
|
scope: ObservableScope,
|
||||||
{
|
{
|
||||||
localMember$ = constant(null),
|
localMember$ = constant(null),
|
||||||
@@ -518,10 +517,25 @@ export function mockMatrixRTCClient(
|
|||||||
fatalError$ = constant(null),
|
fatalError$ = constant(null),
|
||||||
keyRotationSuppressed$ = constant(false),
|
keyRotationSuppressed$ = constant(false),
|
||||||
connectedTransports$ = constant([]),
|
connectedTransports$ = constant([]),
|
||||||
}: MockClientInputs = {},
|
}: MockParticipationInputs = {},
|
||||||
): MatrixRTCClient {
|
): RTCParticipation {
|
||||||
return {
|
const members$ = scope.behavior<RTCMember[]>(
|
||||||
join: vi.fn(),
|
combineLatest([localMember$, remoteMembers$], (local, remote) =>
|
||||||
|
local === null ? remote : [local, ...remote],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const participation$ = new BehaviorSubject<RTCParticipation | null>(null);
|
||||||
|
const slot: RTCSlot = {
|
||||||
|
roomId: "!room:example.org",
|
||||||
|
application: "m.call",
|
||||||
|
id: "ROOM",
|
||||||
|
status$: constant("open"),
|
||||||
|
members$,
|
||||||
|
participation$,
|
||||||
|
join: vi.fn(() => participation$.value!),
|
||||||
|
};
|
||||||
|
const participation: RTCParticipation = {
|
||||||
|
slot,
|
||||||
leave: vi.fn(),
|
leave: vi.fn(),
|
||||||
status$: scope.behavior(
|
status$: scope.behavior(
|
||||||
connected$.pipe(map((c) => (c ? "connected" : "connecting"))),
|
connected$.pipe(map((c) => (c ? "connected" : "connecting"))),
|
||||||
@@ -532,18 +546,30 @@ export function mockMatrixRTCClient(
|
|||||||
fatalError$,
|
fatalError$,
|
||||||
localMember$,
|
localMember$,
|
||||||
remoteMembers$,
|
remoteMembers$,
|
||||||
memberCount$: scope.behavior(
|
publish: vi.fn(async (request: PublishRequest) =>
|
||||||
combineLatest(
|
Promise.resolve(
|
||||||
[localMember$, remoteMembers$],
|
request.source === "microphone"
|
||||||
(local, remote) => remote.length + (local === null ? 0 : 1),
|
? mockAudioTrack()
|
||||||
|
: mockVideoTrack({ source: request.source }),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
unpublish: vi.fn(async () => Promise.resolve()),
|
||||||
keyRotationSuppressed$,
|
keyRotationSuppressed$,
|
||||||
connectedTransports$,
|
connectedTransports$,
|
||||||
setAudioOutputDeviceId: vi.fn(async () => Promise.resolve()),
|
setAudioOutputDeviceId: vi.fn(async () => Promise.resolve()),
|
||||||
sendData: vi.fn(async () => Promise.resolve()),
|
sendData: vi.fn(async () => Promise.resolve()),
|
||||||
data$: NEVER,
|
data$: NEVER,
|
||||||
};
|
};
|
||||||
|
participation$.next(participation);
|
||||||
|
return participation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The slot of a `mockRTCParticipation`, whose `join()` returns that participation. */
|
||||||
|
export function mockRTCSlot(
|
||||||
|
scope: ObservableScope,
|
||||||
|
inputs: MockParticipationInputs = {},
|
||||||
|
): RTCSlot {
|
||||||
|
return mockRTCParticipation(scope, inputs).slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockConfig(
|
export function mockConfig(
|
||||||
|
|||||||
Reference in New Issue
Block a user