fix(call): serve RTC transports to Element Call over MSC4515 — calls work again
CI / Build & Quality Checks (push) Successful in 2m6s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 9s
CI / Trigger Desktop Build (push) Successful in 10s
CI / Playwright smoke (e2e) (push) Successful in 1m42s
CI / Build & Quality Checks (push) Successful in 2m6s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 9s
CI / Trigger Desktop Build (push) Successful in 10s
CI / Playwright smoke (e2e) (push) Successful in 1m42s
Since the upstream v0.25.0 merge, Element Call in widget mode no longer reads .well-known for its LiveKit transport; it asks the HOST via the widget API (org.matrix.msc4515.get_rtc_transports, capability org.matrix.msc4515.rtc_transports). cinny never granted the capability nor implemented WidgetDriver.getRtcTransports(), so discovery returned nothing and every join failed with "Call is not supported" (MISSING_MATRIX_RTC_TRANSPORT) — the [LocalMembership] Multiple Transport Errors line in the browser console. - matrix-widget-api 1.17.0 -> 1.18.0 (adds MSC4515; also changes the sendDelayedEvent driver signature, adapted — parent delay ids were removed from the draft). - Grant MSC4515RtcTransports in getCallCapabilities. - CallWidgetDriver.getRtcTransports(): homeserver /rtc/transports (MSC4143) first, then the .well-known org.matrix.msc4143.rtc_foci list (what matrix.lotusguild.org advertises today). Unit-tested. Server side needs no change: the well-known already carries the livekit focus and the JWT service answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
Generated
+4
-4
@@ -54,7 +54,7 @@
|
|||||||
"linkify-react": "4.3.3",
|
"linkify-react": "4.3.3",
|
||||||
"linkifyjs": "4.3.3",
|
"linkifyjs": "4.3.3",
|
||||||
"matrix-js-sdk": "41.7.0",
|
"matrix-js-sdk": "41.7.0",
|
||||||
"matrix-widget-api": "1.17.0",
|
"matrix-widget-api": "1.18.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "6.3.289",
|
"pdfjs-dist": "6.3.289",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
@@ -10045,9 +10045,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/matrix-widget-api": {
|
"node_modules/matrix-widget-api": {
|
||||||
"version": "1.17.0",
|
"version": "1.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/matrix-widget-api/-/matrix-widget-api-1.18.0.tgz",
|
||||||
"integrity": "sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==",
|
"integrity": "sha512-4T2f2koWmx05p1BLcT/9YGGGPSXpPT+PA4Oap/5fjhXsWPxMGJiGL97YpANMYLKsnE1sScYFeuyxIgdc1Qo+Ew==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/events": "^3.0.0",
|
"@types/events": "^3.0.0",
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lotus-chat",
|
"name": "lotus-chat",
|
||||||
"version": "4.12.3-lotus",
|
"version": "4.12.3-lotus",
|
||||||
"description": "Lotus Chat \u2014 Matrix client for Lotus Guild",
|
"description": "Lotus Chat — Matrix client for Lotus Guild",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
"linkify-react": "4.3.3",
|
"linkify-react": "4.3.3",
|
||||||
"linkifyjs": "4.3.3",
|
"linkifyjs": "4.3.3",
|
||||||
"matrix-js-sdk": "41.7.0",
|
"matrix-js-sdk": "41.7.0",
|
||||||
"matrix-widget-api": "1.17.0",
|
"matrix-widget-api": "1.18.0",
|
||||||
"millify": "6.1.0",
|
"millify": "6.1.0",
|
||||||
"pdfjs-dist": "6.3.289",
|
"pdfjs-dist": "6.3.289",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import type { MatrixClient } from 'matrix-js-sdk';
|
||||||
|
import { CallWidgetDriver } from './CallWidgetDriver';
|
||||||
|
|
||||||
|
// MSC4515: Element Call (widget mode) asks the host for RTC transports. Without a
|
||||||
|
// working answer every call fails with MISSING_MATRIX_RTC_TRANSPORT.
|
||||||
|
function fakeClient(opts: {
|
||||||
|
serverTransports?: unknown[] | Error;
|
||||||
|
wellKnown?: Record<string, unknown>;
|
||||||
|
}): MatrixClient {
|
||||||
|
return {
|
||||||
|
getDeviceId: () => 'DEV',
|
||||||
|
getSafeUserId: () => '@me:example.org',
|
||||||
|
_unstable_getRTCTransports: async () => {
|
||||||
|
if (opts.serverTransports instanceof Error) throw opts.serverTransports;
|
||||||
|
return opts.serverTransports ?? [];
|
||||||
|
},
|
||||||
|
getClientWellKnown: () => opts.wellKnown,
|
||||||
|
} as unknown as MatrixClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focus = { type: 'livekit', livekit_service_url: 'https://matrix.example.org' };
|
||||||
|
|
||||||
|
test('getRtcTransports prefers the homeserver MSC4143 endpoint', async () => {
|
||||||
|
const driver = new CallWidgetDriver(fakeClient({ serverTransports: [focus] }), '!r:x');
|
||||||
|
const r = await driver.getRtcTransports();
|
||||||
|
assert.deepEqual(r.rtc_transports, [focus]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRtcTransports falls back to .well-known rtc_foci when the endpoint 404s', async () => {
|
||||||
|
const driver = new CallWidgetDriver(
|
||||||
|
fakeClient({
|
||||||
|
serverTransports: new Error('404'),
|
||||||
|
wellKnown: { 'org.matrix.msc4143.rtc_foci': [focus, { junk: true }, null] },
|
||||||
|
}),
|
||||||
|
'!r:x',
|
||||||
|
);
|
||||||
|
const r = await driver.getRtcTransports();
|
||||||
|
assert.deepEqual(r.rtc_transports, [focus]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getRtcTransports returns an empty list when nothing is advertised', async () => {
|
||||||
|
const driver = new CallWidgetDriver(
|
||||||
|
fakeClient({ serverTransports: [], wellKnown: { 'm.homeserver': {} } }),
|
||||||
|
'!r:x',
|
||||||
|
);
|
||||||
|
const r = await driver.getRtcTransports();
|
||||||
|
assert.deepEqual(r.rtc_transports, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the MSC4515 capability is granted to the call widget', async () => {
|
||||||
|
const driver = new CallWidgetDriver(fakeClient({}), '!r:x');
|
||||||
|
const allowed = await driver.validateCapabilities(
|
||||||
|
new Set(['org.matrix.msc4515.rtc_transports', 'org.example.not_allowed']),
|
||||||
|
);
|
||||||
|
assert.deepEqual(Array.from(allowed), ['org.matrix.msc4515.rtc_transports']);
|
||||||
|
});
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
type IWidgetApiErrorResponseDataDetails,
|
type IWidgetApiErrorResponseDataDetails,
|
||||||
type ISearchUserDirectoryResult,
|
type ISearchUserDirectoryResult,
|
||||||
type IGetMediaConfigResult,
|
type IGetMediaConfigResult,
|
||||||
|
type IRtcTransportsResult,
|
||||||
|
type IRtcTransport,
|
||||||
OpenIDRequestState,
|
OpenIDRequestState,
|
||||||
SimpleObservable,
|
SimpleObservable,
|
||||||
IOpenIDUpdate,
|
IOpenIDUpdate,
|
||||||
@@ -79,29 +81,18 @@ export class CallWidgetDriver extends WidgetDriver {
|
|||||||
return { roomId, eventId: r.event_id };
|
return { roomId, eventId: r.event_id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matrix-widget-api >= 1.18 dropped the `parentDelayId` argument (MSC4140
|
||||||
|
// parent delays were removed from the spec draft); the signature is now
|
||||||
|
// (delay, eventType, content, stateKey?, roomId?).
|
||||||
public async sendDelayedEvent(
|
public async sendDelayedEvent(
|
||||||
delay: number | null,
|
delay: number,
|
||||||
parentDelayId: string | null,
|
|
||||||
eventType: string,
|
eventType: string,
|
||||||
content: IContent,
|
content: unknown,
|
||||||
stateKey: string | null = null,
|
stateKey: string | null = null,
|
||||||
targetRoomId: string | null = null,
|
targetRoomId: string | null = null,
|
||||||
): Promise<ISendDelayedEventDetails> {
|
): Promise<ISendDelayedEventDetails> {
|
||||||
const roomId = targetRoomId || this.inRoomId;
|
const roomId = targetRoomId || this.inRoomId;
|
||||||
|
const delayOpts = { delay };
|
||||||
let delayOpts;
|
|
||||||
if (delay !== null) {
|
|
||||||
delayOpts = {
|
|
||||||
delay,
|
|
||||||
...(parentDelayId !== null && { parent_delay_id: parentDelayId }),
|
|
||||||
};
|
|
||||||
} else if (parentDelayId !== null) {
|
|
||||||
delayOpts = {
|
|
||||||
parent_delay_id: parentDelayId,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new Error('Must provide at least one of delay or parentDelayId');
|
|
||||||
}
|
|
||||||
|
|
||||||
let r: SendDelayedEventResponse | null;
|
let r: SendDelayedEventResponse | null;
|
||||||
if (stateKey !== null) {
|
if (stateKey !== null) {
|
||||||
@@ -295,6 +286,36 @@ export class CallWidgetDriver extends WidgetDriver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MSC4515: the embedded Element Call asks us for the RTC transports because
|
||||||
|
* a widget can't make authenticated homeserver calls itself. Prefer the
|
||||||
|
* homeserver's MSC4143 `/rtc/transports` endpoint; fall back to the
|
||||||
|
* `org.matrix.msc4143.rtc_foci` list in the client .well-known (what
|
||||||
|
* Synapse deployments without MSC4143 advertise today, incl. ours).
|
||||||
|
* Returning an empty list makes EC report "Call is not supported".
|
||||||
|
*/
|
||||||
|
public async getRtcTransports(): Promise<IRtcTransportsResult> {
|
||||||
|
try {
|
||||||
|
const fromServer = await this.mx._unstable_getRTCTransports();
|
||||||
|
if (Array.isArray(fromServer) && fromServer.length > 0) {
|
||||||
|
return { rtc_transports: fromServer as unknown as IRtcTransport[] };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 404 / M_UNRECOGNIZED on homeservers without MSC4143 — fall through.
|
||||||
|
}
|
||||||
|
const wellKnown = this.mx.getClientWellKnown() as
|
||||||
|
| { 'org.matrix.msc4143.rtc_foci'?: unknown }
|
||||||
|
| undefined;
|
||||||
|
const foci = wellKnown?.['org.matrix.msc4143.rtc_foci'];
|
||||||
|
const transports = Array.isArray(foci)
|
||||||
|
? (foci.filter(
|
||||||
|
(f): f is IRtcTransport =>
|
||||||
|
!!f && typeof f === 'object' && typeof (f as { type?: unknown }).type === 'string',
|
||||||
|
) as IRtcTransport[])
|
||||||
|
: [];
|
||||||
|
return { rtc_transports: transports };
|
||||||
|
}
|
||||||
|
|
||||||
public async getMediaConfig(): Promise<IGetMediaConfigResult> {
|
public async getMediaConfig(): Promise<IGetMediaConfigResult> {
|
||||||
return this.mx.getMediaConfig();
|
return this.mx.getMediaConfig();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ export function getCallCapabilities(
|
|||||||
capabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
capabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||||
capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
|
capabilities.add(MatrixCapabilities.MSC4157SendDelayedEvent);
|
||||||
capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
capabilities.add(MatrixCapabilities.MSC4157UpdateDelayedEvent);
|
||||||
|
// MSC4515: Element Call >= 0.22 in widget mode discovers its LiveKit
|
||||||
|
// transport by asking the host (it no longer reads .well-known itself), so
|
||||||
|
// without this capability every call fails with MISSING_MATRIX_RTC_TRANSPORT.
|
||||||
|
capabilities.add(MatrixCapabilities.MSC4515RtcTransports);
|
||||||
capabilities.add(`org.matrix.msc2762.timeline:${roomId}`);
|
capabilities.add(`org.matrix.msc2762.timeline:${roomId}`);
|
||||||
capabilities.add(`org.matrix.msc2762.state:${roomId}`);
|
capabilities.add(`org.matrix.msc2762.state:${roomId}`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user