mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-26 21:09:21 +00:00
Compare commits
24 Commits
msc4519-do
...
delegate-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85d3cc33d9 | ||
|
|
e6b4aae714 | ||
|
|
ede29bdf1b | ||
|
|
b5b2bd2193 | ||
|
|
21ca382306 | ||
|
|
25e379546e | ||
|
|
0443ef55d1 | ||
|
|
465c413691 | ||
|
|
6cf0e1df51 | ||
|
|
bfa20ef747 | ||
|
|
5955d22c1b | ||
|
|
ce4426858b | ||
|
|
4ec75ecda4 | ||
|
|
b51a33c4e1 | ||
|
|
c3837464a3 | ||
|
|
e7372d4c03 | ||
|
|
69eddd7846 | ||
|
|
de3e4c6a78 | ||
|
|
3abc5a0435 | ||
|
|
8e20d48b1d | ||
|
|
ff57c217aa | ||
|
|
b60e7452b7 | ||
|
|
1c03ad335c | ||
|
|
87873b726f |
@@ -9,7 +9,7 @@ on:
|
||||
- opened
|
||||
- labeled
|
||||
push:
|
||||
branches: [livekit]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
versioning:
|
||||
|
||||
6
.github/workflows/sync-labels.yml
vendored
6
.github/workflows/sync-labels.yml
vendored
@@ -4,12 +4,12 @@ on:
|
||||
|
||||
push:
|
||||
branches:
|
||||
- livekit
|
||||
- main
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- .github/workflows/sync-labels.yml
|
||||
|
||||
permissions: {} # We use ELEMENT_BOT_TOKEN instead
|
||||
permissions: {} # We use LABEL_SYNC_GITHUB_TOKEN instead
|
||||
|
||||
jobs:
|
||||
sync-labels:
|
||||
@@ -20,4 +20,4 @@ jobs:
|
||||
DELETE: true
|
||||
WET: true
|
||||
secrets:
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
ELEMENT_BOT_TOKEN: ${{ secrets.LABEL_SYNC_GITHUB_TOKEN }}
|
||||
|
||||
2
.github/workflows/test.yaml
vendored
2
.github/workflows/test.yaml
vendored
@@ -2,7 +2,7 @@ name: Test
|
||||
on:
|
||||
pull_request: {}
|
||||
push:
|
||||
branches: [livekit]
|
||||
branches: [main]
|
||||
jobs:
|
||||
vitest:
|
||||
name: Run unit tests
|
||||
|
||||
2
.github/workflows/translations-upload.yaml
vendored
2
.github/workflows/translations-upload.yaml
vendored
@@ -2,7 +2,7 @@ name: Upload translation files to Localazy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- livekit
|
||||
- main
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
|
||||
|
||||
2
.github/workflows/zizmor.yml
vendored
2
.github/workflows/zizmor.yml
vendored
@@ -2,7 +2,7 @@ name: GitHub Actions Security Analysis with zizmor 🌈
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["livekit", "full-mesh"]
|
||||
branches: ["main", "full-mesh"]
|
||||
pull_request: {}
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
"server_name": "synapse.m.localhost"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_session": {
|
||||
"wait_for_key_rotation_ms": 3000,
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
"livekit": {
|
||||
"livekit_service_url": "https://livekit-jwt.mydomain.com"
|
||||
},
|
||||
"features": {
|
||||
"feature_use_device_session_member_events": true
|
||||
},
|
||||
"ssla": "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
"matrix_rtc_mode": "compatibility",
|
||||
"matrix_rtc_session": {
|
||||
|
||||
@@ -13,7 +13,7 @@ services:
|
||||
- ./backend/playwright_homeserver-othersite.yaml:/data/cfg/homeserver.yaml:Z
|
||||
element-web:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:9ac7cde6e2f53684f0c28215130d3aca5c41025bb93c57acd0a6fb2246b685f4
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:0332028836603e91689053f157847b4deef9342bd19e3195eeb81793b7e4046e
|
||||
element-web-1:
|
||||
# Pin to a SHA so that upstream cannot break our tests. Renovate handles regular updates.
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:9ac7cde6e2f53684f0c28215130d3aca5c41025bb93c57acd0a6fb2246b685f4
|
||||
image: ghcr.io/element-hq/element-web:develop@sha256:0332028836603e91689053f157847b4deef9342bd19e3195eeb81793b7e4046e
|
||||
|
||||
@@ -5,5 +5,6 @@ This folder contains documentation for setup, usage, and development of Element
|
||||
- [Embedded vs standalone mode](./embedded_standalone.md)
|
||||
- [Url format and parameters](./url_params.md)
|
||||
- [Global JS controls](./controls.md)
|
||||
- [MatrixRTC modes](./matrix_rtc_modes.md)
|
||||
- [Self-Hosting](./self_hosting.md)
|
||||
- [Developing with linked packages](./linking.md)
|
||||
|
||||
48
docs/matrix_rtc_modes.md
Normal file
48
docs/matrix_rtc_modes.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# MatrixRTC modes
|
||||
|
||||
Element Call is in the middle of a transition of how a call session is
|
||||
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.
|
||||
|
||||
Not every homeserver supports sticky events yet. The two MatrixRTC modes
|
||||
controls whether Element Call uses them.
|
||||
|
||||
## The modes
|
||||
|
||||
| Mode | Membership events | SFU selection | JWT endpoint |
|
||||
| --------------- | ----------------- | ------------- | ---------------------------- |
|
||||
| `compatibility` | state events | multi SFU | legacy |
|
||||
| `matrix_2_0` | sticky events | multi SFU | Matrix 2.0 (hashed identity) |
|
||||
|
||||
**`compatibility`** — multi SFU, but still state events. Use it when the
|
||||
homeserver does not support sticky events. This is the default.
|
||||
|
||||
**`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
|
||||
token from the Matrix 2.0 JWT endpoint of the
|
||||
[MatrixRTC Authorization Service](https://github.com/element-hq/lk-jwt-service)
|
||||
and identifies the room by a hashed identity instead of a `livekit_alias`.
|
||||
(Remote memberships always try the new endpoint first and fall back to the
|
||||
legacy one, so remote participants can be on either.)
|
||||
|
||||
## Selecting a mode
|
||||
|
||||
Users can choose a mode under **Settings → Developer → MatrixRTC mode**. The
|
||||
Matrix 2.0 option is disabled if the homeserver does not support sticky events.
|
||||
|
||||
A deployment can pin the mode for all its clients in `config.json`, which
|
||||
disables the Developer Settings choice:
|
||||
|
||||
```json
|
||||
{
|
||||
"matrix_rtc_mode": "compatibility"
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
homeserver without sticky event support makes joining fail with a "sticky events
|
||||
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)",
|
||||
"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": {
|
||||
"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"
|
||||
|
||||
@@ -101,9 +101,7 @@ async function setRtcModeFromSettings(
|
||||
|
||||
// Move to Developer tab now
|
||||
await page.getByRole("tab", { name: "Developer" }).click();
|
||||
if (mode == "legacy") {
|
||||
await page.getByText("Legacy: state events").click();
|
||||
} else if (mode == "2_0") {
|
||||
if (mode == "2_0") {
|
||||
await page.getByText("Matrix 2.0").click();
|
||||
} else {
|
||||
// compat
|
||||
|
||||
@@ -12,9 +12,7 @@ import { HOST1, HOST2, type RtcMode, TestHelpers } from "./test-helpers";
|
||||
|
||||
const modePairs: [RtcMode, RtcMode][] = [
|
||||
["compat", "compat"],
|
||||
["legacy", "legacy"],
|
||||
["legacy", "compat"],
|
||||
["compat", "legacy"],
|
||||
// TODO: Compatibility + Matrix 2.0?
|
||||
];
|
||||
|
||||
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 HOST2 = "https://app.othersite.m.localhost/#/welcome";
|
||||
|
||||
export type RtcMode = "legacy" | "compat" | "2_0";
|
||||
export type RtcMode = "compat" | "2_0";
|
||||
|
||||
export class TestHelpers {
|
||||
public static async startCallInCurrentRoom(
|
||||
@@ -309,9 +309,7 @@ export class TestHelpers {
|
||||
|
||||
// Move to Developer tab now
|
||||
await iframe.getByRole("tab", { name: "Developer" }).click();
|
||||
if (mode == "legacy") {
|
||||
await iframe.getByText("Legacy: state events").click();
|
||||
} else if (mode == "2_0") {
|
||||
if (mode == "2_0") {
|
||||
await iframe.getByText("Matrix 2.0").click();
|
||||
} else {
|
||||
// compat
|
||||
|
||||
56
pnpm-lock.yaml
generated
56
pnpm-lock.yaml
generated
@@ -39,13 +39,13 @@ importers:
|
||||
version: 11.7.12
|
||||
'@livekit/components-core':
|
||||
specifier: ^0.12.0
|
||||
version: 0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
|
||||
version: 0.12.15(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
|
||||
'@livekit/components-react':
|
||||
specifier: ^2.0.0
|
||||
version: 2.9.24(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)
|
||||
version: 2.9.24(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)
|
||||
'@livekit/track-processors':
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))
|
||||
version: 0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))
|
||||
'@mediapipe/tasks-vision':
|
||||
specifier: ^0.10.18
|
||||
version: 0.10.35
|
||||
@@ -132,10 +132,10 @@ importers:
|
||||
version: 10.3.1(react@19.2.8)
|
||||
'@vector-im/compound-design-tokens':
|
||||
specifier: ^10.0.0
|
||||
version: 10.2.3(@types/react@19.2.17)(react@19.2.8)
|
||||
version: 10.2.4(@types/react@19.2.17)(react@19.2.8)
|
||||
'@vector-im/compound-web':
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
version: 10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.4(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.5(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7(supports-color@7.2.0))(@babel/runtime@7.29.7)(rolldown@1.1.5)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0)))(babel-plugin-react-compiler@1.0.0)(vite@8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(sass@1.102.0)(terser@5.46.1)(yaml@2.9.0))
|
||||
@@ -183,7 +183,7 @@ importers:
|
||||
version: 5.88.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)(typescript@5.9.3)
|
||||
livekit-client:
|
||||
specifier: ^2.18.1
|
||||
version: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
version: 2.22.0(@types/dom-mediacapture-record@1.0.22)
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.18.1
|
||||
@@ -3240,14 +3240,20 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>= 16.8.0'
|
||||
|
||||
'@vector-im/compound-design-tokens@10.2.3':
|
||||
resolution: {integrity: sha512-K2xNkjOzYn2Ya0kUqCy7JIYyx9jbkNIWpex2t9m3FUG4wzkNscvaiRJ/a9Wse1aJmwDDARcHU6mUjtHf5os5MA==}
|
||||
'@vector-im/compound-design-tokens@10.2.4':
|
||||
resolution: {integrity: sha512-2aCjhSvJxktPWnvh5s9Xdc0Rd76AfHqbDbN4QjOxo2VIac6zYdLyQbxMUQ5ZeoR9G3vAxILd0H1UvApRfMilpQ==}
|
||||
peerDependencies:
|
||||
'@adobe/leonardo-contrast-colors': ^1.0.0
|
||||
'@types/react': '*'
|
||||
chroma-js: ^3.0.0
|
||||
react: ^17 || ^18 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
'@adobe/leonardo-contrast-colors':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
chroma-js:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
@@ -4459,8 +4465,8 @@ packages:
|
||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||
hasBin: true
|
||||
|
||||
jose@6.2.4:
|
||||
resolution: {integrity: sha512-N8acGzVsQy6M/fjFcxtysNc4Q379TcM5dM/qKkNtsHFji88yANnXTr7BLeP75iPnFwBfQzM/jg2BZ9+HZrHCZA==}
|
||||
jose@6.2.10:
|
||||
resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==}
|
||||
|
||||
jose@6.2.9:
|
||||
resolution: {integrity: sha512-XrchZOFZUl/T3vTwRe8XK+cJrGtMF4th1ARnDfwbBXFKThGhlsxEE4Zu03AD/bjJSt/9jT/mxrOCkJWOg77aPA==}
|
||||
@@ -4608,8 +4614,8 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
livekit-client@2.21.0:
|
||||
resolution: {integrity: sha512-RBUhPkV/sl1nzl8lokVlK5uATPwn0AlsudCBZXissw/kDl9yz8ac4pNJ43iPpMVoHOeBYH/BZ3vUC1adqa/zFQ==}
|
||||
livekit-client@2.22.0:
|
||||
resolution: {integrity: sha512-GLtYQfRh/RsvXaOX1x609bFZ17yyKmKWDqu9JmkMKn9vIFLi2GsapRv9gT8OJO/1R2dsitrkbGmloyUfxWQsaA==}
|
||||
peerDependencies:
|
||||
'@types/dom-mediacapture-record': ^1
|
||||
|
||||
@@ -7064,21 +7070,21 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@livekit/components-core@0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)':
|
||||
'@livekit/components-core@0.12.15(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)':
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.7.6
|
||||
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
livekit-client: 2.22.0(@types/dom-mediacapture-record@1.0.22)
|
||||
loglevel: 1.9.1
|
||||
rxjs: 7.8.2
|
||||
tslib: 2.8.1
|
||||
|
||||
'@livekit/components-react@2.9.24(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)':
|
||||
'@livekit/components-react@2.9.24(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tslib@2.8.1)':
|
||||
dependencies:
|
||||
'@livekit/components-core': 0.12.15(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
|
||||
'@livekit/components-core': 0.12.15(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))(tslib@2.8.1)
|
||||
clsx: 2.1.1
|
||||
events: 3.3.0
|
||||
jose: 6.2.9
|
||||
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
livekit-client: 2.22.0(@types/dom-mediacapture-record@1.0.22)
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
tslib: 2.8.1
|
||||
@@ -7090,11 +7096,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 1.10.1
|
||||
|
||||
'@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22))':
|
||||
'@livekit/track-processors@0.7.2(@types/dom-mediacapture-transform@0.1.11)(livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22))':
|
||||
dependencies:
|
||||
'@mediapipe/tasks-vision': 0.10.35
|
||||
'@types/dom-mediacapture-transform': 0.1.11
|
||||
livekit-client: 2.21.0(@types/dom-mediacapture-record@1.0.22)
|
||||
livekit-client: 2.22.0(@types/dom-mediacapture-record@1.0.22)
|
||||
|
||||
'@matrix-org/matrix-sdk-crypto-wasm@18.4.0': {}
|
||||
|
||||
@@ -8711,12 +8717,12 @@ snapshots:
|
||||
'@use-gesture/core': 10.3.1
|
||||
react: 19.2.8
|
||||
|
||||
'@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8)':
|
||||
'@vector-im/compound-design-tokens@10.2.4(@types/react@19.2.17)(react@19.2.8)':
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.17
|
||||
react: 19.2.8
|
||||
|
||||
'@vector-im/compound-web@10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.3(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
'@vector-im/compound-web@10.0.1(@fontsource/inconsolata@5.3.0)(@fontsource/inter@5.3.0)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(@vector-im/compound-design-tokens@10.2.4(@types/react@19.2.17)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.27.20(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
@@ -8728,7 +8734,7 @@ snapshots:
|
||||
'@radix-ui/react-progress': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@radix-ui/react-separator': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.8)
|
||||
'@vector-im/compound-design-tokens': 10.2.3(@types/react@19.2.17)(react@19.2.8)
|
||||
'@vector-im/compound-design-tokens': 10.2.4(@types/react@19.2.17)(react@19.2.8)
|
||||
classnames: 2.5.1
|
||||
react: 19.2.8
|
||||
vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
@@ -10053,7 +10059,7 @@ snapshots:
|
||||
|
||||
jiti@2.7.0: {}
|
||||
|
||||
jose@6.2.4: {}
|
||||
jose@6.2.10: {}
|
||||
|
||||
jose@6.2.9: {}
|
||||
|
||||
@@ -10197,13 +10203,13 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
livekit-client@2.21.0(@types/dom-mediacapture-record@1.0.22):
|
||||
livekit-client@2.22.0(@types/dom-mediacapture-record@1.0.22):
|
||||
dependencies:
|
||||
'@livekit/mutex': 1.1.1
|
||||
'@livekit/protocol': 1.50.4
|
||||
'@types/dom-mediacapture-record': 1.0.22
|
||||
events: 3.3.0
|
||||
jose: 6.2.4
|
||||
jose: 6.2.10
|
||||
loglevel: 1.9.2
|
||||
sdp-transform: 2.15.0
|
||||
tslib: 2.8.1
|
||||
|
||||
@@ -127,6 +127,7 @@ export const Default: Story = {
|
||||
showFooter: true,
|
||||
hideControls: false,
|
||||
asOverlay: false,
|
||||
showModals: true,
|
||||
sharingScreen: false,
|
||||
audioOutputSwitcher: undefined,
|
||||
reactionIdentifier: undefined,
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface FooterState {
|
||||
/** The footer should be used as an overlay.
|
||||
* (Over the Call Grid) This saves spaces on small screens. */
|
||||
asOverlay: boolean;
|
||||
showModals: boolean;
|
||||
|
||||
buttonSize: "md" | "lg";
|
||||
showLogo: boolean;
|
||||
@@ -121,6 +122,7 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
const asOverlay = useBehavior(vm.asOverlay$);
|
||||
const showFooter = useBehavior(vm.showFooter$);
|
||||
const hideControls = useBehavior(vm.hideControls$);
|
||||
const showModals = useBehavior(vm.showModals$);
|
||||
const layoutSwitchVm = useBehavior(vm.layoutSwitchVm$);
|
||||
const openSettings = useBehavior(vm.openSettings$);
|
||||
const audioEnabled = useBehavior(vm.audioEnabled$);
|
||||
@@ -235,7 +237,8 @@ export const CallFooter: FC<FooterProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (reactionIdentifier && reactionData) {
|
||||
// Reaction button contains a pretty large menu, so treat it like a modal
|
||||
if (reactionIdentifier && reactionData && showModals) {
|
||||
buttons.push(
|
||||
<ReactionToggleButton
|
||||
size={buttonSize}
|
||||
|
||||
@@ -49,6 +49,7 @@ function buildMinimalCallViewModel(layout: Layout): CallViewModel {
|
||||
reactions$: constant({}),
|
||||
tileStoreGeneration$: constant(0),
|
||||
showFooter$: constant(true),
|
||||
showModals$: constant(true),
|
||||
settingsOpen$: constant(false),
|
||||
setSettingsOpen$: constant(() => {}),
|
||||
} as unknown as CallViewModel;
|
||||
|
||||
@@ -161,6 +161,7 @@ export function createCallFooterViewModel(
|
||||
// candidat to move into the FooterViewModel
|
||||
showFooter$: callModel.showFooter$,
|
||||
hideControls$: constant(!showControls),
|
||||
showModals$: callModel.showModals$,
|
||||
asOverlay$: callModel.edgeToEdge$,
|
||||
buttonSize$: scope.behavior(
|
||||
isPip$.pipe(map<boolean, "md" | "lg">((pip) => (pip ? "md" : "lg"))),
|
||||
@@ -168,12 +169,12 @@ export function createCallFooterViewModel(
|
||||
|
||||
openSettings$: scope.behavior(
|
||||
combineLatest([
|
||||
isPip$,
|
||||
callModel.showModals$,
|
||||
callModel.showHeader$,
|
||||
callModel.setSettingsOpen$,
|
||||
]).pipe(
|
||||
map(([isPip, showHeader, setSettingsOpen]) =>
|
||||
!isPip && headerStyle !== HeaderStyle.AppBar && showControls
|
||||
map(([showModals, showHeader, setSettingsOpen]) =>
|
||||
showModals && headerStyle !== HeaderStyle.AppBar && showControls
|
||||
? (): void => setSettingsOpen(true)
|
||||
: undefined,
|
||||
),
|
||||
@@ -239,6 +240,7 @@ export function createLobbyFooterViewModel(
|
||||
showLogo,
|
||||
hideControls: false,
|
||||
asOverlay: false,
|
||||
showModals: true,
|
||||
buttonSize: "lg",
|
||||
openSettings,
|
||||
hangup,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
export enum MatrixRTCMode {
|
||||
/** Legacy single-SFU + user-keyed memberships + legacy JWT endpoint. */
|
||||
Legacy = "legacy",
|
||||
/** Multi-SFU transport, legacy JWT endpoint, no sticky events. */
|
||||
/** Multi-SFU transport, legacy JWT endpoint, state events. */
|
||||
Compatibility = "compatibility",
|
||||
/**
|
||||
* Multi-SFU transport with:
|
||||
@@ -86,15 +84,6 @@ export interface ConfigOptions {
|
||||
* Allow to join group calls without audio and video.
|
||||
*/
|
||||
feature_group_calls_without_video_and_audio?: boolean;
|
||||
|
||||
/**
|
||||
* Send device-specific call session membership state events instead of
|
||||
* legacy user-specific call membership state events.
|
||||
* This setting has no effect when the user joins an active call with
|
||||
* legacy state events. For compatibility, Element Call will always join
|
||||
* active legacy calls with legacy state events.
|
||||
*/
|
||||
feature_use_device_session_member_events?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -179,7 +168,8 @@ export interface ConfigOptions {
|
||||
/**
|
||||
* Pins the {@link MatrixRTCMode} for all clients on this deployment,
|
||||
* overriding any per-user choice from the Developer Settings. If unset,
|
||||
* the user's Developer Settings choice (or its default of `Legacy`) wins.
|
||||
* the user's Developer Settings choice (or its default of `Compatibility`)
|
||||
* wins.
|
||||
*/
|
||||
matrix_rtc_mode?: MatrixRTCMode;
|
||||
|
||||
@@ -266,9 +256,6 @@ export interface ResolvedConfigOptions extends ConfigOptions {
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: ResolvedConfigOptions = {
|
||||
features: {
|
||||
feature_use_device_session_member_events: true,
|
||||
},
|
||||
sync_disconnect_grace_period_ms: 10000,
|
||||
ssla: "https://static.element.io/legal/element-software-and-services-license-agreement-uk-1.pdf",
|
||||
media_quality: {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
NoMatrix2AuthorizationService,
|
||||
} from "../utils/errors";
|
||||
import { doNetworkOperationWithRetry } from "../utils/matrix";
|
||||
import { Config } from "../config/Config";
|
||||
import { JwtEndpointVersion } from "../state/CallViewModel/localMember/LocalTransport";
|
||||
|
||||
/**
|
||||
@@ -85,8 +84,6 @@ export type OpenIDClientParts = Pick<
|
||||
* This function by default uses whatever is possible with the current jwt service installed next to the SFU.
|
||||
* For remote connections this does not matter, since we will not publish there we can rely on the newest option.
|
||||
* For our own connection we can only use the hashed version if we also send the new matrix2.0 sticky events.
|
||||
* @param opts.delayEndpointBaseUrl The URL of the matrix homeserver.
|
||||
* @param opts.delayId The delay id used for the jwt service to manage.
|
||||
* @param logger optional logger.
|
||||
* @returns Object containing the token information
|
||||
* @throws FailToGetOpenIdToken
|
||||
@@ -98,8 +95,6 @@ export async function getSFUConfigWithOpenID(
|
||||
roomId: string,
|
||||
opts?: {
|
||||
forceJwtEndpoint?: JwtEndpointVersion;
|
||||
delayEndpointBaseUrl?: string;
|
||||
delayId?: string;
|
||||
},
|
||||
logger?: Logger,
|
||||
): Promise<SFUConfig> {
|
||||
@@ -121,20 +116,18 @@ export async function getSFUConfigWithOpenID(
|
||||
const forceMatrix2Jwt =
|
||||
opts?.forceJwtEndpoint === JwtEndpointVersion.Matrix_2_0;
|
||||
|
||||
// We want to start using the new endpoint (with optional delay delegation)
|
||||
// if we can use both or if we are forced to use the new one.
|
||||
// We want to start using the new endpoint if we can use both or if we are
|
||||
// forced to use the new one.
|
||||
if (tryBothJwtEndpoints || forceMatrix2Jwt) {
|
||||
try {
|
||||
logger?.info(
|
||||
`Trying to get JWT with delegation for focus ${serviceUrl}...`,
|
||||
);
|
||||
const sfuConfig = await getLiveKitJWTWithDelayDelegation(
|
||||
const sfuConfig = await getLiveKitJWT(
|
||||
membership,
|
||||
serviceUrl,
|
||||
roomId,
|
||||
openIdToken,
|
||||
opts?.delayEndpointBaseUrl,
|
||||
opts?.delayId,
|
||||
);
|
||||
|
||||
return extractFullConfigFromToken(sfuConfig);
|
||||
@@ -154,13 +147,11 @@ export async function getSFUConfigWithOpenID(
|
||||
logger?.info(
|
||||
`Trying to get JWT with legacy endpoint for focus ${serviceUrl}...`,
|
||||
);
|
||||
sfuConfig = await getLiveKitJWT(
|
||||
sfuConfig = await getLiveKitJWTLegacy(
|
||||
membership.deviceId,
|
||||
serviceUrl,
|
||||
roomId,
|
||||
openIdToken,
|
||||
opts?.delayEndpointBaseUrl,
|
||||
opts?.delayId,
|
||||
);
|
||||
logger?.info(`Got JWT from call's active focus URL.`);
|
||||
return extractFullConfigFromToken(sfuConfig);
|
||||
@@ -188,33 +179,14 @@ function extractFullConfigFromToken(sfuConfig: {
|
||||
};
|
||||
}
|
||||
|
||||
async function getLiveKitJWT(
|
||||
async function getLiveKitJWTLegacy(
|
||||
deviceId: string,
|
||||
livekitServiceURL: string,
|
||||
matrixRoomId: string,
|
||||
openIDToken: IOpenIDToken,
|
||||
delayEndpointBaseUrl?: string,
|
||||
delayId?: string,
|
||||
): Promise<{ url: string; jwt: string }> {
|
||||
interface IDelayParams {
|
||||
delay_id?: string;
|
||||
delay_timeout?: number;
|
||||
delay_cs_api_url?: string;
|
||||
}
|
||||
let bodyDalayParts: IDelayParams = {};
|
||||
// Also check for empty string
|
||||
if (delayId && delayEndpointBaseUrl) {
|
||||
const delayTimeoutMs =
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
||||
bodyDalayParts = {
|
||||
delay_id: delayId,
|
||||
delay_timeout: delayTimeoutMs,
|
||||
delay_cs_api_url: delayEndpointBaseUrl,
|
||||
};
|
||||
}
|
||||
|
||||
const makeRequest = async (delayParts: IDelayParams): Promise<Response> => {
|
||||
return await fetch(livekitServiceURL + "/sfu/get", {
|
||||
const res = await doNetworkOperationWithRetry(async () =>
|
||||
fetch(livekitServiceURL + "/sfu/get", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -225,31 +197,9 @@ async function getLiveKitJWT(
|
||||
room: matrixRoomId,
|
||||
openid_token: openIDToken,
|
||||
device_id: deviceId,
|
||||
...delayParts,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const res = await doNetworkOperationWithRetry(async () => {
|
||||
let response = await makeRequest(bodyDalayParts);
|
||||
|
||||
// Old service compatibility check
|
||||
const oldServiceDoesNotSupportDelayParts =
|
||||
response.status === 400 && Object.keys(bodyDalayParts).length > 0;
|
||||
// If http status 400 with M_BAD_JSON and we sent delay parts, retry without them
|
||||
if (oldServiceDoesNotSupportDelayParts) {
|
||||
try {
|
||||
const errorBody = await response.json();
|
||||
if (errorBody.errcode === "M_BAD_JSON") {
|
||||
response = await makeRequest({});
|
||||
}
|
||||
} catch {
|
||||
// If we can't parse the error, treat as real error
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw parseErrorResponse(res, await res.text());
|
||||
@@ -264,17 +214,15 @@ class NotSupportedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLiveKitJWTWithDelayDelegation(
|
||||
export async function getLiveKitJWT(
|
||||
membership: CallMembershipIdentityParts,
|
||||
livekitServiceURL: string,
|
||||
matrixRoomId: string,
|
||||
openIDToken: IOpenIDToken,
|
||||
delayEndpointBaseUrl?: string,
|
||||
delayId?: string,
|
||||
): Promise<{ url: string; jwt: string }> {
|
||||
const { userId, deviceId, memberId } = membership;
|
||||
|
||||
const body = {
|
||||
const body = JSON.stringify({
|
||||
room_id: matrixRoomId,
|
||||
slot_id: "m.call#ROOM",
|
||||
openid_token: openIDToken,
|
||||
@@ -283,27 +231,13 @@ export async function getLiveKitJWTWithDelayDelegation(
|
||||
claimed_user_id: userId,
|
||||
claimed_device_id: deviceId,
|
||||
},
|
||||
};
|
||||
|
||||
let bodyDalayParts = {};
|
||||
// Also check for empty string
|
||||
if (delayId && delayEndpointBaseUrl) {
|
||||
const delayTimeoutMs =
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms;
|
||||
bodyDalayParts = {
|
||||
delay_id: delayId,
|
||||
delay_timeout: delayTimeoutMs,
|
||||
delay_cs_api_url: delayEndpointBaseUrl,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const res = await doNetworkOperationWithRetry(async () => {
|
||||
return await fetch(livekitServiceURL + "/get_token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ ...body, ...bodyDalayParts }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -261,6 +261,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
const overflowing = useBehavior(vm.overflowing$);
|
||||
const showNameTags = useBehavior(vm.showNameTags$);
|
||||
const showHeader = useBehavior(vm.showHeader$);
|
||||
const showModals = useBehavior(vm.showModals$);
|
||||
const settingsOpen = useBehavior(vm.settingsOpen$);
|
||||
const setSettingsOpen = useBehavior(vm.setSettingsOpen$);
|
||||
const earpieceMode = useBehavior(vm.earpieceMode$);
|
||||
@@ -622,7 +623,7 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
{earpieceOverlay}
|
||||
<ReactionsOverlay vm={vm} />
|
||||
{footer}
|
||||
{layout.type !== "pip" && (
|
||||
{showModals && (
|
||||
<>
|
||||
<RageshakeRequestModal {...rageshakeRequestModalProps} />
|
||||
<SettingsModal
|
||||
|
||||
@@ -317,19 +317,15 @@ describe("DeveloperSettingsTab", () => {
|
||||
|
||||
describe("matrix rtc mode", () => {
|
||||
afterEach(() => {
|
||||
matrixRTCModeSetting.setValue(MatrixRTCMode.Legacy);
|
||||
matrixRTCModeSetting.setValue(MatrixRTCMode.Compatibility);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
function getModeRadios(): {
|
||||
legacy: HTMLInputElement;
|
||||
compatibility: HTMLInputElement;
|
||||
matrix20: HTMLInputElement;
|
||||
} {
|
||||
return {
|
||||
legacy: screen.getByDisplayValue(
|
||||
MatrixRTCMode.Legacy,
|
||||
) as HTMLInputElement,
|
||||
compatibility: screen.getByDisplayValue(
|
||||
MatrixRTCMode.Compatibility,
|
||||
) as HTMLInputElement,
|
||||
@@ -359,27 +355,21 @@ describe("DeveloperSettingsTab", () => {
|
||||
|
||||
const radios = getModeRadios();
|
||||
expect(radios.compatibility).toBeChecked();
|
||||
expect(radios.legacy).not.toBeChecked();
|
||||
expect(radios.matrix20).not.toBeChecked();
|
||||
// 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();
|
||||
});
|
||||
|
||||
it.each([
|
||||
MatrixRTCMode.Legacy,
|
||||
MatrixRTCMode.Compatibility,
|
||||
MatrixRTCMode.Matrix_2_0,
|
||||
])(
|
||||
it.each([MatrixRTCMode.Compatibility, MatrixRTCMode.Matrix_2_0])(
|
||||
"disables all radios and shows the config value (%s) as checked when matrix_rtc_mode is set",
|
||||
async (configMode) => {
|
||||
mockConfig({ matrix_rtc_mode: configMode });
|
||||
// Local setting is intentionally different from the config value to
|
||||
// prove config wins.
|
||||
matrixRTCModeSetting.setValue(
|
||||
configMode === MatrixRTCMode.Legacy
|
||||
? MatrixRTCMode.Compatibility
|
||||
: MatrixRTCMode.Legacy,
|
||||
configMode === MatrixRTCMode.Compatibility
|
||||
? MatrixRTCMode.Matrix_2_0
|
||||
: MatrixRTCMode.Compatibility,
|
||||
);
|
||||
const client = createMockMatrixClient();
|
||||
|
||||
@@ -397,13 +387,11 @@ describe("DeveloperSettingsTab", () => {
|
||||
);
|
||||
|
||||
const radios = getModeRadios();
|
||||
expect(radios.legacy).toBeDisabled();
|
||||
expect(radios.compatibility).toBeDisabled();
|
||||
expect(radios.matrix20).toBeDisabled();
|
||||
|
||||
const checkedValue = (
|
||||
{
|
||||
[MatrixRTCMode.Legacy]: radios.legacy,
|
||||
[MatrixRTCMode.Compatibility]: radios.compatibility,
|
||||
[MatrixRTCMode.Matrix_2_0]: radios.matrix20,
|
||||
} as const
|
||||
|
||||
@@ -520,22 +520,6 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
</Heading>
|
||||
{matrixRTCModeForced && <p>Your deployment overrides the mode.</p>}
|
||||
<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
|
||||
name={matrixRTCModeRadioGroup}
|
||||
control={
|
||||
|
||||
@@ -285,50 +285,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_ radix-_r_e_"
|
||||
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_"
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_"
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_b_"
|
||||
id="radix-_r_9_"
|
||||
name="_r_0_"
|
||||
title=""
|
||||
type="radio"
|
||||
@@ -344,13 +304,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_1o4d9_60"
|
||||
for="radix-_r_b_"
|
||||
for="radix-_r_9_"
|
||||
>
|
||||
Compatibility: state events & multi SFU
|
||||
</label>
|
||||
<span
|
||||
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)
|
||||
</span>
|
||||
@@ -366,9 +326,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<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_d_"
|
||||
id="radix-_r_b_"
|
||||
name="_r_0_"
|
||||
title=""
|
||||
type="radio"
|
||||
@@ -384,13 +344,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_1o4d9_60"
|
||||
for="radix-_r_d_"
|
||||
for="radix-_r_b_"
|
||||
>
|
||||
Matrix 2.0: sticky events & multi SFU
|
||||
</label>
|
||||
<span
|
||||
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
|
||||
</span>
|
||||
@@ -491,7 +451,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_k_"
|
||||
aria-describedby="_r_i_"
|
||||
id="cameraToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -521,7 +481,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</label>
|
||||
<p
|
||||
class="_description_1bd8c0"
|
||||
id="_r_k_"
|
||||
id="_r_i_"
|
||||
>
|
||||
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
|
||||
</p>
|
||||
@@ -543,7 +503,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_l_"
|
||||
aria-describedby="_r_j_"
|
||||
id="screenShareToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -573,7 +533,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</label>
|
||||
<p
|
||||
class="_description_1bd8c0"
|
||||
id="_r_l_"
|
||||
id="_r_j_"
|
||||
>
|
||||
Configure resolution, framerate, bitrate, and codec for screen sharing
|
||||
</p>
|
||||
@@ -598,7 +558,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_m_"
|
||||
aria-describedby="_r_k_"
|
||||
checked=""
|
||||
id="echoCancellation"
|
||||
type="checkbox"
|
||||
@@ -636,7 +596,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_n_"
|
||||
aria-describedby="_r_l_"
|
||||
checked=""
|
||||
id="noiseSuppression"
|
||||
type="checkbox"
|
||||
@@ -674,7 +634,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_o_"
|
||||
aria-describedby="_r_m_"
|
||||
checked=""
|
||||
id="autoGainControl"
|
||||
type="checkbox"
|
||||
|
||||
@@ -258,11 +258,9 @@ function mockRingEvent(
|
||||
} as unknown as { event_id: string } & IRTCNotificationContent;
|
||||
}
|
||||
|
||||
describe.each([
|
||||
[MatrixRTCMode.Legacy],
|
||||
[MatrixRTCMode.Compatibility],
|
||||
[MatrixRTCMode.Matrix_2_0],
|
||||
])("CallViewModel (%s mode)", (mode) => {
|
||||
const modes = [[MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0]];
|
||||
|
||||
describe.each(modes)("CallViewModel (%s mode)", (mode) => {
|
||||
const withCallViewModel = withCallViewModelInMode(mode);
|
||||
|
||||
test("participants are retained during a focus switch", () => {
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
import { type IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { type IMembershipManager } from "matrix-js-sdk/lib/matrixrtc/IMembershipManager";
|
||||
|
||||
import {
|
||||
createToggle$,
|
||||
@@ -366,6 +365,11 @@ export interface CallViewModel {
|
||||
*/
|
||||
overflowing$: Behavior<boolean>;
|
||||
|
||||
/**
|
||||
* Whether modals such as settings and reactions should be accessible at all.
|
||||
*/
|
||||
showModals$: Behavior<boolean>;
|
||||
|
||||
settingsOpen$: Behavior<boolean>;
|
||||
setSettingsOpen$: Behavior<(open: boolean) => void>;
|
||||
|
||||
@@ -436,7 +440,7 @@ export function createCallViewModel$(
|
||||
const matrixRTCMode$ =
|
||||
configMatrixRTCMode !== undefined
|
||||
? 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.
|
||||
// The outputs of this block is written under the hbar.
|
||||
@@ -483,22 +487,11 @@ export function createCallViewModel$(
|
||||
memberships$: memberships$,
|
||||
ownMembershipIdentity,
|
||||
client,
|
||||
delayId$: scope.behavior(
|
||||
(
|
||||
fromEvent(
|
||||
matrixRTCSession,
|
||||
MembershipManagerEvent.DelayIdChanged,
|
||||
// The type of reemitted event includes the original emitted as the second arg.
|
||||
) as Observable<[string | undefined, IMembershipManager]>
|
||||
).pipe(map(([delayId]) => delayId ?? null)),
|
||||
matrixRTCSession.delayId ?? null,
|
||||
),
|
||||
roomId: matrixRoom.roomId,
|
||||
forceJwtEndpoint:
|
||||
mode === MatrixRTCMode.Matrix_2_0
|
||||
? JwtEndpointVersion.Matrix_2_0
|
||||
: JwtEndpointVersion.Legacy,
|
||||
useOldestMember: mode === MatrixRTCMode.Legacy,
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -584,9 +577,20 @@ export function createCallViewModel$(
|
||||
);
|
||||
},
|
||||
connectionManager,
|
||||
client,
|
||||
matrixRTCSession,
|
||||
localTransport$,
|
||||
roomId: matrixRoom.roomId,
|
||||
ownMembershipIdentity,
|
||||
delayId$: scope.behavior(
|
||||
(
|
||||
fromEvent(
|
||||
matrixRTCSession,
|
||||
MembershipManagerEvent.DelayIdChanged,
|
||||
) as Observable<[string | undefined]>
|
||||
).pipe(map(([delayId]) => delayId ?? null)),
|
||||
matrixRTCSession.delayId ?? null,
|
||||
),
|
||||
logger: logger.getChild(`[${Date.now()}]`),
|
||||
});
|
||||
|
||||
@@ -1450,6 +1454,11 @@ export function createCallViewModel$(
|
||||
map((naturallyShowFooter) => naturallyShowFooter && showFooterUrlParams),
|
||||
),
|
||||
);
|
||||
|
||||
const showModals$ = scope.behavior(
|
||||
windowMode$.pipe(map((mode) => mode !== "pip")),
|
||||
);
|
||||
|
||||
const settingsOpen$ = new BehaviorSubject(false);
|
||||
const setSettingsOpen$ = constant((open: boolean) => {
|
||||
settingsOpen$.next(open);
|
||||
@@ -1826,6 +1835,7 @@ export function createCallViewModel$(
|
||||
showNameTags$,
|
||||
showHeader$: showHeader$,
|
||||
showFooter$: showFooter$,
|
||||
showModals$,
|
||||
settingsOpen$: settingsOpen$,
|
||||
setSettingsOpen$: setSettingsOpen$,
|
||||
edgeToEdge$,
|
||||
|
||||
@@ -59,7 +59,7 @@ import {
|
||||
|
||||
initializeWidget();
|
||||
|
||||
const MATRIX_RTC_MODE = MatrixRTCMode.Legacy;
|
||||
const MATRIX_RTC_MODE = MatrixRTCMode.Compatibility;
|
||||
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
|
||||
vi.mock("../../../UrlParams", () => ({ getUrlParams }));
|
||||
vi.mock("@livekit/components-core", () => ({
|
||||
@@ -71,12 +71,6 @@ vi.mock("@livekit/components-core", () => ({
|
||||
describe("LocalMembership", () => {
|
||||
describe("enterRTCSession", () => {
|
||||
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({
|
||||
livekit: { livekit_service_url: "http://my-default-service-url.com" },
|
||||
});
|
||||
@@ -95,10 +89,6 @@ describe("LocalMembership", () => {
|
||||
},
|
||||
},
|
||||
memberships: [],
|
||||
getFocusInUse: vi.fn().mockReturnValue(focusFromOlderMembership),
|
||||
getOldestMembership: vi.fn().mockReturnValue({
|
||||
getPreferredFoci: vi.fn().mockReturnValue([focusFromOlderMembership]),
|
||||
}),
|
||||
joinRTCSession: vi.fn(),
|
||||
}) as unknown as MatrixRTCSession;
|
||||
|
||||
@@ -122,18 +112,13 @@ describe("LocalMembership", () => {
|
||||
memberId: "@alice:example.org:DEVICE",
|
||||
userId: "@alice:example.org",
|
||||
},
|
||||
[
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-livekit-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
manageMediaKeys: true,
|
||||
useLegacyMemberEvents: false,
|
||||
}),
|
||||
[],
|
||||
{
|
||||
livekit_alias: "roomId",
|
||||
livekit_service_url: "http://my-livekit-service-url.com",
|
||||
type: "livekit",
|
||||
},
|
||||
expect.objectContaining({ manageMediaKeys: true }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,11 @@ import {
|
||||
MediaDeviceFailure,
|
||||
} from "livekit-client";
|
||||
import { observeParticipantEvents } from "@livekit/components-core";
|
||||
import {
|
||||
type IOpenIDToken,
|
||||
parseErrorResponse,
|
||||
type MatrixClient,
|
||||
} from "matrix-js-sdk";
|
||||
import {
|
||||
Status as RTCSessionStatus,
|
||||
type LivekitTransport,
|
||||
@@ -72,6 +77,7 @@ import {
|
||||
import { type HomeserverConnected } from "./HomeserverConnected.ts";
|
||||
import { type LocalTransport } from "./LocalTransport.ts";
|
||||
import { areLivekitTransportsEqual } from "../remoteMembers/MatrixLivekitMembers.ts";
|
||||
import { doNetworkOperationWithRetry } from "../../../utils/matrix.ts";
|
||||
|
||||
export enum TransportState {
|
||||
/** Not even a transport is available to the LocalMembership */
|
||||
@@ -117,7 +123,6 @@ export type LocalMemberState =
|
||||
};
|
||||
|
||||
/*
|
||||
* - get oldest membership
|
||||
* - get transport to use
|
||||
* - get openId + jwt token
|
||||
* - wait for createTrack() call
|
||||
@@ -137,11 +142,14 @@ interface Props {
|
||||
joinMatrixRTC: (transport: LivekitTransportConfig) => void;
|
||||
homeserverConnected: HomeserverConnected;
|
||||
roomId: string;
|
||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||
localTransport$: Behavior<LocalTransport>;
|
||||
client: Pick<MatrixClient, "getOpenIdToken" | "baseUrl">;
|
||||
matrixRTCSession: Pick<
|
||||
MatrixRTCSession,
|
||||
"updateCallIntent" | "leaveRoomSession"
|
||||
"slotId" | "updateCallIntent" | "leaveRoomSession"
|
||||
>;
|
||||
delayId$: Behavior<string | null>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
@@ -177,8 +185,11 @@ export const createLocalMembership$ = ({
|
||||
joinMatrixRTC,
|
||||
logger: parentLogger,
|
||||
muteStates,
|
||||
client,
|
||||
matrixRTCSession,
|
||||
roomId,
|
||||
ownMembershipIdentity,
|
||||
delayId$,
|
||||
}: Props): {
|
||||
/**
|
||||
* This request to start audio and video tracks.
|
||||
@@ -640,6 +651,61 @@ export const createLocalMembership$ = ({
|
||||
},
|
||||
);
|
||||
|
||||
scope.reconcile(
|
||||
scope.behavior(combineLatest([delayId$, activeTransport$])),
|
||||
async ([delayId, transport]) => {
|
||||
if (delayId !== null && transport !== null) {
|
||||
logger.info(
|
||||
`Delegating delayed event ${delayId} to ${transport.livekit_service_url}…`,
|
||||
);
|
||||
let openIdToken: IOpenIDToken;
|
||||
try {
|
||||
openIdToken = await doNetworkOperationWithRetry(async () =>
|
||||
client.getOpenIdToken(),
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error("Failed to get OpenID token for delegation", e);
|
||||
return;
|
||||
}
|
||||
const body = JSON.stringify({
|
||||
room_id: roomId,
|
||||
slot_id: matrixRTCSession.slotId,
|
||||
member: {
|
||||
id: ownMembershipIdentity.memberId,
|
||||
claimed_user_id: ownMembershipIdentity.userId,
|
||||
claimed_device_id: ownMembershipIdentity.deviceId,
|
||||
},
|
||||
delay_id: delayId,
|
||||
delay_timeout:
|
||||
Config.get().matrix_rtc_session?.delayed_leave_event_delay_ms,
|
||||
delay_cs_api_url: client.baseUrl,
|
||||
openid_token: openIdToken,
|
||||
});
|
||||
try {
|
||||
const res = await doNetworkOperationWithRetry(async () =>
|
||||
fetch(transport.livekit_service_url + "/delegate_delayed_leave", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
}),
|
||||
);
|
||||
if (!res.ok) {
|
||||
if (res.status === 404)
|
||||
logger.warn(
|
||||
`Delayed event delegation not available on ${transport.livekit_service_url}`,
|
||||
);
|
||||
else throw parseErrorResponse(res, await res.text());
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
`Failed to delegate ${delayId} to ${transport.livekit_service_url}`,
|
||||
e,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const participant$ = scope.behavior(
|
||||
localConnection$.pipe(
|
||||
map((c) => c?.livekitRoom?.localParticipant ?? null),
|
||||
@@ -851,9 +917,7 @@ export function enterRTCSession(
|
||||
// have started tracking by the time calls start getting created.
|
||||
// groupCallOTelMembership?.onJoinCall();
|
||||
|
||||
const { features, matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
||||
const useDeviceSessionMemberEvents =
|
||||
features?.feature_use_device_session_member_events;
|
||||
const { matrix_rtc_session: matrixRtcSessionConfig } = Config.get();
|
||||
const { sendNotificationType: notificationType, callIntent } = getUrlParams();
|
||||
const multiSFU =
|
||||
matrixRTCMode === MatrixRTCMode.Compatibility ||
|
||||
@@ -895,9 +959,6 @@ export function enterRTCSession(
|
||||
notificationType,
|
||||
callIntent,
|
||||
manageMediaKeys: encryptMedia,
|
||||
...(useDeviceSessionMemberEvents !== undefined && {
|
||||
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
||||
}),
|
||||
delayedLeaveEventRestartMs:
|
||||
matrixRtcSessionConfig?.delayed_leave_event_restart_ms,
|
||||
delayedLeaveEventDelayMs:
|
||||
|
||||
@@ -13,29 +13,20 @@ import {
|
||||
it,
|
||||
type MockedObject,
|
||||
vi,
|
||||
type MockInstance,
|
||||
} from "vitest";
|
||||
import {
|
||||
type CallMembership,
|
||||
type LivekitTransportConfig,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { BehaviorSubject, filter, lastValueFrom } from "rxjs";
|
||||
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import {
|
||||
mockConfig,
|
||||
flushPromises,
|
||||
ownMemberMock,
|
||||
mockRtcMembership,
|
||||
testScope,
|
||||
} from "../../../utils/test";
|
||||
import {
|
||||
createLocalTransport$,
|
||||
JwtEndpointVersion,
|
||||
type LocalTransportWithSFUConfig,
|
||||
} from "./LocalTransport";
|
||||
import { createLocalTransport$, JwtEndpointVersion } from "./LocalTransport";
|
||||
import { constant } from "../../Behavior";
|
||||
import { Epoch, ObservableScope, trackEpoch } from "../../ObservableScope";
|
||||
import { Epoch, ObservableScope } from "../../ObservableScope";
|
||||
import {
|
||||
MatrixRTCTransportMissingError,
|
||||
FailToGetOpenIdToken,
|
||||
@@ -58,7 +49,6 @@ describe("LocalTransport", () => {
|
||||
const { advertised$, active$ } = createLocalTransport$({
|
||||
scope: testScope(),
|
||||
roomId: "!room:example.org",
|
||||
useOldestMember: false,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -71,7 +61,6 @@ describe("LocalTransport", () => {
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
await flushPromises();
|
||||
|
||||
@@ -101,7 +90,6 @@ describe("LocalTransport", () => {
|
||||
const { advertised$, active$ } = createLocalTransport$({
|
||||
scope,
|
||||
roomId: "!example_room_id",
|
||||
useOldestMember: false,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
baseUrl: "https://example.org",
|
||||
@@ -113,7 +101,6 @@ describe("LocalTransport", () => {
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
active$.subscribe(
|
||||
(o) => observations.push(o),
|
||||
@@ -144,7 +131,6 @@ describe("LocalTransport", () => {
|
||||
const { advertised$, active$ } = createLocalTransport$({
|
||||
scope: testScope(),
|
||||
roomId: "!room:example.org",
|
||||
useOldestMember: false,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
@@ -156,7 +142,6 @@ describe("LocalTransport", () => {
|
||||
},
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: constant("delay_id_mock"),
|
||||
});
|
||||
|
||||
openIdResolver.resolve?.({
|
||||
@@ -185,127 +170,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];
|
||||
|
||||
describe("transport configuration mechanisms", () => {
|
||||
@@ -320,9 +184,7 @@ describe("LocalTransport", () => {
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
scope: testScope(),
|
||||
roomId: "!example_room_id",
|
||||
useOldestMember: false,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: constant(null),
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
baseUrl: "https://example.org",
|
||||
@@ -433,9 +295,7 @@ describe("LocalTransport", () => {
|
||||
scope: testScope(),
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
roomId: "!example_room_id",
|
||||
useOldestMember: false,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: constant(null),
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
getDomain: () => "example.org",
|
||||
@@ -457,85 +317,4 @@ describe("LocalTransport", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not update advertised/active transport on delayID changes, but delay Id delegation should be called", async () => {
|
||||
// For simplicity, we'll just use the config livekit
|
||||
customLivekitUrl.setValue("https://lk.example.org");
|
||||
|
||||
const authCallSpy = vi
|
||||
.spyOn(openIDSFU, "getSFUConfigWithOpenID")
|
||||
.mockResolvedValue(openIdResponse);
|
||||
|
||||
const delayId$ = new BehaviorSubject<string | null>(null);
|
||||
|
||||
const { advertised$, active$ } = createLocalTransport$({
|
||||
scope: testScope(),
|
||||
ownMembershipIdentity: ownMemberMock,
|
||||
roomId: "!example_room_id",
|
||||
// We want multi-sdu
|
||||
useOldestMember: false,
|
||||
forceJwtEndpoint: JwtEndpointVersion.Legacy,
|
||||
delayId$: delayId$,
|
||||
memberships$: constant(new Epoch<CallMembership[]>([])),
|
||||
client: {
|
||||
getDomain: () => "example.org",
|
||||
baseUrl: "https://example.org",
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
_unstable_getRTCTransports: async () => Promise.resolve([]),
|
||||
// These won't be called in this error path but satisfy the type
|
||||
getOpenIdToken: vi.fn(),
|
||||
getDeviceId: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
const advertisedValues: LivekitTransportConfig[] = [];
|
||||
const activeValues: LocalTransportWithSFUConfig[] = [];
|
||||
advertised$
|
||||
.pipe(filter((v) => v !== null))
|
||||
.subscribe((t) => advertisedValues.push(t));
|
||||
active$
|
||||
.pipe(filter((v) => v !== null))
|
||||
.subscribe((t) => activeValues.push(t));
|
||||
|
||||
await flushPromises();
|
||||
|
||||
// we have now an active and an advertised
|
||||
expect(advertisedValues.length).toEqual(1);
|
||||
expect(activeValues.length).toEqual(1);
|
||||
expect(advertisedValues[0]!.livekit_service_url).toEqual(
|
||||
"https://lk.example.org",
|
||||
);
|
||||
expect(activeValues[0]!.transport.livekit_service_url).toEqual(
|
||||
"https://lk.example.org",
|
||||
);
|
||||
|
||||
expect(authCallSpy).toHaveBeenCalledTimes(2);
|
||||
// Now emits 3 new delays id
|
||||
delayId$.next("delay_id_1");
|
||||
await flushPromises();
|
||||
delayId$.next("delay_id_2");
|
||||
await flushPromises();
|
||||
delayId$.next("delay_id_3");
|
||||
await flushPromises();
|
||||
|
||||
// No new emissions should've happened, it is the same transport.
|
||||
expect(advertisedValues.length).toEqual(1);
|
||||
expect(activeValues.length).toEqual(1);
|
||||
|
||||
// Still we should have updated the delayID to auth
|
||||
expect(authCallSpy).toHaveBeenCalledTimes(
|
||||
4 * 2 /* 2 calls for each delayId ?? why */,
|
||||
);
|
||||
|
||||
expect(authCallSpy).toHaveBeenLastCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
delayId: "delay_id_3",
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,24 +7,10 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import {
|
||||
type CallMembership,
|
||||
isLivekitTransportConfig,
|
||||
type LivekitTransportConfig,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { type MatrixClient } from "matrix-js-sdk";
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
first,
|
||||
from,
|
||||
map,
|
||||
merge,
|
||||
type Observable,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
import { distinctUntilChanged, from, map, of, switchMap } from "rxjs";
|
||||
import { logger as rootLogger, type Logger } from "matrix-js-sdk/lib/logger";
|
||||
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
|
||||
|
||||
@@ -47,8 +33,7 @@ import { RtcTransportAutoDiscovery } from "./RtcTransportAutoDiscovery.ts";
|
||||
|
||||
/*
|
||||
* 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
|
||||
* before advertising that choice.
|
||||
* and ensures the SFU path is primed before advertising that choice.
|
||||
*/
|
||||
interface Props {
|
||||
scope: ObservableScope;
|
||||
@@ -61,9 +46,7 @@ interface Props {
|
||||
OpenIDClientParts;
|
||||
// Used by the jwt service to create the livekit room and compute the livekit alias.
|
||||
roomId: string;
|
||||
useOldestMember: boolean;
|
||||
forceJwtEndpoint: JwtEndpointVersion;
|
||||
delayId$: Behavior<string | null>;
|
||||
}
|
||||
|
||||
export enum JwtEndpointVersion {
|
||||
@@ -119,8 +102,6 @@ export interface LocalTransport {
|
||||
/**
|
||||
* 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.
|
||||
* 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)
|
||||
@@ -133,18 +114,9 @@ export const createLocalTransport$ = ({
|
||||
ownMembershipIdentity,
|
||||
client,
|
||||
roomId,
|
||||
useOldestMember,
|
||||
forceJwtEndpoint,
|
||||
delayId$,
|
||||
}: Props): 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({
|
||||
client: client,
|
||||
@@ -181,8 +153,8 @@ export const createLocalTransport$ = ({
|
||||
distinctUntilChanged(areLivekitTransportsEqual),
|
||||
);
|
||||
|
||||
const preferredTransport$ = combineLatest([preferredConfig$, delayId$]).pipe(
|
||||
switchMap(async ([transport, delayId]) => {
|
||||
const preferredTransport$ = preferredConfig$.pipe(
|
||||
switchMap(async (transport) => {
|
||||
try {
|
||||
return await doOpenIdAndJWTFromUrl(
|
||||
transport,
|
||||
@@ -190,7 +162,6 @@ export const createLocalTransport$ = ({
|
||||
ownMembershipIdentity,
|
||||
roomId,
|
||||
client,
|
||||
delayId ?? undefined,
|
||||
logger,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -203,19 +174,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.
|
||||
return {
|
||||
advertised$: scope.behavior(
|
||||
@@ -243,47 +201,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.
|
||||
* We will call `getSFUConfigWithOpenID` once per transport here as it's our
|
||||
@@ -296,7 +213,6 @@ function observerOldestMembership$(
|
||||
* @param membership The identity of the local member.
|
||||
* @param roomId The room ID to use for the JWT.
|
||||
* @param client The client to use for the OpenID token.
|
||||
* @param delayId The delayId to use for the JWT.
|
||||
*
|
||||
* @throws FailToGetOpenIdToken, NoMatrix2AuthorizationService
|
||||
*/
|
||||
@@ -305,12 +221,7 @@ async function doOpenIdAndJWTFromUrl(
|
||||
forceJwtEndpoint: JwtEndpointVersion,
|
||||
membership: CallMembershipIdentityParts,
|
||||
roomId: string,
|
||||
client: Pick<
|
||||
MatrixClient,
|
||||
"getDomain" | "baseUrl" | "_unstable_getRTCTransports"
|
||||
> &
|
||||
OpenIDClientParts,
|
||||
delayId?: string,
|
||||
client: Pick<MatrixClient, "_unstable_getRTCTransports"> & OpenIDClientParts,
|
||||
logger?: Logger,
|
||||
): Promise<LocalTransportWithSFUConfig> {
|
||||
const sfuConfig = await getSFUConfigWithOpenID(
|
||||
@@ -320,8 +231,6 @@ async function doOpenIdAndJWTFromUrl(
|
||||
roomId,
|
||||
{
|
||||
forceJwtEndpoint: forceJwtEndpoint,
|
||||
delayEndpointBaseUrl: client.baseUrl,
|
||||
delayId,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
@@ -331,85 +240,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 {
|
||||
if (
|
||||
e instanceof FailToGetOpenIdToken ||
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
SFURoomCreationRestrictedError,
|
||||
UnknownCallError,
|
||||
} from "../../../utils/errors.ts";
|
||||
import { type JwtEndpointVersion } from "../localMember/LocalTransport.ts";
|
||||
|
||||
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.
|
||||
*/
|
||||
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 */
|
||||
ownMembershipIdentity: CallMembershipIdentityParts;
|
||||
/** The media transport to connect to. */
|
||||
|
||||
@@ -35,11 +35,7 @@ vi.mock("../widget", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
it.each([
|
||||
[MatrixRTCMode.Legacy],
|
||||
[MatrixRTCMode.Compatibility],
|
||||
[MatrixRTCMode.Matrix_2_0],
|
||||
])(
|
||||
it.each([[MatrixRTCMode.Compatibility], [MatrixRTCMode.Matrix_2_0]])(
|
||||
"expect leave when ElementWidgetActions.HangupCall is called (%s mode)",
|
||||
async (mode) => {
|
||||
const pr = Promise.withResolvers<string>();
|
||||
|
||||
@@ -171,7 +171,7 @@ export function getBasicCallViewModelEnvironment(
|
||||
setE2EEEnabled: async () => Promise.resolve(),
|
||||
}),
|
||||
connectionState$: constant(ConnectionState.Connected),
|
||||
matrixRTCMode$: constant(MatrixRTCMode.Legacy),
|
||||
matrixRTCMode$: constant(MatrixRTCMode.Compatibility),
|
||||
...callViewModelOptions,
|
||||
},
|
||||
handRaisedSubject$,
|
||||
|
||||
@@ -237,7 +237,7 @@ export function mockRtcMembership(
|
||||
fociPreferred: [exampleTransport],
|
||||
focusActive: {
|
||||
type: "livekit" as const,
|
||||
focus_selection: "oldest_membership" as const,
|
||||
focus_selection: "multi_sfu" as const,
|
||||
},
|
||||
callId: "",
|
||||
membership: {},
|
||||
@@ -463,9 +463,6 @@ export class MockRTCSession extends TypedEventEmitter<
|
||||
session.reemitEncryptionKeys = vi
|
||||
.fn<() => void>()
|
||||
.mockReturnValue(undefined);
|
||||
session.getOldestMembership = vi
|
||||
.fn<() => CallMembership | undefined>()
|
||||
.mockReturnValue(this.memberships[0]);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user