mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-29 21:15:19 +00:00
Merge pull request #4208 from element-hq/remove-legacy-mode
Remove MatrixRTC legacy mode
This commit is contained in:
@@ -1,36 +1,24 @@
|
|||||||
# MatrixRTC modes
|
# MatrixRTC modes
|
||||||
|
|
||||||
Element Call is in the middle of a transition of how a call session is
|
Element Call is in the middle of a transition of how a call session is
|
||||||
represented and how participants pick an SFU:
|
represented: from room _state_ events (`org.matrix.msc3401.call.member`) to
|
||||||
|
_sticky_ events
|
||||||
|
([MSC4354](https://github.com/matrix-org/matrix-spec-proposals/pull/4354)),
|
||||||
|
which are a much better fit for the short lived, per-device nature of call
|
||||||
|
memberships.
|
||||||
|
|
||||||
- **Membership events**: from room _state_ events
|
Not every homeserver supports sticky events yet. The two MatrixRTC modes
|
||||||
(`org.matrix.msc3401.call.member`) to _sticky_ events
|
controls whether Element Call uses them.
|
||||||
([MSC4354](https://github.com/matrix-org/matrix-spec-proposals/pull/4354)),
|
|
||||||
which are a much better fit for the short lived, per-device nature of call
|
|
||||||
memberships.
|
|
||||||
- **SFU selection**: from "everyone connects to the SFU of the oldest member" to
|
|
||||||
**multi SFU**, where each participant uses its own homeserver's SFU and the
|
|
||||||
SFUs interconnect.
|
|
||||||
|
|
||||||
Not every homeserver supports sticky events yet. Multi SFU is supported on all current (August 2026)
|
|
||||||
element call clients. The three MatrixRTC modes are the steps of that transition,
|
|
||||||
so a deployment can pick the newest one its homeserver and its user base can
|
|
||||||
handle.
|
|
||||||
|
|
||||||
## The modes
|
## The modes
|
||||||
|
|
||||||
| Mode | Membership events | SFU selection | JWT endpoint |
|
| Mode | Membership events | SFU selection | JWT endpoint |
|
||||||
| --------------- | ----------------- | ------------- | ---------------------------- |
|
| --------------- | ----------------- | ------------- | ---------------------------- |
|
||||||
| `legacy` | state events | oldest member | legacy |
|
|
||||||
| `compatibility` | state events | multi SFU | legacy |
|
| `compatibility` | state events | multi SFU | legacy |
|
||||||
| `matrix_2_0` | sticky events | multi SFU | Matrix 2.0 (hashed identity) |
|
| `matrix_2_0` | sticky events | multi SFU | Matrix 2.0 (hashed identity) |
|
||||||
|
|
||||||
**`legacy`** — the lowest common denominator. Use it if calls need to work with
|
**`compatibility`** — multi SFU, but still state events. Use it when the
|
||||||
Element Call clients older than v0.17.0, which cannot handle multi SFU calls. (unused)
|
homeserver does not support sticky events. This is the default.
|
||||||
|
|
||||||
**`compatibility`** — multi SFU, but still state events. Use it when all Element
|
|
||||||
Call clients are v0.17.0 or later but the homeserver does not support sticky
|
|
||||||
events. This is the default. (default)
|
|
||||||
|
|
||||||
**`matrix_2_0`** — the target state. Requires a homeserver that advertises
|
**`matrix_2_0`** — the target state. Requires a homeserver that advertises
|
||||||
MSC4354 and all clients on v0.17.0 or later. The local membership requests its
|
MSC4354 and all clients on v0.17.0 or later. The local membership requests its
|
||||||
@@ -54,7 +42,7 @@ disables the Developer Settings choice:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Valid values are `legacy`, `compatibility` and `matrix_2_0`; an invalid value is
|
Valid values are `compatibility` and `matrix_2_0`; an invalid value is
|
||||||
ignored (with a warning) and the user's choice applies. Pinning `matrix_2_0` on a
|
ignored (with a warning) and the user's choice applies. Pinning `matrix_2_0` on a
|
||||||
homeserver without sticky event support makes joining fail with a "sticky events
|
homeserver without sticky event support makes joining fail with a "sticky events
|
||||||
required" error.
|
required" error.
|
||||||
|
|||||||
@@ -80,10 +80,6 @@
|
|||||||
"description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)",
|
"description": "Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)",
|
||||||
"label": "Compatibility: state events & multi SFU"
|
"label": "Compatibility: state events & multi SFU"
|
||||||
},
|
},
|
||||||
"Legacy": {
|
|
||||||
"description": "Compatible with old versions of EC that do not support multi SFU",
|
|
||||||
"label": "Legacy: state events & oldest membership SFU"
|
|
||||||
},
|
|
||||||
"Matrix_2_0": {
|
"Matrix_2_0": {
|
||||||
"description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later",
|
"description": "Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later",
|
||||||
"label": "Matrix 2.0: sticky events & multi SFU"
|
"label": "Matrix 2.0: sticky events & multi SFU"
|
||||||
|
|||||||
@@ -101,9 +101,7 @@ async function setRtcModeFromSettings(
|
|||||||
|
|
||||||
// Move to Developer tab now
|
// Move to Developer tab now
|
||||||
await page.getByRole("tab", { name: "Developer" }).click();
|
await page.getByRole("tab", { name: "Developer" }).click();
|
||||||
if (mode == "legacy") {
|
if (mode == "2_0") {
|
||||||
await page.getByText("Legacy: state events").click();
|
|
||||||
} else if (mode == "2_0") {
|
|
||||||
await page.getByText("Matrix 2.0").click();
|
await page.getByText("Matrix 2.0").click();
|
||||||
} else {
|
} else {
|
||||||
// compat
|
// compat
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ import { HOST1, HOST2, type RtcMode, TestHelpers } from "./test-helpers";
|
|||||||
|
|
||||||
const modePairs: [RtcMode, RtcMode][] = [
|
const modePairs: [RtcMode, RtcMode][] = [
|
||||||
["compat", "compat"],
|
["compat", "compat"],
|
||||||
["legacy", "legacy"],
|
// TODO: Compatibility + Matrix 2.0?
|
||||||
["legacy", "compat"],
|
|
||||||
["compat", "legacy"],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
modePairs.forEach(([rtcMode1, rtcMode2]) => {
|
modePairs.forEach(([rtcMode1, rtcMode2]) => {
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2026 Element Creations Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
import { widgetTest } from "../fixtures/widget-user";
|
|
||||||
import { HOST1, HOST2, TestHelpers } from "./test-helpers";
|
|
||||||
|
|
||||||
widgetTest(
|
|
||||||
"Bug new joiner was not publishing on correct SFU",
|
|
||||||
async ({ addUser, browserName }) => {
|
|
||||||
test.skip(
|
|
||||||
browserName === "firefox",
|
|
||||||
"This is a bug in the old widget, not a browser problem.",
|
|
||||||
);
|
|
||||||
|
|
||||||
test.slow();
|
|
||||||
|
|
||||||
// 2 users in federation
|
|
||||||
const florian = await addUser("floriant", HOST1);
|
|
||||||
const timo = await addUser("timo", HOST2);
|
|
||||||
|
|
||||||
// Florian creates a room and invites Timo to it
|
|
||||||
const roomName = "Call Room";
|
|
||||||
await TestHelpers.createRoom(roomName, florian.page, [timo.mxId]);
|
|
||||||
|
|
||||||
// Timo joins the room
|
|
||||||
await TestHelpers.acceptRoomInvite(roomName, timo.page);
|
|
||||||
|
|
||||||
// Ensure we are in legacy mode (should be the default)
|
|
||||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
|
||||||
florian.page,
|
|
||||||
"legacy",
|
|
||||||
);
|
|
||||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
|
||||||
timo.page,
|
|
||||||
"legacy",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Let timo create a call
|
|
||||||
await TestHelpers.startCallInCurrentRoom(timo.page, false);
|
|
||||||
await TestHelpers.joinCallFromLobby(timo.page);
|
|
||||||
|
|
||||||
// We want to simulate that the oldest membership authentication is way slower than
|
|
||||||
// the preffered auth.
|
|
||||||
// In this setup, timo advertised$ transport will be it's own, and the active will be the one from florian
|
|
||||||
await florian.page.route(
|
|
||||||
"**/matrix-rtc.othersite.m.localhost/livekit/jwt/**",
|
|
||||||
async (route) => {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // 5 second delay
|
|
||||||
await route.continue();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Florian joins the call
|
|
||||||
await expect(florian.page.getByTestId("join-call-button")).toBeVisible();
|
|
||||||
await florian.page.getByTestId("join-call-button").click();
|
|
||||||
await TestHelpers.joinCallFromLobby(florian.page);
|
|
||||||
|
|
||||||
await florian.page.waitForTimeout(3000);
|
|
||||||
await timo.page.waitForTimeout(3000);
|
|
||||||
|
|
||||||
// We should see 2 video tiles everywhere now
|
|
||||||
for (const user of [timo, florian]) {
|
|
||||||
const frame = user.page
|
|
||||||
.locator('iframe[title="Element Call"]')
|
|
||||||
.contentFrame();
|
|
||||||
await expect(frame.getByTestId("videoTile")).toHaveCount(2);
|
|
||||||
|
|
||||||
// No one should be waiting for media
|
|
||||||
await expect(frame.getByText("Waiting for media...")).not.toBeVisible();
|
|
||||||
|
|
||||||
// There should be 2 video elements, visible and autoplaying
|
|
||||||
await expect(frame.locator("video")).toHaveCount(2, {
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await TestHelpers.expectVisibleVideoCount(frame, 2);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2026 Element Creations Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { expect, test } from "@playwright/test";
|
|
||||||
|
|
||||||
import { widgetTest } from "../fixtures/widget-user";
|
|
||||||
import { HOST1, HOST2, TestHelpers } from "./test-helpers";
|
|
||||||
|
|
||||||
// ## Issue
|
|
||||||
// This test reproduces an issue with the publisher.
|
|
||||||
// When switching local focus, we need to recreate the publisher.
|
|
||||||
// This failed because of a dead lock in the old publishers destruction.
|
|
||||||
//
|
|
||||||
// There are numerus ways to enforece this situation:
|
|
||||||
// - oldest member swap (manually set the oldest member focus and leave with the prev oldest member)
|
|
||||||
// This almost never happens in the real worls since clients will set their preferredFoci list to what the oldest member is.
|
|
||||||
// - switch from oldest member to multi sfu as the NOT the first joiner + the first joiner is on a different sfu than your preferred sfu.
|
|
||||||
//
|
|
||||||
// This test uses the "switch from oldest member to multi sfu" approach.
|
|
||||||
//
|
|
||||||
// It is a copy of federated-call.test.ts in the `["legacy", "legacy"]` setup,
|
|
||||||
// which once connected will make the second user switch to multi sfu.
|
|
||||||
widgetTest(
|
|
||||||
`Test swapping publisher from ${HOST1} to ${HOST2}`,
|
|
||||||
async ({ addUser, browserName }) => {
|
|
||||||
test.slow();
|
|
||||||
test.skip(
|
|
||||||
browserName === "firefox",
|
|
||||||
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
|
|
||||||
);
|
|
||||||
|
|
||||||
const florian = await addUser("floriant", HOST1);
|
|
||||||
const timo = await addUser("timo", HOST2);
|
|
||||||
|
|
||||||
const roomName = "Call Room";
|
|
||||||
|
|
||||||
await TestHelpers.createRoom(roomName, florian.page, [timo.mxId]);
|
|
||||||
|
|
||||||
await TestHelpers.acceptRoomInvite(roomName, timo.page);
|
|
||||||
|
|
||||||
await florian.page.pause();
|
|
||||||
|
|
||||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
|
||||||
florian.page,
|
|
||||||
"legacy",
|
|
||||||
);
|
|
||||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
|
||||||
timo.page,
|
|
||||||
"legacy",
|
|
||||||
);
|
|
||||||
|
|
||||||
await TestHelpers.startCallInCurrentRoom(florian.page, false);
|
|
||||||
await TestHelpers.joinCallFromLobby(florian.page);
|
|
||||||
|
|
||||||
// timo joins
|
|
||||||
await TestHelpers.joinCallInCurrentRoom(timo.page);
|
|
||||||
|
|
||||||
// We should see 2 video tiles everywhere now
|
|
||||||
for (const user of [timo, florian]) {
|
|
||||||
const frame = user.page
|
|
||||||
.locator('iframe[title="Element Call"]')
|
|
||||||
.contentFrame();
|
|
||||||
await expect(frame.getByTestId("videoTile")).toHaveCount(2);
|
|
||||||
|
|
||||||
// Wait for "Waiting for media..." to disappear (with timeout)
|
|
||||||
await expect(frame.getByText("Waiting for media...")).not.toBeVisible({
|
|
||||||
timeout: 10000, // Maximum time to wait
|
|
||||||
});
|
|
||||||
|
|
||||||
// There should be 2 video elements, visible and autoplaying
|
|
||||||
await expect(frame.locator("video")).toHaveCount(2, {
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
await TestHelpers.expectVisibleVideoCount(frame, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// now we switch the mode for timo (second joiner on multi-sfu HOST2 but currently HOST1)
|
|
||||||
await TestHelpers.setEmbeddedElementCallRtcMode(timo.page, "compat");
|
|
||||||
await timo.page.waitForTimeout(3000);
|
|
||||||
|
|
||||||
await TestHelpers.expectVisibleVideoCount(
|
|
||||||
timo.page.locator('iframe[title="Element Call"]').contentFrame(),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -21,7 +21,7 @@ const PASSWORD = "foobarbaz1!";
|
|||||||
export const HOST1 = "https://app.m.localhost/#/welcome";
|
export const HOST1 = "https://app.m.localhost/#/welcome";
|
||||||
export const HOST2 = "https://app.othersite.m.localhost/#/welcome";
|
export const HOST2 = "https://app.othersite.m.localhost/#/welcome";
|
||||||
|
|
||||||
export type RtcMode = "legacy" | "compat" | "2_0";
|
export type RtcMode = "compat" | "2_0";
|
||||||
|
|
||||||
export class TestHelpers {
|
export class TestHelpers {
|
||||||
public static async startCallInCurrentRoom(
|
public static async startCallInCurrentRoom(
|
||||||
@@ -309,9 +309,7 @@ export class TestHelpers {
|
|||||||
|
|
||||||
// Move to Developer tab now
|
// Move to Developer tab now
|
||||||
await iframe.getByRole("tab", { name: "Developer" }).click();
|
await iframe.getByRole("tab", { name: "Developer" }).click();
|
||||||
if (mode == "legacy") {
|
if (mode == "2_0") {
|
||||||
await iframe.getByText("Legacy: state events").click();
|
|
||||||
} else if (mode == "2_0") {
|
|
||||||
await iframe.getByText("Matrix 2.0").click();
|
await iframe.getByText("Matrix 2.0").click();
|
||||||
} else {
|
} else {
|
||||||
// compat
|
// compat
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
* Settings, or pinned for a deployment via `matrix_rtc_mode` in config.json.
|
* Settings, or pinned for a deployment via `matrix_rtc_mode` in config.json.
|
||||||
*/
|
*/
|
||||||
export enum MatrixRTCMode {
|
export enum MatrixRTCMode {
|
||||||
/** Legacy single-SFU + user-keyed memberships + legacy JWT endpoint. */
|
/** Multi-SFU transport, legacy JWT endpoint, state events. */
|
||||||
Legacy = "legacy",
|
|
||||||
/** Multi-SFU transport, legacy JWT endpoint, no sticky events. */
|
|
||||||
Compatibility = "compatibility",
|
Compatibility = "compatibility",
|
||||||
/**
|
/**
|
||||||
* Multi-SFU transport with:
|
* Multi-SFU transport with:
|
||||||
|
|||||||
@@ -317,19 +317,15 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
|
|
||||||
describe("matrix rtc mode", () => {
|
describe("matrix rtc mode", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
matrixRTCModeSetting.setValue(MatrixRTCMode.Legacy);
|
matrixRTCModeSetting.setValue(MatrixRTCMode.Compatibility);
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
function getModeRadios(): {
|
function getModeRadios(): {
|
||||||
legacy: HTMLInputElement;
|
|
||||||
compatibility: HTMLInputElement;
|
compatibility: HTMLInputElement;
|
||||||
matrix20: HTMLInputElement;
|
matrix20: HTMLInputElement;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
legacy: screen.getByDisplayValue(
|
|
||||||
MatrixRTCMode.Legacy,
|
|
||||||
) as HTMLInputElement,
|
|
||||||
compatibility: screen.getByDisplayValue(
|
compatibility: screen.getByDisplayValue(
|
||||||
MatrixRTCMode.Compatibility,
|
MatrixRTCMode.Compatibility,
|
||||||
) as HTMLInputElement,
|
) as HTMLInputElement,
|
||||||
@@ -359,27 +355,21 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
|
|
||||||
const radios = getModeRadios();
|
const radios = getModeRadios();
|
||||||
expect(radios.compatibility).toBeChecked();
|
expect(radios.compatibility).toBeChecked();
|
||||||
expect(radios.legacy).not.toBeChecked();
|
|
||||||
expect(radios.matrix20).not.toBeChecked();
|
expect(radios.matrix20).not.toBeChecked();
|
||||||
// None are disabled by config; only Matrix_2_0 may be disabled by sticky-events support.
|
// None are disabled by config; only Matrix_2_0 may be disabled by sticky-events support.
|
||||||
expect(radios.legacy).not.toBeDisabled();
|
|
||||||
expect(radios.compatibility).not.toBeDisabled();
|
expect(radios.compatibility).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([MatrixRTCMode.Compatibility, MatrixRTCMode.Matrix_2_0])(
|
||||||
MatrixRTCMode.Legacy,
|
|
||||||
MatrixRTCMode.Compatibility,
|
|
||||||
MatrixRTCMode.Matrix_2_0,
|
|
||||||
])(
|
|
||||||
"disables all radios and shows the config value (%s) as checked when matrix_rtc_mode is set",
|
"disables all radios and shows the config value (%s) as checked when matrix_rtc_mode is set",
|
||||||
async (configMode) => {
|
async (configMode) => {
|
||||||
mockConfig({ matrix_rtc_mode: configMode });
|
mockConfig({ matrix_rtc_mode: configMode });
|
||||||
// Local setting is intentionally different from the config value to
|
// Local setting is intentionally different from the config value to
|
||||||
// prove config wins.
|
// prove config wins.
|
||||||
matrixRTCModeSetting.setValue(
|
matrixRTCModeSetting.setValue(
|
||||||
configMode === MatrixRTCMode.Legacy
|
configMode === MatrixRTCMode.Compatibility
|
||||||
? MatrixRTCMode.Compatibility
|
? MatrixRTCMode.Matrix_2_0
|
||||||
: MatrixRTCMode.Legacy,
|
: MatrixRTCMode.Compatibility,
|
||||||
);
|
);
|
||||||
const client = createMockMatrixClient();
|
const client = createMockMatrixClient();
|
||||||
|
|
||||||
@@ -397,13 +387,11 @@ describe("DeveloperSettingsTab", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const radios = getModeRadios();
|
const radios = getModeRadios();
|
||||||
expect(radios.legacy).toBeDisabled();
|
|
||||||
expect(radios.compatibility).toBeDisabled();
|
expect(radios.compatibility).toBeDisabled();
|
||||||
expect(radios.matrix20).toBeDisabled();
|
expect(radios.matrix20).toBeDisabled();
|
||||||
|
|
||||||
const checkedValue = (
|
const checkedValue = (
|
||||||
{
|
{
|
||||||
[MatrixRTCMode.Legacy]: radios.legacy,
|
|
||||||
[MatrixRTCMode.Compatibility]: radios.compatibility,
|
[MatrixRTCMode.Compatibility]: radios.compatibility,
|
||||||
[MatrixRTCMode.Matrix_2_0]: radios.matrix20,
|
[MatrixRTCMode.Matrix_2_0]: radios.matrix20,
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -520,22 +520,6 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
|||||||
</Heading>
|
</Heading>
|
||||||
{matrixRTCModeForced && <p>Your deployment overrides the mode.</p>}
|
{matrixRTCModeForced && <p>Your deployment overrides the mode.</p>}
|
||||||
<Form>
|
<Form>
|
||||||
<InlineField
|
|
||||||
name={matrixRTCModeRadioGroup}
|
|
||||||
control={
|
|
||||||
<RadioControl
|
|
||||||
checked={effectiveMatrixRTCMode === MatrixRTCMode.Legacy}
|
|
||||||
value={MatrixRTCMode.Legacy}
|
|
||||||
disabled={matrixRTCModeForced}
|
|
||||||
onChange={onMatrixRTCModeChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Label>{t("developer_mode.matrixRTCMode.Legacy.label")}</Label>
|
|
||||||
<HelpMessage>
|
|
||||||
{t("developer_mode.matrixRTCMode.Legacy.description")}
|
|
||||||
</HelpMessage>
|
|
||||||
</InlineField>
|
|
||||||
<InlineField
|
<InlineField
|
||||||
name={matrixRTCModeRadioGroup}
|
name={matrixRTCModeRadioGroup}
|
||||||
control={
|
control={
|
||||||
|
|||||||
@@ -285,50 +285,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_container_1ug7n_10"
|
class="_container_1ug7n_10"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
aria-describedby="radix-_r_a_ radix-_r_c_"
|
||||||
class="_input_1ug7n_18"
|
|
||||||
id="radix-_r_9_"
|
|
||||||
name="_r_0_"
|
|
||||||
title=""
|
|
||||||
type="radio"
|
|
||||||
value="legacy"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="_ui_1ug7n_19"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="_inline-field-body_1o4d9_39"
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
class="_label_1o4d9_60"
|
|
||||||
for="radix-_r_9_"
|
|
||||||
>
|
|
||||||
Legacy: state events & oldest membership SFU
|
|
||||||
</label>
|
|
||||||
<span
|
|
||||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
|
||||||
id="radix-_r_a_"
|
|
||||||
>
|
|
||||||
Compatible with old versions of EC that do not support multi SFU
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="_inline-field_1o4d9_33"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="_inline-field-control_1o4d9_45"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="_container_1ug7n_10"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
|
||||||
checked=""
|
checked=""
|
||||||
class="_input_1ug7n_18"
|
class="_input_1ug7n_18"
|
||||||
id="radix-_r_b_"
|
id="radix-_r_9_"
|
||||||
name="_r_0_"
|
name="_r_0_"
|
||||||
title=""
|
title=""
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -344,13 +304,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="_label_1o4d9_60"
|
class="_label_1o4d9_60"
|
||||||
for="radix-_r_b_"
|
for="radix-_r_9_"
|
||||||
>
|
>
|
||||||
Compatibility: state events & multi SFU
|
Compatibility: state events & multi SFU
|
||||||
</label>
|
</label>
|
||||||
<span
|
<span
|
||||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||||
id="radix-_r_c_"
|
id="radix-_r_a_"
|
||||||
>
|
>
|
||||||
Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)
|
Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)
|
||||||
</span>
|
</span>
|
||||||
@@ -366,9 +326,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_container_1ug7n_10"
|
class="_container_1ug7n_10"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
aria-describedby="radix-_r_a_ radix-_r_c_"
|
||||||
class="_input_1ug7n_18"
|
class="_input_1ug7n_18"
|
||||||
id="radix-_r_d_"
|
id="radix-_r_b_"
|
||||||
name="_r_0_"
|
name="_r_0_"
|
||||||
title=""
|
title=""
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -384,13 +344,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="_label_1o4d9_60"
|
class="_label_1o4d9_60"
|
||||||
for="radix-_r_d_"
|
for="radix-_r_b_"
|
||||||
>
|
>
|
||||||
Matrix 2.0: sticky events & multi SFU
|
Matrix 2.0: sticky events & multi SFU
|
||||||
</label>
|
</label>
|
||||||
<span
|
<span
|
||||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||||
id="radix-_r_e_"
|
id="radix-_r_c_"
|
||||||
>
|
>
|
||||||
Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later
|
Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later
|
||||||
</span>
|
</span>
|
||||||
@@ -491,7 +451,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_k_"
|
aria-describedby="_r_i_"
|
||||||
id="cameraToggle"
|
id="cameraToggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
@@ -521,7 +481,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</label>
|
</label>
|
||||||
<p
|
<p
|
||||||
class="_description_1bd8c0"
|
class="_description_1bd8c0"
|
||||||
id="_r_k_"
|
id="_r_i_"
|
||||||
>
|
>
|
||||||
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
|
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
|
||||||
</p>
|
</p>
|
||||||
@@ -543,7 +503,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_l_"
|
aria-describedby="_r_j_"
|
||||||
id="screenShareToggle"
|
id="screenShareToggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
/>
|
/>
|
||||||
@@ -573,7 +533,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
</label>
|
</label>
|
||||||
<p
|
<p
|
||||||
class="_description_1bd8c0"
|
class="_description_1bd8c0"
|
||||||
id="_r_l_"
|
id="_r_j_"
|
||||||
>
|
>
|
||||||
Configure resolution, framerate, bitrate, and codec for screen sharing
|
Configure resolution, framerate, bitrate, and codec for screen sharing
|
||||||
</p>
|
</p>
|
||||||
@@ -598,7 +558,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_m_"
|
aria-describedby="_r_k_"
|
||||||
checked=""
|
checked=""
|
||||||
id="echoCancellation"
|
id="echoCancellation"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -636,7 +596,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_n_"
|
aria-describedby="_r_l_"
|
||||||
checked=""
|
checked=""
|
||||||
id="noiseSuppression"
|
id="noiseSuppression"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -674,7 +634,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
|||||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
aria-describedby="_r_o_"
|
aria-describedby="_r_m_"
|
||||||
checked=""
|
checked=""
|
||||||
id="autoGainControl"
|
id="autoGainControl"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
|||||||
@@ -258,11 +258,9 @@ function mockRingEvent(
|
|||||||
} as unknown as { event_id: string } & IRTCNotificationContent;
|
} as unknown as { event_id: string } & IRTCNotificationContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe.each([
|
const modes = [[MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0]];
|
||||||
[MatrixRTCMode.Legacy],
|
|
||||||
[MatrixRTCMode.Compatibility],
|
describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
||||||
[MatrixRTCMode.Matrix_2_0],
|
|
||||||
])("CallViewModel (%s mode)", (mode) => {
|
|
||||||
const withCallViewModel = withCallViewModelInMode(mode);
|
const withCallViewModel = withCallViewModelInMode(mode);
|
||||||
|
|
||||||
test("participants are retained during a focus switch", () => {
|
test("participants are retained during a focus switch", () => {
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ export function createCallViewModel$(
|
|||||||
const matrixRTCMode$ =
|
const matrixRTCMode$ =
|
||||||
configMatrixRTCMode !== undefined
|
configMatrixRTCMode !== undefined
|
||||||
? constant(configMatrixRTCMode)
|
? constant(configMatrixRTCMode)
|
||||||
: (options.matrixRTCMode$ ?? constant(MatrixRTCMode.Legacy));
|
: (options.matrixRTCMode$ ?? constant(MatrixRTCMode.Compatibility));
|
||||||
|
|
||||||
// Each hbar seperates a block of input variables required for the CallViewModel to function.
|
// Each hbar seperates a block of input variables required for the CallViewModel to function.
|
||||||
// The outputs of this block is written under the hbar.
|
// The outputs of this block is written under the hbar.
|
||||||
@@ -503,7 +503,6 @@ export function createCallViewModel$(
|
|||||||
mode === MatrixRTCMode.Matrix_2_0
|
mode === MatrixRTCMode.Matrix_2_0
|
||||||
? JwtEndpointVersion.Matrix_2_0
|
? JwtEndpointVersion.Matrix_2_0
|
||||||
: JwtEndpointVersion.Legacy,
|
: JwtEndpointVersion.Legacy,
|
||||||
useOldestMember: mode === MatrixRTCMode.Legacy,
|
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import {
|
|||||||
|
|
||||||
initializeWidget();
|
initializeWidget();
|
||||||
|
|
||||||
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
|
const MATRIX_RTC_MODE = MatrixRTCMode.Compatibility;
|
||||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||||
vi.mock("../../../UrlParams", () => ({ getUrlParams }));
|
vi.mock("../../../UrlParams", () => ({ getUrlParams }));
|
||||||
vi.mock("@livekit/components-core", () => ({
|
vi.mock("@livekit/components-core", () => ({
|
||||||
@@ -71,12 +71,6 @@ vi.mock("@livekit/components-core", () => ({
|
|||||||
describe("LocalMembership", () => {
|
describe("LocalMembership", () => {
|
||||||
describe("enterRTCSession", () => {
|
describe("enterRTCSession", () => {
|
||||||
it("It joins the correct Session", () => {
|
it("It joins the correct Session", () => {
|
||||||
const focusFromOlderMembership = {
|
|
||||||
type: "livekit",
|
|
||||||
livekit_service_url: "http://my-oldest-member-service-url.com",
|
|
||||||
livekit_alias: "my-oldest-member-service-alias",
|
|
||||||
};
|
|
||||||
|
|
||||||
mockConfig({
|
mockConfig({
|
||||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||||
});
|
});
|
||||||
@@ -95,10 +89,6 @@ describe("LocalMembership", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
memberships: [],
|
memberships: [],
|
||||||
getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership),
|
|
||||||
getOldestMembership: vi.fn().mockReturnValue({
|
|
||||||
getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]),
|
|
||||||
}),
|
|
||||||
joinRTCSession: vi.fn(),
|
joinRTCSession: vi.fn(),
|
||||||
}) as unknown as MatrixRTCSession;
|
}) as unknown as MatrixRTCSession;
|
||||||
|
|
||||||
@@ -122,14 +112,12 @@ describe("LocalMembership", () => {
|
|||||||
memberId: "@alice:example.org:DEVICE",
|
memberId: "@alice:example.org:DEVICE",
|
||||||
userId: "@alice:example.org",
|
userId: "@alice:example.org",
|
||||||
},
|
},
|
||||||
[
|
[],
|
||||||
{
|
{
|
||||||
livekit_alias: "roomId",
|
livekit_alias: "roomId",
|
||||||
livekit_service_url: "http://my-livekit-service-url.com",
|
livekit_service_url: "http://my-livekit-service-url.com",
|
||||||
type: "livekit",
|
type: "livekit",
|
||||||
},
|
},
|
||||||
],
|
|
||||||
undefined,
|
|
||||||
expect.objectContaining({ manageMediaKeys: true }),
|
expect.objectContaining({ manageMediaKeys: true }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ export type LocalMemberState =
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* - get oldest membership
|
|
||||||
* - get transport to use
|
* - get transport to use
|
||||||
* - get openId + jwt token
|
* - get openId + jwt token
|
||||||
* - wait for createTrack() call
|
* - wait for createTrack() call
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
it,
|
it,
|
||||||
type MockedObject,
|
type MockedObject,
|
||||||
vi,
|
vi,
|
||||||
type MockInstance,
|
|
||||||
} from "vitest";
|
} from "vitest";
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
@@ -26,7 +25,6 @@ import {
|
|||||||
mockConfig,
|
mockConfig,
|
||||||
flushPromises,
|
flushPromises,
|
||||||
ownMemberMock,
|
ownMemberMock,
|
||||||
mockRtcMembership,
|
|
||||||
testScope,
|
testScope,
|
||||||
} from "../../../utils/test";
|
} from "../../../utils/test";
|
||||||
import {
|
import {
|
||||||
@@ -35,7 +33,7 @@ import {
|
|||||||
type LocalTransportWithSFUConfig,
|
type LocalTransportWithSFUConfig,
|
||||||
} from "./LocalTransport";
|
} from "./LocalTransport";
|
||||||
import { constant } from "../../Behavior";
|
import { constant } from "../../Behavior";
|
||||||
import { Epoch, ObservableScope, trackEpoch } from "../../ObservableScope";
|
import { Epoch, ObservableScope } from "../../ObservableScope";
|
||||||
import {
|
import {
|
||||||
MatrixRTCTransportMissingError,
|
MatrixRTCTransportMissingError,
|
||||||
FailToGetOpenIdToken,
|
FailToGetOpenIdToken,
|
||||||
@@ -58,7 +56,6 @@ describe("LocalTransport", () => {
|
|||||||
const { advertised$, active$ } = createLocalTransport$({
|
const { advertised$, active$ } = createLocalTransport$({
|
||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
roomId: "!room:example.org",
|
roomId: "!room:example.org",
|
||||||
useOldestMember: false,
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@@ -101,7 +98,6 @@ describe("LocalTransport", () => {
|
|||||||
const { advertised$, active$ } = createLocalTransport$({
|
const { advertised$, active$ } = createLocalTransport$({
|
||||||
scope,
|
scope,
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
useOldestMember: false,
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
baseUrl: "https://example.org",
|
baseUrl: "https://example.org",
|
||||||
@@ -144,7 +140,6 @@ describe("LocalTransport", () => {
|
|||||||
const { advertised$, active$ } = createLocalTransport$({
|
const { advertised$, active$ } = createLocalTransport$({
|
||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
roomId: "!room:example.org",
|
roomId: "!room:example.org",
|
||||||
useOldestMember: false,
|
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
client: {
|
client: {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
@@ -185,127 +180,6 @@ describe("LocalTransport", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("oldest member mode", () => {
|
|
||||||
const aliceTransport: LivekitTransportConfig = {
|
|
||||||
type: "livekit",
|
|
||||||
livekit_service_url: "https://alice.example.org",
|
|
||||||
};
|
|
||||||
const bobTransport: LivekitTransportConfig = {
|
|
||||||
type: "livekit",
|
|
||||||
livekit_service_url: "https://bob.example.org",
|
|
||||||
};
|
|
||||||
const aliceMembership = mockRtcMembership("@alice:example.org", "AAA", {
|
|
||||||
fociPreferred: [aliceTransport],
|
|
||||||
});
|
|
||||||
const bobMembership = mockRtcMembership("@bob:example.org", "BBB", {
|
|
||||||
fociPreferred: [bobTransport],
|
|
||||||
});
|
|
||||||
|
|
||||||
let openIdSpy: MockInstance<(typeof openIDSFU)["getSFUConfigWithOpenID"]>;
|
|
||||||
beforeEach(() => {
|
|
||||||
openIdSpy = vi
|
|
||||||
.spyOn(openIDSFU, "getSFUConfigWithOpenID")
|
|
||||||
.mockResolvedValue(openIdResponse);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates active transport when oldest member changes", async () => {
|
|
||||||
// Initially, Alice is the only member
|
|
||||||
const memberships$ = new BehaviorSubject([aliceMembership]);
|
|
||||||
|
|
||||||
const scope = testScope();
|
|
||||||
const { advertised$, active$ } = createLocalTransport$({
|
|
||||||
scope,
|
|
||||||
roomId: "!example_room_id",
|
|
||||||
useOldestMember: true,
|
|
||||||
memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
|
|
||||||
client: {
|
|
||||||
getDomain: () => "example.org",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
|
||||||
getOpenIdToken: vi.fn(),
|
|
||||||
getDeviceId: vi.fn(),
|
|
||||||
baseUrl: "https://example.org",
|
|
||||||
},
|
|
||||||
ownMembershipIdentity: ownMemberMock,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
|
||||||
delayId$: constant("delay_id_mock"),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(active$.value).toBe(null);
|
|
||||||
await flushPromises();
|
|
||||||
// SFU config should've been fetched
|
|
||||||
expect(openIdSpy).toHaveBeenCalled();
|
|
||||||
// Alice's transport should be active and advertised
|
|
||||||
expect(active$.value?.transport).toStrictEqual(aliceTransport);
|
|
||||||
expect(advertised$.value).toStrictEqual(aliceTransport);
|
|
||||||
|
|
||||||
// Now Bob joins the call, but Alice is still the oldest member
|
|
||||||
openIdSpy.mockClear();
|
|
||||||
memberships$.next([aliceMembership, bobMembership]);
|
|
||||||
await flushPromises();
|
|
||||||
// No new SFU config should've been fetched
|
|
||||||
expect(openIdSpy).not.toHaveBeenCalled();
|
|
||||||
// Alice's transport should still be active and advertised
|
|
||||||
expect(active$.value?.transport).toStrictEqual(aliceTransport);
|
|
||||||
expect(advertised$.value).toStrictEqual(aliceTransport);
|
|
||||||
|
|
||||||
// Now Bob takes Alice's place as the oldest member
|
|
||||||
openIdSpy.mockClear();
|
|
||||||
memberships$.next([bobMembership, aliceMembership]);
|
|
||||||
// Active transport should reset to null until we have Bob's SFU config
|
|
||||||
expect(active$.value).toStrictEqual(null);
|
|
||||||
await flushPromises();
|
|
||||||
// Bob's SFU config should've been fetched
|
|
||||||
expect(openIdSpy).toHaveBeenCalled();
|
|
||||||
// Bob's transport should be active, but Alice's should remain advertised
|
|
||||||
// (since we don't want the change in oldest member to cause a wave of new
|
|
||||||
// state events)
|
|
||||||
expect(active$.value?.transport).toStrictEqual(bobTransport);
|
|
||||||
expect(advertised$.value).toStrictEqual(aliceTransport);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("advertises preferred transport when no other member exists", async () => {
|
|
||||||
// Initially, there are no members
|
|
||||||
const memberships$ = new BehaviorSubject<CallMembership[]>([]);
|
|
||||||
|
|
||||||
const scope = testScope();
|
|
||||||
const { advertised$, active$ } = createLocalTransport$({
|
|
||||||
scope,
|
|
||||||
roomId: "!example_room_id",
|
|
||||||
useOldestMember: true,
|
|
||||||
memberships$: scope.behavior(memberships$.pipe(trackEpoch())),
|
|
||||||
client: {
|
|
||||||
getDomain: () => "example.org",
|
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
||||||
_unstable_getRTCTransports: async () =>
|
|
||||||
Promise.resolve([aliceTransport]),
|
|
||||||
getOpenIdToken: vi.fn(),
|
|
||||||
getDeviceId: vi.fn(),
|
|
||||||
baseUrl: "https://example.org",
|
|
||||||
},
|
|
||||||
ownMembershipIdentity: ownMemberMock,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
|
||||||
delayId$: constant("delay_id_mock"),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(active$.value).toBe(null);
|
|
||||||
await flushPromises();
|
|
||||||
// Our own preferred transport should be advertised
|
|
||||||
expect(advertised$.value).toStrictEqual(aliceTransport);
|
|
||||||
// No transport should be active however (there is still no oldest member)
|
|
||||||
expect(active$.value).toBe(null);
|
|
||||||
|
|
||||||
// Now Bob joins the call and becomes the oldest member
|
|
||||||
memberships$.next([bobMembership]);
|
|
||||||
await flushPromises();
|
|
||||||
// We should still advertise our own preferred transport (to avoid
|
|
||||||
// unnecessary state changes)
|
|
||||||
expect(advertised$.value).toStrictEqual(aliceTransport);
|
|
||||||
// Bob's transport should become active
|
|
||||||
expect(active$.value?.transport).toBe(bobTransport);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
type LocalTransportProps = Parameters<typeof createLocalTransport$>[0];
|
type LocalTransportProps = Parameters<typeof createLocalTransport$>[0];
|
||||||
|
|
||||||
describe("transport configuration mechanisms", () => {
|
describe("transport configuration mechanisms", () => {
|
||||||
@@ -320,7 +194,6 @@ describe("LocalTransport", () => {
|
|||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
useOldestMember: false,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||||
delayId$: constant(null),
|
delayId$: constant(null),
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
@@ -433,7 +306,6 @@ describe("LocalTransport", () => {
|
|||||||
scope: testScope(),
|
scope: testScope(),
|
||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
useOldestMember: false,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||||
delayId$: constant(null),
|
delayId$: constant(null),
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
@@ -473,7 +345,6 @@ describe("LocalTransport", () => {
|
|||||||
ownMembershipIdentity: ownMemberMock,
|
ownMembershipIdentity: ownMemberMock,
|
||||||
roomId: "!example_room_id",
|
roomId: "!example_room_id",
|
||||||
// We want multi-sdu
|
// We want multi-sdu
|
||||||
useOldestMember: false,
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||||
delayId$: delayId$,
|
delayId$: delayId$,
|
||||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||||
|
|||||||
@@ -7,23 +7,16 @@ Please see LICENSE in the repository root for full details.
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type CallMembership,
|
type CallMembership,
|
||||||
isLivekitTransportConfig,
|
|
||||||
type LivekitTransportConfig,
|
type LivekitTransportConfig,
|
||||||
} from "matrix-js-sdk/lib/matrixrtc";
|
} from "matrix-js-sdk/lib/matrixrtc";
|
||||||
import { type MatrixClient } from "matrix-js-sdk";
|
import { type MatrixClient } from "matrix-js-sdk";
|
||||||
import {
|
import {
|
||||||
catchError,
|
|
||||||
combineLatest,
|
combineLatest,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
first,
|
|
||||||
from,
|
from,
|
||||||
map,
|
map,
|
||||||
merge,
|
|
||||||
type Observable,
|
|
||||||
of,
|
of,
|
||||||
startWith,
|
|
||||||
switchMap,
|
switchMap,
|
||||||
tap,
|
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||||
@@ -47,8 +40,7 @@ import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
* It figures out “which LiveKit focus URL/alias the local user should use,”
|
||||||
* optionally aligning with the oldest member, and ensures the SFU path is primed
|
* and ensures the SFU path is primed before advertising that choice.
|
||||||
* before advertising that choice.
|
|
||||||
*/
|
*/
|
||||||
interface Props {
|
interface Props {
|
||||||
scope: ObservableScope;
|
scope: ObservableScope;
|
||||||
@@ -61,7 +53,6 @@ interface Props {
|
|||||||
OpenIDClientParts;
|
OpenIDClientParts;
|
||||||
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
||||||
roomId: string;
|
roomId: string;
|
||||||
useOldestMember: boolean;
|
|
||||||
forceJwtEndpoint: JwtEndpointVersion;
|
forceJwtEndpoint: JwtEndpointVersion;
|
||||||
delayId$: Behavior<string | null>;
|
delayId$: Behavior<string | null>;
|
||||||
}
|
}
|
||||||
@@ -119,8 +110,6 @@ export interface LocalTransport {
|
|||||||
/**
|
/**
|
||||||
* Connects to the JWT service and determines the transports that the local member should use.
|
* Connects to the JWT service and determines the transports that the local member should use.
|
||||||
*
|
*
|
||||||
* @prop useOldestMember Whether to use the same transport as the oldest member.
|
|
||||||
* This will only update once the first oldest member appears. Will not recompute if the oldest member leaves.
|
|
||||||
* @prop useOldJwtEndpoint Whether to set forceOldJwtEndpoint on the returned transport and to use the old JWT endpoint.
|
* @prop useOldJwtEndpoint Whether to set forceOldJwtEndpoint on the returned transport and to use the old JWT endpoint.
|
||||||
* This is used when the connection manager needs to know if it has to use the legacy endpoint which implies a string concatenated rtcBackendIdentity.
|
* This is used when the connection manager needs to know if it has to use the legacy endpoint which implies a string concatenated rtcBackendIdentity.
|
||||||
* (which is expected for non sticky event based rtc member events)
|
* (which is expected for non sticky event based rtc member events)
|
||||||
@@ -133,18 +122,10 @@ export const createLocalTransport$ = ({
|
|||||||
ownMembershipIdentity,
|
ownMembershipIdentity,
|
||||||
client,
|
client,
|
||||||
roomId,
|
roomId,
|
||||||
useOldestMember,
|
|
||||||
forceJwtEndpoint,
|
forceJwtEndpoint,
|
||||||
delayId$,
|
delayId$,
|
||||||
}: Props): LocalTransport => {
|
}: Props): LocalTransport => {
|
||||||
const logger = rootLogger.getChild("[LocalTransport]");
|
const logger = rootLogger.getChild("[LocalTransport]");
|
||||||
// The LiveKit transport in use by the oldest RTC membership. `null` when the
|
|
||||||
// oldest member has no such transport.
|
|
||||||
const oldestMemberTransport$ = observerOldestMembership$(
|
|
||||||
scope,
|
|
||||||
memberships$,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
const transportDiscovery = new RtcTransportAutoDiscovery({
|
const transportDiscovery = new RtcTransportAutoDiscovery({
|
||||||
client: client,
|
client: client,
|
||||||
@@ -203,19 +184,6 @@ export const createLocalTransport$ = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (useOldestMember) {
|
|
||||||
return observeLocalTransportForOldestMembership(
|
|
||||||
scope,
|
|
||||||
oldestMemberTransport$,
|
|
||||||
preferredTransport$,
|
|
||||||
client,
|
|
||||||
ownMembershipIdentity,
|
|
||||||
roomId,
|
|
||||||
logger,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Multi-SFU mode ---
|
|
||||||
// Always publish on and advertise the preferred transport.
|
// Always publish on and advertise the preferred transport.
|
||||||
return {
|
return {
|
||||||
advertised$: scope.behavior(
|
advertised$: scope.behavior(
|
||||||
@@ -243,47 +211,6 @@ export const createLocalTransport$ = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Observes the oldest member in the room and returns the transport that it uses if it is a livekit transport.
|
|
||||||
* @param scope - The observable scope.
|
|
||||||
* @param memberships$ - The observable of the call's memberships.'
|
|
||||||
*/
|
|
||||||
function observerOldestMembership$(
|
|
||||||
scope: ObservableScope,
|
|
||||||
memberships$: Behavior<Epoch<CallMembership[]>>,
|
|
||||||
logger: Logger,
|
|
||||||
): Behavior<LivekitTransportConfig | null> {
|
|
||||||
return scope.behavior<LivekitTransportConfig | null>(
|
|
||||||
memberships$.pipe(
|
|
||||||
map((memberships) => {
|
|
||||||
const oldestMember = memberships.value[0];
|
|
||||||
if (oldestMember === undefined) {
|
|
||||||
logger.info("Oldest member: not found");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const transport = oldestMember.getTransport(oldestMember);
|
|
||||||
if (transport === undefined) {
|
|
||||||
logger.warn(
|
|
||||||
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has no transport`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!isLivekitTransportConfig(transport)) {
|
|
||||||
logger.warn(
|
|
||||||
`Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has invalid transport`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
logger.info(
|
|
||||||
"Oldest member: ${oldestMember.userId}|${oldestMember.deviceId}|${oldestMember.memberId} has valid transport",
|
|
||||||
);
|
|
||||||
return transport;
|
|
||||||
}),
|
|
||||||
distinctUntilChanged(areLivekitTransportsEqual),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility to ensure the user can authenticate with the SFU.
|
* Utility to ensure the user can authenticate with the SFU.
|
||||||
* We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
* We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
||||||
@@ -331,85 +258,6 @@ async function doOpenIdAndJWTFromUrl(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeLocalTransportForOldestMembership(
|
|
||||||
scope: ObservableScope,
|
|
||||||
oldestMemberTransport$: Behavior<LivekitTransportConfig | null>,
|
|
||||||
preferredTransport$: Observable<LocalTransportWithSFUConfig>,
|
|
||||||
client: Pick<
|
|
||||||
MatrixClient,
|
|
||||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
|
||||||
> &
|
|
||||||
OpenIDClientParts,
|
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts,
|
|
||||||
roomId: string,
|
|
||||||
logger: Logger,
|
|
||||||
): LocalTransport {
|
|
||||||
// Ensure we can authenticate with the SFU.
|
|
||||||
const authenticatedOldestMemberTransport$ = oldestMemberTransport$.pipe(
|
|
||||||
switchMap((transport) => {
|
|
||||||
// Oldest member not available -we are first- (or invalid SFU config).
|
|
||||||
if (transport === null) return of(null);
|
|
||||||
|
|
||||||
// Whenever there is transport change we want to revert
|
|
||||||
// to no transport while we do the authentication.
|
|
||||||
// So do a from(promise) here to be able to startWith(null)
|
|
||||||
return from(
|
|
||||||
doOpenIdAndJWTFromUrl(
|
|
||||||
transport,
|
|
||||||
JwtEndpointVersion.Legacy,
|
|
||||||
ownMembershipIdentity,
|
|
||||||
roomId,
|
|
||||||
client,
|
|
||||||
undefined,
|
|
||||||
logger,
|
|
||||||
),
|
|
||||||
).pipe(
|
|
||||||
catchError((e: unknown) => {
|
|
||||||
logger.error(
|
|
||||||
`Failed to authenticate to transport ${transport.livekit_service_url}`,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
throw mapAuthErrorToUserFriendlyError(e);
|
|
||||||
}),
|
|
||||||
startWith(null),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Oldest member mode ---
|
|
||||||
return {
|
|
||||||
// Never update the transport that we advertise in our membership. Just
|
|
||||||
// take the first valid oldest member or preferred transport that we learn
|
|
||||||
// about, and stick with that. This avoids unnecessary SFU hops and room
|
|
||||||
// state changes.
|
|
||||||
advertised$: scope.behavior(
|
|
||||||
merge(
|
|
||||||
authenticatedOldestMemberTransport$.pipe(
|
|
||||||
map((t) => t?.transport ?? null),
|
|
||||||
),
|
|
||||||
preferredTransport$.pipe(map((t) => t.transport)),
|
|
||||||
).pipe(
|
|
||||||
first((t) => t !== null),
|
|
||||||
tap((t) =>
|
|
||||||
logger.info(`Advertise transport: ${t.livekit_service_url}`),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
// Publish on the transport used by the oldest member.
|
|
||||||
active$: scope.behavior(
|
|
||||||
authenticatedOldestMemberTransport$.pipe(
|
|
||||||
tap((t) =>
|
|
||||||
logger.info(
|
|
||||||
`Publish on transport: ${t?.transport.livekit_service_url}`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapAuthErrorToUserFriendlyError(e: unknown): Error {
|
function mapAuthErrorToUserFriendlyError(e: unknown): Error {
|
||||||
if (
|
if (
|
||||||
e instanceof FailToGetOpenIdToken ||
|
e instanceof FailToGetOpenIdToken ||
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import {
|
|||||||
SFURoomCreationRestrictedError,
|
SFURoomCreationRestrictedError,
|
||||||
UnknownCallError,
|
UnknownCallError,
|
||||||
} from "../../../utils/errors.ts";
|
} from "../../../utils/errors.ts";
|
||||||
import { type JwtEndpointVersion } from "../localMember/LocalTransport.ts";
|
|
||||||
|
|
||||||
export interface ConnectionOpts {
|
export interface ConnectionOpts {
|
||||||
/**
|
/**
|
||||||
@@ -44,11 +43,6 @@ export interface ConnectionOpts {
|
|||||||
* On top the local transport will send additional data to the jwt server to use delayed event delegation.
|
* On top the local transport will send additional data to the jwt server to use delayed event delegation.
|
||||||
*/
|
*/
|
||||||
existingSFUConfig?: SFUConfig;
|
existingSFUConfig?: SFUConfig;
|
||||||
/**
|
|
||||||
* For local connections that use the oldest member pattern. here we have not prefetched the sfuConfig
|
|
||||||
* and hence we need to let the connection do the jwt token fetching.
|
|
||||||
*/
|
|
||||||
forceJwtEndpoint?: JwtEndpointVersion;
|
|
||||||
/** The identity parts to use on this connection */
|
/** The identity parts to use on this connection */
|
||||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||||
/** The media transport to connect to. */
|
/** The media transport to connect to. */
|
||||||
|
|||||||
@@ -35,11 +35,7 @@ vi.mock("../widget", () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it.each([
|
it.each([[MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0]])(
|
||||||
[MatrixRTCMode.Legacy],
|
|
||||||
[MatrixRTCMode.Compatibility],
|
|
||||||
[MatrixRTCMode.Matrix_2_0],
|
|
||||||
])(
|
|
||||||
"expect leave when ElementWidgetActions.HangupCall is called (%s mode)",
|
"expect leave when ElementWidgetActions.HangupCall is called (%s mode)",
|
||||||
async (mode) => {
|
async (mode) => {
|
||||||
const pr = Promise.withResolvers<string>();
|
const pr = Promise.withResolvers<string>();
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export function getBasicCallViewModelEnvironment(
|
|||||||
setE2EEEnabled: async () => Promise.resolve(),
|
setE2EEEnabled: async () => Promise.resolve(),
|
||||||
}),
|
}),
|
||||||
connectionState$: constant(ConnectionState.Connected),
|
connectionState$: constant(ConnectionState.Connected),
|
||||||
matrixRTCMode$: constant(MatrixRTCMode.Legacy),
|
matrixRTCMode$: constant(MatrixRTCMode.Compatibility),
|
||||||
...callViewModelOptions,
|
...callViewModelOptions,
|
||||||
},
|
},
|
||||||
handRaisedSubject$,
|
handRaisedSubject$,
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export function mockRtcMembership(
|
|||||||
fociPreferred: [exampleTransport],
|
fociPreferred: [exampleTransport],
|
||||||
focusActive: {
|
focusActive: {
|
||||||
type: "livekit" as const,
|
type: "livekit" as const,
|
||||||
focus_selection: "oldest_membership" as const,
|
focus_selection: "multi_sfu" as const,
|
||||||
},
|
},
|
||||||
callId: "",
|
callId: "",
|
||||||
membership: {},
|
membership: {},
|
||||||
@@ -463,9 +463,6 @@ export class MockRTCSession extends TypedEventEmitter<
|
|||||||
session.reemitEncryptionKeys = vi
|
session.reemitEncryptionKeys = vi
|
||||||
.fn<() => void>()
|
.fn<() => void>()
|
||||||
.mockReturnValue(undefined);
|
.mockReturnValue(undefined);
|
||||||
session.getOldestMembership = vi
|
|
||||||
.fn<() => CallMembership | undefined>()
|
|
||||||
.mockReturnValue(this.memberships[0]);
|
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user