1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-23 17:02:25 +03:00

Allow multiple rtc sessions per room (with different sessionDescriptions) (#4945)

* Introduce sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* Make sessionDescription part of a MatrixRTCSession

Signed-off-by: Timo K <toger5@hotmail.de>

* Make session manager only menage session for one sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* make membership manager aware about session (application + id)
Before this was just hardcoded to a call session

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests

Signed-off-by: Timo K <toger5@hotmail.de>

* fix doc comments

Signed-off-by: Timo K <toger5@hotmail.de>

* Make fields private,  improve comments, improve whitespace, don't use deprecated fields

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for other application end event

Signed-off-by: Timo K <toger5@hotmail.de>

* rename call -> session

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests

Signed-off-by: Timo K <toger5@hotmail.de>

* remove id check since its already part of `deepCompare(membership.sessionDescription, sessionDescription)`

Signed-off-by: Timo K <toger5@hotmail.de>

* remove scope related tests. The id should be the only thing that scopes sessions. everything else is application (session type) specific

Signed-off-by: Timo K <toger5@hotmail.de>

* review

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for custom sessionDescription

Signed-off-by: Timo K <toger5@hotmail.de>

* callMembershipsForRoom to default to call

Signed-off-by: Timo K <toger5@hotmail.de>

* roomSessionForRoom backwards compatible (And deprecate the call specific method)

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
This commit is contained in:
Timo
2025-08-19 13:58:57 +02:00
committed by GitHub
parent 2b46579bd8
commit 2abf7ca795
7 changed files with 263 additions and 114 deletions

View File

@@ -26,6 +26,8 @@ const mockFocus = { type: "mock" };
const textEncoder = new TextEncoder();
const callSession = { id: "", application: "m.call" };
describe("MatrixRTCSession", () => {
let client: MatrixClient;
let sess: MatrixRTCSession | undefined;
@@ -49,14 +51,14 @@ describe("MatrixRTCSession", () => {
it("creates a room-scoped session from room state", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].callId).toEqual("");
expect(sess?.memberships[0].sessionDescription.id).toEqual("");
expect(sess?.memberships[0].scope).toEqual("m.room");
expect(sess?.memberships[0].application).toEqual("m.call");
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
expect(sess?.memberships[0].isExpired()).toEqual(false);
expect(sess?.callId).toEqual("");
expect(sess?.sessionDescription.id).toEqual("");
});
it("ignores memberships where application is not m.call", () => {
@@ -64,7 +66,7 @@ describe("MatrixRTCSession", () => {
application: "not-m.call",
});
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships).toHaveLength(0);
});
@@ -74,7 +76,7 @@ describe("MatrixRTCSession", () => {
scope: "m.room",
});
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships).toHaveLength(0);
});
@@ -86,7 +88,7 @@ describe("MatrixRTCSession", () => {
const mockRoom = makeMockRoom([membershipTemplate, expiredMembership]);
jest.advanceTimersByTime(2000);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].deviceId).toEqual("AAAAAAA");
jest.useRealTimers();
@@ -95,7 +97,7 @@ describe("MatrixRTCSession", () => {
it("ignores memberships events of members not in the room", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
mockRoom.hasMembershipState = (state) => state === KnownMembership.Join;
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships.length).toEqual(0);
});
@@ -106,14 +108,14 @@ describe("MatrixRTCSession", () => {
expiredMembership.created_ts = 500;
expiredMembership.expires = 1000;
const mockRoom = makeMockRoom([expiredMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships[0].getAbsoluteExpiry()).toEqual(1500);
jest.useRealTimers();
});
it("returns empty session if no membership events are present", () => {
const mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships).toHaveLength(0);
});
@@ -148,7 +150,7 @@ describe("MatrixRTCSession", () => {
}),
}),
};
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
expect(sess.memberships).toHaveLength(0);
});
@@ -183,7 +185,7 @@ describe("MatrixRTCSession", () => {
}),
}),
};
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom as unknown as Room);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom as unknown as Room, callSession);
expect(sess.memberships).toHaveLength(0);
});
@@ -191,7 +193,7 @@ describe("MatrixRTCSession", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.device_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
const sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
const sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess.memberships).toHaveLength(0);
});
@@ -199,23 +201,7 @@ describe("MatrixRTCSession", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.call_id as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores memberships with no scope", () => {
const testMembership = Object.assign({}, membershipTemplate);
(testMembership.scope as string | undefined) = undefined;
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
expect(sess.memberships).toHaveLength(0);
});
it("ignores anything that's not a room-scoped call (for now)", () => {
const testMembership = Object.assign({}, membershipTemplate);
testMembership.scope = "m.user";
const mockRoom = makeMockRoom([testMembership]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess.memberships).toHaveLength(0);
});
});
@@ -230,7 +216,7 @@ describe("MatrixRTCSession", () => {
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess.getOldestMembership()!.deviceId).toEqual("old");
jest.useRealTimers();
});
@@ -255,7 +241,7 @@ describe("MatrixRTCSession", () => {
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
type: "livekit",
@@ -275,7 +261,7 @@ describe("MatrixRTCSession", () => {
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
type: "livekit",
@@ -302,7 +288,7 @@ describe("MatrixRTCSession", () => {
client._unstable_updateDelayedEvent = jest.fn();
mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
});
afterEach(async () => {
@@ -385,7 +371,7 @@ describe("MatrixRTCSession", () => {
describe("onMembershipsChanged", () => {
it("does not emit if no membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
@@ -396,7 +382,7 @@ describe("MatrixRTCSession", () => {
it("emits on membership changes", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
const onMembershipsChanged = jest.fn();
sess.on(MatrixRTCSessionEvent.MembershipsChanged, onMembershipsChanged);
@@ -451,7 +437,7 @@ describe("MatrixRTCSession", () => {
client.encryptAndSendToDevice = sendToDeviceMock;
mockRoom = makeMockRoom([]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
});
afterEach(async () => {
@@ -570,7 +556,7 @@ describe("MatrixRTCSession", () => {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
// joining will trigger an initial key send
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
@@ -619,7 +605,7 @@ describe("MatrixRTCSession", () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
const keysSentPromise1 = new Promise<EncryptionKeysEventContent>((resolve) => {
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
@@ -670,7 +656,7 @@ describe("MatrixRTCSession", () => {
const mockRoom = makeMockRoom([member1, member2]);
mockRoomState(mockRoom, [member1, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
@@ -715,7 +701,7 @@ describe("MatrixRTCSession", () => {
};
const mockRoom = makeMockRoom([member1, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
await keysSentPromise1;
@@ -779,7 +765,7 @@ describe("MatrixRTCSession", () => {
device_id: "BBBBBBB",
});
const mockRoom = makeMockRoom([membershipTemplate, member2]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
const onMyEncryptionKeyChanged = jest.fn();
sess.on(
@@ -869,7 +855,7 @@ describe("MatrixRTCSession", () => {
if (i === 0) {
// if first time around then set up the session
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
} else {
// otherwise update the state reducing the membership each time in order to trigger key rotation
@@ -895,7 +881,7 @@ describe("MatrixRTCSession", () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
const keysSentPromise1 = new Promise((resolve) => {
sendEventMock.mockImplementation(resolve);
@@ -936,7 +922,7 @@ describe("MatrixRTCSession", () => {
});
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, {
manageMediaKeys: true,
@@ -959,7 +945,7 @@ describe("MatrixRTCSession", () => {
describe("receiving", () => {
it("collects keys from encryption events", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
@@ -984,7 +970,7 @@ describe("MatrixRTCSession", () => {
it("collects keys at non-zero indices", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
@@ -1010,7 +996,7 @@ describe("MatrixRTCSession", () => {
it("collects keys by merging", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
makeMockEvent("io.element.call.encryption_keys", "@bob:example.org", "1234roomId", {
@@ -1061,7 +1047,7 @@ describe("MatrixRTCSession", () => {
it("ignores older keys at same index", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
makeMockEvent(
@@ -1120,7 +1106,7 @@ describe("MatrixRTCSession", () => {
it("key timestamps are treated as monotonic", async () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
makeMockEvent(
@@ -1164,7 +1150,7 @@ describe("MatrixRTCSession", () => {
it("ignores keys event for the local participant", () => {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
mockRoom.emitTimelineEvent(
@@ -1187,7 +1173,7 @@ describe("MatrixRTCSession", () => {
jest.useFakeTimers();
try {
const mockRoom = makeMockRoom([membershipTemplate]);
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
// defaults to getTs()
jest.setSystemTime(1000);

View File

@@ -16,8 +16,9 @@ limitations under the License.
import { ClientEvent, EventTimeline, MatrixClient } from "../../../src";
import { RoomStateEvent } from "../../../src/models/room-state";
import { MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { MatrixRTCSessionManager, MatrixRTCSessionManagerEvents } from "../../../src/matrixrtc/MatrixRTCSessionManager";
import { makeMockRoom, membershipTemplate, mockRoomState } from "./mocks";
import { logger } from "../../../src/logger";
describe("MatrixRTCSessionManager", () => {
let client: MatrixClient;
@@ -47,6 +48,21 @@ describe("MatrixRTCSessionManager", () => {
}
});
it("Doesn't fire event if unrelated sessions starts", () => {
const onStarted = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
}
});
it("Fires event when session ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
@@ -59,9 +75,75 @@ describe("MatrixRTCSessionManager", () => {
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("")[0];
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
it("Fires correctly with for with custom sessionDescription", () => {
const onStarted = jest.fn();
const onEnded = jest.fn();
// create a session manager with a custom session description
const sessionManager = new MatrixRTCSessionManager(logger, client, { id: "test", application: "m.notCall" });
// manually start the session manager (its not the default one started by the client)
sessionManager.start();
sessionManager.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
sessionManager.on(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
try {
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
client.emit(ClientEvent.Room, room1);
expect(onStarted).not.toHaveBeenCalled();
onStarted.mockClear();
const room2 = makeMockRoom([{ ...membershipTemplate, application: "m.notCall", call_id: "test" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1, room2]);
client.emit(ClientEvent.Room, room2);
expect(onStarted).toHaveBeenCalled();
onStarted.mockClear();
mockRoomState(room2, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room2);
const roomState = room2.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).toHaveBeenCalled();
onEnded.mockClear();
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
const roomStateOther = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEventOther = roomStateOther.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEventOther, roomStateOther, null);
expect(onEnded).not.toHaveBeenCalled();
} finally {
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionStarted, onStarted);
client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
}
});
it("Doesn't fire event if unrelated sessions ends", () => {
const onEnded = jest.fn();
client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, onEnded);
const room1 = makeMockRoom([{ ...membershipTemplate, application: "m.other_app" }]);
jest.spyOn(client, "getRooms").mockReturnValue([room1]);
jest.spyOn(client, "getRoom").mockReturnValue(room1);
client.emit(ClientEvent.Room, room1);
mockRoomState(room1, [{ user_id: membershipTemplate.user_id }]);
const roomState = room1.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
const membEvent = roomState.getStateEvents("org.matrix.msc3401.call.member")[0];
client.emit(RoomStateEvent.Events, membEvent, roomState, null);
expect(onEnded).not.toHaveBeenCalledWith(room1.roomId, client.matrixRTC.getActiveRoomSession(room1));
});
});

View File

@@ -64,6 +64,8 @@ function createAsyncHandle<T>(method: MockedFunction<any>) {
return { reject, resolve };
}
const callSession = { id: "", application: "m.call" };
describe("MembershipManager", () => {
let client: MockClient;
let room: Room;
@@ -95,12 +97,12 @@ describe("MembershipManager", () => {
describe("isActivated()", () => {
it("defaults to false", () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
expect(manager.isActivated()).toEqual(false);
});
it("returns true after join()", () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([]);
expect(manager.isActivated()).toEqual(true);
});
@@ -114,7 +116,7 @@ describe("MembershipManager", () => {
const updateDelayedEventHandle = createAsyncHandle<void>(client._unstable_updateDelayedEvent as Mock);
// Test
const memberManager = new MembershipManager(undefined, room, client, () => undefined);
const memberManager = new MembershipManager(undefined, room, client, () => undefined, callSession);
memberManager.join([focus], focusActive);
// expects
await waitForMockCall(client.sendStateEvent, Promise.resolve({ event_id: "id" }));
@@ -130,7 +132,7 @@ describe("MembershipManager", () => {
focus_active: focusActive,
scope: "m.room",
},
"_@alice:example.org_AAAAAAA",
"_@alice:example.org_AAAAAAA_m.call",
);
updateDelayedEventHandle.resolve?.();
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
@@ -138,13 +140,13 @@ describe("MembershipManager", () => {
{ delay: 8000 },
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA",
"_@alice:example.org_AAAAAAA_m.call",
);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
});
it("reschedules delayed leave event if sending state cancels it", async () => {
const memberManager = new MembershipManager(undefined, room, client, () => undefined);
const memberManager = new MembershipManager(undefined, room, client, () => undefined, callSession);
const waitForSendState = waitForMockCall(client.sendStateEvent);
const waitForUpdateDelaye = waitForMockCallOnce(
client._unstable_updateDelayedEvent,
@@ -189,7 +191,7 @@ describe("MembershipManager", () => {
});
});
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA`;
const userStateKey = `${!useOwnedStateEvents ? "_" : ""}@alice:example.org_AAAAAAA_m.call`;
// preparing the delayed disconnect should handle ratelimiting
const sendDelayedStateAttempt = new Promise<void>((resolve) => {
const error = new MatrixError({ errcode: "M_LIMIT_EXCEEDED" });
@@ -220,6 +222,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
callSession,
);
manager.join([focus], focusActive);
@@ -276,7 +279,7 @@ describe("MembershipManager", () => {
describe("delayed leave event", () => {
it("does not try again to schedule a delayed leave event if not supported", () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
delayedHandle.reject?.(
new UnsupportedDelayedEventsEndpointError(
@@ -288,7 +291,7 @@ describe("MembershipManager", () => {
});
it("does try to schedule a delayed leave event again if rate limited", async () => {
const delayedHandle = createAsyncHandle(client._unstable_sendDelayedStateEvent as Mock);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
delayedHandle.reject?.(new HTTPError("rate limited", 429, undefined));
await jest.advanceTimersByTimeAsync(5000);
@@ -300,6 +303,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
callSession,
);
manager.join([focus], focusActive);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledWith(
@@ -307,7 +311,7 @@ describe("MembershipManager", () => {
{ delay: 123456 },
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA",
"_@alice:example.org_AAAAAAA_m.call",
);
});
});
@@ -319,6 +323,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
callSession,
);
// Join with the membership manager
manager.join([focus], focusActive);
@@ -351,7 +356,13 @@ describe("MembershipManager", () => {
});
it("uses membershipEventExpiryMs from config", async () => {
const manager = new MembershipManager({ membershipEventExpiryMs: 1234567 }, room, client, () => undefined);
const manager = new MembershipManager(
{ membershipEventExpiryMs: 1234567 },
room,
client,
() => undefined,
callSession,
);
manager.join([focus], focusActive);
await waitForMockCall(client.sendStateEvent);
@@ -370,12 +381,12 @@ describe("MembershipManager", () => {
type: "livekit",
},
},
"_@alice:example.org_AAAAAAA",
"_@alice:example.org_AAAAAAA_m.call",
);
});
it("does nothing if join called when already joined", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await waitForMockCall(client.sendStateEvent);
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
@@ -387,7 +398,7 @@ describe("MembershipManager", () => {
describe("leave()", () => {
// TODO add rate limit cases.
it("resolves delayed leave event when leave is called", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
await manager.leave();
@@ -395,7 +406,7 @@ describe("MembershipManager", () => {
expect(client.sendStateEvent).toHaveBeenCalled();
});
it("send leave event when leave is called and resolving delayed leave fails", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
(client._unstable_updateDelayedEvent as Mock<any>).mockRejectedValue("unknown");
@@ -406,11 +417,11 @@ describe("MembershipManager", () => {
room.roomId,
"org.matrix.msc3401.call.member",
{},
"_@alice:example.org_AAAAAAA",
"_@alice:example.org_AAAAAAA_m.call",
);
});
it("does nothing if not joined", () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
expect(async () => await manager.leave()).not.toThrow();
expect(client._unstable_sendDelayedStateEvent).not.toHaveBeenCalled();
expect(client.sendStateEvent).not.toHaveBeenCalled();
@@ -420,7 +431,7 @@ describe("MembershipManager", () => {
describe("getsActiveFocus", () => {
it("gets the correct active focus with oldest_membership", () => {
const getOldestMembership = jest.fn();
const manager = new MembershipManager({}, room, client, getOldestMembership);
const manager = new MembershipManager({}, room, client, getOldestMembership, callSession);
// Before joining the active focus should be undefined (see FocusInUse on MatrixRTCSession)
expect(manager.getActiveFocus()).toBe(undefined);
manager.join([focus], focusActive);
@@ -455,7 +466,7 @@ describe("MembershipManager", () => {
});
it("does not provide focus if the selection method is unknown", () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], Object.assign(focusActive, { type: "unknown_type" }));
expect(manager.getActiveFocus()).toBe(undefined);
});
@@ -463,7 +474,7 @@ describe("MembershipManager", () => {
describe("onRTCSessionMemberUpdate()", () => {
it("does nothing if not joined", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
await manager.onRTCSessionMemberUpdate([mockCallMembership(membershipTemplate, room.roomId)]);
await jest.advanceTimersToNextTimerAsync();
expect(client.sendStateEvent).not.toHaveBeenCalled();
@@ -471,7 +482,7 @@ describe("MembershipManager", () => {
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
});
it("does nothing if own membership still present", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
const myMembership = (client.sendStateEvent as Mock).mock.calls[0][2];
@@ -495,7 +506,7 @@ describe("MembershipManager", () => {
expect(client._unstable_updateDelayedEvent).not.toHaveBeenCalled();
});
it("recreates membership if it is missing", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
@@ -513,7 +524,7 @@ describe("MembershipManager", () => {
});
it("updates the UpdateExpiry entry in the action scheduler", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
// clearing all mocks before checking what happens when calling: `onRTCSessionMemberUpdate`
@@ -547,6 +558,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
{ id: "", application: "m.call" },
);
manager.join([focus], focusActive);
await jest.advanceTimersByTimeAsync(1);
@@ -578,6 +590,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
{ id: "", application: "m.call" },
);
manager.join([focus], focusActive);
await waitForMockCall(client.sendStateEvent);
@@ -600,14 +613,14 @@ describe("MembershipManager", () => {
});
describe("status updates", () => {
it("starts 'Disconnected'", () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
expect(manager.status).toBe(Status.Disconnected);
});
it("emits 'Connection' and 'Connected' after join", async () => {
const handleDelayedEvent = createAsyncHandle<void>(client._unstable_sendDelayedStateEvent);
const handleStateEvent = createAsyncHandle<void>(client.sendStateEvent);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
expect(manager.status).toBe(Status.Disconnected);
const connectEmit = jest.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
@@ -621,7 +634,7 @@ describe("MembershipManager", () => {
expect(connectEmit).toHaveBeenCalledWith(Status.Connecting, Status.Connected);
});
it("emits 'Disconnecting' and 'Disconnected' after leave", async () => {
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
const connectEmit = jest.fn();
manager.on(MembershipManagerEvent.StatusChanged, connectEmit);
manager.join([focus], focusActive);
@@ -637,7 +650,7 @@ describe("MembershipManager", () => {
it("sends retry if call membership event is still valid at time of retry", async () => {
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
expect(client._unstable_sendDelayedStateEvent).toHaveBeenCalledTimes(1);
@@ -664,7 +677,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }),
),
);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
// Should call _unstable_sendDelayedStateEvent but not sendStateEvent because of the
// RateLimit error.
manager.join([focus], focusActive);
@@ -684,7 +697,7 @@ describe("MembershipManager", () => {
it("abandons retry loop if leave() was called before sending state event", async () => {
const handle = createAsyncHandle(client._unstable_sendDelayedStateEvent);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
handle.reject?.(
new MatrixError(
@@ -719,7 +732,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }),
),
);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive);
// Hit rate limit
@@ -752,7 +765,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "2" }),
),
);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive, delayEventSendError);
for (let i = 0; i < 10; i++) {
@@ -772,7 +785,7 @@ describe("MembershipManager", () => {
new Headers({ "Retry-After": "1" }),
),
);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive, delayEventRestartError);
for (let i = 0; i < 10; i++) {
@@ -783,7 +796,7 @@ describe("MembershipManager", () => {
it("falls back to using pure state events when some error occurs while sending delayed events", async () => {
const unrecoverableError = jest.fn();
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(new HTTPError("unknown", 601));
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive, unrecoverableError);
await waitForMockCall(client.sendStateEvent);
expect(unrecoverableError).not.toHaveBeenCalledWith();
@@ -797,6 +810,7 @@ describe("MembershipManager", () => {
room,
client,
() => undefined,
callSession,
);
manager.join([focus], focusActive, unrecoverableError);
for (let retries = 0; retries < 7; retries++) {
@@ -814,7 +828,7 @@ describe("MembershipManager", () => {
(client._unstable_sendDelayedStateEvent as Mock<any>).mockRejectedValue(
new UnsupportedDelayedEventsEndpointError("not supported", "sendDelayedStateEvent"),
);
const manager = new MembershipManager({}, room, client, () => undefined);
const manager = new MembershipManager({}, room, client, () => undefined, callSession);
manager.join([focus], focusActive, unrecoverableError);
await jest.advanceTimersByTimeAsync(1);
@@ -828,7 +842,7 @@ it("Should prefix log with MembershipManager used", () => {
const client = makeMockClient("@alice:example.org", "AAAAAAA");
const room = makeMockRoom([membershipTemplate]);
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, logger);
const membershipManager = new MembershipManager(undefined, room, client, () => undefined, callSession, logger);
const spy = jest.spyOn(console, "error");
// Double join

View File

@@ -18,6 +18,7 @@ import { type MatrixEvent } from "../matrix.ts";
import { deepCompare } from "../utils.ts";
import { type Focus } from "./focus.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
import { type SessionDescription } from "./MatrixRTCSession.ts";
/**
* The default duration in milliseconds that a membership is considered valid for.
@@ -130,6 +131,9 @@ export class CallMembership {
return this.parentEvent.getId();
}
/**
* @deprecated Use sessionDescription.id instead.
*/
public get callId(): string {
return this.membershipData.call_id;
}
@@ -138,6 +142,13 @@ export class CallMembership {
return this.membershipData.device_id;
}
public get sessionDescription(): SessionDescription {
return {
application: this.membershipData.application,
id: this.membershipData.call_id,
};
}
public get application(): string | undefined {
return this.membershipData.application;
}

View File

@@ -26,7 +26,7 @@ import { type Focus } from "./focus.ts";
import { KnownMembership } from "../@types/membership.ts";
import { MembershipManager } from "./MembershipManager.ts";
import { EncryptionManager, type IEncryptionManager } from "./EncryptionManager.ts";
import { logDurationSync } from "../utils.ts";
import { deepCompare, logDurationSync } from "../utils.ts";
import { type Statistics, type RTCNotificationType } from "./types.ts";
import { RoomKeyTransport } from "./RoomKeyTransport.ts";
import type { IMembershipManager } from "./IMembershipManager.ts";
@@ -74,6 +74,14 @@ export interface SessionConfig {
notificationType?: RTCNotificationType;
}
/**
* The session description is used to identify a session. Used in the state event.
*/
export interface SessionDescription {
id: string;
application: string;
}
// The names follow these principles:
// - we use the technical term delay if the option is related to delayed events.
// - we use delayedLeaveEvent if the option is related to the delayed leave event.
@@ -86,7 +94,7 @@ export interface MembershipConfig {
* Use the new Manager.
*
* Default: `false`.
* @deprecated does nothing anymore we always default to the new memberhip manager.
* @deprecated does nothing anymore we always default to the new membership manager.
*/
useNewMembershipManager?: boolean;
@@ -254,10 +262,27 @@ export class MatrixRTCSession extends TypedEventEmitter<
}
/**
* Returns all the call memberships for a room, oldest first
* Returns all the call memberships for a room that match the provided `sessionDescription`,
* oldest first.
*
* @deprecated Use `MatrixRTCSession.sessionMembershipsForRoom` instead.
*/
public static callMembershipsForRoom(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
): CallMembership[] {
return MatrixRTCSession.sessionMembershipsForRoom(room, {
id: "",
application: "m.call",
});
}
/**
* Returns all the call memberships for a room that match the provided `sessionDescription`,
* oldest first.
*/
public static sessionMembershipsForRoom(
room: Pick<Room, "getLiveTimeline" | "roomId" | "hasMembershipState">,
sessionDescription: SessionDescription,
): CallMembership[] {
const logger = rootLogger.getChild(`[MatrixRTCSession ${room.roomId}]`);
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS);
@@ -290,15 +315,10 @@ export class MatrixRTCSession extends TypedEventEmitter<
try {
const membership = new CallMembership(memberEvent, membershipData);
if (membership.application !== "m.call") {
// Only process MatrixRTC sessions associated with calls
logger.info("Skipping non-call MatrixRTC session");
continue;
}
if (membership.callId !== "" || membership.scope !== "m.room") {
// for now, just ignore anything that isn't a room scope call
logger.info(`Ignoring user-scoped call`);
if (!deepCompare(membership.sessionDescription, sessionDescription)) {
logger.info(
`Ignoring membership of user ${membership.sender} for a different session: ${JSON.stringify(membership.sessionDescription)}`,
);
continue;
}
@@ -329,12 +349,33 @@ export class MatrixRTCSession extends TypedEventEmitter<
}
/**
* Return the MatrixRTC session for the room, whether there are currently active members or not
* Return the MatrixRTC session for the room.
* This returned session can be used to find out if there are active room call sessions
* for the requested room.
*
* This method is an alias for `MatrixRTCSession.sessionForRoom` with
* sessionDescription `{ id: "", application: "m.call" }`.
*
* @deprecated Use `MatrixRTCSession.sessionForRoom` with sessionDescription `{ id: "", application: "m.call" }` instead.
*/
public static roomSessionForRoom(client: MatrixClient, room: Room): MatrixRTCSession {
const callMemberships = MatrixRTCSession.callMembershipsForRoom(room);
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, { id: "", application: "m.call" });
return new MatrixRTCSession(client, room, callMemberships, { id: "", application: "m.call" });
}
return new MatrixRTCSession(client, room, callMemberships);
/**
* Return the MatrixRTC session for the room.
* This returned session can be used to find out if there are active sessions
* for the requested room and `sessionDescription`.
*/
public static sessionForRoom(
client: MatrixClient,
room: Room,
sessionDescription: SessionDescription,
): MatrixRTCSession {
const callMemberships = MatrixRTCSession.sessionMembershipsForRoom(room, sessionDescription);
return new MatrixRTCSession(client, room, callMemberships, sessionDescription);
}
/**
@@ -379,10 +420,15 @@ export class MatrixRTCSession extends TypedEventEmitter<
"getLiveTimeline" | "roomId" | "getVersion" | "hasMembershipState" | "on" | "off"
>,
public memberships: CallMembership[],
/**
* The session description is used to define the exact session this object is tracking.
* A session is distinct from another session if one of those properties differ: `roomSubset.roomId`, `sessionDescription.application`, `sessionDescription.id`.
*/
public readonly sessionDescription: SessionDescription,
) {
super();
this.logger = rootLogger.getChild(`[MatrixRTCSession ${roomSubset.roomId}]`);
this._callId = memberships[0]?.callId;
this._callId = memberships[0]?.sessionDescription.id;
const roomState = this.roomSubset.getLiveTimeline().getState(EventTimeline.FORWARDS);
// TODO: double check if this is actually needed. Should be covered by refreshRoom in MatrixRTCSessionManager
roomState?.on(RoomStateEvent.Members, this.onRoomMemberUpdate);
@@ -440,6 +486,7 @@ export class MatrixRTCSession extends TypedEventEmitter<
this.roomSubset,
this.client,
() => this.getOldestMembership(),
this.sessionDescription,
this.logger,
);
@@ -648,9 +695,9 @@ export class MatrixRTCSession extends TypedEventEmitter<
*/
private recalculateSessionMembers = (): void => {
const oldMemberships = this.memberships;
this.memberships = MatrixRTCSession.callMembershipsForRoom(this.room);
this.memberships = MatrixRTCSession.sessionMembershipsForRoom(this.room, this.sessionDescription);
this._callId = this._callId ?? this.memberships[0]?.callId;
this._callId = this._callId ?? this.memberships[0]?.sessionDescription.id;
const changed =
oldMemberships.length != this.memberships.length ||

View File

@@ -20,7 +20,7 @@ import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import { type Room } from "../models/room.ts";
import { type RoomState, RoomStateEvent } from "../models/room-state.ts";
import { type MatrixEvent } from "../models/event.ts";
import { MatrixRTCSession } from "./MatrixRTCSession.ts";
import { MatrixRTCSession, type SessionDescription } from "./MatrixRTCSession.ts";
import { EventType } from "../@types/event.ts";
export enum MatrixRTCSessionManagerEvents {
@@ -37,6 +37,9 @@ type EventHandlerMap = {
/**
* Holds all active MatrixRTC session objects and creates new ones as events arrive.
* One `MatrixRTCSessionManager` is required for each MatrixRTC sessionDescription (application, session id) that the client wants to support.
* If no application type is specified in the constructor, the default is "m.call".
*
* This interface is UNSTABLE and may change without warning.
*/
export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionManagerEvents, EventHandlerMap> {
@@ -53,6 +56,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
public constructor(
rootLogger: Logger,
private client: MatrixClient,
private readonly sessionDescription: SessionDescription = { id: "", application: "m.call" }, // Default to the Matrix Call application
) {
super();
this.logger = rootLogger.getChild("[MatrixRTCSessionManager]");
@@ -62,7 +66,7 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
// We shouldn't need to null-check here, but matrix-client.spec.ts mocks getRooms
// returning nothing, and breaks tests if you change it to return an empty array :'(
for (const room of this.client.getRooms() ?? []) {
const session = MatrixRTCSession.roomSessionForRoom(this.client, room);
const session = MatrixRTCSession.sessionForRoom(this.client, room, this.sessionDescription);
if (session.memberships.length > 0) {
this.roomSessions.set(room.roomId, session);
}
@@ -96,7 +100,10 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
*/
public getRoomSession(room: Room): MatrixRTCSession {
if (!this.roomSessions.has(room.roomId)) {
this.roomSessions.set(room.roomId, MatrixRTCSession.roomSessionForRoom(this.client, room));
this.roomSessions.set(
room.roomId,
MatrixRTCSession.sessionForRoom(this.client, room, this.sessionDescription),
);
}
return this.roomSessions.get(room.roomId)!;

View File

@@ -26,7 +26,7 @@ import { type CallMembership, DEFAULT_EXPIRE_DURATION, type SessionMembershipDat
import { type Focus } from "./focus.ts";
import { isMyMembership, Status } from "./types.ts";
import { isLivekitFocusActive } from "./LivekitFocus.ts";
import { type MembershipConfig } from "./MatrixRTCSession.ts";
import { type SessionDescription, type MembershipConfig } from "./MatrixRTCSession.ts";
import { ActionScheduler, type ActionUpdate } from "./MembershipManagerActionScheduler.ts";
import { TypedEventEmitter } from "../models/typed-event-emitter.ts";
import {
@@ -101,7 +101,7 @@ export enum MembershipActionType {
// -> MembershipActionType.Update if the timeout has passed so the next update is required.
SendScheduledDelayedLeaveEvent = "SendScheduledDelayedLeaveEvent",
// -> MembershipActionType.SendLeaveEvent on failiour (not found) we need to send the leave manually and cannot use the scheduled delayed event
// -> MembershipActionType.SendLeaveEvent on failure (not found) we need to send the leave manually and cannot use the scheduled delayed event
// -> DelayedLeaveActionType.SendScheduledDelayedLeaveEvent on error we try again.
SendLeaveEvent = "SendLeaveEvent",
@@ -294,6 +294,7 @@ export class MembershipManager
| "_unstable_updateDelayedEvent"
>,
private getOldestMembership: () => CallMembership | undefined,
public readonly sessionDescription: SessionDescription,
parentLogger?: Logger,
) {
super();
@@ -700,7 +701,7 @@ export class MembershipManager
// HELPERS
private makeMembershipStateKey(localUserId: string, localDeviceId: string): string {
const stateKey = `${localUserId}_${localDeviceId}`;
const stateKey = `${localUserId}_${localDeviceId}_${this.sessionDescription.application}${this.sessionDescription.id}`;
if (/^org\.matrix\.msc(3757|3779)\b/.exec(this.room.getVersion())) {
return stateKey;
} else {
@@ -713,9 +714,10 @@ export class MembershipManager
*/
private makeMyMembership(expires: number): SessionMembershipData {
return {
call_id: "",
// TODO: use the new format for m.rtc.member events where call_id becomes session.id
application: this.sessionDescription.application,
call_id: this.sessionDescription.id,
scope: "m.room",
application: "m.call",
device_id: this.deviceId,
expires,
focus_active: { type: "livekit", focus_selection: "oldest_membership" },