You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Make the js-sdk compatible with MSC preferred foci and active focus. (#4195)
* Refactor to preferred and active foci. Signed-off-by: Timo K <toger5@hotmail.de> * make the sdk compatible with MSC4143 but still be backwards compatible * comment fixes * also fallback to legacy if the current member event is legacy * use XOR types * use EitherAnd * make livekit Foucs types simpler * review * fix tests * test work * more review + more tests * remove unnecassary await that is in conflict with the comment * make joinRoomSession sync * Update src/matrixrtc/MatrixRTCSession.ts Co-authored-by: Andrew Ferrazzutti <af_0_af@hotmail.com> * review * fix * test * review * review * comment clarification * typo --------- Signed-off-by: Timo K <toger5@hotmail.de> Co-authored-by: Andrew Ferrazzutti <af_0_af@hotmail.com>
This commit is contained in:
@@ -15,16 +15,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MatrixEvent } from "../../../src";
|
import { MatrixEvent } from "../../../src";
|
||||||
import { CallMembership, CallMembershipData } from "../../../src/matrixrtc/CallMembership";
|
import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
|
||||||
|
|
||||||
const membershipTemplate: CallMembershipData = {
|
|
||||||
call_id: "",
|
|
||||||
scope: "m.room",
|
|
||||||
application: "m.call",
|
|
||||||
device_id: "AAAAAAA",
|
|
||||||
expires: 5000,
|
|
||||||
membershipID: "bloop",
|
|
||||||
};
|
|
||||||
|
|
||||||
function makeMockEvent(originTs = 0): MatrixEvent {
|
function makeMockEvent(originTs = 0): MatrixEvent {
|
||||||
return {
|
return {
|
||||||
@@ -34,96 +25,175 @@ function makeMockEvent(originTs = 0): MatrixEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("CallMembership", () => {
|
describe("CallMembership", () => {
|
||||||
it("rejects membership with no expiry and no expires_ts", () => {
|
describe("CallMembershipDataLegacy", () => {
|
||||||
expect(() => {
|
const membershipTemplate: CallMembershipDataLegacy = {
|
||||||
new CallMembership(
|
call_id: "",
|
||||||
makeMockEvent(),
|
scope: "m.room",
|
||||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
|
application: "m.call",
|
||||||
|
device_id: "AAAAAAA",
|
||||||
|
expires: 5000,
|
||||||
|
membershipID: "bloop",
|
||||||
|
foci_active: [{ type: "livekit" }],
|
||||||
|
};
|
||||||
|
it("rejects membership with no expiry and no expires_ts", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(
|
||||||
|
makeMockEvent(),
|
||||||
|
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }),
|
||||||
|
);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects membership with no device_id", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects membership with no call_id", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allow membership with no scope", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
it("rejects with malformatted expires_ts", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
it("rejects with malformatted expires", () => {
|
||||||
|
expect(() => {
|
||||||
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses event timestamp if no created_ts", () => {
|
||||||
|
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||||
|
expect(membership.createdTs()).toEqual(12345);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses created_ts if present", () => {
|
||||||
|
const membership = new CallMembership(
|
||||||
|
makeMockEvent(12345),
|
||||||
|
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
||||||
);
|
);
|
||||||
}).toThrow();
|
expect(membership.createdTs()).toEqual(67890);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes absolute expiry time based on expires", () => {
|
||||||
|
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
|
||||||
|
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("computes absolute expiry time based on expires_ts", () => {
|
||||||
|
const membership = new CallMembership(
|
||||||
|
makeMockEvent(1000),
|
||||||
|
Object.assign({}, membershipTemplate, { expires_ts: 6000 }),
|
||||||
|
);
|
||||||
|
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("considers memberships unexpired if local age low enough", () => {
|
||||||
|
const fakeEvent = makeMockEvent(1000);
|
||||||
|
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
||||||
|
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||||
|
expect(membership.isExpired()).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("considers memberships expired when local age large", () => {
|
||||||
|
const fakeEvent = makeMockEvent(1000);
|
||||||
|
fakeEvent.localTimestamp = Date.now() - 6000;
|
||||||
|
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||||
|
expect(membership.isExpired()).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns preferred foci", () => {
|
||||||
|
const fakeEvent = makeMockEvent();
|
||||||
|
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||||
|
const membership = new CallMembership(
|
||||||
|
fakeEvent,
|
||||||
|
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
|
||||||
|
);
|
||||||
|
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects membership with no device_id", () => {
|
describe("SessionMembershipData", () => {
|
||||||
expect(() => {
|
const membershipTemplate: SessionMembershipData = {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
call_id: "",
|
||||||
}).toThrow();
|
scope: "m.room",
|
||||||
});
|
application: "m.call",
|
||||||
|
device_id: "AAAAAAA",
|
||||||
|
focus_active: { type: "livekit" },
|
||||||
|
foci_preferred: [{ type: "livekit" }],
|
||||||
|
};
|
||||||
|
|
||||||
it("rejects membership with no call_id", () => {
|
it("rejects membership with no device_id", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined }));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects membership with no scope", () => {
|
it("rejects membership with no call_id", () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined }));
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
});
|
});
|
||||||
it("rejects with malformatted expires_ts", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
it("rejects with malformatted expires", () => {
|
|
||||||
expect(() => {
|
|
||||||
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" }));
|
|
||||||
}).toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses event timestamp if no created_ts", () => {
|
it("allow membership with no scope", () => {
|
||||||
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
expect(() => {
|
||||||
expect(membership.createdTs()).toEqual(12345);
|
new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined }));
|
||||||
});
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it("uses created_ts if present", () => {
|
it("uses event timestamp if no created_ts", () => {
|
||||||
const membership = new CallMembership(
|
const membership = new CallMembership(makeMockEvent(12345), membershipTemplate);
|
||||||
makeMockEvent(12345),
|
expect(membership.createdTs()).toEqual(12345);
|
||||||
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
});
|
||||||
);
|
|
||||||
expect(membership.createdTs()).toEqual(67890);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("computes absolute expiry time based on expires", () => {
|
it("uses created_ts if present", () => {
|
||||||
const membership = new CallMembership(makeMockEvent(1000), membershipTemplate);
|
const membership = new CallMembership(
|
||||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
makeMockEvent(12345),
|
||||||
});
|
Object.assign({}, membershipTemplate, { created_ts: 67890 }),
|
||||||
|
);
|
||||||
|
expect(membership.createdTs()).toEqual(67890);
|
||||||
|
});
|
||||||
|
|
||||||
it("computes absolute expiry time based on expires_ts", () => {
|
it("considers memberships unexpired if local age low enough", () => {
|
||||||
const membership = new CallMembership(
|
const fakeEvent = makeMockEvent(1000);
|
||||||
makeMockEvent(1000),
|
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
||||||
Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: 6000 }),
|
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
||||||
);
|
expect(membership.isExpired()).toEqual(false);
|
||||||
expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it("considers memberships unexpired if local age low enough", () => {
|
it("returns preferred foci", () => {
|
||||||
const fakeEvent = makeMockEvent(1000);
|
const fakeEvent = makeMockEvent();
|
||||||
fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000);
|
const mockFocus = { type: "this_is_a_mock_focus" };
|
||||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
const membership = new CallMembership(
|
||||||
expect(membership.isExpired()).toEqual(false);
|
fakeEvent,
|
||||||
});
|
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
|
||||||
|
);
|
||||||
it("considers memberships expired when local age large", () => {
|
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
|
||||||
const fakeEvent = makeMockEvent(1000);
|
});
|
||||||
fakeEvent.localTimestamp = Date.now() - 6000;
|
|
||||||
const membership = new CallMembership(fakeEvent, membershipTemplate);
|
|
||||||
expect(membership.isExpired()).toEqual(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns active foci", () => {
|
|
||||||
const fakeEvent = makeMockEvent();
|
|
||||||
const mockFocus = { type: "this_is_a_mock_focus" };
|
|
||||||
const membership = new CallMembership(
|
|
||||||
fakeEvent,
|
|
||||||
Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }),
|
|
||||||
);
|
|
||||||
expect(membership.getActiveFoci()).toEqual([mockFocus]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("expiry calculation", () => {
|
describe("expiry calculation", () => {
|
||||||
let fakeEvent: MatrixEvent;
|
let fakeEvent: MatrixEvent;
|
||||||
let membership: CallMembership;
|
let membership: CallMembership;
|
||||||
|
const membershipTemplate: CallMembershipDataLegacy = {
|
||||||
|
call_id: "",
|
||||||
|
scope: "m.room",
|
||||||
|
application: "m.call",
|
||||||
|
device_id: "AAAAAAA",
|
||||||
|
expires: 5000,
|
||||||
|
membershipID: "bloop",
|
||||||
|
foci_active: [{ type: "livekit" }],
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// server origin timestamp for this event is 1000
|
// server origin timestamp for this event is 1000
|
||||||
|
60
spec/unit/matrixrtc/LivekitFocus.spec.ts
Normal file
60
spec/unit/matrixrtc/LivekitFocus.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
|
||||||
|
|
||||||
|
describe("LivekitFocus", () => {
|
||||||
|
it("isLivekitFocus", () => {
|
||||||
|
expect(
|
||||||
|
isLivekitFocus({
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: "http://test.com",
|
||||||
|
livekit_alias: "test",
|
||||||
|
}),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
it("isLivekitFocusActive", () => {
|
||||||
|
expect(
|
||||||
|
isLivekitFocusActive({
|
||||||
|
type: "livekit",
|
||||||
|
focus_selection: "oldest_membership",
|
||||||
|
}),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
|
||||||
|
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
|
||||||
|
});
|
||||||
|
it("isLivekitFocusConfig", () => {
|
||||||
|
expect(
|
||||||
|
isLivekitFocusConfig({
|
||||||
|
type: "livekit",
|
||||||
|
livekit_service_url: "http://test.com",
|
||||||
|
}),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy();
|
||||||
|
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
|
||||||
|
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
@@ -29,6 +29,7 @@ const membershipTemplate: CallMembershipData = {
|
|||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
expires: 60 * 60 * 1000,
|
expires: 60 * 60 * 1000,
|
||||||
membershipID: "bloop",
|
membershipID: "bloop",
|
||||||
|
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockFocus = { type: "mock" };
|
const mockFocus = { type: "mock" };
|
||||||
@@ -198,6 +199,64 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getsActiveFocus", () => {
|
||||||
|
const activeFociConfig = { type: "livekit", livekit_service_url: "https://active.url" };
|
||||||
|
it("gets the correct active focus with oldest_membership", () => {
|
||||||
|
const mockRoom = makeMockRoom([
|
||||||
|
Object.assign({}, membershipTemplate, {
|
||||||
|
device_id: "foo",
|
||||||
|
created_ts: 500,
|
||||||
|
foci_active: [activeFociConfig],
|
||||||
|
}),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
|
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||||
|
type: "livekit",
|
||||||
|
focus_selection: "oldest_membership",
|
||||||
|
});
|
||||||
|
expect(sess.getActiveFocus()).toBe(activeFociConfig);
|
||||||
|
});
|
||||||
|
it("does not provide focus if the selction method is unknown", () => {
|
||||||
|
const mockRoom = makeMockRoom([
|
||||||
|
Object.assign({}, membershipTemplate, {
|
||||||
|
device_id: "foo",
|
||||||
|
created_ts: 500,
|
||||||
|
foci_active: [activeFociConfig],
|
||||||
|
}),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
|
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }], {
|
||||||
|
type: "livekit",
|
||||||
|
focus_selection: "unknown",
|
||||||
|
});
|
||||||
|
expect(sess.getActiveFocus()).toBe(undefined);
|
||||||
|
});
|
||||||
|
it("gets the correct active focus legacy", () => {
|
||||||
|
const mockRoom = makeMockRoom([
|
||||||
|
Object.assign({}, membershipTemplate, {
|
||||||
|
device_id: "foo",
|
||||||
|
created_ts: 500,
|
||||||
|
foci_active: [activeFociConfig],
|
||||||
|
}),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "old", created_ts: 1000 }),
|
||||||
|
Object.assign({}, membershipTemplate, { device_id: "bar", created_ts: 2000 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
|
sess.joinRoomSession([{ type: "livekit", livekit_service_url: "htts://test.org" }]);
|
||||||
|
expect(sess.getActiveFocus()).toBe(activeFociConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("joining", () => {
|
describe("joining", () => {
|
||||||
let mockRoom: Room;
|
let mockRoom: Room;
|
||||||
let sendStateEventMock: jest.Mock;
|
let sendStateEventMock: jest.Mock;
|
||||||
@@ -223,13 +282,13 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows joined once join is called", () => {
|
it("shows joined once join is called", () => {
|
||||||
sess!.joinRoomSession([mockFocus]);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
expect(sess!.isJoined()).toEqual(true);
|
expect(sess!.isJoined()).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends a membership event when joining a call", () => {
|
it("sends a membership event when joining a call", () => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
sess!.joinRoomSession([mockFocus]);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||||
mockRoom!.roomId,
|
mockRoom!.roomId,
|
||||||
EventType.GroupCallMemberPrefix,
|
EventType.GroupCallMemberPrefix,
|
||||||
@@ -242,7 +301,8 @@ describe("MatrixRTCSession", () => {
|
|||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
expires: 3600000,
|
expires: 3600000,
|
||||||
expires_ts: Date.now() + 3600000,
|
expires_ts: Date.now() + 3600000,
|
||||||
foci_active: [{ type: "mock" }],
|
foci_active: [mockFocus],
|
||||||
|
|
||||||
membershipID: expect.stringMatching(".*"),
|
membershipID: expect.stringMatching(".*"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -253,11 +313,11 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("does nothing if join called when already joined", () => {
|
it("does nothing if join called when already joined", () => {
|
||||||
sess!.joinRoomSession([mockFocus]);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus]);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
expect(client.sendStateEvent).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -274,7 +334,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
|
const sendStateEventMock = jest.fn().mockImplementation(resolveFn);
|
||||||
client.sendStateEvent = sendStateEventMock;
|
client.sendStateEvent = sendStateEventMock;
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus]);
|
sess!.joinRoomSession([mockFocus], mockFocus);
|
||||||
|
|
||||||
const eventContent = await eventSentPromise;
|
const eventContent = await eventSentPromise;
|
||||||
|
|
||||||
@@ -308,7 +368,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
expires: 3600000 * 2,
|
expires: 3600000 * 2,
|
||||||
expires_ts: 1000 + 3600000 * 2,
|
expires_ts: 1000 + 3600000 * 2,
|
||||||
foci_active: [{ type: "mock" }],
|
foci_active: [mockFocus],
|
||||||
created_ts: 1000,
|
created_ts: 1000,
|
||||||
membershipID: expect.stringMatching(".*"),
|
membershipID: expect.stringMatching(".*"),
|
||||||
},
|
},
|
||||||
@@ -322,7 +382,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("creates a key when joining", () => {
|
it("creates a key when joining", () => {
|
||||||
sess!.joinRoomSession([mockFocus], true);
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
|
const keys = sess?.getKeysForParticipant("@alice:example.org", "AAAAAAA");
|
||||||
expect(keys).toHaveLength(1);
|
expect(keys).toHaveLength(1);
|
||||||
|
|
||||||
@@ -336,7 +396,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sendEventMock.mockImplementation(resolve);
|
sendEventMock.mockImplementation(resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], true);
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
|
|
||||||
await eventSentPromise;
|
await eventSentPromise;
|
||||||
|
|
||||||
@@ -372,7 +432,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], true);
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
jest.advanceTimersByTime(10000);
|
jest.advanceTimersByTime(10000);
|
||||||
|
|
||||||
await eventSentPromise;
|
await eventSentPromise;
|
||||||
@@ -394,7 +454,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
sess!.joinRoomSession([mockFocus], true);
|
sess!.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
|
|
||||||
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
|
expect(client.cancelPendingEvent).toHaveBeenCalledWith(eventSentinel);
|
||||||
});
|
});
|
||||||
@@ -409,7 +469,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sendEventMock.mockImplementation(resolve);
|
sendEventMock.mockImplementation(resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus], true);
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
await keysSentPromise1;
|
await keysSentPromise1;
|
||||||
|
|
||||||
sendEventMock.mockClear();
|
sendEventMock.mockClear();
|
||||||
@@ -462,7 +522,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
sendEventMock.mockImplementation((_roomId, _evType, payload) => resolve(payload));
|
||||||
});
|
});
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus], true);
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
const firstKeysPayload = await keysSentPromise1;
|
const firstKeysPayload = await keysSentPromise1;
|
||||||
expect(firstKeysPayload.keys).toHaveLength(1);
|
expect(firstKeysPayload.keys).toHaveLength(1);
|
||||||
|
|
||||||
@@ -499,7 +559,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
sendEventMock.mockImplementation(resolve);
|
sendEventMock.mockImplementation(resolve);
|
||||||
});
|
});
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus], true);
|
sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
|
||||||
await keysSentPromise1;
|
await keysSentPromise1;
|
||||||
|
|
||||||
sendEventMock.mockClear();
|
sendEventMock.mockClear();
|
||||||
@@ -595,7 +655,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
|
|
||||||
jest.advanceTimersByTime(10000);
|
jest.advanceTimersByTime(10000);
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus]);
|
sess.joinRoomSession([mockFocus], mockFocus);
|
||||||
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||||
mockRoomNoExpired!.roomId,
|
mockRoomNoExpired!.roomId,
|
||||||
@@ -631,7 +691,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
]);
|
]);
|
||||||
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
|
||||||
|
|
||||||
sess.joinRoomSession([mockFocus]);
|
sess.joinRoomSession([mockFocus], mockFocus);
|
||||||
|
|
||||||
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
expect(client.sendStateEvent).toHaveBeenCalledWith(
|
||||||
mockRoom!.roomId,
|
mockRoom!.roomId,
|
||||||
@@ -645,6 +705,7 @@ describe("MatrixRTCSession", () => {
|
|||||||
device_id: "OTHERDEVICE",
|
device_id: "OTHERDEVICE",
|
||||||
expires: 3600000,
|
expires: 3600000,
|
||||||
created_ts: 1000,
|
created_ts: 1000,
|
||||||
|
foci_active: [{ type: "livekit", livekit_service_url: "https://lk.url" }],
|
||||||
membershipID: expect.stringMatching(".*"),
|
membershipID: expect.stringMatching(".*"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -35,6 +35,7 @@ const membershipTemplate: CallMembershipData = {
|
|||||||
device_id: "AAAAAAA",
|
device_id: "AAAAAAA",
|
||||||
expires: 60 * 60 * 1000,
|
expires: 60 * 60 * 1000,
|
||||||
membershipID: "bloop",
|
membershipID: "bloop",
|
||||||
|
foci_active: [{ type: "test" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("MatrixRTCSessionManager", () => {
|
describe("MatrixRTCSessionManager", () => {
|
||||||
|
@@ -57,6 +57,7 @@ import {
|
|||||||
} from "../webrtc/callEventTypes";
|
} from "../webrtc/callEventTypes";
|
||||||
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
|
import { EncryptionKeysEventContent, ICallNotifyContent } from "../matrixrtc/types";
|
||||||
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
|
import { M_POLL_END, M_POLL_START, PollEndEventContent, PollStartEventContent } from "./polls";
|
||||||
|
import { SessionMembershipData } from "../matrixrtc/CallMembership";
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
// Room state events
|
// Room state events
|
||||||
@@ -356,7 +357,10 @@ export interface StateEvents {
|
|||||||
|
|
||||||
// MSC3401
|
// MSC3401
|
||||||
[EventType.GroupCallPrefix]: IGroupCallRoomState;
|
[EventType.GroupCallPrefix]: IGroupCallRoomState;
|
||||||
[EventType.GroupCallMemberPrefix]: XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>;
|
[EventType.GroupCallMemberPrefix]: XOR<
|
||||||
|
XOR<IGroupCallRoomMemberState, ExperimentalGroupCallRoomMemberState>,
|
||||||
|
XOR<SessionMembershipData, {}>
|
||||||
|
>;
|
||||||
|
|
||||||
// MSC3089
|
// MSC3089
|
||||||
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
|
[UNSTABLE_MSC3089_BRANCH.name]: MSC3089EventContent;
|
||||||
|
@@ -14,52 +14,114 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { EitherAnd } from "matrix-events-sdk/lib/types";
|
||||||
|
|
||||||
import { MatrixEvent } from "../matrix";
|
import { MatrixEvent } from "../matrix";
|
||||||
import { deepCompare } from "../utils";
|
import { deepCompare } from "../utils";
|
||||||
import { Focus } from "./focus";
|
import { Focus } from "./focus";
|
||||||
|
import { isLivekitFocusActive } from "./LivekitFocus";
|
||||||
|
|
||||||
type CallScope = "m.room" | "m.user";
|
type CallScope = "m.room" | "m.user";
|
||||||
|
|
||||||
// Represents an entry in the memberships section of an m.call.member event as it is on the wire
|
// Represents an entry in the memberships section of an m.call.member event as it is on the wire
|
||||||
export interface CallMembershipData {
|
|
||||||
application?: string;
|
// There are two different data interfaces. One for the Legacy types and one compliant with MSC4143
|
||||||
|
|
||||||
|
// MSC4143 (MatrixRTC) session membership data
|
||||||
|
|
||||||
|
export type SessionMembershipData = {
|
||||||
|
application: string;
|
||||||
|
call_id: string;
|
||||||
|
device_id: string;
|
||||||
|
|
||||||
|
focus_active: Focus;
|
||||||
|
foci_preferred: Focus[];
|
||||||
|
created_ts?: number;
|
||||||
|
|
||||||
|
// Application specific data
|
||||||
|
scope?: CallScope;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSessionMembershipData = (data: CallMembershipData): data is SessionMembershipData =>
|
||||||
|
"focus_active" in data;
|
||||||
|
|
||||||
|
const checkSessionsMembershipData = (data: any, errors: string[]): data is SessionMembershipData => {
|
||||||
|
const prefix = "Malformed session membership event: ";
|
||||||
|
if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
|
||||||
|
if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
|
||||||
|
if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
|
||||||
|
if (typeof data.focus_active?.type !== "string") errors.push(prefix + "focus_active.type must be a string");
|
||||||
|
if (!Array.isArray(data.foci_preferred)) errors.push(prefix + "foci_preferred must be an array");
|
||||||
|
// optional parameters
|
||||||
|
if (data.created_ts && typeof data.created_ts !== "number") errors.push(prefix + "created_ts must be number");
|
||||||
|
|
||||||
|
// application specific data (we first need to check if they exist)
|
||||||
|
if (data.scope && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
|
||||||
|
return errors.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Legacy session membership data
|
||||||
|
|
||||||
|
export type CallMembershipDataLegacy = {
|
||||||
|
application: string;
|
||||||
call_id: string;
|
call_id: string;
|
||||||
scope: CallScope;
|
scope: CallScope;
|
||||||
device_id: string;
|
device_id: string;
|
||||||
created_ts?: number;
|
|
||||||
expires?: number;
|
|
||||||
expires_ts?: number;
|
|
||||||
foci_active?: Focus[];
|
|
||||||
membershipID: string;
|
membershipID: string;
|
||||||
}
|
created_ts?: number;
|
||||||
|
foci_active?: Focus[];
|
||||||
|
} & EitherAnd<{ expires: number }, { expires_ts: number }>;
|
||||||
|
|
||||||
|
export const isLegacyCallMembershipData = (data: CallMembershipData): data is CallMembershipDataLegacy =>
|
||||||
|
"membershipID" in data;
|
||||||
|
|
||||||
|
const checkCallMembershipDataLegacy = (data: any, errors: string[]): data is CallMembershipDataLegacy => {
|
||||||
|
const prefix = "Malformed legacy rtc membership event: ";
|
||||||
|
if (!("expires" in data || "expires_ts" in data)) {
|
||||||
|
errors.push(prefix + "expires_ts or expires must be present");
|
||||||
|
}
|
||||||
|
if ("expires" in data) {
|
||||||
|
if (typeof data.expires !== "number") {
|
||||||
|
errors.push(prefix + "expires must be numeric");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("expires_ts" in data) {
|
||||||
|
if (typeof data.expires_ts !== "number") {
|
||||||
|
errors.push(prefix + "expires_ts must be numeric");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof data.device_id !== "string") errors.push(prefix + "device_id must be string");
|
||||||
|
if (typeof data.call_id !== "string") errors.push(prefix + "call_id must be string");
|
||||||
|
if (typeof data.application !== "string") errors.push(prefix + "application must be a string");
|
||||||
|
if (typeof data.membershipID !== "string") errors.push(prefix + "membershipID must be a string");
|
||||||
|
// optional elements
|
||||||
|
if (data.created_ts && typeof data.created_ts !== "number") errors.push(prefix + "created_ts must be number");
|
||||||
|
// application specific data (we first need to check if they exist)
|
||||||
|
if (data.scope && typeof data.scope !== "string") errors.push(prefix + "scope must be string");
|
||||||
|
return errors.length === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CallMembershipData = CallMembershipDataLegacy | SessionMembershipData;
|
||||||
|
|
||||||
export class CallMembership {
|
export class CallMembership {
|
||||||
public static equal(a: CallMembership, b: CallMembership): boolean {
|
public static equal(a: CallMembership, b: CallMembership): boolean {
|
||||||
return deepCompare(a.data, b.data);
|
return deepCompare(a.membershipData, b.membershipData);
|
||||||
}
|
}
|
||||||
|
private membershipData: CallMembershipData;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private parentEvent: MatrixEvent,
|
private parentEvent: MatrixEvent,
|
||||||
private data: CallMembershipData,
|
data: any,
|
||||||
) {
|
) {
|
||||||
if (!(data.expires || data.expires_ts)) {
|
const sessionErrors: string[] = [];
|
||||||
throw new Error("Malformed membership: expires_ts or expires must be present");
|
const legacyErrors: string[] = [];
|
||||||
|
if (!checkSessionsMembershipData(data, sessionErrors) && !checkCallMembershipDataLegacy(data, legacyErrors)) {
|
||||||
|
throw Error(
|
||||||
|
`unknown CallMembership data. Does not match legacy call.member (${legacyErrors.join(" & ")}) events nor MSC4143 (${sessionErrors.join(" & ")})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.membershipData = data;
|
||||||
}
|
}
|
||||||
if (data.expires) {
|
|
||||||
if (typeof data.expires !== "number") {
|
|
||||||
throw new Error("Malformed membership: expires must be numeric");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (data.expires_ts) {
|
|
||||||
if (typeof data.expires_ts !== "number") {
|
|
||||||
throw new Error("Malformed membership: expires_ts must be numeric");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof data.device_id !== "string") throw new Error("Malformed membership event: device_id must be string");
|
|
||||||
if (typeof data.call_id !== "string") throw new Error("Malformed membership event: call_id must be string");
|
|
||||||
if (typeof data.scope !== "string") throw new Error("Malformed membership event: scope must be string");
|
|
||||||
if (!parentEvent.getSender()) throw new Error("Invalid parent event: sender is null");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get sender(): string | undefined {
|
public get sender(): string | undefined {
|
||||||
@@ -67,62 +129,89 @@ export class CallMembership {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get callId(): string {
|
public get callId(): string {
|
||||||
return this.data.call_id;
|
return this.membershipData.call_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get deviceId(): string {
|
public get deviceId(): string {
|
||||||
return this.data.device_id;
|
return this.membershipData.device_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get application(): string | undefined {
|
public get application(): string | undefined {
|
||||||
return this.data.application;
|
return this.membershipData.application;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get scope(): CallScope {
|
public get scope(): CallScope | undefined {
|
||||||
return this.data.scope;
|
return this.membershipData.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get membershipID(): string {
|
public get membershipID(): string {
|
||||||
return this.data.membershipID;
|
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.membershipID;
|
||||||
|
// the createdTs behaves equivalent to the membershipID.
|
||||||
|
// we only need the field for the legacy member envents where we needed to update them
|
||||||
|
// synapse ignores sending state events if they have the same content.
|
||||||
|
else return this.createdTs().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public createdTs(): number {
|
public createdTs(): number {
|
||||||
return this.data.created_ts ?? this.parentEvent.getTs();
|
return this.membershipData.created_ts ?? this.parentEvent.getTs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAbsoluteExpiry(): number {
|
public getAbsoluteExpiry(): number | undefined {
|
||||||
if (this.data.expires) {
|
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
|
||||||
return this.createdTs() + this.data.expires;
|
if ("expires" in this.membershipData) {
|
||||||
|
// we know createdTs exists since we already do the isLegacyCallMembershipData check
|
||||||
|
return this.createdTs() + this.membershipData.expires;
|
||||||
} else {
|
} else {
|
||||||
// We know it exists because we checked for this in the constructor.
|
// We know it exists because we checked for this in the constructor.
|
||||||
return this.data.expires_ts!;
|
return this.membershipData.expires_ts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// gets the expiry time of the event, converted into the device's local time
|
// gets the expiry time of the event, converted into the device's local time
|
||||||
public getLocalExpiry(): number {
|
public getLocalExpiry(): number | undefined {
|
||||||
if (this.data.expires) {
|
if (!isLegacyCallMembershipData(this.membershipData)) return undefined;
|
||||||
|
if ("expires" in this.membershipData) {
|
||||||
|
// we know createdTs exists since we already do the isLegacyCallMembershipData check
|
||||||
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
|
const relativeCreationTime = this.parentEvent.getTs() - this.createdTs();
|
||||||
|
|
||||||
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
|
const localCreationTs = this.parentEvent.localTimestamp - relativeCreationTime;
|
||||||
|
|
||||||
return localCreationTs + this.data.expires;
|
return localCreationTs + this.membershipData.expires;
|
||||||
} else {
|
} else {
|
||||||
// With expires_ts we cannot convert to local time.
|
// With expires_ts we cannot convert to local time.
|
||||||
// TODO: Check the server timestamp and compute a diff to local time.
|
// TODO: Check the server timestamp and compute a diff to local time.
|
||||||
return this.data.expires_ts!;
|
return this.membershipData.expires_ts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMsUntilExpiry(): number {
|
public getMsUntilExpiry(): number | undefined {
|
||||||
return this.getLocalExpiry() - Date.now();
|
if (isLegacyCallMembershipData(this.membershipData)) return this.getLocalExpiry()! - Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
public isExpired(): boolean {
|
public isExpired(): boolean {
|
||||||
return this.getMsUntilExpiry() <= 0;
|
if (isLegacyCallMembershipData(this.membershipData)) return this.getMsUntilExpiry()! <= 0;
|
||||||
|
|
||||||
|
// MSC4143 events expire by being updated. So if the event exists, its not expired.
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getActiveFoci(): Focus[] {
|
public getPreferredFoci(): Focus[] {
|
||||||
return this.data.foci_active ?? [];
|
// To support both, the new and the old MatrixRTC memberships have two cases based
|
||||||
|
// on the availablitiy of `foci_preferred`
|
||||||
|
if (isLegacyCallMembershipData(this.membershipData)) return this.membershipData.foci_active ?? [];
|
||||||
|
|
||||||
|
// MSC4143 style membership
|
||||||
|
return this.membershipData.foci_preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFocusSelection(): string | undefined {
|
||||||
|
if (isLegacyCallMembershipData(this.membershipData)) {
|
||||||
|
return "oldest_membership";
|
||||||
|
} else {
|
||||||
|
const focusActive = this.membershipData.focus_active;
|
||||||
|
if (isLivekitFocusActive(focusActive)) {
|
||||||
|
return focusActive.focus_selection;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
39
src/matrixrtc/LivekitFocus.ts
Normal file
39
src/matrixrtc/LivekitFocus.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Focus } from "./focus";
|
||||||
|
|
||||||
|
export interface LivekitFocusConfig extends Focus {
|
||||||
|
type: "livekit";
|
||||||
|
livekit_service_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLivekitFocusConfig = (object: any): object is LivekitFocusConfig =>
|
||||||
|
object.type === "livekit" && "livekit_service_url" in object;
|
||||||
|
|
||||||
|
export interface LivekitFocus extends LivekitFocusConfig {
|
||||||
|
livekit_alias: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLivekitFocus = (object: any): object is LivekitFocus =>
|
||||||
|
isLivekitFocusConfig(object) && "livekit_alias" in object;
|
||||||
|
|
||||||
|
export interface LivekitFocusActive extends Focus {
|
||||||
|
type: "livekit";
|
||||||
|
focus_selection: "oldest_membership";
|
||||||
|
}
|
||||||
|
export const isLivekitFocusActive = (object: any): object is LivekitFocusActive =>
|
||||||
|
object.type === "livekit" && "focus_selection" in object;
|
@@ -20,7 +20,13 @@ import { EventTimeline } from "../models/event-timeline";
|
|||||||
import { Room } from "../models/room";
|
import { Room } from "../models/room";
|
||||||
import { MatrixClient } from "../client";
|
import { MatrixClient } from "../client";
|
||||||
import { EventType } from "../@types/event";
|
import { EventType } from "../@types/event";
|
||||||
import { CallMembership, CallMembershipData } from "./CallMembership";
|
import {
|
||||||
|
CallMembership,
|
||||||
|
CallMembershipData,
|
||||||
|
CallMembershipDataLegacy,
|
||||||
|
SessionMembershipData,
|
||||||
|
isLegacyCallMembershipData,
|
||||||
|
} from "./CallMembership";
|
||||||
import { RoomStateEvent } from "../models/room-state";
|
import { RoomStateEvent } from "../models/room-state";
|
||||||
import { Focus } from "./focus";
|
import { Focus } from "./focus";
|
||||||
import { randomString, secureRandomBase64Url } from "../randomstring";
|
import { randomString, secureRandomBase64Url } from "../randomstring";
|
||||||
@@ -29,6 +35,8 @@ import { decodeBase64, encodeUnpaddedBase64 } from "../base64";
|
|||||||
import { KnownMembership } from "../@types/membership";
|
import { KnownMembership } from "../@types/membership";
|
||||||
import { MatrixError } from "../http-api/errors";
|
import { MatrixError } from "../http-api/errors";
|
||||||
import { MatrixEvent } from "../models/event";
|
import { MatrixEvent } from "../models/event";
|
||||||
|
import { isLivekitFocusActive } from "./LivekitFocus";
|
||||||
|
import { ExperimentalGroupCallRoomMemberState } from "../webrtc/groupCall";
|
||||||
|
|
||||||
const MEMBERSHIP_EXPIRY_TIME = 60 * 60 * 1000;
|
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 MEMBER_EVENT_CHECK_PERIOD = 2 * 60 * 1000; // How often we check to see if we need to re-send our member event
|
||||||
@@ -57,7 +65,7 @@ export enum MatrixRTCSessionEvent {
|
|||||||
MembershipsChanged = "memberships_changed",
|
MembershipsChanged = "memberships_changed",
|
||||||
// We joined or left the session: our own local idea of whether we are joined,
|
// We joined or left the session: our own local idea of whether we are joined,
|
||||||
// separate from MembershipsChanged, ie. independent of whether our member event
|
// separate from MembershipsChanged, ie. independent of whether our member event
|
||||||
// has succesfully gone through.
|
// has successfully gone through.
|
||||||
JoinStateChanged = "join_state_changed",
|
JoinStateChanged = "join_state_changed",
|
||||||
// The key used to encrypt media has changed
|
// The key used to encrypt media has changed
|
||||||
EncryptionKeyChanged = "encryption_key_changed",
|
EncryptionKeyChanged = "encryption_key_changed",
|
||||||
@@ -75,7 +83,20 @@ export type MatrixRTCSessionEventHandlerMap = {
|
|||||||
participantId: string,
|
participantId: string,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
export interface JoinSessionConfig {
|
||||||
|
/** 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;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -102,12 +123,16 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
private makeNewKeyTimeout?: ReturnType<typeof setTimeout>;
|
private makeNewKeyTimeout?: ReturnType<typeof setTimeout>;
|
||||||
private setNewKeyTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
private setNewKeyTimeouts = new Set<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
private activeFoci: Focus[] | undefined;
|
// This is a Focus with the specified fields for an ActiveFocus (e.g. LivekitFocusActive for type="livekit")
|
||||||
|
private ownFocusActive?: Focus;
|
||||||
|
// This is a Foci array that contains the Focus objects this user is aware of and proposes to use.
|
||||||
|
private ownFociPreferred?: Focus[];
|
||||||
|
|
||||||
private updateCallMembershipRunning = false;
|
private updateCallMembershipRunning = false;
|
||||||
private needCallMembershipUpdate = false;
|
private needCallMembershipUpdate = false;
|
||||||
|
|
||||||
private manageMediaKeys = false;
|
private manageMediaKeys = false;
|
||||||
|
private useLegacyMemberEvents = true;
|
||||||
// userId:deviceId => array of keys
|
// userId:deviceId => array of keys
|
||||||
private encryptionKeys = new Map<string, Array<Uint8Array>>();
|
private encryptionKeys = new Map<string, Array<Uint8Array>>();
|
||||||
private lastEncryptionKeyUpdateRequest?: number;
|
private lastEncryptionKeyUpdateRequest?: number;
|
||||||
@@ -134,21 +159,33 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
|
|
||||||
const callMemberships: CallMembership[] = [];
|
const callMemberships: CallMembership[] = [];
|
||||||
for (const memberEvent of callMemberEvents) {
|
for (const memberEvent of callMemberEvents) {
|
||||||
const eventMemberships: CallMembershipData[] = memberEvent.getContent()["memberships"];
|
const content = memberEvent.getContent();
|
||||||
if (eventMemberships === undefined) {
|
let membershipContents: any[] = [];
|
||||||
continue;
|
// We first decide if its a MSC4143 event (per device state key)
|
||||||
|
if ("memberships" in content) {
|
||||||
|
// we have a legacy (one event for all devices) event
|
||||||
|
if (!Array.isArray(content["memberships"])) {
|
||||||
|
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
membershipContents = content["memberships"];
|
||||||
|
} else {
|
||||||
|
// We have a MSC4143 event membership event
|
||||||
|
if (Object.keys(content).length !== 0) {
|
||||||
|
// We checked for empty content to not try to construct CallMembership's with {}.
|
||||||
|
membershipContents.push(content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!Array.isArray(eventMemberships)) {
|
if (membershipContents.length === 0) {
|
||||||
logger.warn(`Malformed member event from ${memberEvent.getSender()}: memberships is not an array`);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const membershipData of eventMemberships) {
|
for (const membershipData of membershipContents) {
|
||||||
try {
|
try {
|
||||||
const membership = new CallMembership(memberEvent, membershipData);
|
const membership = new CallMembership(memberEvent, membershipData);
|
||||||
|
|
||||||
if (membership.callId !== "" || membership.scope !== "m.room") {
|
if (membership.callId !== "" || membership.scope !== "m.room") {
|
||||||
// for now, just ignore anything that isn't the a room scope call
|
// for now, just ignore anything that isn't a room scope call
|
||||||
logger.info(`Ignoring user-scoped call`);
|
logger.info(`Ignoring user-scoped call`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -202,6 +239,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns true if we intend to be participating in the MatrixRTC session.
|
* Returns true if we intend to be participating in the MatrixRTC session.
|
||||||
|
* This is determined by checking if the relativeExpiry has been set.
|
||||||
*/
|
*/
|
||||||
public isJoined(): boolean {
|
public isJoined(): boolean {
|
||||||
return this.relativeExpiry !== undefined;
|
return this.relativeExpiry !== undefined;
|
||||||
@@ -232,30 +270,34 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
* desired.
|
* desired.
|
||||||
* This method will return immediately and the session will be joined in the background.
|
* This method will return immediately and the session will be joined in the background.
|
||||||
*
|
*
|
||||||
* @param activeFoci - The list of foci to set as currently active in the call member event
|
* @param fociActive - The object representing the active focus. (This depends on the focus type.)
|
||||||
* @param manageMediaKeys - If true, generate and share a a media key for this participant,
|
* @param fociPreferred - The list of preferred foci this member proposes to use/knows/has access to.
|
||||||
* and emit MatrixRTCSessionEvent.EncryptionKeyChanged when
|
* For the livekit case this is a list of foci generated from the homeserver well-known, the current rtc session,
|
||||||
* media keys for other participants become available.
|
* or optionally other room members homeserver well known.
|
||||||
|
* @param joinConfig - Additional configuration for the joined session.
|
||||||
*/
|
*/
|
||||||
public joinRoomSession(activeFoci: Focus[], manageMediaKeys?: boolean): void {
|
public joinRoomSession(fociPreferred: Focus[], fociActive?: Focus, joinConfig?: JoinSessionConfig): void {
|
||||||
if (this.isJoined()) {
|
if (this.isJoined()) {
|
||||||
logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`);
|
logger.info(`Already joined to session in room ${this.room.roomId}: ignoring join call`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${manageMediaKeys}`);
|
this.ownFocusActive = fociActive;
|
||||||
this.activeFoci = activeFoci;
|
this.ownFociPreferred = fociPreferred;
|
||||||
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME;
|
this.relativeExpiry = MEMBERSHIP_EXPIRY_TIME;
|
||||||
this.manageMediaKeys = manageMediaKeys ?? false;
|
this.manageMediaKeys = joinConfig?.manageMediaKeys ?? this.manageMediaKeys;
|
||||||
|
this.useLegacyMemberEvents = joinConfig?.useLegacyMemberEvents ?? this.useLegacyMemberEvents;
|
||||||
this.membershipId = randomString(5);
|
this.membershipId = randomString(5);
|
||||||
this.emit(MatrixRTCSessionEvent.JoinStateChanged, true);
|
|
||||||
if (manageMediaKeys) {
|
logger.info(`Joining call session in room ${this.room.roomId} with manageMediaKeys=${this.manageMediaKeys}`);
|
||||||
|
if (joinConfig?.manageMediaKeys) {
|
||||||
this.makeNewSenderKey();
|
this.makeNewSenderKey();
|
||||||
this.requestKeyEventSend();
|
this.requestKeyEventSend();
|
||||||
}
|
}
|
||||||
// We don't wait for this, mostly because it may fail and schedule a retry, so this
|
// We don't wait for this, mostly because it may fail and schedule a retry, so this
|
||||||
// function returning doesn't really mean anything at all.
|
// function returning doesn't really mean anything at all.
|
||||||
this.triggerCallMembershipEventUpdate();
|
this.triggerCallMembershipEventUpdate();
|
||||||
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,7 +337,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
|
|
||||||
logger.info(`Leaving call session in room ${this.room.roomId}`);
|
logger.info(`Leaving call session in room ${this.room.roomId}`);
|
||||||
this.relativeExpiry = undefined;
|
this.relativeExpiry = undefined;
|
||||||
this.activeFoci = undefined;
|
this.ownFocusActive = undefined;
|
||||||
this.manageMediaKeys = false;
|
this.manageMediaKeys = false;
|
||||||
this.membershipId = undefined;
|
this.membershipId = undefined;
|
||||||
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
|
this.emit(MatrixRTCSessionEvent.JoinStateChanged, false);
|
||||||
@@ -315,6 +357,21 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getActiveFocus(): Focus | undefined {
|
||||||
|
if (this.ownFocusActive && isLivekitFocusActive(this.ownFocusActive)) {
|
||||||
|
// A livekit active focus
|
||||||
|
if (this.ownFocusActive.focus_selection === "oldest_membership") {
|
||||||
|
const oldestMembership = this.getOldestMembership();
|
||||||
|
return oldestMembership?.getPreferredFoci()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.ownFocusActive) {
|
||||||
|
// we use the legacy call.member events so default to oldest member
|
||||||
|
const oldestMembership = this.getOldestMembership();
|
||||||
|
return oldestMembership?.getPreferredFoci()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
|
public getKeysForParticipant(userId: string, deviceId: string): Array<Uint8Array> | undefined {
|
||||||
return this.encryptionKeys.get(getParticipantId(userId, deviceId));
|
return this.encryptionKeys.get(getParticipantId(userId, deviceId));
|
||||||
}
|
}
|
||||||
@@ -344,7 +401,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
* @param userId - The user ID of the participant
|
* @param userId - The user ID of the participant
|
||||||
* @param deviceId - Device ID of the participant
|
* @param deviceId - Device ID of the participant
|
||||||
* @param encryptionKeyIndex - The index of the key to set
|
* @param encryptionKeyIndex - The index of the key to set
|
||||||
* @param encryptionKeyString - The string represenation of the key to set in base64
|
* @param encryptionKeyString - The string representation of the key to set in base64
|
||||||
* @param delayBeforeuse - If true, delay before emitting a key changed event. Useful when setting
|
* @param delayBeforeuse - If true, delay before emitting a key changed event. Useful when setting
|
||||||
* encryption keys for the local participant to allow time for the key to
|
* encryption keys for the local participant to allow time for the key to
|
||||||
* be distributed.
|
* be distributed.
|
||||||
@@ -379,7 +436,7 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new sender key and add it at the next available index
|
* Generate a new sender key and add it at the next available index
|
||||||
* @param delayBeforeUse - If true, wait for a short period before settign the key for the
|
* @param delayBeforeUse - If true, wait for a short period before setting the key for the
|
||||||
* media encryptor to use. If false, set the key immediately.
|
* media encryptor to use. If false, set the key immediately.
|
||||||
*/
|
*/
|
||||||
private makeNewSenderKey(delayBeforeUse = false): void {
|
private makeNewSenderKey(delayBeforeUse = false): void {
|
||||||
@@ -488,7 +545,9 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
let soonestExpiry;
|
let soonestExpiry;
|
||||||
for (const membership of this.memberships) {
|
for (const membership of this.memberships) {
|
||||||
const thisExpiry = membership.getMsUntilExpiry();
|
const thisExpiry = membership.getMsUntilExpiry();
|
||||||
if (soonestExpiry === undefined || thisExpiry < soonestExpiry) {
|
// If getMsUntilExpiry is undefined we have a MSC4143 (MatrixRTC) compliant event - it never expires
|
||||||
|
// but will be reliably resent on disconnect.
|
||||||
|
if (thisExpiry !== undefined && (soonestExpiry === undefined || thisExpiry < soonestExpiry)) {
|
||||||
soonestExpiry = thisExpiry;
|
soonestExpiry = thisExpiry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,6 +561,13 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
return this.memberships[0];
|
return this.memberships[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFocusInUse(): Focus | undefined {
|
||||||
|
const oldestMembership = this.getOldestMembership();
|
||||||
|
if (oldestMembership?.getFocusSelection() === "oldest_membership") {
|
||||||
|
return oldestMembership.getPreferredFoci()[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onCallEncryption = (event: MatrixEvent): void => {
|
public onCallEncryption = (event: MatrixEvent): void => {
|
||||||
const userId = event.getSender();
|
const userId = event.getSender();
|
||||||
const content = event.getContent<EncryptionKeysEventContent>();
|
const content = event.getContent<EncryptionKeysEventContent>();
|
||||||
@@ -613,30 +679,41 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
* Constructs our own membership
|
* Constructs our own membership
|
||||||
* @param prevMembership - The previous value of our call membership, if any
|
* @param prevMembership - The previous value of our call membership, if any
|
||||||
*/
|
*/
|
||||||
private makeMyMembership(prevMembership?: CallMembership): CallMembershipData {
|
private makeMyMembershipLegacy(deviceId: string, prevMembership?: CallMembership): CallMembershipDataLegacy {
|
||||||
if (this.relativeExpiry === undefined) {
|
if (this.relativeExpiry === undefined) {
|
||||||
throw new Error("Tried to create our own membership event when we're not joined!");
|
throw new Error("Tried to create our own membership event when we're not joined!");
|
||||||
}
|
}
|
||||||
if (this.membershipId === undefined) {
|
if (this.membershipId === undefined) {
|
||||||
throw new Error("Tried to create our own membership event when we have no membership ID!");
|
throw new Error("Tried to create our own membership event when we have no membership ID!");
|
||||||
}
|
}
|
||||||
|
const createdTs = prevMembership?.createdTs();
|
||||||
const m: CallMembershipData = {
|
return {
|
||||||
call_id: "",
|
call_id: "",
|
||||||
scope: "m.room",
|
scope: "m.room",
|
||||||
application: "m.call",
|
application: "m.call",
|
||||||
device_id: this.client.getDeviceId()!,
|
device_id: deviceId,
|
||||||
expires: this.relativeExpiry,
|
expires: this.relativeExpiry,
|
||||||
foci_active: this.activeFoci,
|
// TODO: Date.now() should be the origin_server_ts (now).
|
||||||
|
expires_ts: this.relativeExpiry + (createdTs ?? Date.now()),
|
||||||
|
// we use the fociPreferred since this is the list of foci.
|
||||||
|
// it is named wrong in the Legacy events.
|
||||||
|
foci_active: this.ownFociPreferred,
|
||||||
membershipID: this.membershipId,
|
membershipID: this.membershipId,
|
||||||
|
...(createdTs ? { created_ts: createdTs } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Constructs our own membership
|
||||||
|
*/
|
||||||
|
private makeMyMembership(deviceId: string): SessionMembershipData {
|
||||||
|
return {
|
||||||
|
call_id: "",
|
||||||
|
scope: "m.room",
|
||||||
|
application: "m.call",
|
||||||
|
device_id: deviceId,
|
||||||
|
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
||||||
|
foci_preferred: this.ownFociPreferred ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (prevMembership) m.created_ts = prevMembership.createdTs();
|
|
||||||
if (m.created_ts) m.expires_ts = m.created_ts + (m.expires ?? 0);
|
|
||||||
// TODO: Date.now() should be the origin_server_ts (now).
|
|
||||||
else m.expires_ts = Date.now() + (m.expires ?? 0);
|
|
||||||
|
|
||||||
return m;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -646,36 +723,41 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
myPrevMembershipData?: CallMembershipData,
|
myPrevMembershipData?: CallMembershipData,
|
||||||
myPrevMembership?: CallMembership,
|
myPrevMembership?: CallMembership,
|
||||||
): boolean {
|
): boolean {
|
||||||
// work out if we need to update our membership event
|
if (myPrevMembership && myPrevMembership.getMsUntilExpiry() === undefined) return false;
|
||||||
let needsUpdate = false;
|
|
||||||
// Need to update if there's a membership for us but we're not joined (valid or otherwise)
|
// Need to update if there's a membership for us but we're not joined (valid or otherwise)
|
||||||
if (!this.isJoined() && myPrevMembershipData) needsUpdate = true;
|
if (!this.isJoined()) return !!myPrevMembershipData;
|
||||||
if (this.isJoined()) {
|
|
||||||
// ...or if we are joined, but there's no valid membership event
|
// ...or if we are joined, but there's no valid membership event
|
||||||
if (!myPrevMembership) {
|
if (!myPrevMembership) return true;
|
||||||
needsUpdate = true;
|
|
||||||
} else if (myPrevMembership.getMsUntilExpiry() < MEMBERSHIP_EXPIRY_TIME / 2) {
|
const expiryTime = myPrevMembership.getMsUntilExpiry();
|
||||||
// ...or if the expiry time needs bumping
|
if (expiryTime !== undefined && expiryTime < MEMBERSHIP_EXPIRY_TIME / 2) {
|
||||||
needsUpdate = true;
|
// ...or if the expiry time needs bumping
|
||||||
this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME;
|
this.relativeExpiry! += MEMBERSHIP_EXPIRY_TIME;
|
||||||
}
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return needsUpdate;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private makeNewMembership(deviceId: string): SessionMembershipData | {} {
|
||||||
|
// If we're joined, add our own
|
||||||
|
if (this.isJoined()) {
|
||||||
|
return this.makeMyMembership(deviceId);
|
||||||
|
}
|
||||||
|
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 alonng 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 makeNewMemberships(
|
private makeNewLegacyMemberships(
|
||||||
oldMemberships: CallMembershipData[],
|
oldMemberships: CallMembershipData[],
|
||||||
|
localDeviceId: string,
|
||||||
myCallMemberEvent?: MatrixEvent,
|
myCallMemberEvent?: MatrixEvent,
|
||||||
myPrevMembership?: CallMembership,
|
myPrevMembership?: CallMembership,
|
||||||
): CallMembershipData[] {
|
): ExperimentalGroupCallRoomMemberState {
|
||||||
const localDeviceId = this.client.getDeviceId();
|
|
||||||
if (!localDeviceId) throw new Error("Local device ID is null!");
|
|
||||||
|
|
||||||
const filterExpired = (m: CallMembershipData): boolean => {
|
const filterExpired = (m: CallMembershipData): boolean => {
|
||||||
let membershipObj;
|
let membershipObj;
|
||||||
try {
|
try {
|
||||||
@@ -704,10 +786,10 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
|
|
||||||
// If we're joined, add our own
|
// If we're joined, add our own
|
||||||
if (this.isJoined()) {
|
if (this.isJoined()) {
|
||||||
newMemberships.push(this.makeMyMembership(myPrevMembership));
|
newMemberships.push(this.makeMyMembershipLegacy(localDeviceId, myPrevMembership));
|
||||||
}
|
}
|
||||||
|
|
||||||
return newMemberships;
|
return { memberships: newMemberships };
|
||||||
}
|
}
|
||||||
|
|
||||||
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
private triggerCallMembershipEventUpdate = async (): Promise<void> => {
|
||||||
@@ -742,46 +824,54 @@ export class MatrixRTCSession extends TypedEventEmitter<MatrixRTCSessionEvent, M
|
|||||||
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
if (!localUserId || !localDeviceId) throw new Error("User ID or device ID was null!");
|
||||||
|
|
||||||
const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId) ?? undefined;
|
const myCallMemberEvent = roomState.getStateEvents(EventType.GroupCallMemberPrefix, localUserId) ?? undefined;
|
||||||
const content = myCallMemberEvent?.getContent<Record<any, unknown>>() ?? {};
|
const content = myCallMemberEvent?.getContent() ?? {};
|
||||||
const memberships: CallMembershipData[] = Array.isArray(content["memberships"]) ? content["memberships"] : [];
|
const legacy = "memberships" in content || this.useLegacyMemberEvents;
|
||||||
|
let newContent: {} | ExperimentalGroupCallRoomMemberState | SessionMembershipData = {};
|
||||||
const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId);
|
if (legacy) {
|
||||||
let myPrevMembership;
|
let myPrevMembership: CallMembership | undefined;
|
||||||
try {
|
// We know its CallMembershipDataLegacy
|
||||||
if (myCallMemberEvent && myPrevMembershipData && myPrevMembershipData.membershipID === this.membershipId) {
|
const memberships: CallMembershipDataLegacy[] = Array.isArray(content["memberships"])
|
||||||
myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData);
|
? content["memberships"]
|
||||||
|
: [];
|
||||||
|
const myPrevMembershipData = memberships.find((m) => m.device_id === localDeviceId);
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
myCallMemberEvent &&
|
||||||
|
myPrevMembershipData &&
|
||||||
|
isLegacyCallMembershipData(myPrevMembershipData) &&
|
||||||
|
myPrevMembershipData.membershipID === this.membershipId
|
||||||
|
) {
|
||||||
|
myPrevMembership = new CallMembership(myCallMemberEvent, myPrevMembershipData);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// This would indicate a bug or something weird if our own call membership
|
||||||
|
// wasn't valid
|
||||||
|
logger.warn("Our previous call membership was invalid - this shouldn't happen.", e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
if (myPrevMembership) {
|
||||||
// This would indicate a bug or something weird if our own call membership
|
logger.debug(`${myPrevMembership.getMsUntilExpiry()} until our membership expires`);
|
||||||
// wasn't valid
|
}
|
||||||
logger.warn("Our previous call membership was invalid - this shouldn't happen.", e);
|
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
|
||||||
|
// nothing to do - reschedule the check again
|
||||||
|
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
newContent = this.makeNewLegacyMemberships(memberships, localDeviceId, myCallMemberEvent, myPrevMembership);
|
||||||
|
} else {
|
||||||
|
newContent = this.makeNewMembership(localDeviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (myPrevMembership) {
|
|
||||||
logger.debug(`${myPrevMembership.getMsUntilExpiry()} until our membership expires`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.membershipEventNeedsUpdate(myPrevMembershipData, myPrevMembership)) {
|
|
||||||
// nothing to do - reschedule the check again
|
|
||||||
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newContent = {
|
|
||||||
memberships: this.makeNewMemberships(memberships, myCallMemberEvent, myPrevMembership),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.client.sendStateEvent(
|
await this.client.sendStateEvent(
|
||||||
this.room.roomId,
|
this.room.roomId,
|
||||||
EventType.GroupCallMemberPrefix,
|
EventType.GroupCallMemberPrefix,
|
||||||
newContent,
|
newContent,
|
||||||
localUserId,
|
this.useLegacyMemberEvents ? localUserId : `${localUserId}_${localDeviceId}`,
|
||||||
);
|
);
|
||||||
logger.info(`Sent updated call member event.`);
|
logger.info(`Sent updated call member event.`);
|
||||||
|
|
||||||
// check periodically to see if we need to refresh our member event
|
// check periodically to see if we need to refresh our member event
|
||||||
if (this.isJoined()) {
|
if (this.isJoined() && legacy) {
|
||||||
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
|
this.memberEventTimeout = setTimeout(this.triggerCallMembershipEventUpdate, MEMBER_EVENT_CHECK_PERIOD);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@@ -73,9 +73,9 @@ export class MatrixRTCSessionManager extends TypedEventEmitter<MatrixRTCSessionM
|
|||||||
}
|
}
|
||||||
this.roomSessions.clear();
|
this.roomSessions.clear();
|
||||||
|
|
||||||
this.client.removeListener(ClientEvent.Room, this.onRoom);
|
this.client.off(ClientEvent.Room, this.onRoom);
|
||||||
this.client.removeListener(RoomEvent.Timeline, this.onTimeline);
|
this.client.off(RoomEvent.Timeline, this.onTimeline);
|
||||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomState);
|
this.client.off(RoomStateEvent.Events, this.onRoomState);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,4 +21,5 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
export interface Focus {
|
export interface Focus {
|
||||||
type: string;
|
type: string;
|
||||||
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user