1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-07 23:02:56 +03:00

Allow configuration of MatrixRTC timers when calling joinRoomSession() (#4510)

This commit is contained in:
Hugh Nimmo-Smith
2024-11-11 15:35:05 +00:00
committed by GitHub
parent 6855ace642
commit 581b3209ab
2 changed files with 151 additions and 30 deletions

View File

@@ -467,6 +467,36 @@ describe("MatrixRTCSession", () => {
jest.useRealTimers(); jest.useRealTimers();
}); });
it("uses membershipExpiryTimeout from join config", async () => {
const realSetTimeout = setTimeout;
jest.useFakeTimers();
sess!.joinRoomSession([mockFocus], mockFocus, { membershipExpiryTimeout: 60000 });
await Promise.race([sentStateEvent, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client.sendStateEvent).toHaveBeenCalledWith(
mockRoom!.roomId,
EventType.GroupCallMemberPrefix,
{
memberships: [
{
application: "m.call",
scope: "m.room",
call_id: "",
device_id: "AAAAAAA",
expires: 60000,
expires_ts: Date.now() + 60000,
foci_active: [mockFocus],
membershipID: expect.stringMatching(".*"),
},
],
},
"@alice:example.org",
);
await Promise.race([sentDelayedState, new Promise((resolve) => realSetTimeout(resolve, 500))]);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(0);
jest.useRealTimers();
});
describe("non-legacy calls", () => { describe("non-legacy calls", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" }; const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" }; const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };

View File

@@ -42,20 +42,6 @@ import { sleep } from "../utils.ts";
const logger = rootLogger.getChild("MatrixRTCSession"); const logger = rootLogger.getChild("MatrixRTCSession");
const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000;
const MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event
const CALL_MEMBER_EVENT_RETRY_DELAY_MIN = 3000;
const UPDATE_ENCRYPTION_KEY_THROTTLE = 3000;
// A delay after a member leaves before we create and publish a new key, because people
// tend to leave calls at the same time
const MAKE_KEY_DELAY = 3000;
// The delay between creating and sending a new key and starting to encrypt with it. This gives others
// a chance to receive the new key to minimise the chance they don't get media they can't decrypt.
// The total time between a member leaving and the call switching to new keys is therefore
// MAKE_KEY_DELAY + SEND_KEY_DELAY
const USE_KEY_DELAY = 5000;
const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`; const getParticipantId = (userId: string, deviceId: string): string => `${userId}:${deviceId}`;
const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId); const getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);
@@ -87,12 +73,15 @@ export type MatrixRTCSessionEventHandlerMap = {
participantId: string, participantId: string,
) => void; ) => void;
}; };
export interface JoinSessionConfig { export interface JoinSessionConfig {
/** If true, generate and share a media key for this participant, /**
* If true, generate and share a media key for this participant,
* and emit MatrixRTCSessionEvent.EncryptionKeyChanged when * and emit MatrixRTCSessionEvent.EncryptionKeyChanged when
* media keys for other participants become available. * media keys for other participants become available.
*/ */
manageMediaKeys?: boolean; manageMediaKeys?: boolean;
/** Lets you configure how the events for the session are formatted. /** Lets you configure how the events for the session are formatted.
* - legacy: use one event with a membership array. * - legacy: use one event with a membership array.
* - MSC4143: use one event per membership (with only one membership per event) * - MSC4143: use one event per membership (with only one membership per event)
@@ -100,7 +89,64 @@ export interface JoinSessionConfig {
* `CallMembershipDataLegacy` and `SessionMembershipData` * `CallMembershipDataLegacy` and `SessionMembershipData`
*/ */
useLegacyMemberEvents?: boolean; useLegacyMemberEvents?: boolean;
/**
* The timeout (in milliseconds) after we joined the call, that our membership should expire
* unless we have explicitly updated it.
*/
membershipExpiryTimeout?: number;
/**
* The period (in milliseconds) with which we check that our membership event still exists on the
* server. If it is not found we create it again.
*/
memberEventCheckPeriod?: number;
/**
* The minimum delay (in milliseconds) after which we will retry sending the membership event if it
* failed to send.
*/
callMemberEventRetryDelayMinimum?: number;
/**
* The jitter (in milliseconds) which is added to callMemberEventRetryDelayMinimum before retrying
* sending the membership event. e.g. if this is set to 1000, then a random delay of between 0 and 1000
* milliseconds will be added.
*/
callMemberEventRetryJitter?: number;
/**
* The minimum time (in milliseconds) between each attempt to send encryption key(s).
* e.g. if this is set to 1000, then we will send at most one key event every second.
*/
updateEncryptionKeyThrottle?: number;
/**
* The delay (in milliseconds) after a member leaves before we create and publish a new key, because people
* tend to leave calls at the same time.
*/
makeKeyDelay?: number;
/**
* The delay (in milliseconds) between creating and sending a new key and starting to encrypt with it. This
* gives other a chance to receive the new key to minimise the chance they don't get media they can't decrypt.
* The total time between a member leaving and the call switching to new keys is therefore:
* makeKeyDelay + useKeyDelay
*/
useKeyDelay?: number;
/**
* The timeout (in milliseconds) after which the server will consider the membership to have expired if it
* has not received a keep-alive from the client.
*/
membershipServerSideExpiryTimeout?: number;
/**
* The period (in milliseconds) that the client will send membership keep-alives to the server.
*/
membershipKeepAlivePeriod?: number;
} }
/** /**
* A MatrixRTCSession manages the membership & properties of a MatrixRTC session. * A MatrixRTCSession manages the membership & properties of a MatrixRTC session.
* This class doesn't deal with media at all, just membership & properties of a session. * This class doesn't deal with media at all, just membership & properties of a session.
@@ -109,10 +155,47 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// The session Id of the call, this is the call_id of the call Member event. // The session Id of the call, this is the call_id of the call Member event.
private _callId: string | undefined; private _callId: string | undefined;
// How many ms after we joined the call, that our membership should expire, or undefined
// if we're not yet joined
private relativeExpiry: number | undefined; private relativeExpiry: number | undefined;
// undefined means not yet joined
private joinConfig?: JoinSessionConfig;
private get membershipExpiryTimeout(): number {
return this.joinConfig?.membershipExpiryTimeout ?? 60 * 60 * 1000;
}
private get memberEventCheckPeriod(): number {
return this.joinConfig?.memberEventCheckPeriod ?? 2 * 60 * 1000;
}
private get callMemberEventRetryDelayMinimum(): number {
return this.joinConfig?.callMemberEventRetryDelayMinimum ?? 3_000;
}
private get updateEncryptionKeyThrottle(): number {
return this.joinConfig?.updateEncryptionKeyThrottle ?? 3_000;
}
private get makeKeyDelay(): number {
return this.joinConfig?.makeKeyDelay ?? 3_000;
}
private get useKeyDelay(): number {
return this.joinConfig?.useKeyDelay ?? 5_000;
}
private get membershipServerSideExpiryTimeout(): number {
return this.joinConfig?.membershipServerSideExpiryTimeout ?? 8_000;
}
private get membershipKeepAlivePeriod(): number {
return this.joinConfig?.membershipKeepAlivePeriod ?? 5_000;
}
private get callMemberEventRetryJitter(): number {
return this.joinConfig?.callMemberEventRetryJitter ?? 2_000;
}
// An identifier for our membership of the call. This will allow us to easily recognise // An identifier for our membership of the call. This will allow us to easily recognise
// whether a membership was sent by this session or is stale from some other time. // whether a membership was sent by this session or is stale from some other time.
// It also forces our membership events to be unique, because otherwise we could try // It also forces our membership events to be unique, because otherwise we could try
@@ -320,7 +403,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.ownFocusActive = fociActive; this.ownFocusActive = fociActive;
this.ownFociPreferred = fociPreferred; this.ownFociPreferred = fociPreferred;
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME; this.joinConfig = joinConfig;
this.relativeExpiry = this.membershipExpiryTimeout;
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys; this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents; this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
this.membershipId = randomString(5); this.membershipId = randomString(5);
@@ -373,6 +457,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.setNewKeyTimeouts.clear(); this.setNewKeyTimeouts.clear();
logger.info(`Leaving call session in room ${this.room.roomId}`); logger.info(`Leaving call session in room ${this.room.roomId}`);
this.joinConfig = undefined;
this.relativeExpiry = undefined; this.relativeExpiry = undefined;
this.ownFocusActive = undefined; this.ownFocusActive = undefined;
this.manageMediaKeys = false; this.manageMediaKeys = false;
@@ -515,7 +600,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.currentEncryptionKeyIndex = encryptionKeyIndex; this.currentEncryptionKeyIndex = encryptionKeyIndex;
} }
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId); this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
}, USE_KEY_DELAY); }, this.useKeyDelay);
this.setNewKeyTimeouts.add(useKeyTimeout); this.setNewKeyTimeouts.add(useKeyTimeout);
} else { } else {
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) { if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
@@ -554,11 +639,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if ( if (
this.lastEncryptionKeyUpdateRequest && this.lastEncryptionKeyUpdateRequest &&
this.lastEncryptionKeyUpdateRequest + UPDATE_ENCRYPTION_KEY_THROTTLE > Date.now() this.lastEncryptionKeyUpdateRequest + this.updateEncryptionKeyThrottle > Date.now()
) { ) {
logger.info("Last encryption key event sent too recently: postponing"); logger.info("Last encryption key event sent too recently: postponing");
if (this.keysEventUpdateTimeout === undefined) { if (this.keysEventUpdateTimeout === undefined) {
this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, UPDATE_ENCRYPTION_KEY_THROTTLE); this.keysEventUpdateTimeout = setTimeout(
this.sendEncryptionKeysEvent,
this.updateEncryptionKeyThrottle,
);
} }
return; return;
} }
@@ -799,7 +887,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (anyLeft) { if (anyLeft) {
logger.debug(`Member(s) have left: queueing sender key rotation`); logger.debug(`Member(s) have left: queueing sender key rotation`);
this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, MAKE_KEY_DELAY); this.makeNewKeyTimeout = setTimeout(this.onRotateKeyTimeout, this.makeKeyDelay);
} else if (anyJoined) { } else if (anyJoined) {
logger.debug(`New member(s) have joined: re-sending keys`); logger.debug(`New member(s) have joined: re-sending keys`);
this.requestSendCurrentKey(); this.requestSendCurrentKey();
@@ -887,9 +975,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (!myPrevMembership) return true; if (!myPrevMembership) return true;
const expiryTime = myPrevMembership.getMsUntilExpiry(); const expiryTime = myPrevMembership.getMsUntilExpiry();
if (expiryTime !== undefined && expiryTime < MEMBERSHIP_EXPIRY_TIME / 2) { if (expiryTime !== undefined && expiryTime < this.membershipExpiryTimeout / 2) {
// ...or if the expiry time needs bumping // ...or if the expiry time needs bumping
this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME; this.relativeExpiry! += this.membershipExpiryTimeout;
return true; return true;
} }
@@ -904,7 +992,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
return {}; return {};
} }
/** /**
* Makes a new membership list given the old list alonng with this user's previous membership event * Makes a new membership list given the old list along with this user's previous membership event
* (if any) and this device's previous membership (if any) * (if any) and this device's previous membership (if any)
*/ */
private makeNewLegacyMemberships( private makeNewLegacyMemberships(
@@ -1010,7 +1098,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
} }
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) { if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
// nothing to do - reschedule the check again // nothing to do - reschedule the check again
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD); this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
this.memberEventCheckPeriod,
);
return; return;
} }
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership); newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
@@ -1030,7 +1121,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// check periodically to see if we need to refresh our member event // check periodically to see if we need to refresh our member event
this.memberEventTimeout = setTimeout( this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate, this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD, this.memberEventCheckPeriod,
); );
} }
} else if (this.isJoined()) { } else if (this.isJoined()) {
@@ -1041,7 +1132,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.client._unstable_sendDelayedStateEvent( this.client._unstable_sendDelayedStateEvent(
this.room.roomId, this.room.roomId,
{ {
delay: 8000, delay: this.membershipServerSideExpiryTimeout,
}, },
EventType.GroupCallMemberPrefix, EventType.GroupCallMemberPrefix,
{}, // leave event {}, // leave event
@@ -1108,7 +1199,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
} }
logger.info("Sent updated call member event."); logger.info("Sent updated call member event.");
} catch (e) { } catch (e) {
const resendDelay = CALL_MEMBER_EVENT_RETRY_DELAY_MIN + Math.random() * 2000; const resendDelay = this.callMemberEventRetryDelayMinimum + Math.random() * this.callMemberEventRetryJitter;
logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`); logger.warn(`Failed to send call member event (retrying in ${resendDelay}): ${e}`);
await sleep(resendDelay); await sleep(resendDelay);
await this.triggerCallMembershipEventUpdate(); await this.triggerCallMembershipEventUpdate();
@@ -1116,7 +1207,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
} }
private scheduleDelayDisconnection(): void { private scheduleDelayDisconnection(): void {
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000); this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod);
} }
private readonly delayDisconnection = async (): Promise<void> => { private readonly delayDisconnection = async (): Promise<void> => {