Skip to content

Commit

Permalink
Allow configuration of MatrixRTC timers when calling joinRoomSession() (
Browse files Browse the repository at this point in the history
  • Loading branch information
hughns authored Nov 11, 2024
1 parent 6855ace commit 581b320
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 30 deletions.
30 changes: 30 additions & 0 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,36 @@ describe("MatrixRTCSession", () => {
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", () => {
const activeFocusConfig = { type: "livekit", livekit_service_url: "https://active.url" };
const activeFocus = { type: "livekit", focus_selection: "oldest_membership" };
Expand Down
151 changes: 121 additions & 30 deletions src/matrixrtc/MatrixRTCSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,6 @@ import { sleep } from "../utils.ts";

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 getParticipantIdFromMembership = (m: CallMembership): string => getParticipantId(m.sender!, m.deviceId);

Expand Down Expand Up @@ -87,20 +73,80 @@ export type MatrixRTCSessionEventHandlerMap = {
participantId: string,
) => void;
};

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
* media keys for other participants become available.
*/
manageMediaKeys?: boolean;

/** Lets you configure how the events for the session are formatted.
* - legacy: use one event with a membership array.
* - MSC4143: use one event per membership (with only one membership per event)
* More details can be found in MSC4143 and by checking the types:
* `CallMembershipDataLegacy` and `SessionMembershipData`
*/
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.
* This class doesn't deal with media at all, just membership & properties of a session.
Expand All @@ -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.
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;

// 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
// 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
Expand Down Expand Up @@ -320,7 +403,8 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M

this.ownFocusActive = fociActive;
this.ownFociPreferred = fociPreferred;
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME;
this.joinConfig = joinConfig;
this.relativeExpiry = this.membershipExpiryTimeout;
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
this.membershipId = randomString(5);
Expand Down Expand Up @@ -373,6 +457,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.setNewKeyTimeouts.clear();

logger.info(`Leaving call session in room ${this.room.roomId}`);
this.joinConfig = undefined;
this.relativeExpiry = undefined;
this.ownFocusActive = undefined;
this.manageMediaKeys = false;
Expand Down Expand Up @@ -515,7 +600,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.currentEncryptionKeyIndex = encryptionKeyIndex;
}
this.emit(MatrixRTCSessionEvent.EncryptionKeyChanged, keyBin, encryptionKeyIndex, participantId);
}, USE_KEY_DELAY);
}, this.useKeyDelay);
this.setNewKeyTimeouts.add(useKeyTimeout);
} else {
if (userId === this.client.getUserId() && deviceId === this.client.getDeviceId()) {
Expand Down Expand Up @@ -554,11 +639,14 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M

if (
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");
if (this.keysEventUpdateTimeout === undefined) {
this.keysEventUpdateTimeout = setTimeout(this.sendEncryptionKeysEvent, UPDATE_ENCRYPTION_KEY_THROTTLE);
this.keysEventUpdateTimeout = setTimeout(
this.sendEncryptionKeysEvent,
this.updateEncryptionKeyThrottle,
);
}
return;
}
Expand Down Expand Up @@ -799,7 +887,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M

if (anyLeft) {
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) {
logger.debug(`New member(s) have joined: re-sending keys`);
this.requestSendCurrentKey();
Expand Down Expand Up @@ -887,9 +975,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
if (!myPrevMembership) return true;

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
this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME;
this.relativeExpiry! += this.membershipExpiryTimeout;
return true;
}

Expand All @@ -904,7 +992,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
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)
*/
private makeNewLegacyMemberships(
Expand Down Expand Up @@ -1010,7 +1098,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
// nothing to do - reschedule the check again
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
this.memberEventCheckPeriod,
);
return;
}
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
Expand All @@ -1030,7 +1121,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
// check periodically to see if we need to refresh our member event
this.memberEventTimeout = setTimeout(
this.triggerCallMembershipEventUpdate,
MEMBER_EVENT_CHECK_PERIOD,
this.memberEventCheckPeriod,
);
}
} else if (this.isJoined()) {
Expand All @@ -1041,7 +1132,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
this.client._unstable_sendDelayedStateEvent(
this.room.roomId,
{
delay: 8000,
delay: this.membershipServerSideExpiryTimeout,
},
EventType.GroupCallMemberPrefix,
{}, // leave event
Expand Down Expand Up @@ -1108,15 +1199,15 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
}
logger.info("Sent updated call member event.");
} 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}`);
await sleep(resendDelay);
await this.triggerCallMembershipEventUpdate();
}
}

private scheduleDelayDisconnection(): void {
this.memberEventTimeout = setTimeout(this.delayDisconnection, 5000);
this.memberEventTimeout = setTimeout(this.delayDisconnection, this.membershipKeepAlivePeriod);
}

private readonly delayDisconnection = async (): Promise<void> => {
Expand Down

0 comments on commit 581b320

Please sign in to comment.