Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
41a2f47
WIP
robintown Nov 19, 2024
209eecd
temp
toger5 Aug 27, 2025
6156d4c
Fix imports
robintown Aug 27, 2025
b61e39a
Fix checkSessionsMembershipData thinking foci_preferred is required
robintown Aug 27, 2025
ca4a9c6
Merge branch 'develop' into voip-team/multi-SFU
robintown Sep 25, 2025
29879e8
incorporate CallMembership changes
toger5 Sep 30, 2025
86f33f9
use correct event type
toger5 Sep 30, 2025
bb7c23d
fix sonar cube conerns
toger5 Sep 30, 2025
8a5a8cd
callMembership tests
toger5 Sep 30, 2025
25f4d6f
make test correct
toger5 Sep 30, 2025
84a3d56
make sonar cube happy (it does not know about the type constraints...)
toger5 Sep 30, 2025
5bc970c
remove created_ts from RtcMembership
toger5 Sep 30, 2025
d94d02d
fix imports
toger5 Sep 30, 2025
74b793c
Update src/matrixrtc/IMembershipManager.ts
toger5 Oct 1, 2025
e829a7b
rename LivekitFocus.ts -> LivekitTransport.ts
toger5 Oct 1, 2025
f70cb14
add details to `getTransport`
toger5 Oct 1, 2025
a343e8c
Merge branch 'develop' into voip-team/multi-SFU
toger5 Oct 1, 2025
11f610d
review
toger5 Oct 7, 2025
66f202a
use DEFAULT_EXPIRE_DURATION in tests
toger5 Oct 7, 2025
4643844
fix test `does not provide focus if the selection method is unknown`
toger5 Oct 7, 2025
1057495
Update src/matrixrtc/CallMembership.ts
toger5 Oct 8, 2025
7b0dbbf
Move `m.call.intent` into the `application` section for rtc member ev…
toger5 Oct 8, 2025
cf14007
review on rtc object validation code.
toger5 Oct 8, 2025
23b60c4
user id check
toger5 Oct 8, 2025
62b5b50
review: Refactor RTC membership handling and improve error handling
toger5 Oct 8, 2025
5a3a26a
docstring updates
toger5 Oct 8, 2025
093c561
add back deprecated `getFocusInUse` & `getActiveFocus`
toger5 Oct 8, 2025
a850496
ci
toger5 Oct 8, 2025
6513f3b
Update src/matrixrtc/CallMembership.ts
toger5 Oct 8, 2025
27801ce
lint
toger5 Oct 8, 2025
d1df0c8
make test less strict for ew tests
toger5 Oct 8, 2025
b9cd1e0
Typescript downstream test adjustments
toger5 Oct 8, 2025
caeae6c
err
toger5 Oct 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 221 additions & 11 deletions spec/unit/matrixrtc/CallMembership.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ import {
CallMembership,
type SessionMembershipData,
DEFAULT_EXPIRE_DURATION,
type RtcMembershipData,
} from "../../../src/matrixrtc/CallMembership";
import { membershipTemplate } from "./mocks";

function makeMockEvent(originTs = 0): MatrixEvent {
return {
getTs: jest.fn().mockReturnValue(originTs),
getSender: jest.fn().mockReturnValue("@alice:example.org"),
getId: jest.fn().mockReturnValue("$eventid"),
} as unknown as MatrixEvent;
}

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

const membershipTemplate: SessionMembershipData = {
call_id: "",
scope: "m.room",
application: "m.call",
device_id: "AAAAAAA",
focus_active: { type: "livekit" },
foci_preferred: [{ type: "livekit" }],
"call_id": "",
"scope": "m.room",
"application": "m.call",
"device_id": "AAAAAAA",
"focus_active": { type: "livekit", focus_selection: "oldest_membership" },
"foci_preferred": [{ type: "livekit" }],
"m.call.intent": "voice",
};

it("rejects membership with no device_id", () => {
Expand Down Expand Up @@ -94,11 +97,218 @@ describe("CallMembership", () => {
it("returns preferred foci", () => {
const fakeEvent = makeMockEvent();
const mockFocus = { type: "this_is_a_mock_focus" };
const membership = new CallMembership(
fakeEvent,
Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }),
);
expect(membership.getPreferredFoci()).toEqual([mockFocus]);
const membership = new CallMembership(fakeEvent, { ...membershipTemplate, foci_preferred: [mockFocus] });
expect(membership.transports).toEqual([mockFocus]);
});

describe("getTransport", () => {
const mockFocus = { type: "this_is_a_mock_focus" };
const oldestMembership = new CallMembership(makeMockEvent(), membershipTemplate);
it("gets the correct active transport with oldest_membership", () => {
const membership = new CallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
});

// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);

// If there is an older member we use its focus.
expect(membership.getTransport(oldestMembership)).toBe(membershipTemplate.foci_preferred[0]);
});

it("gets the correct active transport with multi_sfu", () => {
const membership = new CallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
});

// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);

// If there is an older member we still use our own focus in multi sfu.
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
});
it("does not provide focus if the selection method is unknown", () => {
const membership = new CallMembership(makeMockEvent(), {
...membershipTemplate,
foci_preferred: [mockFocus],
focus_active: { type: "livekit", focus_selection: "unknown" },
});

// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toBeUndefined();
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
it("returns correct eventId", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
expect(membership.slotId).toBe("m.call#");
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
});
it("returns correct deviceId", () => {
expect(membership.deviceId).toBe("AAAAAAA");
});
it("returns correct call intent", () => {
expect(membership.callIntent).toBe("voice");
});
it("returns correct application", () => {
expect(membership.application).toStrictEqual("m.call");
});
it("returns correct applicationData", () => {
expect(membership.applicationData).toStrictEqual({ "type": "m.call", "m.call.intent": "voice" });
});
it("returns correct scope", () => {
expect(membership.scope).toBe("m.room");
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("0");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
expect(membership.isExpired()).toBe(true);
});
});
});

describe("RtcMembershipData", () => {
const membershipTemplate: RtcMembershipData = {
"slot_id": "m.call#",
"application": { "type": "m.call", "m.call.id": "", "m.call.intent": "voice" },
"member": { user_id: "@alice:example.org", device_id: "AAAAAAA", id: "xyzHASHxyz" },
"rtc_transports": [{ type: "livekit" }],
"m.call.intent": "voice",
"versions": [],
};

it("rejects membership with no slot_id", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, slot_id: undefined });
}).toThrow();
});

it("rejects membership with no application", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, application: undefined });
}).toThrow();
});

it("rejects membership with incorrect application", () => {
expect(() => {
new CallMembership(makeMockEvent(), {
...membershipTemplate,
application: { wrong_type_key: "unknown" },
});
}).toThrow();
});

it("rejects membership with no member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: undefined });
}).toThrow();
});

it("rejects membership with incorrect member", () => {
expect(() => {
new CallMembership(makeMockEvent(), { ...membershipTemplate, member: { i: "test" } });
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@@test" },
});
}).toThrow();
expect(() => {
new CallMembership(makeMockEvent(), {
...membershipTemplate,
member: { id: "test", device_id: "test", user_id: "@test:user.id" },
});
}).not.toThrow();
});

it("considers memberships unexpired if local age low enough", () => {
// TODO link prev event
});

it("considers memberships expired if local age large enough", () => {
// TODO link prev event
});

describe("getTransport", () => {
it("gets the correct active transport with oldest_membership", () => {
const oldestMembership = new CallMembership(makeMockEvent(), {
...membershipTemplate,
rtc_transports: [{ type: "oldest_transport" }],
});
const membership = new CallMembership(makeMockEvent(), membershipTemplate);

// if we are the oldest member we use our focus.
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });

// If there is an older member we use our own focus focus. (RtcMembershipData always uses multi sfu)
expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
});
});
describe("correct values from computed fields", () => {
const membership = new CallMembership(makeMockEvent(), membershipTemplate);
it("returns correct sender", () => {
expect(membership.sender).toBe("@alice:example.org");
});
it("returns correct eventId", () => {
expect(membership.eventId).toBe("$eventid");
});
it("returns correct slot_id", () => {
expect(membership.slotId).toBe("m.call#");
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
});
it("returns correct deviceId", () => {
expect(membership.deviceId).toBe("AAAAAAA");
});
it("returns correct call intent", () => {
expect(membership.callIntent).toBe("voice");
});
it("returns correct application", () => {
expect(membership.application).toStrictEqual("m.call");
});
it("returns correct applicationData", () => {
expect(membership.applicationData).toStrictEqual({
"type": "m.call",
"m.call.id": "",
"m.call.intent": "voice",
});
});
it("returns correct scope", () => {
expect(membership.scope).toBe(undefined);
});
it("returns correct membershipID", () => {
expect(membership.membershipID).toBe("xyzHASHxyz");
});
it("returns correct unused fields", () => {
expect(membership.getAbsoluteExpiry()).toBe(undefined);
expect(membership.getMsUntilExpiry()).toBe(undefined);
expect(membership.isExpired()).toBe(false);
});
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus";
import {
isLivekitTransport,
isLivekitFocusSelection,
isLivekitTransportConfig,
} from "../../../src/matrixrtc/LivekitTransport";

describe("LivekitFocus", () => {
it("isLivekitFocus", () => {
expect(
isLivekitFocus({
isLivekitTransport({
type: "livekit",
livekit_service_url: "http://test.com",
livekit_alias: "test",
}),
).toBeTruthy();
expect(isLivekitFocus({ type: "livekit" })).toBeFalsy();
expect(isLivekitTransport({ type: "livekit" })).toBeFalsy();
expect(
isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }),
isLivekitTransport({ 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" }),
isLivekitTransport({ 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" }),
isLivekitTransport({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }),
).toBeFalsy();
});
it("isLivekitFocusActive", () => {
expect(
isLivekitFocusActive({
isLivekitFocusSelection({
type: "livekit",
focus_selection: "oldest_membership",
}),
).toBeTruthy();
expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
expect(isLivekitFocusSelection({ type: "livekit" })).toBeFalsy();
expect(isLivekitFocusSelection({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy();
});
it("isLivekitFocusConfig", () => {
expect(
isLivekitFocusConfig({
isLivekitTransportConfig({
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();
expect(isLivekitTransportConfig({ type: "livekit" })).toBeFalsy();
expect(isLivekitTransportConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy();
expect(isLivekitTransportConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy();
});
});
10 changes: 6 additions & 4 deletions spec/unit/matrixrtc/MatrixRTCSession.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ describe("MatrixRTCSession", () => {

sess = MatrixRTCSession.sessionForRoom(client, mockRoom, callSession);
expect(sess?.memberships.length).toEqual(1);
expect(sess?.memberships[0].sessionDescription.id).toEqual("");
expect(sess?.memberships[0].slotDescription.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?.sessionDescription.id).toEqual("");
expect(sess?.slotDescription.id).toEqual("");
});

it("ignores memberships where application is not m.call", () => {
Expand Down Expand Up @@ -268,7 +268,9 @@ describe("MatrixRTCSession", () => {
type: "livekit",
focus_selection: "oldest_membership",
});
expect(sess.getActiveFocus()).toBe(firstPreferredFocus);
expect(sess.resolveActiveFocus(sess.memberships.find((m) => m.deviceId === "old"))).toBe(
firstPreferredFocus,
);
jest.useRealTimers();
});
it("does not provide focus if the selection method is unknown", () => {
Expand All @@ -288,7 +290,7 @@ describe("MatrixRTCSession", () => {
type: "livekit",
focus_selection: "unknown",
});
expect(sess.getActiveFocus()).toBe(undefined);
expect(sess.resolveActiveFocus(sess.memberships.find((m) => m.deviceId === "old"))).toBe(undefined);
});
});

Expand Down
Loading