mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
[SDK] split the clint object into -> slot and participation
This commit is contained in:
@@ -46,25 +46,22 @@ describe("createLocalMembership$", () => {
|
||||
});
|
||||
|
||||
local.connectionState$.next(MediaConnectionState.Connected);
|
||||
expect(membership.state$.value).toEqual({
|
||||
media: PublishState.WaitingForUser,
|
||||
matrix: Status.Connected,
|
||||
});
|
||||
|
||||
membership.requestJoinAndPublish();
|
||||
await flushPromises();
|
||||
expect(membership.state$.value).toEqual({
|
||||
media: PublishState.Publishing,
|
||||
matrix: Status.Connected,
|
||||
});
|
||||
expect(membership.connected$.value).toBe(true);
|
||||
|
||||
membership.leave();
|
||||
expect(membership.state$.value).toEqual({
|
||||
media: PublishState.WaitingForUser,
|
||||
matrix: Status.Connected,
|
||||
});
|
||||
});
|
||||
|
||||
it("publishes only while joined and the homeserver is reachable", async () => {
|
||||
const { membership, local, homeserver } = setup();
|
||||
expect(local.setPublishing).toHaveBeenLastCalledWith(false);
|
||||
|
||||
membership.requestJoinAndPublish();
|
||||
expect(local.setPublishing).toHaveBeenLastCalledWith(true);
|
||||
|
||||
homeserver.combined$.next([false, "sync"]);
|
||||
@@ -72,7 +69,7 @@ describe("createLocalMembership$", () => {
|
||||
homeserver.combined$.next([true, null]);
|
||||
expect(local.setPublishing).toHaveBeenLastCalledWith(true);
|
||||
|
||||
membership.requestDisconnect();
|
||||
membership.leave();
|
||||
expect(local.setPublishing).toHaveBeenLastCalledWith(false);
|
||||
// A sync outage after leaving must not bring the media back
|
||||
homeserver.combined$.next([false, "sync"]);
|
||||
@@ -82,10 +79,8 @@ describe("createLocalMembership$", () => {
|
||||
});
|
||||
|
||||
it("joins with the delegated timings and hands over each delay id where the backend can", async () => {
|
||||
const { membership, prepared$, delayId$, joinMatrixRTC, delegate } =
|
||||
setup();
|
||||
const { prepared$, delayId$, joinMatrixRTC, delegate } = setup();
|
||||
prepared$.next({ transport, canDelegateDelayedLeave: true });
|
||||
membership.requestJoinAndPublish();
|
||||
await flushPromises();
|
||||
expect(joinMatrixRTC).toHaveBeenCalledWith(
|
||||
transport,
|
||||
@@ -101,10 +96,8 @@ describe("createLocalMembership$", () => {
|
||||
});
|
||||
|
||||
it("never hands the leave over where the backend cannot take it", async () => {
|
||||
const { membership, prepared$, delayId$, joinMatrixRTC, delegate } =
|
||||
setup();
|
||||
const { prepared$, delayId$, joinMatrixRTC, delegate } = setup();
|
||||
prepared$.next({ transport, canDelegateDelayedLeave: false });
|
||||
membership.requestJoinAndPublish();
|
||||
delayId$.next("delay-1");
|
||||
await flushPromises();
|
||||
expect(joinMatrixRTC).toHaveBeenCalledWith(
|
||||
@@ -124,7 +117,6 @@ describe("createLocalMembership$", () => {
|
||||
const { membership, local, prepared$ } = setup();
|
||||
prepared$.next({ transport, canDelegateDelayedLeave: false });
|
||||
local.connectionState$.next(MediaConnectionState.Connected);
|
||||
membership.requestJoinAndPublish();
|
||||
|
||||
local.connectionState$.next(MediaConnectionState.Reconnecting);
|
||||
expect(membership.state$.value).toEqual({
|
||||
|
||||
@@ -89,9 +89,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export interface LocalMembership {
|
||||
requestJoinAndPublish: () => void;
|
||||
requestDisconnect: () => void;
|
||||
joinRequested$: Behavior<boolean>;
|
||||
/** Sends the leave and stops publishing. Final: there is no joining again. */
|
||||
leave: () => void;
|
||||
state$: Behavior<LocalMemberState>;
|
||||
/** Fully connected: to the homeserver, the session and the transport. */
|
||||
connected$: Behavior<boolean>;
|
||||
@@ -101,8 +100,8 @@ export interface LocalMembership {
|
||||
}
|
||||
|
||||
/**
|
||||
* The local member's state machine: waits for the transport, publishes once
|
||||
* asked to join, and enters and leaves the MatrixRTC session in step.
|
||||
* The local member's state machine: waits for the transport, then publishes
|
||||
* and enters the MatrixRTC session, and leaves both in step on `leave()`.
|
||||
*/
|
||||
export function createLocalMembership$({
|
||||
scope,
|
||||
@@ -131,22 +130,22 @@ export function createLocalMembership$({
|
||||
null,
|
||||
);
|
||||
|
||||
const joinRequested$ = new BehaviorSubject(false);
|
||||
const joined$ = new BehaviorSubject(true);
|
||||
const matrixError$ = new BehaviorSubject<MatrixRTCError | null>(null);
|
||||
|
||||
// Nothing leaves this device while it may already have been dropped from
|
||||
// the session: the member would show as away while still being heard
|
||||
combineLatest(
|
||||
[joinRequested$, homeserverConnected.combined$],
|
||||
(join, [connected]) => join && connected,
|
||||
[joined$, homeserverConnected.combined$],
|
||||
(joined, [connected]) => joined && connected,
|
||||
)
|
||||
.pipe(distinctUntilChanged(), scope.bind())
|
||||
.subscribe((publish) => local.setPublishing(publish));
|
||||
|
||||
scope.reconcile(
|
||||
scope.behavior(combineLatest([transport$, joinRequested$])),
|
||||
async ([prepared, shouldJoin]) => {
|
||||
if (prepared === null || !shouldJoin) return;
|
||||
scope.behavior(combineLatest([transport$, joined$])),
|
||||
async ([prepared, joined]) => {
|
||||
if (prepared === null || !joined) return;
|
||||
try {
|
||||
joinMatrixRTC(
|
||||
prepared.transport,
|
||||
@@ -191,8 +190,8 @@ export function createLocalMembership$({
|
||||
});
|
||||
|
||||
const mediaState$ = scope.behavior<LocalMemberMediaState>(
|
||||
combineLatest([local.connectionState$, joinRequested$]).pipe(
|
||||
map(([connectionState, shouldPublish]) => {
|
||||
combineLatest([local.connectionState$, joined$]).pipe(
|
||||
map(([connectionState, joined]) => {
|
||||
if (connectionState !== MediaConnectionState.Connected)
|
||||
return {
|
||||
connection:
|
||||
@@ -200,9 +199,7 @@ export function createLocalMembership$({
|
||||
? toMatrixRTCError(connectionState)
|
||||
: connectionState,
|
||||
};
|
||||
return shouldPublish
|
||||
? PublishState.Publishing
|
||||
: PublishState.WaitingForUser;
|
||||
return joined ? PublishState.Publishing : PublishState.WaitingForUser;
|
||||
}),
|
||||
distinctUntilChanged(deepCompare),
|
||||
),
|
||||
@@ -260,9 +257,7 @@ export function createLocalMembership$({
|
||||
);
|
||||
|
||||
return {
|
||||
requestJoinAndPublish: () => joinRequested$.next(true),
|
||||
requestDisconnect: () => joinRequested$.next(false),
|
||||
joinRequested$,
|
||||
leave: () => joined$.next(false),
|
||||
state$,
|
||||
connected$,
|
||||
reconnecting$,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { type MatrixClient } from "matrix-js-sdk";
|
||||
import { type Logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type Transport } from "matrix-js-sdk/lib/matrixrtc";
|
||||
|
||||
import { type MatrixRTCClientOptions } from "../api";
|
||||
import { type RTCParticipationOptions } from "../api";
|
||||
import { MatrixRTCTransportMissingError } from "../errors";
|
||||
import { TransportAutoDiscovery } from "./TransportAutoDiscovery";
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function discoverLocalTransport(
|
||||
{
|
||||
transportUrl,
|
||||
fallbackTransportUrl,
|
||||
}: Pick<MatrixRTCClientOptions, "transportUrl" | "fallbackTransportUrl">,
|
||||
}: Pick<RTCParticipationOptions, "transportUrl" | "fallbackTransportUrl">,
|
||||
logger: Logger,
|
||||
): Promise<Transport> {
|
||||
const transport = transportUrl
|
||||
|
||||
@@ -6,6 +6,7 @@ Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { deepCompare } from "matrix-js-sdk/lib/utils";
|
||||
import { distinctUntilChanged, map } from "rxjs";
|
||||
|
||||
import { type Behavior } from "../reactive/Behavior";
|
||||
@@ -16,16 +17,15 @@ import {
|
||||
type RemoteRTCMember,
|
||||
type RTCMember,
|
||||
} from "../api";
|
||||
import {
|
||||
type LocalMediaBackend,
|
||||
type MemberMediaFields,
|
||||
} from "../media-backend/api";
|
||||
import { type LocalMemberMedia, type MemberMedia } from "../media-api";
|
||||
import { type TransportRegistry } from "./Transports";
|
||||
|
||||
/** What every member is built from, besides its own membership and media. */
|
||||
/** What every member is built from, besides its own membership. */
|
||||
export interface MemberContext {
|
||||
metadata: ReturnType<typeof createMatrixMemberMetadata$>;
|
||||
transports: TransportRegistry;
|
||||
/** Whose memberships are `local`. */
|
||||
own: { userId: string; deviceId: string };
|
||||
}
|
||||
|
||||
/** Everything that tells one membership from another, for keying items. */
|
||||
@@ -40,50 +40,21 @@ export function membershipKeys(
|
||||
];
|
||||
}
|
||||
|
||||
export function createRemoteRTCMember(
|
||||
/** The slot's view of a membership: identity, room state and transport, no media. */
|
||||
export function createRTCMember(
|
||||
scope: ObservableScope,
|
||||
membership$: Behavior<CallMembership>,
|
||||
media: MemberMediaFields,
|
||||
context: MemberContext,
|
||||
): RemoteRTCMember {
|
||||
{ metadata, transports, own }: MemberContext,
|
||||
): RTCMember {
|
||||
const { userId, deviceId, memberId, rtcBackendIdentity } = membership$.value;
|
||||
return {
|
||||
...createRTCMember(scope, membership$, context),
|
||||
local: false,
|
||||
tracks$: media.tracks$,
|
||||
encryptionError$: media.encryptionError$,
|
||||
};
|
||||
}
|
||||
|
||||
export function createLocalRTCMember(
|
||||
scope: ObservableScope,
|
||||
membership$: Behavior<CallMembership>,
|
||||
local: Pick<
|
||||
LocalMediaBackend,
|
||||
"tracks$" | "encryptionError$" | "publish" | "unpublish"
|
||||
>,
|
||||
context: MemberContext,
|
||||
): LocalRTCMember {
|
||||
return {
|
||||
...createRTCMember(scope, membership$, context),
|
||||
local: true,
|
||||
tracks$: local.tracks$,
|
||||
encryptionError$: local.encryptionError$,
|
||||
publish: async (request) => local.publish(request),
|
||||
unpublish: async (id) => local.unpublish(id),
|
||||
};
|
||||
}
|
||||
|
||||
function createRTCMember(
|
||||
scope: ObservableScope,
|
||||
membership$: Behavior<CallMembership>,
|
||||
{ metadata, transports }: MemberContext,
|
||||
): Omit<RTCMember, "local" | "tracks$" | "encryptionError$"> {
|
||||
const { userId, deviceId, rtcBackendIdentity } = membership$.value;
|
||||
return {
|
||||
id: rtcBackendIdentity,
|
||||
local: userId === own.userId && deviceId === own.deviceId,
|
||||
// Read off the membership for now; the backend will derive it once the
|
||||
// identity scheme is its own and not the js-sdk's
|
||||
rtcBackendIdentity,
|
||||
userId,
|
||||
deviceId,
|
||||
membership$,
|
||||
memberId,
|
||||
displayName$: scope.behavior(
|
||||
metadata
|
||||
.createDisplayNameBehavior$(scope, userId)
|
||||
@@ -97,5 +68,26 @@ function createRTCMember(
|
||||
map((transport) => transport && transports.get(transport)),
|
||||
),
|
||||
),
|
||||
applicationData$: scope.behavior(
|
||||
membership$.pipe(
|
||||
map((membership) => membership.applicationData),
|
||||
distinctUntilChanged(deepCompare),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** The participation's view of a remote member: the slot's member plus its media. */
|
||||
export function createRemoteRTCMember(
|
||||
member: RTCMember,
|
||||
media: MemberMedia,
|
||||
): RemoteRTCMember {
|
||||
return { ...member, local: false, ...media };
|
||||
}
|
||||
|
||||
export function createLocalRTCMember(
|
||||
member: RTCMember,
|
||||
media: LocalMemberMedia,
|
||||
): LocalRTCMember {
|
||||
return { ...member, local: true, ...media };
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
PublishState,
|
||||
TransportState,
|
||||
} from "./LocalMember";
|
||||
import { fatalError, sessionStatus } from "./status";
|
||||
import { fatalError, participationStatus } from "./status";
|
||||
|
||||
const publishing: LocalMemberState = {
|
||||
media: PublishState.Publishing,
|
||||
@@ -27,31 +27,39 @@ const connecting: LocalMemberState = {
|
||||
};
|
||||
const error = new MatrixRTCError("gone");
|
||||
|
||||
describe("sessionStatus", () => {
|
||||
describe("participationStatus", () => {
|
||||
it.each<[string, LocalMemberState, boolean, boolean, boolean, string]>([
|
||||
[
|
||||
"no transport yet",
|
||||
TransportState.Waiting,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"waitingForTransport",
|
||||
],
|
||||
["not asked to join", connecting, false, false, false, "disconnected"],
|
||||
["joining", connecting, true, false, false, "connecting"],
|
||||
["connected", publishing, true, true, false, "connected"],
|
||||
["dropped after connecting", connecting, true, false, true, "reconnecting"],
|
||||
["a transport error", error, true, false, false, "disconnected"],
|
||||
["joining", connecting, false, false, false, "connecting"],
|
||||
["connected", publishing, true, false, false, "connected"],
|
||||
[
|
||||
"dropped after connecting",
|
||||
connecting,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
"reconnecting",
|
||||
],
|
||||
["a transport error", error, false, false, false, "disconnected"],
|
||||
[
|
||||
"a matrix error",
|
||||
{ ...publishing, matrix: error },
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
"disconnected",
|
||||
],
|
||||
])("%s", (_name, state, joinRequested, connected, reconnecting, expected) => {
|
||||
expect(sessionStatus(state, joinRequested, connected, reconnecting)).toBe(
|
||||
["left", publishing, true, false, true, "left"],
|
||||
["left after an error", error, false, false, true, "left"],
|
||||
])("%s", (_name, state, connected, reconnecting, left, expected) => {
|
||||
expect(participationStatus(state, connected, reconnecting, left)).toBe(
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,24 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type MatrixRTCError, type ConnectionStatus } from "../api";
|
||||
import { type MatrixRTCError, type ParticipationStatus } from "../api";
|
||||
import { type LocalMemberState, TransportState } from "./LocalMember";
|
||||
|
||||
/** The local member's state, collapsed to what a host shows. */
|
||||
export function sessionStatus(
|
||||
export function participationStatus(
|
||||
state: LocalMemberState,
|
||||
joinRequested: boolean,
|
||||
connected: boolean,
|
||||
reconnecting: boolean,
|
||||
): ConnectionStatus {
|
||||
left: boolean,
|
||||
): ParticipationStatus {
|
||||
if (left) return "left";
|
||||
if (fatalError(state) !== null) return "disconnected";
|
||||
if (state === TransportState.Waiting) return "waitingForTransport";
|
||||
if (connected) return "connected";
|
||||
if (reconnecting) return "reconnecting";
|
||||
return joinRequested ? "connecting" : "disconnected";
|
||||
return "connecting";
|
||||
}
|
||||
|
||||
/** The error that stops the session, if the state holds one. */
|
||||
/** The error that stops the participation, if the state holds one. */
|
||||
export function fatalError(state: LocalMemberState): MatrixRTCError | null {
|
||||
if (state === TransportState.Waiting) return null;
|
||||
if (state instanceof Error) return state;
|
||||
|
||||
Reference in New Issue
Block a user