mirror of
https://github.com/vector-im/element-call.git
synced 2026-09-10 21:55:19 +00:00
Merge pull request #4242 from element-hq/delayed-leave-timings
Adapt delayed leave timings when delegation is available
This commit is contained in:
@@ -9,8 +9,6 @@
|
|||||||
"matrix_rtc_session": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
"delayed_leave_event_delay_ms": 18000,
|
|
||||||
"delayed_leave_event_restart_ms": 4000,
|
|
||||||
"network_error_retry_ms": 100
|
"network_error_retry_ms": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
"matrix_rtc_session": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
"delayed_leave_event_delay_ms": 18000,
|
|
||||||
"delayed_leave_event_restart_ms": 4000,
|
|
||||||
"network_error_retry_ms": 100
|
"network_error_retry_ms": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
"matrix_rtc_session": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
"delayed_leave_event_delay_ms": 18000,
|
|
||||||
"delayed_leave_event_restart_ms": 4000,
|
|
||||||
"network_error_retry_ms": 100
|
"network_error_retry_ms": 100
|
||||||
},
|
},
|
||||||
"posthog": {
|
"posthog": {
|
||||||
|
|||||||
@@ -9,8 +9,6 @@
|
|||||||
"matrix_rtc_session": {
|
"matrix_rtc_session": {
|
||||||
"wait_for_key_rotation_ms": 3000,
|
"wait_for_key_rotation_ms": 3000,
|
||||||
"membership_event_expiry_ms": 180000000,
|
"membership_event_expiry_ms": 180000000,
|
||||||
"delayed_leave_event_delay_ms": 18000,
|
|
||||||
"delayed_leave_event_restart_ms": 4000,
|
|
||||||
"network_error_retry_ms": 100
|
"network_error_retry_ms": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+55
-33
@@ -24,6 +24,35 @@ export enum MatrixRTCMode {
|
|||||||
Matrix_2_0 = "matrix_2_0",
|
Matrix_2_0 = "matrix_2_0",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DelayedLeaveTimings {
|
||||||
|
/**
|
||||||
|
* The delay (in milliseconds) with which delayed leave events are sent.
|
||||||
|
*
|
||||||
|
* If the server receives no keep-alives from the client for any longer than
|
||||||
|
* this duration, it will send the leave event, automatically removing the
|
||||||
|
* user from the call.
|
||||||
|
*/
|
||||||
|
delay_ms?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How frequently (in milliseconds) the client sends keep-alives to the server
|
||||||
|
* to restart the timer for a delayed leave event. Should be less than
|
||||||
|
* {@link DelayedLeaveTimings.delay_ms}.
|
||||||
|
*/
|
||||||
|
restart_ms?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time (in milliseconds) after which we consider a delayed event restart HTTP request to have failed.
|
||||||
|
* Setting this to a lower value will result in more frequent retries, but then we will also give up earlier.
|
||||||
|
*
|
||||||
|
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
|
||||||
|
* helps by keeping more delayed event reset candidates in flight,
|
||||||
|
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
|
||||||
|
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
||||||
|
*/
|
||||||
|
restart_timeout_ms?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConfigOptions {
|
export interface ConfigOptions {
|
||||||
/**
|
/**
|
||||||
* The Posthog endpoint to which analytics data will be sent.
|
* The Posthog endpoint to which analytics data will be sent.
|
||||||
@@ -184,29 +213,6 @@ export interface ConfigOptions {
|
|||||||
*/
|
*/
|
||||||
wait_for_key_rotation_ms?: number;
|
wait_for_key_rotation_ms?: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* The duration (in milliseconds) after the most recent keep-alive (delayed leave event restart)
|
|
||||||
* that the server waits before sending the leave MatrixRTC membership event.
|
|
||||||
*/
|
|
||||||
delayed_leave_event_delay_ms?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The time (in milliseconds) after which we consider a delayed event restart http request to have failed.
|
|
||||||
* Setting this to a lower value will result in more frequent retries but also a higher chance of failiour.
|
|
||||||
*
|
|
||||||
* In the presence of network packet loss (hurting TCP connections), the custom delayedEventRestartLocalTimeoutMs
|
|
||||||
* helps by keeping more delayed event reset candidates in flight,
|
|
||||||
* improving the chances of a successful reset. (its is equivalent to the js-sdk `localTimeout` configuration,
|
|
||||||
* but only applies to calls to the `_unstable_updateDelayedEvent` endpoint with a body of `{action:"restart"}`.)
|
|
||||||
*/
|
|
||||||
delayed_leave_event_restart_local_timeout_ms?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The time interval (in milliseconds) at which the client sends membership keep-alive
|
|
||||||
* messages to the server by restarting the timer for the delayed leave event.
|
|
||||||
*/
|
|
||||||
delayed_leave_event_restart_ms?: number;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* How long we wait before retrying after a network error on any of the requests.
|
* How long we wait before retrying after a network error on any of the requests.
|
||||||
*/
|
*/
|
||||||
@@ -231,9 +237,28 @@ export interface ConfigOptions {
|
|||||||
* Defaults to the js-sdk default (undefined). Which means that rotation will always happen.
|
* Defaults to the js-sdk default (undefined). Which means that rotation will always happen.
|
||||||
*/
|
*/
|
||||||
key_rotation_participant_limit?: number;
|
key_rotation_participant_limit?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timing options for delayed leave events, which are used to remove a user
|
||||||
|
* from a call when they lose connection.
|
||||||
|
*/
|
||||||
|
delayed_leave?: DelayedLeaveTimings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timing options for delayed leave events, in cases where the ability to
|
||||||
|
* send the event can be delegated to the SFU.
|
||||||
|
*
|
||||||
|
* We recommend setting {@link DelayedLeaveTimings.delay_ms} >>
|
||||||
|
* {@link sync_disconnect_grace_period_ms} here.
|
||||||
|
*/
|
||||||
|
delegated_delayed_leave?: DelayedLeaveTimings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResolvedDelayedLeaveTimings extends DelayedLeaveTimings {
|
||||||
|
delay_ms: number; // Required
|
||||||
|
}
|
||||||
|
|
||||||
// Overrides members from ConfigOptions that are always provided by the
|
// Overrides members from ConfigOptions that are always provided by the
|
||||||
// default config and are therefore non-optional.
|
// default config and are therefore non-optional.
|
||||||
export interface ResolvedConfigOptions extends ConfigOptions {
|
export interface ResolvedConfigOptions extends ConfigOptions {
|
||||||
@@ -257,19 +282,15 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
|||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
matrix_rtc_session: {
|
matrix_rtc_session: ConfigOptions["matrix_rtc_session"] & {
|
||||||
wait_for_key_rotation_ms?: number;
|
|
||||||
delayed_leave_event_delay_ms: number;
|
|
||||||
delayed_leave_event_restart_local_timeout_ms?: number;
|
|
||||||
delayed_leave_event_restart_ms?: number;
|
|
||||||
network_error_retry_ms: number;
|
network_error_retry_ms: number;
|
||||||
membership_event_expiry_ms?: number;
|
delayed_leave: ResolvedDelayedLeaveTimings;
|
||||||
key_rotation_participant_limit?: number;
|
delegated_delayed_leave: ResolvedDelayedLeaveTimings;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||||
sync_disconnect_grace_period_ms: 10000,
|
sync_disconnect_grace_period_ms: 10_000,
|
||||||
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||||
media_quality: {
|
media_quality: {
|
||||||
video_codec: "vp8",
|
video_codec: "vp8",
|
||||||
@@ -285,7 +306,8 @@ export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
matrix_rtc_session: {
|
matrix_rtc_session: {
|
||||||
delayed_leave_event_delay_ms: 10000,
|
network_error_retry_ms: 1_000,
|
||||||
network_error_retry_ms: 1000,
|
delayed_leave: { delay_ms: 18_000, restart_ms: 4_000 },
|
||||||
|
delegated_delayed_leave: { delay_ms: 3_600_000, restart_ms: 300_000 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ describe("getSFUConfigWithOpenID", () => {
|
|||||||
// Verify, that the request contains the expected delay parameters
|
// Verify, that the request contains the expected delay parameters
|
||||||
if (
|
if (
|
||||||
body.delay_id === "mock_delay_id" &&
|
body.delay_id === "mock_delay_id" &&
|
||||||
body.delay_timeout === 10000 &&
|
body.delay_timeout === 3600000 &&
|
||||||
body.delay_cs_api_url === "https://homeserverserver.org/cs_api"
|
body.delay_cs_api_url === "https://homeserverserver.org/cs_api"
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
@@ -229,7 +229,7 @@ describe("getSFUConfigWithOpenID", () => {
|
|||||||
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
||||||
expect(calls[0][1]).toStrictEqual({
|
expect(calls[0][1]).toStrictEqual({
|
||||||
// check if it uses correct delayID!
|
// check if it uses correct delayID!
|
||||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -239,7 +239,7 @@ describe("getSFUConfigWithOpenID", () => {
|
|||||||
expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get");
|
expect(calls[1][0]).toStrictEqual("https://sfu.example.org/sfu/get");
|
||||||
|
|
||||||
expect(calls[1][1]).toStrictEqual({
|
expect(calls[1][1]).toStrictEqual({
|
||||||
body: '{"room":"!example_room_id","device_id":"DEVICE","delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
body: '{"room":"!example_room_id","device_id":"DEVICE","delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
@@ -284,7 +284,7 @@ describe("getSFUConfigWithOpenID", () => {
|
|||||||
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
expect(calls[0][0]).toStrictEqual("https://sfu.example.org/get_token");
|
||||||
expect(calls[0][1]).toStrictEqual({
|
expect(calls[0][1]).toStrictEqual({
|
||||||
// check if it uses correct delayID!
|
// check if it uses correct delayID!
|
||||||
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":10000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
body: '{"room_id":"!example_room_id","slot_id":"m.call#ROOM","member":{"id":"@alice:example.org:DEVICE","claimed_user_id":"@alice:example.org","claimed_device_id":"DEVICE"},"delay_id":"mock_delay_id","delay_timeout":3600000,"delay_cs_api_url":"https://matrix.homeserverserver.org"}',
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|||||||
+15
-19
@@ -19,7 +19,7 @@ import {
|
|||||||
} from "../utils/errors";
|
} from "../utils/errors";
|
||||||
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
||||||
import { Config } from "../config/Config";
|
import { Config } from "../config/Config";
|
||||||
import { JwtEndpointVersion } from "../state/CallViewModel/localMember/LocalTransport";
|
import { MatrixRTCMode } from "../config/ConfigOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration and access tokens provided by the SFU on successful authentication.
|
* Configuration and access tokens provided by the SFU on successful authentication.
|
||||||
@@ -80,11 +80,10 @@ export type OpenIDClientParts = Pick<
|
|||||||
* @param serviceUrl The URL of the livekit SFU service
|
* @param serviceUrl The URL of the livekit SFU service
|
||||||
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
|
* @param roomId The room id used in the jwt request. This is NOT the livekit_alias. The jwt service will provide the alias. It maps matrix room ids <-> Livekit aliases.
|
||||||
* @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token.
|
* @param opts Additional options to modify which endpoint with which data will be used to acquire the jwt token.
|
||||||
* @param opts.forceJwtEndpoint This will use the old jwt endpoint which will create the rtc backend identity based on string concatenation
|
* @param opts.matrixRTCMode Determines which version of the JWT endpoint to use, which affects whether the
|
||||||
* instead of a hash.
|
* RTC backend identity is based on string concatenation (legacy) or a hash (Matrix 2.0).
|
||||||
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
|
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
|
||||||
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
|
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
|
||||||
* For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events.
|
|
||||||
* @param opts.delayEndpointBaseUrl The URL of the matrix homeserver.
|
* @param opts.delayEndpointBaseUrl The URL of the matrix homeserver.
|
||||||
* @param opts.delayId The delay id used for the jwt service to manage.
|
* @param opts.delayId The delay id used for the jwt service to manage.
|
||||||
* @param logger optional logger.
|
* @param logger optional logger.
|
||||||
@@ -97,7 +96,7 @@ export async function getSFUConfigWithOpenID(
|
|||||||
serviceUrl: string,
|
serviceUrl: string,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
opts?: {
|
opts?: {
|
||||||
forceJwtEndpoint?: JwtEndpointVersion;
|
matrixRTCMode?: MatrixRTCMode;
|
||||||
delayEndpointBaseUrl?: string;
|
delayEndpointBaseUrl?: string;
|
||||||
delayId?: string;
|
delayId?: string;
|
||||||
},
|
},
|
||||||
@@ -116,19 +115,18 @@ export async function getSFUConfigWithOpenID(
|
|||||||
logger?.debug("Got openID token", openIdToken);
|
logger?.debug("Got openID token", openIdToken);
|
||||||
let sfuConfig: { url: string; jwt: string } | undefined;
|
let sfuConfig: { url: string; jwt: string } | undefined;
|
||||||
|
|
||||||
const tryBothJwtEndpoints = opts?.forceJwtEndpoint === undefined; // This is for SFUs where we do not publish.
|
const tryBothJwtEndpoints = opts?.matrixRTCMode === undefined; // This is for SFUs where we do not publish.
|
||||||
|
|
||||||
const forceMatrix2Jwt =
|
const forceMatrix2Jwt = opts?.matrixRTCMode === MatrixRTCMode.Matrix_2_0;
|
||||||
opts?.forceJwtEndpoint === JwtEndpointVersion.Matrix_2_0;
|
|
||||||
|
|
||||||
// We want to start using the new endpoint (with optional delay delegation)
|
// We want to start using the new endpoint (with optional delay delegation)
|
||||||
// if we can use both or if we are forced to use the new one.
|
// if we can use both or if we are forced to use the new one.
|
||||||
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
||||||
try {
|
try {
|
||||||
logger?.info(
|
logger?.info(
|
||||||
`Trying to get JWT with delegation for focus ${serviceUrl}...`,
|
`Trying to get JWT via default endpoint for focus ${serviceUrl}...`,
|
||||||
);
|
);
|
||||||
const sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
const sfuConfig = await getLiveKitJWT(
|
||||||
membership,
|
membership,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -154,7 +152,7 @@ export async function getSFUConfigWithOpenID(
|
|||||||
logger?.info(
|
logger?.info(
|
||||||
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
|
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
|
||||||
);
|
);
|
||||||
sfuConfig = await getLiveKitJWT(
|
sfuConfig = await getLiveKitJWTLegacy(
|
||||||
membership.deviceId,
|
membership.deviceId,
|
||||||
serviceUrl,
|
serviceUrl,
|
||||||
roomId,
|
roomId,
|
||||||
@@ -188,7 +186,7 @@ function extractFullConfigFromToken(sfuConfig: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLiveKitJWT(
|
async function getLiveKitJWTLegacy(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
matrixRoomId: string,
|
matrixRoomId: string,
|
||||||
@@ -204,11 +202,10 @@ async function getLiveKitJWT(
|
|||||||
let bodyDalayParts: IDelayParams = {};
|
let bodyDalayParts: IDelayParams = {};
|
||||||
// Also check for empty string
|
// Also check for empty string
|
||||||
if (delayId && delayEndpointBaseUrl) {
|
if (delayId && delayEndpointBaseUrl) {
|
||||||
const delayTimeoutMs =
|
|
||||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
|
||||||
bodyDalayParts = {
|
bodyDalayParts = {
|
||||||
delay_id: delayId,
|
delay_id: delayId,
|
||||||
delay_timeout: delayTimeoutMs,
|
delay_timeout:
|
||||||
|
Config.get().matrix_rtc_session.delegated_delayed_leave.delay_ms,
|
||||||
delay_cs_api_url: delayEndpointBaseUrl,
|
delay_cs_api_url: delayEndpointBaseUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -264,7 +261,7 @@ class NotSupportedError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveKitJWTWithDelayDelegation(
|
export async function getLiveKitJWT(
|
||||||
membership: CallMembershipIdentityParts,
|
membership: CallMembershipIdentityParts,
|
||||||
livekitServiceURL: string,
|
livekitServiceURL: string,
|
||||||
matrixRoomId: string,
|
matrixRoomId: string,
|
||||||
@@ -288,11 +285,10 @@ export async function getLiveKitJWTWithDelayDelegation(
|
|||||||
let bodyDalayParts = {};
|
let bodyDalayParts = {};
|
||||||
// Also check for empty string
|
// Also check for empty string
|
||||||
if (delayId && delayEndpointBaseUrl) {
|
if (delayId && delayEndpointBaseUrl) {
|
||||||
const delayTimeoutMs =
|
|
||||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
|
||||||
bodyDalayParts = {
|
bodyDalayParts = {
|
||||||
delay_id: delayId,
|
delay_id: delayId,
|
||||||
delay_timeout: delayTimeoutMs,
|
delay_timeout:
|
||||||
|
Config.get().matrix_rtc_session.delegated_delayed_leave.delay_ms,
|
||||||
delay_cs_api_url: delayEndpointBaseUrl,
|
delay_cs_api_url: delayEndpointBaseUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
|
|||||||
encryptionSystem: props.e2eeSystem,
|
encryptionSystem: props.e2eeSystem,
|
||||||
autoLeaveWhenOthersLeft,
|
autoLeaveWhenOthersLeft,
|
||||||
waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
|
waitForCallPickup: waitForCallPickup && sendNotificationType === "ring",
|
||||||
matrixRTCMode$: matrixRTCModeSetting.value$,
|
// We merely sample the current mode here, so the user would need to
|
||||||
|
// manually rejoin to switch to a different one.
|
||||||
|
matrixRTCMode: matrixRTCModeSetting.value$.value,
|
||||||
},
|
},
|
||||||
reactionsReader.raisedHands$,
|
reactionsReader.raisedHands$,
|
||||||
reactionsReader.reactions$,
|
reactionsReader.reactions$,
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import { type IMembershipManager } from "matrix-js-sdk/lib/matrixrtc/IMembership
|
|||||||
import {
|
import {
|
||||||
createToggle$,
|
createToggle$,
|
||||||
filterBehavior,
|
filterBehavior,
|
||||||
generateItem,
|
|
||||||
generateItems,
|
generateItems,
|
||||||
pauseWhen,
|
pauseWhen,
|
||||||
} from "../../utils/observable";
|
} from "../../utils/observable";
|
||||||
@@ -64,7 +63,10 @@ import {
|
|||||||
showReactions,
|
showReactions,
|
||||||
} from "../../settings/settings";
|
} from "../../settings/settings";
|
||||||
import { Config } from "../../config/Config";
|
import { Config } from "../../config/Config";
|
||||||
import { MatrixRTCMode } from "../../config/ConfigOptions";
|
import {
|
||||||
|
MatrixRTCMode,
|
||||||
|
type ResolvedDelayedLeaveTimings,
|
||||||
|
} from "../../config/ConfigOptions";
|
||||||
import { isFirefox, platform } from "../../Platform";
|
import { isFirefox, platform } from "../../Platform";
|
||||||
import { setPipEnabled$ } from "../../controls";
|
import { setPipEnabled$ } from "../../controls";
|
||||||
import { TileStore } from "../TileStore";
|
import { TileStore } from "../TileStore";
|
||||||
@@ -110,7 +112,6 @@ import {
|
|||||||
} from "./localMember/LocalMember.ts";
|
} from "./localMember/LocalMember.ts";
|
||||||
import {
|
import {
|
||||||
createLocalTransport$,
|
createLocalTransport$,
|
||||||
JwtEndpointVersion,
|
|
||||||
type LocalTransport,
|
type LocalTransport,
|
||||||
} from "./localMember/LocalTransport.ts";
|
} from "./localMember/LocalTransport.ts";
|
||||||
import {
|
import {
|
||||||
@@ -188,7 +189,7 @@ export interface CallViewModelOptions {
|
|||||||
/** Optional value overriding the connection factory, for testing purposes. */
|
/** Optional value overriding the connection factory, for testing purposes. */
|
||||||
connectionFactory?: ConnectionFactory;
|
connectionFactory?: ConnectionFactory;
|
||||||
/** The version & compatibility mode of MatrixRTC that we should use. */
|
/** The version & compatibility mode of MatrixRTC that we should use. */
|
||||||
matrixRTCMode$?: Behavior<MatrixRTCMode>;
|
matrixRTCMode?: MatrixRTCMode;
|
||||||
/** Optional behavior overriding for the screensharing, for testing */
|
/** Optional behavior overriding for the screensharing, for testing */
|
||||||
toggleScreensharing?: () => void;
|
toggleScreensharing?: () => void;
|
||||||
}
|
}
|
||||||
@@ -450,10 +451,8 @@ export function createCallViewModel$(
|
|||||||
const configMatrixRTCMode = Config.get().matrix_rtc_mode as
|
const configMatrixRTCMode = Config.get().matrix_rtc_mode as
|
||||||
| MatrixRTCMode
|
| MatrixRTCMode
|
||||||
| undefined;
|
| undefined;
|
||||||
const matrixRTCMode$ =
|
const matrixRTCMode =
|
||||||
configMatrixRTCMode !== undefined
|
configMatrixRTCMode ?? options.matrixRTCMode ?? MatrixRTCMode.Compatibility;
|
||||||
? constant(configMatrixRTCMode)
|
|
||||||
: (options.matrixRTCMode$ ?? constant(MatrixRTCMode.Compatibility));
|
|
||||||
|
|
||||||
// Each hbar seperates a block of input variables required for the CallViewModel to function.
|
// Each hbar seperates a block of input variables required for the CallViewModel to function.
|
||||||
// The outputs of this block is written under the hbar.
|
// The outputs of this block is written under the hbar.
|
||||||
@@ -487,38 +486,16 @@ export function createCallViewModel$(
|
|||||||
memberId: uuidv4(),
|
memberId: uuidv4(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const localTransport$ = scope.behavior(
|
const localTransport =
|
||||||
matrixRTCMode$.pipe(
|
options.localTransport ??
|
||||||
generateItem(
|
createLocalTransport$({
|
||||||
"CallViewModel localTransport$",
|
scope: scope,
|
||||||
// Re-create LocalTransport whenever the mode changes
|
memberships$: memberships$,
|
||||||
(mode) => ({ keys: [mode], data: undefined }),
|
ownMembershipIdentity,
|
||||||
(scope, _data$, mode) =>
|
client,
|
||||||
options.localTransport ??
|
roomId: matrixRoom.roomId,
|
||||||
createLocalTransport$({
|
matrixRTCMode,
|
||||||
scope: scope,
|
});
|
||||||
memberships$: memberships$,
|
|
||||||
ownMembershipIdentity,
|
|
||||||
client,
|
|
||||||
delayId$: scope.behavior(
|
|
||||||
(
|
|
||||||
fromEvent(
|
|
||||||
matrixRTCSession,
|
|
||||||
MembershipManagerEvent.DelayIdChanged,
|
|
||||||
// The type of reemitted event includes the original emitted as the second arg.
|
|
||||||
) as Observable<[string | undefined, IMembershipManager]>
|
|
||||||
).pipe(map(([delayId]) => delayId ?? null)),
|
|
||||||
matrixRTCSession.delayId ?? null,
|
|
||||||
),
|
|
||||||
roomId: matrixRoom.roomId,
|
|
||||||
forceJwtEndpoint:
|
|
||||||
mode === MatrixRTCMode.Matrix_2_0
|
|
||||||
? JwtEndpointVersion.Matrix_2_0
|
|
||||||
: JwtEndpointVersion.Legacy,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectionFactory =
|
const connectionFactory =
|
||||||
options.connectionFactory ??
|
options.connectionFactory ??
|
||||||
@@ -536,8 +513,7 @@ export function createCallViewModel$(
|
|||||||
scope: scope,
|
scope: scope,
|
||||||
connectionFactory: connectionFactory,
|
connectionFactory: connectionFactory,
|
||||||
localTransport$: scope.behavior(
|
localTransport$: scope.behavior(
|
||||||
localTransport$.pipe(
|
localTransport.active$.pipe(
|
||||||
switchMap((t) => t.active$),
|
|
||||||
catchError((e: unknown) => {
|
catchError((e: unknown) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
"could not pass local transport to createConnectionManager$. localTransport$ threw an error",
|
"could not pass local transport to createConnectionManager$. localTransport$ threw an error",
|
||||||
@@ -562,16 +538,6 @@ export function createCallViewModel$(
|
|||||||
localUser: { userId, deviceId },
|
localUser: { userId, deviceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const connectOptions$ = scope.behavior(
|
|
||||||
matrixRTCMode$.pipe(
|
|
||||||
map((mode) => ({
|
|
||||||
encryptMedia: livekitKeyProvider !== undefined,
|
|
||||||
// TODO. This might need to get called again on each change of matrixRTCMode...
|
|
||||||
matrixRTCMode: mode,
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const localMembership = createLocalMembership$({
|
const localMembership = createLocalMembership$({
|
||||||
scope,
|
scope,
|
||||||
homeserverConnected: createHomeserverConnected$(
|
homeserverConnected: createHomeserverConnected$(
|
||||||
@@ -580,12 +546,19 @@ export function createCallViewModel$(
|
|||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
),
|
),
|
||||||
muteStates,
|
muteStates,
|
||||||
joinMatrixRTC: (transport: LivekitTransportConfig) => {
|
joinMatrixRTC: (
|
||||||
|
transport: LivekitTransportConfig,
|
||||||
|
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
||||||
|
) => {
|
||||||
return enterRTCSession(
|
return enterRTCSession(
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
transport,
|
transport,
|
||||||
connectOptions$.value,
|
{
|
||||||
|
encryptMedia: livekitKeyProvider !== undefined,
|
||||||
|
matrixRTCMode,
|
||||||
|
delayedLeaveTimings,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
createPublisherFactory: (connection: Connection) => {
|
createPublisherFactory: (connection: Connection) => {
|
||||||
@@ -600,9 +573,23 @@ export function createCallViewModel$(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
connectionManager,
|
connectionManager,
|
||||||
|
client,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
localTransport$,
|
localTransport,
|
||||||
roomId: matrixRoom.roomId,
|
roomId: matrixRoom.roomId,
|
||||||
|
baseUrl: client.baseUrl,
|
||||||
|
ownMembershipIdentity,
|
||||||
|
delayId$: scope.behavior(
|
||||||
|
(
|
||||||
|
fromEvent(
|
||||||
|
matrixRTCSession,
|
||||||
|
MembershipManagerEvent.DelayIdChanged,
|
||||||
|
// The type of reemitted event includes the original emitted as the second arg.
|
||||||
|
) as Observable<[string | undefined, IMembershipManager]>
|
||||||
|
).pipe(map(([delayId]) => delayId ?? null)),
|
||||||
|
matrixRTCSession.delayId ?? null,
|
||||||
|
),
|
||||||
|
matrixRTCMode,
|
||||||
logger: logger.getChild(`[${Date.now()}]`),
|
logger: logger.getChild(`[${Date.now()}]`),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export function withCallViewModel(mode: MatrixRTCMode) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
matrixRTCMode$: constant(mode),
|
matrixRTCMode: mode,
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
raisedHands$,
|
raisedHands$,
|
||||||
|
|||||||
@@ -19,13 +19,18 @@ import {
|
|||||||
beforeAll,
|
beforeAll,
|
||||||
afterAll,
|
afterAll,
|
||||||
beforeEach,
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import { BehaviorSubject, map, of } from "rxjs";
|
import { BehaviorSubject, map, of } from "rxjs";
|
||||||
import { logger } from "matrix-js-sdk/lib/logger";
|
import { logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type LocalParticipant, type LocalTrack } from "livekit-client";
|
import { type LocalParticipant, type LocalTrack } from "livekit-client";
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../analytics/PosthogAnalytics";
|
||||||
import { MatrixRTCMode } from "../../../config/ConfigOptions";
|
import {
|
||||||
|
MatrixRTCMode,
|
||||||
|
type ResolvedDelayedLeaveTimings,
|
||||||
|
} from "../../../config/ConfigOptions";
|
||||||
import { type HomeserverDisconnectReason } from "./HomeserverConnected";
|
import { type HomeserverDisconnectReason } from "./HomeserverConnected";
|
||||||
import {
|
import {
|
||||||
flushPromises,
|
flushPromises,
|
||||||
@@ -35,6 +40,7 @@ import {
|
|||||||
mockMuteStates,
|
mockMuteStates,
|
||||||
withTestScheduler,
|
withTestScheduler,
|
||||||
ownMemberMock,
|
ownMemberMock,
|
||||||
|
testScope,
|
||||||
} from "../../../utils/test";
|
} from "../../../utils/test";
|
||||||
import {
|
import {
|
||||||
TransportState,
|
TransportState,
|
||||||
@@ -58,6 +64,7 @@ import {
|
|||||||
type LocalTransport,
|
type LocalTransport,
|
||||||
type LocalTransportWithSFUConfig,
|
type LocalTransportWithSFUConfig,
|
||||||
} from "./LocalTransport";
|
} from "./LocalTransport";
|
||||||
|
import * as openIDSFU from "../../../livekit/openIDSFU";
|
||||||
|
|
||||||
initializeWidget();
|
initializeWidget();
|
||||||
|
|
||||||
@@ -95,112 +102,112 @@ describe("watchScreenShareToggle", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("LocalMembership", () => {
|
const timings: ResolvedDelayedLeaveTimings = {
|
||||||
describe("enterRTCSession", () => {
|
delay_ms: 10000,
|
||||||
it("It joins the correct Session", () => {
|
restart_ms: 4000,
|
||||||
mockConfig({
|
restart_timeout_ms: 1000,
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
};
|
||||||
});
|
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
const delegatedTimings: ResolvedDelayedLeaveTimings = {
|
||||||
room: {
|
delay_ms: timings.delay_ms * 10,
|
||||||
roomId: "roomId",
|
restart_ms: timings.restart_ms! * 10,
|
||||||
client: {
|
restart_timeout_ms: timings.restart_timeout_ms! * 10,
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
};
|
||||||
getOpenIdToken: vi.fn().mockResolvedValue({
|
|
||||||
access_token: "ACCCESS_TOKEN",
|
|
||||||
token_type: "Bearer",
|
|
||||||
matrix_server_name: "localhost",
|
|
||||||
expires_in: 10000,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
memberships: [],
|
|
||||||
joinRTCSession: vi.fn(),
|
|
||||||
}) as unknown as MatrixRTCSession;
|
|
||||||
|
|
||||||
enterRTCSession(
|
const mockedClient = {
|
||||||
mockedSession,
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
ownMemberMock,
|
getDeviceId: vi.fn().mockReturnValue("AAAA"),
|
||||||
{
|
getOpenIdToken: vi.fn().mockResolvedValue({
|
||||||
livekit_alias: "roomId",
|
access_token: "ACCCESS_TOKEN",
|
||||||
livekit_service_url: "http://my-livekit-service-url.com",
|
token_type: "Bearer",
|
||||||
type: "livekit",
|
matrix_server_name: "localhost",
|
||||||
},
|
expires_in: 10000,
|
||||||
{
|
}),
|
||||||
encryptMedia: true,
|
};
|
||||||
matrixRTCMode: MATRIX_RTC_MODE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith(
|
describe("enterRTCSession", () => {
|
||||||
{
|
const transport: LivekitTransportConfig = {
|
||||||
deviceId: "DEVICE",
|
livekit_alias: "roomId",
|
||||||
memberId: "@alice:example.org:DEVICE",
|
livekit_service_url: "http://my-livekit-service-url.com",
|
||||||
userId: "@alice:example.org",
|
type: "livekit",
|
||||||
},
|
};
|
||||||
[],
|
|
||||||
{
|
|
||||||
livekit_alias: "roomId",
|
|
||||||
livekit_service_url: "http://my-livekit-service-url.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
expect.objectContaining({ manageMediaKeys: true }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("passes keyRotationParticipantLimit from config to joinRTCSession", () => {
|
const options = {
|
||||||
mockConfig({
|
encryptMedia: true,
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
matrixRTCMode: MATRIX_RTC_MODE,
|
||||||
matrix_rtc_session: {
|
delayedLeaveTimings: timings,
|
||||||
delayed_leave_event_delay_ms: 0,
|
};
|
||||||
network_error_retry_ms: 0,
|
|
||||||
key_rotation_participant_limit: 50,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockedSession = vi.mocked({
|
const mockedSession = vi.mocked({
|
||||||
room: {
|
room: {
|
||||||
roomId: "roomId",
|
roomId: "roomId",
|
||||||
client: {
|
client: mockedClient,
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
},
|
||||||
getOpenIdToken: vi.fn().mockResolvedValue({
|
memberships: [],
|
||||||
access_token: "ACCCESS_TOKEN",
|
joinRTCSession: vi.fn(),
|
||||||
token_type: "Bearer",
|
}) as unknown as MatrixRTCSession;
|
||||||
matrix_server_name: "localhost",
|
|
||||||
expires_in: 10000,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
memberships: [],
|
|
||||||
joinRTCSession: vi.fn(),
|
|
||||||
}) as unknown as MatrixRTCSession;
|
|
||||||
|
|
||||||
enterRTCSession(
|
beforeEach(() =>
|
||||||
mockedSession,
|
mockConfig({
|
||||||
ownMemberMock,
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
{
|
}),
|
||||||
livekit_alias: "roomId",
|
);
|
||||||
livekit_service_url: "http://my-livekit-service-url.com",
|
|
||||||
type: "livekit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encryptMedia: true,
|
|
||||||
matrixRTCMode: MATRIX_RTC_MODE,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith(
|
it("It joins the correct Session", () => {
|
||||||
expect.any(Object),
|
enterRTCSession(mockedSession, ownMemberMock, transport, options);
|
||||||
[],
|
|
||||||
expect.any(Object),
|
expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith(
|
||||||
expect.objectContaining({
|
{
|
||||||
keyRotationParticipantLimit: 50,
|
deviceId: "DEVICE",
|
||||||
}),
|
memberId: "@alice:example.org:DEVICE",
|
||||||
);
|
userId: "@alice:example.org",
|
||||||
});
|
},
|
||||||
|
[],
|
||||||
|
transport,
|
||||||
|
expect.objectContaining({ manageMediaKeys: true }),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes keyRotationParticipantLimit from config to joinRTCSession", () => {
|
||||||
|
mockConfig({
|
||||||
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
|
matrix_rtc_session: {
|
||||||
|
network_error_retry_ms: 0,
|
||||||
|
key_rotation_participant_limit: 50,
|
||||||
|
delayed_leave: timings,
|
||||||
|
delegated_delayed_leave: timings,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
enterRTCSession(mockedSession, ownMemberMock, transport, options);
|
||||||
|
|
||||||
|
expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith(
|
||||||
|
expect.any(Object),
|
||||||
|
[],
|
||||||
|
expect.any(Object),
|
||||||
|
expect.objectContaining({
|
||||||
|
keyRotationParticipantLimit: 50,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the specified delayed leave timings", () => {
|
||||||
|
enterRTCSession(mockedSession, ownMemberMock, transport, options);
|
||||||
|
|
||||||
|
expect(mockedSession.joinRTCSession).toHaveBeenLastCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.objectContaining({
|
||||||
|
delayedLeaveEventRestartMs: timings.restart_ms,
|
||||||
|
delayedLeaveEventDelayMs: timings.delay_ms,
|
||||||
|
delayedLeaveEventRestartLocalTimeoutMs: timings.restart_timeout_ms,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("LocalMembership", () => {
|
||||||
const defaultCreateLocalMemberValues = {
|
const defaultCreateLocalMemberValues = {
|
||||||
options: constant({
|
options: constant({
|
||||||
encryptMedia: false,
|
encryptMedia: false,
|
||||||
@@ -226,8 +233,30 @@ describe("LocalMembership", () => {
|
|||||||
rtsSession$: constant(RTCMemberStatus.Connected),
|
rtsSession$: constant(RTCMemberStatus.Connected),
|
||||||
},
|
},
|
||||||
roomId: "!test-room-id:example.org",
|
roomId: "!test-room-id:example.org",
|
||||||
|
baseUrl: "https://matrix.example.org",
|
||||||
|
ownMembershipIdentity: ownMemberMock,
|
||||||
|
client: mockedClient,
|
||||||
|
delayId$: constant(null),
|
||||||
|
matrixRTCMode: MATRIX_RTC_MODE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockConfig({
|
||||||
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
|
matrix_rtc_session: {
|
||||||
|
network_error_retry_ms: 1000,
|
||||||
|
delayed_leave: timings,
|
||||||
|
delegated_delayed_leave: delegatedTimings,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fetchMock.catch(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
void (await fetchMock.flush());
|
||||||
|
fetchMock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
it("throws error on missing RTC config error", () => {
|
it("throws error on missing RTC config error", () => {
|
||||||
withTestScheduler(({ scope, hot, behavior, expectObservable }) => {
|
withTestScheduler(({ scope, hot, behavior, expectObservable }) => {
|
||||||
const localTransport$ = scope.behavior<null | LivekitTransportConfig>(
|
const localTransport$ = scope.behavior<null | LivekitTransportConfig>(
|
||||||
@@ -254,9 +283,8 @@ describe("LocalMembership", () => {
|
|||||||
scope,
|
scope,
|
||||||
...defaultCreateLocalMemberValues,
|
...defaultCreateLocalMemberValues,
|
||||||
connectionManager: mockConnectionManager,
|
connectionManager: mockConnectionManager,
|
||||||
localTransport$: behavior("a", { a: aLocalTransport }),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
localMembership.requestJoinAndPublish();
|
|
||||||
|
|
||||||
expectObservable(localMembership.localMemberState$).toBe("ne", {
|
expectObservable(localMembership.localMemberState$).toBe("ne", {
|
||||||
n: TransportState.Waiting,
|
n: TransportState.Waiting,
|
||||||
@@ -299,9 +327,8 @@ describe("LocalMembership", () => {
|
|||||||
scope,
|
scope,
|
||||||
...defaultCreateLocalMemberValues,
|
...defaultCreateLocalMemberValues,
|
||||||
connectionManager: mockConnectionManager,
|
connectionManager: mockConnectionManager,
|
||||||
localTransport$: behavior("a", { a: aLocalTransport }),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
localMembership.requestJoinAndPublish();
|
|
||||||
|
|
||||||
expectObservable(localMembership.localMemberState$).toBe("n-e", {
|
expectObservable(localMembership.localMemberState$).toBe("n-e", {
|
||||||
n: TransportState.Waiting,
|
n: TransportState.Waiting,
|
||||||
@@ -317,8 +344,8 @@ describe("LocalMembership", () => {
|
|||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
|
|
||||||
const aLocalTransport: LocalTransport = {
|
const aLocalTransport: LocalTransport = {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockConnectionManager = {
|
const mockConnectionManager = {
|
||||||
@@ -336,7 +363,7 @@ describe("LocalMembership", () => {
|
|||||||
leaveRoomSession: vi.fn(),
|
leaveRoomSession: vi.fn(),
|
||||||
},
|
},
|
||||||
connectionManager: mockConnectionManager,
|
connectionManager: mockConnectionManager,
|
||||||
localTransport$: new BehaviorSubject(aLocalTransport),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
const expextedLog =
|
const expextedLog =
|
||||||
"'not connected yet' while updating the call intent (this is expected on startup)";
|
"'not connected yet' while updating the call intent (this is expected on startup)";
|
||||||
@@ -397,12 +424,90 @@ describe("LocalMembership", () => {
|
|||||||
livekitRoom: mockLivekitRoom({}),
|
livekitRoom: mockLivekitRoom({}),
|
||||||
} as unknown as Connection;
|
} as unknown as Connection;
|
||||||
|
|
||||||
|
const authCallSpy = vi
|
||||||
|
.spyOn(openIDSFU, "getSFUConfigWithOpenID")
|
||||||
|
.mockImplementation(() => mockedClient.getOpenIdToken());
|
||||||
|
afterEach(() => authCallSpy.mockClear());
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["no", null, timings],
|
||||||
|
[
|
||||||
|
"homeserver",
|
||||||
|
"https://matrix.example.org/_matrix/client/unstable/io.element.msc4195/rtc/livekit/delegate_delayed_leave",
|
||||||
|
delegatedTimings,
|
||||||
|
],
|
||||||
|
["transport", "/a/delegate_delayed_leave", delegatedTimings],
|
||||||
|
])(
|
||||||
|
"joins session with %s delegation support",
|
||||||
|
async (_serviceName, delegationUrl, delayedLeaveTimings) => {
|
||||||
|
const scope = testScope();
|
||||||
|
const joinMatrixRTC = vi.fn();
|
||||||
|
const delayId$ = new BehaviorSubject<string | null>(null);
|
||||||
|
|
||||||
|
if (delegationUrl !== null)
|
||||||
|
fetchMock.post(delegationUrl, () => ({ status: 401, body: {} }));
|
||||||
|
|
||||||
|
const localMembership = createLocalMembership$({
|
||||||
|
scope,
|
||||||
|
...defaultCreateLocalMemberValues,
|
||||||
|
connectionManager: {
|
||||||
|
connectionManagerData$: constant(
|
||||||
|
new Epoch(new ConnectionManagerData()),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
joinMatrixRTC,
|
||||||
|
localTransport: {
|
||||||
|
advertised$: constant(aTransport),
|
||||||
|
active$: constant(aTransportWithSFUConfig),
|
||||||
|
},
|
||||||
|
delayId$,
|
||||||
|
});
|
||||||
|
|
||||||
|
localMembership.requestJoinAndPublish();
|
||||||
|
void (await fetchMock.flush());
|
||||||
|
await flushPromises();
|
||||||
|
// Joins with timings appropriate for the level of delegation support
|
||||||
|
expect(joinMatrixRTC).toHaveBeenCalledWith(
|
||||||
|
aTransport,
|
||||||
|
delayedLeaveTimings,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(authCallSpy).not.toHaveBeenCalled();
|
||||||
|
delayId$.next("leave1");
|
||||||
|
await flushPromises();
|
||||||
|
if (delegationUrl === null) {
|
||||||
|
expect(authCallSpy).not.toHaveBeenCalled();
|
||||||
|
} else {
|
||||||
|
// Delegation is supported in this test case, so go on to check that
|
||||||
|
// LocalMember actually performs delegation
|
||||||
|
const expectDelegation = (delayId: string) =>
|
||||||
|
expect(authCallSpy).toHaveBeenLastCalledWith(
|
||||||
|
mockedClient,
|
||||||
|
ownMemberMock,
|
||||||
|
"a",
|
||||||
|
"!test-room-id:example.org",
|
||||||
|
{
|
||||||
|
matrixRTCMode: MATRIX_RTC_MODE,
|
||||||
|
delayEndpointBaseUrl: "https://matrix.example.org",
|
||||||
|
delayId,
|
||||||
|
},
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expectDelegation("leave1");
|
||||||
|
delayId$.next("leave2"); // Can change delegated leaves
|
||||||
|
await flushPromises();
|
||||||
|
expectDelegation("leave2");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("recreates publisher if new connection is used, always unpublish and end tracks", async () => {
|
it("recreates publisher if new connection is used, always unpublish and end tracks", async () => {
|
||||||
const scope = new ObservableScope();
|
const scope = new ObservableScope();
|
||||||
|
|
||||||
const activeTransport$ = new BehaviorSubject(aTransportWithSFUConfig);
|
const activeTransport$ = new BehaviorSubject(aTransportWithSFUConfig);
|
||||||
const aLocalTransport: LocalTransport = {
|
const aLocalTransport: LocalTransport = {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: activeTransport$,
|
active$: activeTransport$,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -440,7 +545,7 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject(aLocalTransport),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
activeTransport$.next({
|
activeTransport$.next({
|
||||||
@@ -471,7 +576,7 @@ describe("LocalMembership", () => {
|
|||||||
const publishers: Publisher[] = [];
|
const publishers: Publisher[] = [];
|
||||||
|
|
||||||
const tracks$ = new BehaviorSubject<LocalTrack[]>([]);
|
const tracks$ = new BehaviorSubject<LocalTrack[]>([]);
|
||||||
const publishing$ = new BehaviorSubject<boolean>(false);
|
const publishing$ = constant<boolean>(false);
|
||||||
defaultCreateLocalMemberValues.createPublisherFactory.mockImplementation(
|
defaultCreateLocalMemberValues.createPublisherFactory.mockImplementation(
|
||||||
() => {
|
() => {
|
||||||
const p = {
|
const p = {
|
||||||
@@ -495,8 +600,8 @@ describe("LocalMembership", () => {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const aLocalTransport: LocalTransport = {
|
const aLocalTransport: LocalTransport = {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
};
|
};
|
||||||
|
|
||||||
const connectionManagerData = new ConnectionManagerData();
|
const connectionManagerData = new ConnectionManagerData();
|
||||||
@@ -508,7 +613,7 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject(aLocalTransport),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
expect(publisherFactory).toHaveBeenCalledOnce();
|
expect(publisherFactory).toHaveBeenCalledOnce();
|
||||||
@@ -536,7 +641,7 @@ describe("LocalMembership", () => {
|
|||||||
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
|
new BehaviorSubject<null | LocalTransportWithSFUConfig>(null);
|
||||||
|
|
||||||
const aLocalTransport: LocalTransport = {
|
const aLocalTransport: LocalTransport = {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: activeTransport$,
|
active$: activeTransport$,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -579,7 +684,7 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$,
|
connectionManagerData$,
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject(aLocalTransport),
|
localTransport: aLocalTransport,
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -714,10 +819,10 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject({
|
localTransport: {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -754,10 +859,10 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject({
|
localTransport: {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -796,18 +901,19 @@ describe("LocalMembership", () => {
|
|||||||
scope,
|
scope,
|
||||||
...defaultCreateLocalMemberValues,
|
...defaultCreateLocalMemberValues,
|
||||||
homeserverConnected: {
|
homeserverConnected: {
|
||||||
combined$: new BehaviorSubject<
|
combined$: constant<[boolean, HomeserverDisconnectReason | null]>([
|
||||||
[boolean, HomeserverDisconnectReason | null]
|
true,
|
||||||
>([true, null]),
|
null,
|
||||||
|
]),
|
||||||
rtsSession$: constant(RTCMemberStatus.Connected),
|
rtsSession$: constant(RTCMemberStatus.Connected),
|
||||||
},
|
},
|
||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject({
|
localTransport: {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -848,10 +954,10 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject({
|
localTransport: {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
@@ -917,10 +1023,10 @@ describe("LocalMembership", () => {
|
|||||||
connectionManager: {
|
connectionManager: {
|
||||||
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
connectionManagerData$: constant(new Epoch(connectionManagerData)),
|
||||||
},
|
},
|
||||||
localTransport$: new BehaviorSubject({
|
localTransport: {
|
||||||
advertised$: new BehaviorSubject(aTransport),
|
advertised$: constant(aTransport),
|
||||||
active$: new BehaviorSubject(aTransportWithSFUConfig),
|
active$: constant(aTransportWithSFUConfig),
|
||||||
}),
|
},
|
||||||
});
|
});
|
||||||
return { scope, localMembership };
|
return { scope, localMembership };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
MediaDeviceFailure,
|
MediaDeviceFailure,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { observeParticipantEvents } from "@livekit/components-core";
|
import { observeParticipantEvents } from "@livekit/components-core";
|
||||||
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
Status as RTCSessionStatus,
|
Status as RTCSessionStatus,
|
||||||
type LivekitTransport,
|
type LivekitTransport,
|
||||||
@@ -62,7 +63,10 @@ import {
|
|||||||
screenShareCodec,
|
screenShareCodec,
|
||||||
parseResolution,
|
parseResolution,
|
||||||
} from "../../../settings/settings.ts";
|
} from "../../../settings/settings.ts";
|
||||||
import { MatrixRTCMode } from "../../../config/ConfigOptions.ts";
|
import {
|
||||||
|
MatrixRTCMode,
|
||||||
|
type ResolvedDelayedLeaveTimings,
|
||||||
|
} from "../../../config/ConfigOptions.ts";
|
||||||
import { Config } from "../../../config/Config.ts";
|
import { Config } from "../../../config/Config.ts";
|
||||||
import {
|
import {
|
||||||
ConnectionState,
|
ConnectionState,
|
||||||
@@ -72,6 +76,8 @@ import {
|
|||||||
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
||||||
import { type LocalTransport } from "./LocalTransport.ts";
|
import { type LocalTransport } from "./LocalTransport.ts";
|
||||||
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
||||||
|
import { or$ } from "../../../utils/observable.ts";
|
||||||
|
import { getSFUConfigWithOpenID } from "../../../livekit/openIDSFU.ts";
|
||||||
|
|
||||||
export enum TransportState {
|
export enum TransportState {
|
||||||
/** Not even a transport is available to the LocalMembership */
|
/** Not even a transport is available to the LocalMembership */
|
||||||
@@ -133,14 +139,22 @@ interface Props {
|
|||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
connectionManager: IConnectionManager;
|
connectionManager: IConnectionManager;
|
||||||
createPublisherFactory: (connection: Connection) => Publisher;
|
createPublisherFactory: (connection: Connection) => Publisher;
|
||||||
joinMatrixRTC: (transport: LivekitTransportConfig) => void;
|
joinMatrixRTC: (
|
||||||
|
transport: LivekitTransportConfig,
|
||||||
|
delayedLeaveTimings: ResolvedDelayedLeaveTimings,
|
||||||
|
) => void;
|
||||||
homeserverConnected: HomeserverConnected;
|
homeserverConnected: HomeserverConnected;
|
||||||
roomId: string;
|
roomId: string;
|
||||||
localTransport$: Behavior<LocalTransport>;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
|
localTransport: LocalTransport;
|
||||||
|
client: Pick<MatrixClient, "getDeviceId" | "getOpenIdToken">;
|
||||||
matrixRTCSession: Pick<
|
matrixRTCSession: Pick<
|
||||||
MatrixRTCSession,
|
MatrixRTCSession,
|
||||||
"updateCallIntent" | "leaveRoomSession"
|
"updateCallIntent" | "leaveRoomSession"
|
||||||
>;
|
>;
|
||||||
|
baseUrl: string;
|
||||||
|
delayId$: Behavior<string | null>;
|
||||||
|
matrixRTCMode: MatrixRTCMode;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +169,12 @@ interface Props {
|
|||||||
* @param props.createPublisherFactory Factory to create a publisher once we have a connection.
|
* @param props.createPublisherFactory Factory to create a publisher once we have a connection.
|
||||||
* @param props.joinMatrixRTC Callback to join the matrix RTC session once we have a transport.
|
* @param props.joinMatrixRTC Callback to join the matrix RTC session once we have a transport.
|
||||||
* @param props.homeserverConnected The homeserver connected state.
|
* @param props.homeserverConnected The homeserver connected state.
|
||||||
* @param props.localTransport$ The transport to advertise in our membership.
|
* @param props.localTransport The transport to advertise in our membership.
|
||||||
* @param props.logger The logger to use.
|
* @param props.logger The logger to use.
|
||||||
* @param props.muteStates The mute states for video and audio.
|
* @param props.muteStates The mute states for video and audio.
|
||||||
* @param props.matrixRTCSession The matrix RTC session to join.
|
* @param props.matrixRTCSession The matrix RTC session to join.
|
||||||
|
* @param props.baseUrl Base URL of the homeserver.
|
||||||
|
* @param props.delayId$ ID of the delayed leave event to delegate to the SFU.
|
||||||
* @param props.roomId The room ID used as the call identifier in analytics events.
|
* @param props.roomId The room ID used as the call identifier in analytics events.
|
||||||
* @returns
|
* @returns
|
||||||
* - publisher: The handle to create tracks and publish them to the room.
|
* - publisher: The handle to create tracks and publish them to the room.
|
||||||
@@ -170,14 +186,19 @@ interface Props {
|
|||||||
export const createLocalMembership$ = ({
|
export const createLocalMembership$ = ({
|
||||||
scope,
|
scope,
|
||||||
connectionManager,
|
connectionManager,
|
||||||
localTransport$,
|
localTransport,
|
||||||
homeserverConnected,
|
homeserverConnected,
|
||||||
createPublisherFactory,
|
createPublisherFactory,
|
||||||
joinMatrixRTC,
|
joinMatrixRTC,
|
||||||
logger: parentLogger,
|
logger: parentLogger,
|
||||||
muteStates,
|
muteStates,
|
||||||
|
client,
|
||||||
matrixRTCSession,
|
matrixRTCSession,
|
||||||
|
baseUrl,
|
||||||
roomId,
|
roomId,
|
||||||
|
ownMembershipIdentity,
|
||||||
|
delayId$,
|
||||||
|
matrixRTCMode,
|
||||||
}: Props): {
|
}: Props): {
|
||||||
/**
|
/**
|
||||||
* This request to start audio and video tracks.
|
* This request to start audio and video tracks.
|
||||||
@@ -236,25 +257,69 @@ export const createLocalMembership$ = ({
|
|||||||
return of(null);
|
return of(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is the transport that we will advertise in our membership.
|
async function checkDelegationSupport(
|
||||||
const advertisedTransport$ = localTransport$.pipe(
|
endpointUrl: string,
|
||||||
switchMap((lt) => lt.advertised$),
|
serviceName: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
logger.info(`Checking whether ${serviceName} supports delegation…`);
|
||||||
|
try {
|
||||||
|
// Bluntly hit the endpoint without auth to check for a 404. Unfortunately
|
||||||
|
// we can't wrap this in a retry loop, as many servers don't just disable
|
||||||
|
// delegation support, but in fact are from a time before the endpoint
|
||||||
|
// existed at all, therefore we can hit CORS errors which would just gum
|
||||||
|
// up the retry loop. (May be revisited after Matrix 2.0.)
|
||||||
|
const res = await fetch(endpointUrl, { method: "POST" });
|
||||||
|
if (res.status === 404) {
|
||||||
|
logger.warn(`${serviceName} does not support delegation`);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.info(`${serviceName} supports delegation`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to determine whether ${serviceName} supports delegation, assuming no support`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeserverSupportsDelegation = checkDelegationSupport(
|
||||||
|
baseUrl +
|
||||||
|
"/_matrix/client/unstable/io.element.msc4195/rtc/livekit/delegate_delayed_leave",
|
||||||
|
"homeserver",
|
||||||
|
);
|
||||||
|
|
||||||
|
// The transport that we will advertise in our membership, paired with info as
|
||||||
|
// to whether delayed event delegation is supported
|
||||||
|
const joinParams$ = localTransport.advertised$.pipe(
|
||||||
catchError(handleTransportError),
|
catchError(handleTransportError),
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
|
switchMap((transport) => {
|
||||||
|
if (transport === null) return of(null);
|
||||||
|
const transportSupportsDelegation = checkDelegationSupport(
|
||||||
|
transport.livekit_service_url + "/delegate_delayed_leave",
|
||||||
|
`transport ${transport.livekit_service_url}`,
|
||||||
|
);
|
||||||
|
return or$(
|
||||||
|
from(homeserverSupportsDelegation),
|
||||||
|
from(transportSupportsDelegation),
|
||||||
|
).pipe(
|
||||||
|
map((delegationSupported) => ({ transport, delegationSupported })),
|
||||||
|
startWith(null),
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
|
// Unwrap the local transport and set the state of the LocalMembership to error in case the transport is an error.
|
||||||
const activeTransport$ = scope.behavior(
|
const activeTransport$ = scope.behavior(
|
||||||
localTransport$.pipe(
|
combineLatest([localTransport.active$, localTransport.advertised$]).pipe(
|
||||||
switchMap((lt) => {
|
map(([active, advertised]) => {
|
||||||
return combineLatest([lt.active$, lt.advertised$]).pipe(
|
// Our policy is to not publish to another transport if our prefered transport is miss-configured
|
||||||
map(([active, advertised]) => {
|
if (advertised == null) return null;
|
||||||
// Our policy is to not publish to another transport if our prefered transport is miss-configured
|
|
||||||
if (advertised == null) return null;
|
|
||||||
|
|
||||||
return active?.transport ?? null;
|
return active?.transport ?? null;
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
catchError(handleTransportError),
|
catchError(handleTransportError),
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
@@ -617,16 +682,20 @@ export const createLocalMembership$ = ({
|
|||||||
|
|
||||||
// Keep matrix rtc session in sync with advertisedTransport$, connectRequested$
|
// Keep matrix rtc session in sync with advertisedTransport$, connectRequested$
|
||||||
scope.reconcile(
|
scope.reconcile(
|
||||||
scope.behavior(
|
scope.behavior(combineLatest([joinParams$, joinAndPublishRequested$])),
|
||||||
combineLatest([advertisedTransport$, joinAndPublishRequested$]),
|
async ([joinParams, shouldConnect]) => {
|
||||||
),
|
if (!joinParams) return;
|
||||||
async ([transport, shouldConnect]) => {
|
|
||||||
if (!transport) return;
|
|
||||||
// if shouldConnect=false we will do the disconnect as the cleanup from the previous reconcile iteration.
|
// if shouldConnect=false we will do the disconnect as the cleanup from the previous reconcile iteration.
|
||||||
if (!shouldConnect) return;
|
if (!shouldConnect) return;
|
||||||
|
const sessionConfig = Config.get().matrix_rtc_session;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
joinMatrixRTC(transport);
|
joinMatrixRTC(
|
||||||
|
joinParams.transport,
|
||||||
|
joinParams.delegationSupported
|
||||||
|
? sessionConfig.delegated_delayed_leave
|
||||||
|
: sessionConfig.delayed_leave,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error entering RTC session", error);
|
logger.error("Error entering RTC session", error);
|
||||||
if (error instanceof Error)
|
if (error instanceof Error)
|
||||||
@@ -653,6 +722,34 @@ export const createLocalMembership$ = ({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Delegate delayed leaves to the SFU
|
||||||
|
scope.reconcile(
|
||||||
|
scope.behavior(combineLatest([joinParams$, delayId$])),
|
||||||
|
async ([joinParams, delayId]) => {
|
||||||
|
if (joinParams?.delegationSupported && delayId !== null) {
|
||||||
|
try {
|
||||||
|
// This will technically cause the service to issue a new JWT token,
|
||||||
|
// but it's safe to discard. We're only interested in triggering
|
||||||
|
// delegation.
|
||||||
|
await getSFUConfigWithOpenID(
|
||||||
|
client,
|
||||||
|
ownMembershipIdentity,
|
||||||
|
joinParams.transport.livekit_service_url,
|
||||||
|
roomId,
|
||||||
|
{ matrixRTCMode, delayEndpointBaseUrl: baseUrl, delayId },
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: Surface this to the user as a service interruption?
|
||||||
|
logger.error(
|
||||||
|
`Failed to delegate leave to ${joinParams.transport.livekit_service_url}`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Pause upstream of all local media tracks when we're disconnected from
|
// Pause upstream of all local media tracks when we're disconnected from
|
||||||
// MatrixRTC, because it can be an unpleasant surprise for the app to say
|
// MatrixRTC, because it can be an unpleasant surprise for the app to say
|
||||||
// 'reconnecting' and yet still be transmitting your media to others.
|
// 'reconnecting' and yet still be transmitting your media to others.
|
||||||
@@ -864,6 +961,7 @@ export function observeSharingScreen$(p: Participant): Observable<boolean> {
|
|||||||
interface EnterRTCSessionOptions {
|
interface EnterRTCSessionOptions {
|
||||||
encryptMedia: boolean;
|
encryptMedia: boolean;
|
||||||
matrixRTCMode: MatrixRTCMode;
|
matrixRTCMode: MatrixRTCMode;
|
||||||
|
delayedLeaveTimings: ResolvedDelayedLeaveTimings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -876,6 +974,7 @@ interface EnterRTCSessionOptions {
|
|||||||
* @param rtcSession - The MatrixRTCSession to join.
|
* @param rtcSession - The MatrixRTCSession to join.
|
||||||
* @param ownMembershipIdentity - Options for entering the RTC session.
|
* @param ownMembershipIdentity - Options for entering the RTC session.
|
||||||
* @param transport - The LivekitTransport to use for this session.
|
* @param transport - The LivekitTransport to use for this session.
|
||||||
|
* @param delayedLeaveTimings - The preferred timings for delayed leave events.
|
||||||
* @param options - `encryptMedia`: Whether to encrypt media `matrixRTCMode`: The Matrix RTC mode to use.
|
* @param options - `encryptMedia`: Whether to encrypt media `matrixRTCMode`: The Matrix RTC mode to use.
|
||||||
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
|
* @throws If the widget could not send ElementWidgetActions.JoinCall action.
|
||||||
*/
|
*/
|
||||||
@@ -884,9 +983,8 @@ export function enterRTCSession(
|
|||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
ownMembershipIdentity: CallMembershipIdentityParts,
|
||||||
transport: LivekitTransportConfig,
|
transport: LivekitTransportConfig,
|
||||||
options: EnterRTCSessionOptions,
|
{ encryptMedia, matrixRTCMode, delayedLeaveTimings }: EnterRTCSessionOptions,
|
||||||
): void {
|
): void {
|
||||||
const { encryptMedia, matrixRTCMode } = options;
|
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
||||||
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
|
||||||
|
|
||||||
@@ -894,7 +992,11 @@ export function enterRTCSession(
|
|||||||
// have started tracking by the time calls start getting created.
|
// have started tracking by the time calls start getting created.
|
||||||
// groupCallOTelMembership?.onJoinCall();
|
// groupCallOTelMembership?.onJoinCall();
|
||||||
|
|
||||||
const { matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
const {
|
||||||
|
sync_disconnect_grace_period_ms: gracePeriod,
|
||||||
|
matrix_rtc_session: sessionConfig,
|
||||||
|
} = Config.get();
|
||||||
|
const retryInterval = sessionConfig.network_error_retry_ms;
|
||||||
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
||||||
const multiSFU =
|
const multiSFU =
|
||||||
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
||||||
@@ -912,16 +1014,11 @@ export function enterRTCSession(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculates `maximumNetworkErrorRetryCount`. The connection is failed if EITHER:
|
// Set maximumNetworkErrorRetryCount such that we will consider the client
|
||||||
// - The /sync loop is unresponsive for > `gracePeriod` ms, or
|
// disconnected as soon as either it fails to sync for longer than the grace
|
||||||
// - A delayed leave event is emitted (after `leaveDelay` ms period).
|
// period, or it is likely that a delayed leave event has been sent.
|
||||||
// Note: Use leaveDelay >> gracePeriod for delegated leave events.
|
|
||||||
const gracePeriod = Config.get().sync_disconnect_grace_period_ms;
|
|
||||||
const leaveDelay = matrixRtcSessionConfig?.delayed_leave_event_delay_ms;
|
|
||||||
const retryInterval = matrixRtcSessionConfig?.network_error_retry_ms;
|
|
||||||
|
|
||||||
// Math.min is used to account for the respective worst case: /sync not available or leave event emitted.
|
// Math.min is used to account for the respective worst case: /sync not available or leave event emitted.
|
||||||
const maxWaitTime = Math.min(gracePeriod, leaveDelay);
|
const maxWaitTime = Math.min(gracePeriod, delayedLeaveTimings.delay_ms);
|
||||||
const maximumNetworkErrorRetryCount =
|
const maximumNetworkErrorRetryCount =
|
||||||
Math.ceil(maxWaitTime / retryInterval) + 1;
|
Math.ceil(maxWaitTime / retryInterval) + 1;
|
||||||
|
|
||||||
@@ -936,18 +1033,14 @@ export function enterRTCSession(
|
|||||||
notificationType,
|
notificationType,
|
||||||
callIntent,
|
callIntent,
|
||||||
manageMediaKeys: encryptMedia,
|
manageMediaKeys: encryptMedia,
|
||||||
delayedLeaveEventRestartMs:
|
delayedLeaveEventRestartMs: delayedLeaveTimings.restart_ms,
|
||||||
matrixRtcSessionConfig?.delayed_leave_event_restart_ms,
|
delayedLeaveEventDelayMs: delayedLeaveTimings.delay_ms,
|
||||||
delayedLeaveEventDelayMs:
|
|
||||||
matrixRtcSessionConfig?.delayed_leave_event_delay_ms,
|
|
||||||
delayedLeaveEventRestartLocalTimeoutMs:
|
delayedLeaveEventRestartLocalTimeoutMs:
|
||||||
matrixRtcSessionConfig?.delayed_leave_event_restart_local_timeout_ms,
|
delayedLeaveTimings.restart_timeout_ms,
|
||||||
networkErrorRetryMs: matrixRtcSessionConfig?.network_error_retry_ms,
|
networkErrorRetryMs: sessionConfig.network_error_retry_ms,
|
||||||
makeKeyDelay: matrixRtcSessionConfig?.wait_for_key_rotation_ms,
|
makeKeyDelay: sessionConfig.wait_for_key_rotation_ms,
|
||||||
membershipEventExpiryMs:
|
membershipEventExpiryMs: sessionConfig.membership_event_expiry_ms,
|
||||||
matrixRtcSessionConfig?.membership_event_expiry_ms,
|
keyRotationParticipantLimit: sessionConfig.key_rotation_participant_limit,
|
||||||
keyRotationParticipantLimit:
|
|
||||||
matrixRtcSessionConfig?.key_rotation_participant_limit,
|
|
||||||
unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0,
|
unstableSendStickyEvents: matrixRTCMode === MatrixRTCMode.Matrix_2_0,
|
||||||
maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount,
|
maximumNetworkErrorRetryCount: maximumNetworkErrorRetryCount,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ import {
|
|||||||
type MockedObject,
|
type MockedObject,
|
||||||
vi,
|
vi,
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import {
|
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||||
type CallMembership,
|
import { lastValueFrom } from "rxjs";
|
||||||
type LivekitTransportConfig,
|
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
|
||||||
import { BehaviorSubject, filter, lastValueFrom } from "rxjs";
|
|
||||||
import fetchMock from "fetch-mock";
|
import fetchMock from "fetch-mock";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -27,11 +24,7 @@ import {
|
|||||||
ownMemberMock,
|
ownMemberMock,
|
||||||
testScope,
|
testScope,
|
||||||
} from "../../../utils/test";
|
} from "../../../utils/test";
|
||||||
import {
|
import { createLocalTransport$ } from "./LocalTransport";
|
||||||
createLocalTransport$,
|
|
||||||
JwtEndpointVersion,
|
|
||||||
type LocalTransportWithSFUConfig,
|
|
||||||
} from "./LocalTransport";
|
|
||||||
import { constant } from "../../Behavior";
|
import { constant } from "../../Behavior";
|
||||||
import { Epoch, ObservableScope } from "../../ObservableScope";
|
import { Epoch, ObservableScope } from "../../ObservableScope";
|
||||||
import {
|
import {
|
||||||
@@ -41,6 +34,7 @@ import {
|
|||||||
import * as openIDSFU from "../../../livekit/openIDSFU";
|
import * as openIDSFU from "../../../livekit/openIDSFU";
|
||||||
import { customLivekitUrl } from "../../../settings/settings";
|
import { customLivekitUrl } from "../../../settings/settings";
|
||||||
import { testJWTToken } from "../../../utils/test-fixtures";
|
import { testJWTToken } from "../../../utils/test-fixtures";
|
||||||
|
import { MatrixRTCMode } from "../../../config/ConfigOptions";
|
||||||
|
|
||||||
describe("LocalTransport", () => {
|
describe("LocalTransport", () => {
|
||||||
const openIdResponse: openIDSFU.SFUConfig = {
|
const openIdResponse: openIDSFU.SFUConfig = {
|
||||||
@@ -61,14 +55,12 @@ describe("LocalTransport", () => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
getDomain: () => "example.org",
|
getDomain: () => "example.org",
|
||||||
baseUrl: "example.org",
|
|
||||||
// These won't be called in this error path but satisfy the type
|
// These won't be called in this error path but satisfy the type
|
||||||
getOpenIdToken: vi.fn(),
|
getOpenIdToken: vi.fn(),
|
||||||
getDeviceId: vi.fn(),
|
getDeviceId: vi.fn(),
|
||||||
},
|
},
|
||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
delayId$: constant("delay_id_mock"),
|
|
||||||
});
|
});
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -100,7 +92,6 @@ describe("LocalTransport", () => {
|
|||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
baseUrl: "https://example.org",
|
|
||||||
getDomain: () => "example.org",
|
getDomain: () => "example.org",
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
@@ -108,8 +99,7 @@ describe("LocalTransport", () => {
|
|||||||
getDeviceId: vi.fn(),
|
getDeviceId: vi.fn(),
|
||||||
},
|
},
|
||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
delayId$: constant("delay_id_mock"),
|
|
||||||
});
|
});
|
||||||
active$.subscribe(
|
active$.subscribe(
|
||||||
(o) => observations.push(o),
|
(o) => observations.push(o),
|
||||||
@@ -147,11 +137,9 @@ describe("LocalTransport", () => {
|
|||||||
getDomain: () => "example.org",
|
getDomain: () => "example.org",
|
||||||
getOpenIdToken: vi.fn(),
|
getOpenIdToken: vi.fn(),
|
||||||
getDeviceId: vi.fn(),
|
getDeviceId: vi.fn(),
|
||||||
baseUrl: "https://example.org",
|
|
||||||
},
|
},
|
||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
delayId$: constant("delay_id_mock"),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
openIdResolver.resolve?.({
|
openIdResolver.resolve?.({
|
||||||
@@ -194,11 +182,9 @@ describe("LocalTransport", () => {
|
|||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
delayId$: constant(null),
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
baseUrl: "https://example.org",
|
|
||||||
getDomain: vi.fn().mockReturnValue("example.org"),
|
getDomain: vi.fn().mockReturnValue("example.org"),
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
|
_unstable_getRTCTransports: vi.fn().mockResolvedValue([]),
|
||||||
@@ -306,12 +292,10 @@ describe("LocalTransport", () => {
|
|||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
delayId$: constant(null),
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
getDomain: () => "example.org",
|
getDomain: () => "example.org",
|
||||||
baseUrl: "https://example.org",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||||
// These won't be called in this error path but satisfy the type
|
// These won't be called in this error path but satisfy the type
|
||||||
@@ -329,84 +313,4 @@ describe("LocalTransport", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not update advertised/active transport on delayID changes, but delay Id delegation should be called", async () => {
|
|
||||||
// For simplicity, we'll just use the config livekit
|
|
||||||
customLivekitUrl.setValue("https://lk.example.org");
|
|
||||||
|
|
||||||
const authCallSpy = vi
|
|
||||||
.spyOn(openIDSFU, "getSFUConfigWithOpenID")
|
|
||||||
.mockResolvedValue(openIdResponse);
|
|
||||||
|
|
||||||
const delayId$ = new BehaviorSubject<string | null>(null);
|
|
||||||
|
|
||||||
const { advertised$, active$ } = createLocalTransport$({
|
|
||||||
scope: testScope(),
|
|
||||||
ownMembershipIdentity: ownMemberMock,
|
|
||||||
roomId: "!example_room_id",
|
|
||||||
// We want multi-sdu
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
|
||||||
delayId$: delayId$,
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
|
||||||
client: {
|
|
||||||
getDomain: () => "example.org",
|
|
||||||
baseUrl: "https://example.org",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
|
||||||
// These won't be called in this error path but satisfy the type
|
|
||||||
getOpenIdToken: vi.fn(),
|
|
||||||
getDeviceId: vi.fn(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const advertisedValues: LivekitTransportConfig[] = [];
|
|
||||||
const activeValues: LocalTransportWithSFUConfig[] = [];
|
|
||||||
advertised$
|
|
||||||
.pipe(filter((v) => v !== null))
|
|
||||||
.subscribe((t) => advertisedValues.push(t));
|
|
||||||
active$
|
|
||||||
.pipe(filter((v) => v !== null))
|
|
||||||
.subscribe((t) => activeValues.push(t));
|
|
||||||
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
// we have now an active and an advertised
|
|
||||||
expect(advertisedValues.length).toEqual(1);
|
|
||||||
expect(activeValues.length).toEqual(1);
|
|
||||||
expect(advertisedValues[0]!.livekit_service_url).toEqual(
|
|
||||||
"https://lk.example.org",
|
|
||||||
);
|
|
||||||
expect(activeValues[0]!.transport.livekit_service_url).toEqual(
|
|
||||||
"https://lk.example.org",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(authCallSpy).toHaveBeenCalledTimes(2);
|
|
||||||
// Now emits 3 new delays id
|
|
||||||
delayId$.next("delay_id_1");
|
|
||||||
await flushPromises();
|
|
||||||
delayId$.next("delay_id_2");
|
|
||||||
await flushPromises();
|
|
||||||
delayId$.next("delay_id_3");
|
|
||||||
await flushPromises();
|
|
||||||
|
|
||||||
// No new emissions should've happened, it is the same transport.
|
|
||||||
expect(advertisedValues.length).toEqual(1);
|
|
||||||
expect(activeValues.length).toEqual(1);
|
|
||||||
|
|
||||||
// Still we should have updated the delayID to auth
|
|
||||||
expect(authCallSpy).toHaveBeenCalledTimes(
|
|
||||||
4 * 2 /* 2 calls for each delayId ?? why */,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(authCallSpy).toHaveBeenLastCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.objectContaining({
|
|
||||||
delayId: "delay_id_3",
|
|
||||||
}),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,14 +10,7 @@ import {
|
|||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import {
|
import { distinctUntilChanged, from, map, of, switchMap } from "rxjs";
|
||||||
combineLatest,
|
|
||||||
distinctUntilChanged,
|
|
||||||
from,
|
|
||||||
map,
|
|
||||||
of,
|
|
||||||
switchMap,
|
|
||||||
} from "rxjs";
|
|
||||||
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
|
|
||||||
@@ -37,6 +30,7 @@ import {
|
|||||||
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
||||||
import { customLivekitUrl } from "../../../settings/settings.ts";
|
import { customLivekitUrl } from "../../../settings/settings.ts";
|
||||||
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
||||||
|
import { type MatrixRTCMode } from "../../../config/ConfigOptions.ts";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
||||||
@@ -46,20 +40,11 @@ interface Props {
|
|||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
memberships$: Behavior<Epoch<CallMembership[]>>;
|
memberships$: Behavior<Epoch<CallMembership[]>>;
|
||||||
client: Pick<
|
client: Pick<MatrixClient, "getDomain" | "_unstable_getRTCTransports"> &
|
||||||
MatrixClient,
|
|
||||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
|
||||||
> &
|
|
||||||
OpenIDClientParts;
|
OpenIDClientParts;
|
||||||
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
||||||
roomId: string;
|
roomId: string;
|
||||||
forceJwtEndpoint: JwtEndpointVersion;
|
matrixRTCMode: MatrixRTCMode;
|
||||||
delayId$: Behavior<string | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum JwtEndpointVersion {
|
|
||||||
Legacy = "legacy",
|
|
||||||
Matrix_2_0 = "matrix_2_0",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO livekit_alias-cleanup
|
// TODO livekit_alias-cleanup
|
||||||
@@ -122,8 +107,7 @@ export const createLocalTransport$ = ({
|
|||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
forceJwtEndpoint,
|
matrixRTCMode,
|
||||||
delayId$,
|
|
||||||
}: Props): LocalTransport => {
|
}: Props): LocalTransport => {
|
||||||
const logger = rootLogger.getChild("[LocalTransport]");
|
const logger = rootLogger.getChild("[LocalTransport]");
|
||||||
|
|
||||||
@@ -138,40 +122,36 @@ export const createLocalTransport$ = ({
|
|||||||
transportDiscovery.discoverPreferredTransport(),
|
transportDiscovery.discoverPreferredTransport(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const preferredConfig$ = customLivekitUrl.value$
|
const preferredConfig$ = customLivekitUrl.value$.pipe(
|
||||||
.pipe(
|
switchMap((customUrl) => {
|
||||||
switchMap((customUrl) => {
|
if (customUrl) {
|
||||||
if (customUrl) {
|
return of({
|
||||||
return of({
|
type: "livekit",
|
||||||
type: "livekit",
|
livekit_service_url: customUrl,
|
||||||
livekit_service_url: customUrl,
|
} as LivekitTransportConfig);
|
||||||
} as LivekitTransportConfig);
|
} else {
|
||||||
} else {
|
return discoveredTransport$;
|
||||||
return discoveredTransport$;
|
}
|
||||||
}
|
}),
|
||||||
}),
|
map((config) => {
|
||||||
)
|
if (!config) {
|
||||||
.pipe(
|
// Bubbled up from the preferredConfig$ observable.
|
||||||
map((config) => {
|
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
|
||||||
if (!config) {
|
}
|
||||||
// Bubbled up from the preferredConfig$ observable.
|
return config;
|
||||||
throw new MatrixRTCTransportMissingError(client.getDomain() ?? "");
|
}),
|
||||||
}
|
distinctUntilChanged(areLivekitTransportsEqual),
|
||||||
return config;
|
);
|
||||||
}),
|
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
|
||||||
);
|
|
||||||
|
|
||||||
const preferredTransport$ = combineLatest([preferredConfig$, delayId$]).pipe(
|
const preferredTransport$ = preferredConfig$.pipe(
|
||||||
switchMap(async ([transport, delayId]) => {
|
switchMap(async (transport) => {
|
||||||
try {
|
try {
|
||||||
return await doOpenIdAndJWTFromUrl(
|
return await doOpenIdAndJWTFromUrl(
|
||||||
transport,
|
transport,
|
||||||
forceJwtEndpoint,
|
matrixRTCMode,
|
||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
roomId,
|
roomId,
|
||||||
client,
|
client,
|
||||||
delayId ?? undefined,
|
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -193,21 +173,7 @@ export const createLocalTransport$ = ({
|
|||||||
),
|
),
|
||||||
null,
|
null,
|
||||||
),
|
),
|
||||||
active$: scope.behavior(
|
active$: scope.behavior(preferredTransport$, null),
|
||||||
preferredTransport$.pipe(
|
|
||||||
// XXX: WORK AROUND due to a reconnection glitch.
|
|
||||||
// To remove when we have a proper way to refresh the delegation event ID without refreshing
|
|
||||||
// the whole credentials.
|
|
||||||
// We deliberately hide any changes to the SFU config because we
|
|
||||||
// do not want the app to reconnect whenever the JWT
|
|
||||||
// token changes due to us delegating a new delayed event. The
|
|
||||||
// initial SFU config for the transport is all the app needs.
|
|
||||||
distinctUntilChanged((prev, next) =>
|
|
||||||
areLivekitTransportsEqual(prev.transport, next.transport),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -219,25 +185,19 @@ export const createLocalTransport$ = ({
|
|||||||
* use we don't want to risk any issues by re-using a token.
|
* use we don't want to risk any issues by re-using a token.
|
||||||
*
|
*
|
||||||
* @param transport The transport to authenticate with.
|
* @param transport The transport to authenticate with.
|
||||||
* @param forceJwtEndpoint Whether to force the JWT endpoint to be used.
|
* @param matrixRTCMode Whether to force the JWT endpoint to be used.
|
||||||
* @param membership The identity of the local member.
|
* @param membership The identity of the local member.
|
||||||
* @param roomId The room ID to use for the JWT.
|
* @param roomId The room ID to use for the JWT.
|
||||||
* @param client The client to use for the OpenID token.
|
* @param client The client to use for the OpenID token.
|
||||||
* @param delayId The delayId to use for the JWT.
|
|
||||||
*
|
*
|
||||||
* @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService
|
* @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService
|
||||||
*/
|
*/
|
||||||
async function doOpenIdAndJWTFromUrl(
|
async function doOpenIdAndJWTFromUrl(
|
||||||
transport: LivekitTransportConfig,
|
transport: LivekitTransportConfig,
|
||||||
forceJwtEndpoint: JwtEndpointVersion,
|
matrixRTCMode: MatrixRTCMode,
|
||||||
membership: CallMembershipIdentityParts,
|
membership: CallMembershipIdentityParts,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
client: Pick<
|
client: Pick<MatrixClient, "_unstable_getRTCTransports"> & OpenIDClientParts,
|
||||||
MatrixClient,
|
|
||||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
|
||||||
> &
|
|
||||||
OpenIDClientParts,
|
|
||||||
delayId?: string,
|
|
||||||
logger?: Logger,
|
logger?: Logger,
|
||||||
): Promise<LocalTransportWithSFUConfig> {
|
): Promise<LocalTransportWithSFUConfig> {
|
||||||
const sfuConfig = await getSFUConfigWithOpenID(
|
const sfuConfig = await getSFUConfigWithOpenID(
|
||||||
@@ -245,11 +205,7 @@ async function doOpenIdAndJWTFromUrl(
|
|||||||
membership,
|
membership,
|
||||||
transport.livekit_service_url,
|
transport.livekit_service_url,
|
||||||
roomId,
|
roomId,
|
||||||
{
|
{ matrixRTCMode },
|
||||||
forceJwtEndpoint: forceJwtEndpoint,
|
|
||||||
delayEndpointBaseUrl: client.baseUrl,
|
|
||||||
delayId,
|
|
||||||
},
|
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -9,9 +9,30 @@ import { expect, test } from "vitest";
|
|||||||
import { type Observable, of, Subject, switchMap } from "rxjs";
|
import { type Observable, of, Subject, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { withTestScheduler } from "./test";
|
import { withTestScheduler } from "./test";
|
||||||
import { filterBehavior, generateItems, pauseWhen } from "./observable";
|
import { or$, filterBehavior, generateItems, pauseWhen } from "./observable";
|
||||||
import { type Behavior } from "../state/Behavior";
|
import { type Behavior } from "../state/Behavior";
|
||||||
|
|
||||||
|
const yesNo = {
|
||||||
|
y: true,
|
||||||
|
n: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
test("or$", () => {
|
||||||
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
|
const input1Marbles = "ny--n--";
|
||||||
|
const input2Marbles = "n-y--n-";
|
||||||
|
const input3Marbles = "n--y--n";
|
||||||
|
const outputMarbles = "nyyyyyn";
|
||||||
|
expectObservable(
|
||||||
|
or$(
|
||||||
|
behavior(input1Marbles, yesNo),
|
||||||
|
behavior(input2Marbles, yesNo),
|
||||||
|
behavior(input3Marbles, yesNo),
|
||||||
|
),
|
||||||
|
).toBe(outputMarbles, yesNo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("pauseWhen", () => {
|
test("pauseWhen", () => {
|
||||||
withTestScheduler(({ behavior, expectObservable }) => {
|
withTestScheduler(({ behavior, expectObservable }) => {
|
||||||
const inputMarbles = " abcdefgh-i-jk-";
|
const inputMarbles = " abcdefgh-i-jk-";
|
||||||
|
|||||||
+4
-38
@@ -114,13 +114,11 @@ export function getValue<T>(state$: Observable<T>): T {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Observable that has a value of true whenever all its inputs are
|
* Creates an Observable that has a value of true whenever some of its inputs
|
||||||
* true.
|
* are true.
|
||||||
*
|
|
||||||
* @public
|
|
||||||
*/
|
*/
|
||||||
export function and$(...inputs: Observable<boolean>[]): Observable<boolean> {
|
export function or$(...inputs: Observable<boolean>[]): Observable<boolean> {
|
||||||
return combineLatest(inputs, (...flags) => flags.every((flag) => flag));
|
return combineLatest(inputs, (...flags) => flags.some((flag) => flag));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -229,38 +227,6 @@ export function filterBehavior<T, S extends T>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maps a changing input value to an item whose lifetime is tied to a certain
|
|
||||||
* computed key. The item may capture some dynamic data from the input.
|
|
||||||
*/
|
|
||||||
export function generateItem<
|
|
||||||
Input,
|
|
||||||
Keys extends [unknown, ...unknown[]],
|
|
||||||
Data,
|
|
||||||
Item,
|
|
||||||
>(
|
|
||||||
name: string,
|
|
||||||
generator: (input: Input) => { keys: readonly [...Keys]; data: Data },
|
|
||||||
factory: (
|
|
||||||
scope: ObservableScope,
|
|
||||||
data$: Behavior<Data>,
|
|
||||||
...keys: Keys
|
|
||||||
) => Item,
|
|
||||||
): OperatorFunction<Input, Item> {
|
|
||||||
return (input$) =>
|
|
||||||
input$.pipe(
|
|
||||||
generateItemsInternal(
|
|
||||||
name,
|
|
||||||
function* (input) {
|
|
||||||
yield generator(input);
|
|
||||||
},
|
|
||||||
factory,
|
|
||||||
(items) => items,
|
|
||||||
),
|
|
||||||
map(([item]) => item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateItemsInternal<
|
function generateItemsInternal<
|
||||||
Input,
|
Input,
|
||||||
Keys extends [unknown, ...unknown[]],
|
Keys extends [unknown, ...unknown[]],
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
setE2EEEnabled: async () => Promise.resolve(),
|
setE2EEEnabled: async () => Promise.resolve(),
|
||||||
}),
|
}),
|
||||||
connectionState$: constant(ConnectionState.Connected),
|
connectionState$: constant(ConnectionState.Connected),
|
||||||
matrixRTCMode$: constant(MatrixRTCMode.Compatibility),
|
matrixRTCMode: MatrixRTCMode.Compatibility,
|
||||||
...callViewModelOptions,
|
...callViewModelOptions,
|
||||||
},
|
},
|
||||||
handRaisedSubject$,
|
handRaisedSubject$,
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ export default defineConfig((env) =>
|
|||||||
data: {
|
data: {
|
||||||
matrix_rtc_session: {
|
matrix_rtc_session: {
|
||||||
wait_for_key_rotation_ms: 5000,
|
wait_for_key_rotation_ms: 5000,
|
||||||
delayed_leave_event_restart_ms: 4000,
|
|
||||||
delayed_leave_event_delay_ms: 18000,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user