Skip to content

Commit fd949fe

Browse files
toger5robintown
andauthored
[MatrixRTC] Multi SFU support + m.rtc.member event type support (#5022)
* WIP * temp Signed-off-by: Timo K <[email protected]> * Fix imports * Fix checkSessionsMembershipData thinking foci_preferred is required * incorporate CallMembership changes - rename Focus -> Transport - add RtcMembershipData (next to `sessionMembershipData`) - make `new CallMembership` initializable with both - move oldest member calculation into CallMembership Signed-off-by: Timo K <[email protected]> * use correct event type Signed-off-by: Timo K <[email protected]> * fix sonar cube conerns Signed-off-by: Timo K <[email protected]> * callMembership tests Signed-off-by: Timo K <[email protected]> * make test correct Signed-off-by: Timo K <[email protected]> * make sonar cube happy (it does not know about the type constraints...) Signed-off-by: Timo K <[email protected]> * remove created_ts from RtcMembership Signed-off-by: Timo K <[email protected]> * fix imports Signed-off-by: Timo K <[email protected]> * Update src/matrixrtc/IMembershipManager.ts Co-authored-by: Robin <[email protected]> * rename LivekitFocus.ts -> LivekitTransport.ts Signed-off-by: Timo K <[email protected]> * add details to `getTransport` Signed-off-by: Timo K <[email protected]> * review Signed-off-by: Timo K <[email protected]> * use DEFAULT_EXPIRE_DURATION in tests Signed-off-by: Timo K <[email protected]> * fix test `does not provide focus if the selection method is unknown` Signed-off-by: Timo K <[email protected]> * Update src/matrixrtc/CallMembership.ts Co-authored-by: Robin <[email protected]> * Move `m.call.intent` into the `application` section for rtc member events. Signed-off-by: Timo K <[email protected]> * review on rtc object validation code. Signed-off-by: Timo K <[email protected]> * user id check Signed-off-by: Timo K <[email protected]> * review: Refactor RTC membership handling and improve error handling Signed-off-by: Timo K <[email protected]> * docstring updates Signed-off-by: Timo K <[email protected]> * add back deprecated `getFocusInUse` & `getActiveFocus` Signed-off-by: Timo K <[email protected]> * ci Signed-off-by: Timo K <[email protected]> * Update src/matrixrtc/CallMembership.ts Co-authored-by: Robin <[email protected]> * lint Signed-off-by: Timo K <[email protected]> * make test less strict for ew tests Signed-off-by: Timo K <[email protected]> * Typescript downstream test adjustments Signed-off-by: Timo K <[email protected]> * err Signed-off-by: Timo K <[email protected]> --------- Signed-off-by: Timo K <[email protected]> Co-authored-by: Robin <[email protected]>
1 parent 7b3aed8 commit fd949fe

16 files changed

+971
-402
lines changed

spec/unit/matrixrtc/CallMembership.spec.ts

Lines changed: 274 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@ import {
1919
CallMembership,
2020
type SessionMembershipData,
2121
DEFAULT_EXPIRE_DURATION,
22+
type RtcMembershipData,
2223
} from "../../../src/matrixrtc/CallMembership";
2324
import { membershipTemplate } from "./mocks";
2425

2526
function makeMockEvent(originTs = 0): MatrixEvent {
2627
return {
2728
getTs: jest.fn().mockReturnValue(originTs),
2829
getSender: jest.fn().mockReturnValue("@alice:example.org"),
30+
getId: jest.fn().mockReturnValue("$eventid"),
2931
} as unknown as MatrixEvent;
3032
}
3133

@@ -40,12 +42,13 @@ describe("CallMembership", () => {
4042
});
4143

4244
const membershipTemplate: SessionMembershipData = {
43-
call_id: "",
44-
scope: "m.room",
45-
application: "m.call",
46-
device_id: "AAAAAAA",
47-
focus_active: { type: "livekit" },
48-
foci_preferred: [{ type: "livekit" }],
45+
"call_id": "",
46+
"scope": "m.room",
47+
"application": "m.call",
48+
"device_id": "AAAAAAA",
49+
"focus_active": { type: "livekit", focus_selection: "oldest_membership" },
50+
"foci_preferred": [{ type: "livekit" }],
51+
"m.call.intent": "voice",
4952
};
5053

5154
it("rejects membership with no device_id", () => {
@@ -94,11 +97,271 @@ describe("CallMembership", () => {
9497
it("returns preferred foci", () => {
9598
const fakeEvent = makeMockEvent();
9699
const mockFocus = { type: "this_is_a_mock_focus" };
97-
const membership = new CallMembership(
98-
fakeEvent,
99-
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
100-
);
101-
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
100+
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
101+
expect(membership.transports).toEqual([mockFocus]);
102+
});
103+
104+
describe("getTransport", () => {
105+
const mockFocus = { type: "this_is_a_mock_focus" };
106+
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate);
107+
it("gets the correct active transport with oldest_membership", () => {
108+
const membership = new CallMembership(makeMockEvent(), {
109+
...membershipTemplate,
110+
foci_preferred: [mockFocus],
111+
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
112+
});
113+
114+
// if we are the oldest member we use our focus.
115+
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
116+
117+
// If there is an older member we use its focus.
118+
expect(membership.getTransport(oldestMembership)).toBe(membershipTemplate.foci_preferred[0]);
119+
});
120+
121+
it("gets the correct active transport with multi_sfu", () => {
122+
const membership = new CallMembership(makeMockEvent(), {
123+
...membershipTemplate,
124+
foci_preferred: [mockFocus],
125+
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
126+
});
127+
128+
// if we are the oldest member we use our focus.
129+
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
130+
131+
// If there is an older member we still use our own focus in multi sfu.
132+
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
133+
});
134+
it("does not provide focus if the selection method is unknown", () => {
135+
const membership = new CallMembership(makeMockEvent(), {
136+
...membershipTemplate,
137+
foci_preferred: [mockFocus],
138+
focus_active: { type: "livekit", focus_selection: "unknown" },
139+
});
140+
141+
// if we are the oldest member we use our focus.
142+
expect(membership.getTransport(membership)).toBeUndefined();
143+
});
144+
});
145+
describe("correct values from computed fields", () => {
146+
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
147+
it("returns correct sender", () => {
148+
expect(membership.sender).toBe("@alice:example.org");
149+
});
150+
it("returns correct eventId", () => {
151+
expect(membership.eventId).toBe("$eventid");
152+
});
153+
it("returns correct slot_id", () => {
154+
expect(membership.slotId).toBe("m.call#");
155+
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
156+
});
157+
it("returns correct deviceId", () => {
158+
expect(membership.deviceId).toBe("AAAAAAA");
159+
});
160+
it("returns correct call intent", () => {
161+
expect(membership.callIntent).toBe("voice");
162+
});
163+
it("returns correct application", () => {
164+
expect(membership.application).toStrictEqual("m.call");
165+
});
166+
it("returns correct applicationData", () => {
167+
expect(membership.applicationData).toStrictEqual({ "type": "m.call", "m.call.intent": "voice" });
168+
});
169+
it("returns correct scope", () => {
170+
expect(membership.scope).toBe("m.room");
171+
});
172+
it("returns correct membershipID", () => {
173+
expect(membership.membershipID).toBe("0");
174+
});
175+
it("returns correct unused fields", () => {
176+
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
177+
expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
178+
expect(membership.isExpired()).toBe(true);
179+
});
180+
});
181+
});
182+
183+
describe("RtcMembershipData", () => {
184+
const membershipTemplate: RtcMembershipData = {
185+
slot_id: "m.call#",
186+
application: { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
187+
member: { user_id: "@alice:example.org", device_id: "AAAAAAA", id: "xyzHASHxyz" },
188+
rtc_transports: [{ type: "livekit" }],
189+
versions: [],
190+
msc4354_sticky_key: "abc123",
191+
};
192+
193+
it("rejects membership with no slot_id", () => {
194+
expect(() => {
195+
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
196+
}).toThrow();
197+
});
198+
it("rejects membership with invalid slot_id", () => {
199+
expect(() => {
200+
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "invalid_slot_id" });
201+
}).toThrow();
202+
});
203+
it("accepts membership with valid slot_id", () => {
204+
expect(() => {
205+
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: "m.call#" });
206+
}).not.toThrow();
207+
});
208+
209+
it("rejects membership with no application", () => {
210+
expect(() => {
211+
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
212+
}).toThrow();
213+
});
214+
215+
it("rejects membership with incorrect application", () => {
216+
expect(() => {
217+
new CallMembership(makeMockEvent(), {
218+
...membershipTemplate,
219+
application: { wrong_type_key: "unknown" },
220+
});
221+
}).toThrow();
222+
});
223+
224+
it("rejects membership with no member", () => {
225+
expect(() => {
226+
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
227+
}).toThrow();
228+
});
229+
230+
it("rejects membership with incorrect member", () => {
231+
expect(() => {
232+
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
233+
}).toThrow();
234+
expect(() => {
235+
new CallMembership(makeMockEvent(), {
236+
...membershipTemplate,
237+
member: { id: "test", device_id: "test", user_id_wrong: "test" },
238+
});
239+
}).toThrow();
240+
expect(() => {
241+
new CallMembership(makeMockEvent(), {
242+
...membershipTemplate,
243+
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
244+
});
245+
}).toThrow();
246+
expect(() => {
247+
new CallMembership(makeMockEvent(), {
248+
...membershipTemplate,
249+
member: { id: "test", device_id: "test", user_id: "@@test" },
250+
});
251+
}).toThrow();
252+
expect(() => {
253+
new CallMembership(makeMockEvent(), {
254+
...membershipTemplate,
255+
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
256+
});
257+
}).toThrow();
258+
});
259+
it("rejects membership with incorrect sticky_key", () => {
260+
expect(() => {
261+
new CallMembership(makeMockEvent(), membershipTemplate);
262+
}).not.toThrow();
263+
expect(() => {
264+
new CallMembership(makeMockEvent(), {
265+
...membershipTemplate,
266+
sticky_key: 1,
267+
msc4354_sticky_key: undefined,
268+
});
269+
}).toThrow();
270+
expect(() => {
271+
new CallMembership(makeMockEvent(), {
272+
...membershipTemplate,
273+
sticky_key: "1",
274+
msc4354_sticky_key: undefined,
275+
});
276+
}).not.toThrow();
277+
expect(() => {
278+
new CallMembership(makeMockEvent(), { ...membershipTemplate, msc4354_sticky_key: undefined });
279+
}).toThrow();
280+
expect(() => {
281+
new CallMembership(makeMockEvent(), {
282+
...membershipTemplate,
283+
msc4354_sticky_key: 1,
284+
sticky_key: "valid",
285+
});
286+
}).toThrow();
287+
expect(() => {
288+
new CallMembership(makeMockEvent(), {
289+
...membershipTemplate,
290+
msc4354_sticky_key: "valid",
291+
sticky_key: "valid",
292+
});
293+
}).not.toThrow();
294+
expect(() => {
295+
new CallMembership(makeMockEvent(), {
296+
...membershipTemplate,
297+
msc4354_sticky_key: "valid_but_different",
298+
sticky_key: "valid",
299+
});
300+
}).toThrow();
301+
});
302+
303+
it("considers memberships unexpired if local age low enough", () => {
304+
// TODO link prev event
305+
});
306+
307+
it("considers memberships expired if local age large enough", () => {
308+
// TODO link prev event
309+
});
310+
311+
describe("getTransport", () => {
312+
it("gets the correct active transport with oldest_membership", () => {
313+
const oldestMembership = new CallMembership(makeMockEvent(), {
314+
...membershipTemplate,
315+
rtc_transports: [{ type: "oldest_transport" }],
316+
});
317+
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
318+
319+
// if we are the oldest member we use our focus.
320+
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
321+
322+
// If there is an older member we use our own focus focus. (RtcMembershipData always uses multi sfu)
323+
expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
324+
});
325+
});
326+
describe("correct values from computed fields", () => {
327+
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
328+
it("returns correct sender", () => {
329+
expect(membership.sender).toBe("@alice:example.org");
330+
});
331+
it("returns correct eventId", () => {
332+
expect(membership.eventId).toBe("$eventid");
333+
});
334+
it("returns correct slot_id", () => {
335+
expect(membership.slotId).toBe("m.call#");
336+
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
337+
});
338+
it("returns correct deviceId", () => {
339+
expect(membership.deviceId).toBe("AAAAAAA");
340+
});
341+
it("returns correct call intent", () => {
342+
expect(membership.callIntent).toBe("voice");
343+
});
344+
it("returns correct application", () => {
345+
expect(membership.application).toStrictEqual("m.call");
346+
});
347+
it("returns correct applicationData", () => {
348+
expect(membership.applicationData).toStrictEqual({
349+
"type": "m.call",
350+
"m.call.id": "",
351+
"m.call.intent": "voice",
352+
});
353+
});
354+
it("returns correct scope", () => {
355+
expect(membership.scope).toBe(undefined);
356+
});
357+
it("returns correct membershipID", () => {
358+
expect(membership.membershipID).toBe("xyzHASHxyz");
359+
});
360+
it("returns correct unused fields", () => {
361+
expect(membership.getAbsoluteExpiry()).toBe(undefined);
362+
expect(membership.getMsUntilExpiry()).toBe(undefined);
363+
expect(membership.isExpired()).toBe(false);
364+
});
102365
});
103366
});
104367

spec/unit/matrixrtc/LivekitFocus.spec.ts renamed to spec/unit/matrixrtc/LivekitTransport.spec.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,47 +14,51 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
17+
import {
18+
isLivekitTransport,
19+
isLivekitFocusSelection,
20+
isLivekitTransportConfig,
21+
} from "../../../src/matrixrtc/LivekitTransport";
1822

1923
describe("LivekitFocus", () => {
2024
it("isLivekitFocus", () => {
2125
expect(
22-
isLivekitFocus({
26+
isLivekitTransport({
2327
type: "livekit",
2428
livekit_service_url: "http://test.com",
2529
livekit_alias: "test",
2630
}),
2731
).toBeTruthy();
28-
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
32+
expect(isLivekitTransport({ type: "livekit" })).toBeFalsy();
2933
expect(
30-
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
34+
isLivekitTransport({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
3135
).toBeFalsy();
3236
expect(
33-
isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
37+
isLivekitTransport({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }),
3438
).toBeFalsy();
3539
expect(
36-
isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
40+
isLivekitTransport({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
3741
).toBeFalsy();
3842
});
3943
it("isLivekitFocusActive", () => {
4044
expect(
41-
isLivekitFocusActive({
45+
isLivekitFocusSelection({
4246
type: "livekit",
4347
focus_selection: "oldest_membership",
4448
}),
4549
).toBeTruthy();
46-
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
47-
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
50+
expect(isLivekitFocusSelection({ type: "livekit" })).toBeFalsy();
51+
expect(isLivekitFocusSelection({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
4852
});
4953
it("isLivekitFocusConfig", () => {
5054
expect(
51-
isLivekitFocusConfig({
55+
isLivekitTransportConfig({
5256
type: "livekit",
5357
livekit_service_url: "http://test.com",
5458
}),
5559
).toBeTruthy();
56-
expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy();
57-
expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
58-
expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
60+
expect(isLivekitTransportConfig({ type: "livekit" })).toBeFalsy();
61+
expect(isLivekitTransportConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
62+
expect(isLivekitTransportConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
5963
});
6064
});

0 commit comments

Comments
 (0)