mirror of
https://github.com/vector-im/element-call.git
synced 2026-11-07 00:52:28 +00:00
Publish on full mesh from Element Call
A developer setting switches the published transport to full mesh; members on either transport are received whatever it says. The widget allows the full mesh signalling events while the setting is on, and asks for TURN servers. The developer panel shows a full mesh transport with its peer connections. A widget e2e spec covers two members calling over full mesh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
14dc93c078
commit
682edd25a9
@@ -78,6 +78,9 @@
|
||||
"device_id": "Device ID: {{id}}",
|
||||
"duplicate_tiles_label": "Number of additional tile copies per participant",
|
||||
"environment_variables": "Environment variables",
|
||||
"full_mesh_peer_connections": "Full mesh peer connections",
|
||||
"full_mesh_publishing": "Publish on full mesh (MSC4575) instead of LiveKit",
|
||||
"full_mesh_transport": "Full mesh: {{device}}",
|
||||
"hostname": "Hostname: {{hostname}}",
|
||||
"livekit_server_info": "LiveKit Server Info",
|
||||
"livekit_sfu": "LiveKit SFU: {{url}}",
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
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, type Page, test } from "@playwright/test";
|
||||
|
||||
import { widgetTest } from "../fixtures/widget-user";
|
||||
import { HOST1, TestHelpers } from "./test-helpers";
|
||||
|
||||
/**
|
||||
* Records the LiveKit signalling websockets a page opens: the SFU is reached
|
||||
* with an access token in the url, which full mesh never needs.
|
||||
*/
|
||||
function recordSfuSockets(page: Page): string[] {
|
||||
const urls: string[] = [];
|
||||
page.on("websocket", (ws) => {
|
||||
if (ws.url().includes("access_token")) urls.push(ws.url());
|
||||
});
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts recording the types of the to-device messages the widget on a page
|
||||
* asks its host to send, and returns a function reading them. On the wire
|
||||
* they are encrypted, so the type is only visible in the widget API.
|
||||
*/
|
||||
async function recordWidgetToDevice(
|
||||
page: Page,
|
||||
): Promise<() => Promise<string[]>> {
|
||||
await page.evaluate(() => {
|
||||
const types: string[] = [];
|
||||
Object.assign(window, { widgetToDeviceTypes: types });
|
||||
window.addEventListener("message", ({ data }) => {
|
||||
const {
|
||||
action,
|
||||
response,
|
||||
data: request,
|
||||
} = (data ?? {}) as {
|
||||
action?: string;
|
||||
response?: unknown;
|
||||
data?: { type?: string };
|
||||
};
|
||||
if (action === "send_to_device" && response === undefined)
|
||||
types.push(request?.type ?? "");
|
||||
});
|
||||
});
|
||||
return async () =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(window as unknown as { widgetToDeviceTypes: string[] })
|
||||
.widgetToDeviceTypes,
|
||||
);
|
||||
}
|
||||
|
||||
const cases: { name: string; fullMesh: [boolean, boolean] }[] = [
|
||||
// Both publish on full mesh: the media flows without any SFU
|
||||
{ name: "both on full mesh", fullMesh: [true, true] },
|
||||
// One on each: Florian receives Valere over LiveKit without publishing there
|
||||
{ name: "full mesh and LiveKit mixed", fullMesh: [true, false] },
|
||||
];
|
||||
|
||||
cases.forEach(({ name, fullMesh: [florianMesh, valereMesh] }) => {
|
||||
widgetTest(`Full mesh call: ${name}`, async ({ addUser, browserName }) => {
|
||||
test.skip(
|
||||
browserName === "firefox",
|
||||
"The is test is not working on firefox CI environment. No mic/audio device inputs so cam/mic are disabled",
|
||||
);
|
||||
test.slow();
|
||||
|
||||
// Distinct per case: the cases run in parallel and register at once
|
||||
const suffix = `${Number(florianMesh)}${Number(valereMesh)}`;
|
||||
const [florian, valere] = await Promise.all([
|
||||
addUser(`florian${suffix}`, HOST1),
|
||||
addUser(`valere${suffix}`, HOST1),
|
||||
]);
|
||||
// Element Web doesn't auto-approve the full mesh signalling for Element
|
||||
// Call yet, so it asks for it whenever a widget with full mesh opens
|
||||
for (const { page } of [florian, valere])
|
||||
await page.addLocatorHandler(
|
||||
page.getByRole("heading", { name: "Approve widget permissions" }),
|
||||
async () => page.getByRole("button", { name: "Approve" }).click(),
|
||||
);
|
||||
const sockets = {
|
||||
florian: recordSfuSockets(florian.page),
|
||||
valere: recordSfuSockets(valere.page),
|
||||
};
|
||||
|
||||
const roomName = "Full Mesh Room";
|
||||
await TestHelpers.createRoom(roomName, florian.page, [valere.mxId]);
|
||||
await TestHelpers.acceptRoomInvite(roomName, valere.page);
|
||||
|
||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
||||
florian.page,
|
||||
"2_0",
|
||||
{ fullMesh: florianMesh },
|
||||
);
|
||||
await TestHelpers.openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
||||
valere.page,
|
||||
"2_0",
|
||||
{ fullMesh: valereMesh },
|
||||
);
|
||||
|
||||
await TestHelpers.startCallInCurrentRoom(florian.page, false);
|
||||
await TestHelpers.joinCallFromLobby(florian.page);
|
||||
await TestHelpers.joinCallInCurrentRoom(valere.page);
|
||||
|
||||
// A widget only asks for the full mesh signalling with full mesh turned
|
||||
// on, so a LiveKit publisher in Element Web can't receive full mesh yet:
|
||||
// the other's media arrives if it is on LiveKit or we have full mesh
|
||||
const receives = {
|
||||
florian: !valereMesh || florianMesh,
|
||||
valere: !florianMesh || valereMesh,
|
||||
};
|
||||
// In parallel: assertions on a page also run its permission handler
|
||||
await Promise.all(
|
||||
(["florian", "valere"] as const).map(async (name) => {
|
||||
const frame = { florian, valere }[name].page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
await expect(frame.getByTestId("videoTile")).toHaveCount(2, {
|
||||
timeout: 15000,
|
||||
});
|
||||
const waiting = frame.getByText("Waiting for media...");
|
||||
if (receives[name]) {
|
||||
// Over whichever transport carries it
|
||||
await expect(waiting).not.toBeVisible({ timeout: 20000 });
|
||||
await TestHelpers.expectVisibleVideoCount(frame, 2);
|
||||
} else {
|
||||
await expect(waiting).toBeVisible({ timeout: 20000 });
|
||||
await TestHelpers.expectVisibleVideoCount(frame, 1);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (florianMesh && valereMesh) {
|
||||
// Nobody published on LiveKit, so nobody connected to an SFU
|
||||
expect(sockets.florian).toEqual([]);
|
||||
expect(sockets.valere).toEqual([]);
|
||||
} else {
|
||||
// Florian receives Valere's media from the SFU without publishing there
|
||||
expect(sockets.florian.length).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Florian sees Valere's camera go off: over the track metadata for full
|
||||
// mesh, without renegotiating
|
||||
const toDevice = await recordWidgetToDevice(valere.page);
|
||||
const valereFrame = valere.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
await valereFrame.getByRole("switch", { name: "Stop video" }).click();
|
||||
const florianFrame = florian.page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame();
|
||||
await TestHelpers.expectVisibleVideoCount(florianFrame, 1);
|
||||
// On the connection's control channel, not through the homeserver.
|
||||
// Candidates may still trickle there until the channel is up
|
||||
if (florianMesh && valereMesh)
|
||||
expect(
|
||||
(await toDevice()).filter(
|
||||
(type) => type.includes("full_mesh") && !type.endsWith(".candidates"),
|
||||
),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -306,6 +306,7 @@ export class TestHelpers {
|
||||
public static async openWidgetSetEmbeddedElementCallRtcModeCloseWidget(
|
||||
page: Page,
|
||||
mode: RtcMode,
|
||||
{ fullMesh = false }: { fullMesh?: boolean } = {},
|
||||
): Promise<void> {
|
||||
await page.getByRole("button", { name: "Video call" }).click({
|
||||
timeout: 5000,
|
||||
@@ -314,7 +315,7 @@ export class TestHelpers {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
await TestHelpers.setEmbeddedElementCallRtcMode(page, mode);
|
||||
await TestHelpers.setEmbeddedElementCallRtcMode(page, mode, { fullMesh });
|
||||
await page.getByRole("button", { name: "Close lobby" }).click();
|
||||
}
|
||||
|
||||
@@ -327,6 +328,7 @@ export class TestHelpers {
|
||||
public static async setEmbeddedElementCallRtcMode(
|
||||
page: Page,
|
||||
mode: RtcMode,
|
||||
{ fullMesh = false }: { fullMesh?: boolean } = {},
|
||||
): Promise<void> {
|
||||
const iframe = page.locator('iframe[title="Element Call"]').contentFrame();
|
||||
|
||||
@@ -344,6 +346,10 @@ export class TestHelpers {
|
||||
// compat
|
||||
await iframe.getByText("Compatibility: state events").click();
|
||||
}
|
||||
// Publish on the full mesh transport (MSC4575) instead of LiveKit
|
||||
await iframe
|
||||
.getByText("Publish on full mesh", { exact: false })
|
||||
.setChecked(fullMesh);
|
||||
await iframe.getByTestId("modal_close").click();
|
||||
}
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ import { ReactionsOverlay } from "./ReactionsOverlay";
|
||||
import { CallEventAudioRenderer } from "./CallEventAudioRenderer";
|
||||
import {
|
||||
createRTCSlot,
|
||||
FULL_MESH_TRANSPORT_TYPE,
|
||||
type RTCParticipationOptions,
|
||||
type RTCSlotOptions,
|
||||
constant,
|
||||
@@ -85,6 +86,7 @@ import {
|
||||
cameraFramerate,
|
||||
cameraResolution,
|
||||
customTransport,
|
||||
fullMeshPublishing,
|
||||
echoCancellationSetting,
|
||||
matrixRTCMode as matrixRTCModeSetting,
|
||||
noiseSuppressionSetting,
|
||||
@@ -837,7 +839,9 @@ export function rtcParticipationOptions(
|
||||
delegatedDelayedLeave: session.delegated_delayed_leave,
|
||||
},
|
||||
mediaQuality: config.media_quality,
|
||||
transport: customTransport.value$.value ?? undefined,
|
||||
transport: fullMeshPublishing.value$.value
|
||||
? { type: FULL_MESH_TRANSPORT_TYPE }
|
||||
: (customTransport.value$.value ?? undefined),
|
||||
fallbackTransport: config.livekit && {
|
||||
type: "livekit",
|
||||
livekit_service_url: config.livekit.livekit_service_url,
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
|
||||
matrixRTCMode as matrixRTCModeSetting,
|
||||
customTransport as customTransportSetting,
|
||||
fullMeshPublishing as fullMeshPublishingSetting,
|
||||
advancedScreenShare as advancedScreenShareSetting,
|
||||
screenShareResolution as screenShareResolutionSetting,
|
||||
screenShareFramerate as screenShareFramerateSetting,
|
||||
@@ -68,6 +69,7 @@ import { Slider } from "../Slider";
|
||||
import { useUrlParams } from "../UrlParams";
|
||||
import {
|
||||
authenticateWithTransport,
|
||||
FULL_MESH_TRANSPORT_TYPE,
|
||||
type TransportMetadata,
|
||||
} from "@element-hq/matrixrtc-sdk";
|
||||
import { useBehavior } from "../useBehavior";
|
||||
@@ -175,6 +177,9 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
const [customTransportUpdateError, setCustomTransportUpdateError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [fullMeshPublishing, setFullMeshPublishing] = useSetting(
|
||||
fullMeshPublishingSetting,
|
||||
);
|
||||
const [customTransport, setCustomTransport] = useSetting(
|
||||
customTransportSetting,
|
||||
);
|
||||
@@ -417,6 +422,17 @@ export const DeveloperSettingsTab: FC<Props> = ({
|
||||
)}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="fullMeshPublishing"
|
||||
type="checkbox"
|
||||
checked={fullMeshPublishing}
|
||||
label={t("developer_mode.full_mesh_publishing")}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>): void =>
|
||||
setFullMeshPublishing(event.target.checked)
|
||||
}
|
||||
/>
|
||||
</FieldRow>
|
||||
<FieldRow>
|
||||
<InputField
|
||||
id="debugTileLayout"
|
||||
@@ -665,11 +681,21 @@ function transportText(transport: UnstableLivekitTransport | null): string {
|
||||
const TransportBox: FC<TransportInfo> = ({ transport, local }) => {
|
||||
const { t } = useTranslation();
|
||||
const resolved = useBehavior(transport.resolved$);
|
||||
const fullMesh = transport.type === FULL_MESH_TRANSPORT_TYPE;
|
||||
return (
|
||||
<div className={styles.livekit_room_box}>
|
||||
<h4>{t("developer_mode.livekit_sfu", { url: transport.id })}</h4>
|
||||
<h4>
|
||||
{fullMesh
|
||||
? t("developer_mode.full_mesh_transport", {
|
||||
device: transport.raw.device_id,
|
||||
})
|
||||
: t("developer_mode.livekit_sfu", { url: transport.id })}
|
||||
</h4>
|
||||
<p>
|
||||
{t("developer_mode.livekit_server_info")}({local ? "local" : "remote"})
|
||||
{fullMesh
|
||||
? t("developer_mode.full_mesh_peer_connections")
|
||||
: t("developer_mode.livekit_server_info")}
|
||||
({local ? "local" : "remote"})
|
||||
</p>
|
||||
<pre className={styles.pre}>
|
||||
{resolved === undefined
|
||||
|
||||
@@ -51,6 +51,43 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_2_"
|
||||
id="fullMeshPublishing"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
for="fullMeshPublishing"
|
||||
>
|
||||
<div
|
||||
class="_checkbox_1bd8c0"
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
height="24"
|
||||
stroke="#000"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m20 6-11 11-5-5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
Publish on full mesh (MSC4575) instead of LiveKit
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="_fieldRow_1bd8c0"
|
||||
>
|
||||
<div
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_3_"
|
||||
id="debugTileLayout"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -87,7 +124,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_3_"
|
||||
aria-describedby="_r_4_"
|
||||
id="showConnectionStats"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -124,7 +161,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_4_"
|
||||
aria-describedby="_r_5_"
|
||||
id="muteAllAudio"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -162,7 +199,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_5_"
|
||||
aria-describedby="_r_6_"
|
||||
id="alwaysShowIphoneEarpiece"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -199,7 +236,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_6_"
|
||||
aria-describedby="_r_7_"
|
||||
id="enableLivekitExtendedLogs"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -237,7 +274,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_1o4d9_60"
|
||||
for="radix-_r_7_"
|
||||
for="radix-_r_8_"
|
||||
>
|
||||
Custom transport JSON
|
||||
</label>
|
||||
@@ -245,9 +282,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_controls_17lij_8"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_8_"
|
||||
aria-describedby="radix-_r_9_"
|
||||
class="_control_d83jn_10"
|
||||
id="radix-_r_7_"
|
||||
id="radix-_r_8_"
|
||||
name="input"
|
||||
title=""
|
||||
value=""
|
||||
@@ -255,7 +292,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</div>
|
||||
<span
|
||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||
id="radix-_r_8_"
|
||||
id="radix-_r_9_"
|
||||
>
|
||||
Currently, no overwrite is set. Url from config is used.
|
||||
</span>
|
||||
@@ -285,10 +322,10 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_"
|
||||
aria-describedby="radix-_r_b_ radix-_r_d_"
|
||||
checked=""
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_9_"
|
||||
id="radix-_r_a_"
|
||||
name="_r_0_"
|
||||
title=""
|
||||
type="radio"
|
||||
@@ -304,13 +341,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_1o4d9_60"
|
||||
for="radix-_r_9_"
|
||||
for="radix-_r_a_"
|
||||
>
|
||||
Compatibility: state events & multi SFU
|
||||
</label>
|
||||
<span
|
||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||
id="radix-_r_a_"
|
||||
id="radix-_r_b_"
|
||||
>
|
||||
Compatible with homeservers that do not support sticky events (but all other EC clients are v0.17.0 or later)
|
||||
</span>
|
||||
@@ -326,9 +363,9 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_container_1ug7n_10"
|
||||
>
|
||||
<input
|
||||
aria-describedby="radix-_r_a_ radix-_r_c_"
|
||||
aria-describedby="radix-_r_b_ radix-_r_d_"
|
||||
class="_input_1ug7n_18"
|
||||
id="radix-_r_b_"
|
||||
id="radix-_r_c_"
|
||||
name="_r_0_"
|
||||
title=""
|
||||
type="radio"
|
||||
@@ -344,13 +381,13 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
>
|
||||
<label
|
||||
class="_label_1o4d9_60"
|
||||
for="radix-_r_b_"
|
||||
for="radix-_r_c_"
|
||||
>
|
||||
Matrix 2.0: sticky events & multi SFU
|
||||
</label>
|
||||
<span
|
||||
class="_message_1o4d9_86 _help-message_1o4d9_92"
|
||||
id="radix-_r_c_"
|
||||
id="radix-_r_d_"
|
||||
>
|
||||
Compatible only with homservers supporting sticky events and all EC clients v0.17.0 or later
|
||||
</span>
|
||||
@@ -415,7 +452,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_i_"
|
||||
aria-describedby="_r_j_"
|
||||
id="cameraToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -445,7 +482,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</label>
|
||||
<p
|
||||
class="_description_1bd8c0"
|
||||
id="_r_i_"
|
||||
id="_r_j_"
|
||||
>
|
||||
Configure resolution, framerate, bitrate, and codec for camera video. Changes apply on next call join.
|
||||
</p>
|
||||
@@ -467,7 +504,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_j_"
|
||||
aria-describedby="_r_k_"
|
||||
id="screenShareToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
@@ -497,7 +534,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
</label>
|
||||
<p
|
||||
class="_description_1bd8c0"
|
||||
id="_r_j_"
|
||||
id="_r_k_"
|
||||
>
|
||||
Configure resolution, framerate, bitrate, and codec for screen sharing
|
||||
</p>
|
||||
@@ -522,7 +559,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_k_"
|
||||
aria-describedby="_r_l_"
|
||||
checked=""
|
||||
id="echoCancellation"
|
||||
type="checkbox"
|
||||
@@ -560,7 +597,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_l_"
|
||||
aria-describedby="_r_m_"
|
||||
checked=""
|
||||
id="noiseSuppression"
|
||||
type="checkbox"
|
||||
@@ -598,7 +635,7 @@ exports[`DeveloperSettingsTab > renders and matches snapshot 1`] = `
|
||||
class="_field_1bd8c0 _checkboxField_1bd8c0"
|
||||
>
|
||||
<input
|
||||
aria-describedby="_r_m_"
|
||||
aria-describedby="_r_n_"
|
||||
checked=""
|
||||
id="autoGainControl"
|
||||
type="checkbox"
|
||||
|
||||
@@ -167,6 +167,15 @@ export const customTransport = new Setting<UnstableLivekitTransport | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
/**
|
||||
* Publish on the full mesh transport (MSC4575) instead of LiveKit. Members on
|
||||
* either transport are received whatever this says.
|
||||
*/
|
||||
export const fullMeshPublishing = new Setting<boolean>(
|
||||
"full-mesh-publishing",
|
||||
false,
|
||||
);
|
||||
|
||||
export type VideoCodec = "vp8" | "vp9" | "h264" | "av1";
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ describe("widget", () => {
|
||||
receiveState,
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: false,
|
||||
turnServers: true,
|
||||
rtcTransports: true,
|
||||
rtcLivekitGetToken: true,
|
||||
rtcLivekitDelegateDelayedLeave: true,
|
||||
|
||||
+11
-2
@@ -21,8 +21,12 @@ import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||
import { LazyEventEmitter } from "./LazyEventEmitter";
|
||||
import { getUrlParams } from "./UrlParams";
|
||||
import { Config } from "./config/Config";
|
||||
import { seedSettingsFromConfig } from "./settings/settings";
|
||||
import {
|
||||
fullMeshPublishing,
|
||||
seedSettingsFromConfig,
|
||||
} from "./settings/settings";
|
||||
import { ElementCallReactionEventType } from "./reactions";
|
||||
import { fullMeshSignallingEventTypes } from "@element-hq/matrixrtc-sdk";
|
||||
|
||||
// Subset of the actions in element-web
|
||||
export enum ElementWidgetActions {
|
||||
@@ -162,6 +166,10 @@ export const initializeWidget = (
|
||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||
EventType.CallReplaces,
|
||||
EventType.CallEncryptionKeysPrefix,
|
||||
// Full mesh signalling (MSC4575), only where it is turned on: Element
|
||||
// Web doesn't auto-approve these for Element Call yet, and would ask
|
||||
// the user on every call
|
||||
...(fullMeshPublishing.getValue() ? fullMeshSignallingEventTypes : []),
|
||||
];
|
||||
|
||||
const client = createRoomWidgetClient(
|
||||
@@ -173,7 +181,8 @@ export const initializeWidget = (
|
||||
receiveState,
|
||||
sendToDevice: sendRecvToDevice,
|
||||
receiveToDevice: sendRecvToDevice,
|
||||
turnServers: false,
|
||||
// Full mesh connects through the homeserver's TURN server
|
||||
turnServers: true,
|
||||
rtcTransports: true,
|
||||
rtcLivekitGetToken: true,
|
||||
rtcLivekitDelegateDelayedLeave: true,
|
||||
|
||||
Reference in New Issue
Block a user