From b59275b3e1c22c2eb6463227567ed8b61c963325 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:37:56 +0000 Subject: [PATCH 01/10] Add ability to specify sticky events in URI. --- src/UrlParams.ts | 6 ++++++ src/state/CallViewModel.ts | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 4eb692986..33bf0d02c 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -141,6 +141,11 @@ export interface UrlProperties { * can be "light", "dark", "light-high-contrast" or "dark-high-contrast". */ theme: string | null; + /** + * Whether or not the call should be held using the sticky event implementation, + * where possible. + */ + preferStickyEvents: boolean; } /** @@ -501,6 +506,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { sentryDsn: parser.getParam("sentryDsn"), sentryEnvironment: parser.getParam("sentryEnvironment"), e2eEnabled: parser.getFlagParam("enableE2EE", true), + preferStickyEvents: parser.getFlagParam("preferStickyEvents", false), }; const configuration: Partial = { diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index d7735b260..e7b58a3ee 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -1834,7 +1834,9 @@ export class CallViewModel { await enterRTCSession(this.matrixRTCSession, advertised.transport, { encryptMedia: this.options.encryptionSystem.kind !== E2eeType.NONE, useMultiSfu: advertised.multiSfu, - preferStickyEvents: advertised.preferStickyEvents, + preferStickyEvents: + this.urlParams.preferStickyEvents && + advertised.preferStickyEvents, }); } catch (e) { logger.error("Error entering RTC session", e); From c98397a6c88de044e690da8e53536e8fce597989 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Mon, 27 Oct 2025 11:47:53 +0000 Subject: [PATCH 02/10] update docs --- docs/url-params.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/url-params.md b/docs/url-params.md index b2af8416f..2a97a5530 100644 --- a/docs/url-params.md +++ b/docs/url-params.md @@ -72,6 +72,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st | `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. | | `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. | | `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. | +| `preferStickyEvents` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables using sticky events to derive call membership. | ### Widget-only parameters From 36ea3e9effe2b1cccabd581c9e71c9ea968a4f09 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 09:25:58 +0000 Subject: [PATCH 03/10] Refactor to remove preferStickyEvents --- docs/url-params.md | 2 +- locales/en/app.json | 7 +++---- src/UrlParams.ts | 4 ++-- src/settings/DeveloperSettingsTab.tsx | 29 ++++----------------------- src/settings/settings.ts | 5 ----- src/state/CallViewModel.ts | 19 ++++++------------ 6 files changed, 16 insertions(+), 50 deletions(-) diff --git a/docs/url-params.md b/docs/url-params.md index 2a97a5530..34485d46f 100644 --- a/docs/url-params.md +++ b/docs/url-params.md @@ -72,7 +72,7 @@ These parameters are relevant to both [widget](./embedded-standalone.md) and [st | `sendNotificationType` | `ring` or `notification` | No | No | Will send a "ring" or "notification" `m.rtc.notification` event if the user is the first one in the call. | | `autoLeaveWhenOthersLeft` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Whether the app should automatically leave the call when there is no one left in the call. | | `waitForCallPickup` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | When sending a notification, show UI that the app is awaiting an answer, play a dial tone, and (in widget mode) auto-close the widget once the notification expires. | -| `preferStickyEvents` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables using sticky events to derive call membership. | +| `multiSFU` | `true` or `false` | No, defaults to `false` | No, defaults to `false` | Enables experimental new multiSFU support. | ### Widget-only parameters diff --git a/locales/en/app.json b/locales/en/app.json index 11267439f..0e6f92d7f 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -72,11 +72,10 @@ "livekit_server_info": "LiveKit Server Info", "livekit_sfu": "LiveKit SFU: {{url}}", "matrix_id": "Matrix ID: {{id}}", - "multi_sfu": "Multi-SFU media transport", "mute_all_audio": "Mute all audio (participants, reactions, join sounds)", - "prefer_sticky_events": { - "description": "Improves reliability of calls (requires homeserver support)", - "label": "Prefer sticky events" + "multi_sfu": { + "description": "Allows multiple SFUs to be present in a call (requires homeserver support)", + "label": "Multi-SFU media transport" }, "show_connection_stats": "Show connection statistics", "url_params": "URL parameters" diff --git a/src/UrlParams.ts b/src/UrlParams.ts index 33bf0d02c..d8acaed15 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -145,7 +145,7 @@ export interface UrlProperties { * Whether or not the call should be held using the sticky event implementation, * where possible. */ - preferStickyEvents: boolean; + multiSFU: boolean; } /** @@ -506,7 +506,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { sentryDsn: parser.getParam("sentryDsn"), sentryEnvironment: parser.getParam("sentryEnvironment"), e2eEnabled: parser.getFlagParam("enableE2EE", true), - preferStickyEvents: parser.getFlagParam("preferStickyEvents", false), + multiSFU: parser.getFlagParam("useMultiSFU", false), }; const configuration: Partial = { diff --git a/src/settings/DeveloperSettingsTab.tsx b/src/settings/DeveloperSettingsTab.tsx index 08c22557c..28178cfb0 100644 --- a/src/settings/DeveloperSettingsTab.tsx +++ b/src/settings/DeveloperSettingsTab.tsx @@ -29,7 +29,6 @@ import { multiSfu as multiSfuSetting, muteAllAudio as muteAllAudioSetting, alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting, - preferStickyEvents as preferStickyEventsSetting, } from "./settings"; import type { Room as LivekitRoom } from "livekit-client"; import styles from "./DeveloperSettingsTab.module.css"; @@ -59,10 +58,6 @@ export const DeveloperSettingsTab: FC = ({ client, livekitRooms }) => { }); }, [client]); - const [preferStickyEvents, setPreferStickyEvents] = useSetting( - preferStickyEventsSetting, - ); - const [showConnectionStats, setShowConnectionStats] = useSetting( showConnectionStatsSetting, ); @@ -146,22 +141,6 @@ export const DeveloperSettingsTab: FC = ({ client, livekitRooms }) => { } /> - - ): void => { - setPreferStickyEvents(event.target.checked); - }, - [setPreferStickyEvents], - )} - /> - = ({ client, livekitRooms }) => { ): void => { setMultiSfu(event.target.checked); diff --git a/src/settings/settings.ts b/src/settings/settings.ts index b58db9837..6d1f7ff29 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -83,11 +83,6 @@ export const showConnectionStats = new Setting( false, ); -export const preferStickyEvents = new Setting( - "prefer-sticky-events", - false, -); - export const audioInput = new Setting( "audio-input", undefined, diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index e7b58a3ee..b8d3379ca 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -91,7 +91,6 @@ import { duplicateTiles, multiSfu, playReactionsSound, - preferStickyEvents, showReactions, } from "../settings/settings"; import { isFirefox } from "../Platform"; @@ -288,15 +287,10 @@ export class CallViewModel { switchMap((joined) => joined ? combineLatest( - [ - this.preferredTransport$, - this.memberships$, - multiSfu.value$, - preferStickyEvents.value$, - ], - (preferred, memberships, preferMultiSfu, preferStickyEvents) => { + [this.preferredTransport$, this.memberships$, multiSfu.value$], + (preferred, memberships, preferMultiSfu) => { // Multi-SFU must be implicitly enabled when using sticky events - const multiSfu = preferStickyEvents || preferMultiSfu; + const multiSfu = preferMultiSfu; const oldestMembership = this.matrixRTCSession.getOldestMembership(); @@ -333,7 +327,7 @@ export class CallViewModel { remote, preferred, multiSfu, - preferStickyEvents, + preferStickyEvents: multiSfu, }; }, ) @@ -1834,9 +1828,8 @@ export class CallViewModel { await enterRTCSession(this.matrixRTCSession, advertised.transport, { encryptMedia: this.options.encryptionSystem.kind !== E2eeType.NONE, useMultiSfu: advertised.multiSfu, - preferStickyEvents: - this.urlParams.preferStickyEvents && - advertised.preferStickyEvents, + // Multi-SFU enables sticky events. + preferStickyEvents: advertised.multiSfu ?? this.urlParams.multiSFU, }); } catch (e) { logger.error("Error entering RTC session", e); From 8dde9942751eadd2788caa4a72e6477e56bb46d9 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 09:28:28 +0000 Subject: [PATCH 04/10] fix --- locales/en/app.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en/app.json b/locales/en/app.json index 0e6f92d7f..cebb1b38e 100644 --- a/locales/en/app.json +++ b/locales/en/app.json @@ -72,11 +72,11 @@ "livekit_server_info": "LiveKit Server Info", "livekit_sfu": "LiveKit SFU: {{url}}", "matrix_id": "Matrix ID: {{id}}", - "mute_all_audio": "Mute all audio (participants, reactions, join sounds)", "multi_sfu": { "description": "Allows multiple SFUs to be present in a call (requires homeserver support)", "label": "Multi-SFU media transport" }, + "mute_all_audio": "Mute all audio (participants, reactions, join sounds)", "show_connection_stats": "Show connection statistics", "url_params": "URL parameters" }, From 890508ce539d1cdab824b051d232e39efc746b58 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 09:31:02 +0000 Subject: [PATCH 05/10] cleanup --- src/state/CallViewModel.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index b8d3379ca..fcdce6b20 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -281,17 +281,13 @@ export class CallViewModel { remote: { membership: CallMembership; transport: LivekitTransport }[]; preferred: Async; multiSfu: boolean; - preferStickyEvents: boolean; } | null> = this.scope.behavior( this.joined$.pipe( switchMap((joined) => joined ? combineLatest( [this.preferredTransport$, this.memberships$, multiSfu.value$], - (preferred, memberships, preferMultiSfu) => { - // Multi-SFU must be implicitly enabled when using sticky events - const multiSfu = preferMultiSfu; - + (preferred, memberships, multiSfu) => { const oldestMembership = this.matrixRTCSession.getOldestMembership(); const remote = memberships.flatMap((m) => { @@ -327,7 +323,6 @@ export class CallViewModel { remote, preferred, multiSfu, - preferStickyEvents: multiSfu, }; }, ) @@ -362,7 +357,6 @@ export class CallViewModel { */ private readonly advertisedTransport$: Behavior<{ multiSfu: boolean; - preferStickyEvents: boolean; transport: LivekitTransport; } | null> = this.scope.behavior( this.transports$.pipe( @@ -371,7 +365,6 @@ export class CallViewModel { transports.preferred.state === "ready" ? { multiSfu: transports.multiSfu, - preferStickyEvents: transports.preferStickyEvents, // In non-multi-SFU mode we should always advertise the preferred // SFU to minimize the number of membership updates transport: transports.multiSfu @@ -382,7 +375,6 @@ export class CallViewModel { ), distinctUntilChanged<{ multiSfu: boolean; - preferStickyEvents: boolean; transport: LivekitTransport; } | null>(deepCompare), ), From 1f0367f21f12eeb570445b465455975e36001ce0 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Tue, 28 Oct 2025 09:42:48 +0000 Subject: [PATCH 06/10] fix param parse --- src/UrlParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UrlParams.ts b/src/UrlParams.ts index d8acaed15..873e4d9b4 100644 --- a/src/UrlParams.ts +++ b/src/UrlParams.ts @@ -506,7 +506,7 @@ export const computeUrlParams = (search = "", hash = ""): UrlParams => { sentryDsn: parser.getParam("sentryDsn"), sentryEnvironment: parser.getParam("sentryEnvironment"), e2eEnabled: parser.getFlagParam("enableE2EE", true), - multiSFU: parser.getFlagParam("useMultiSFU", false), + multiSFU: parser.getFlagParam("multiSFU", false), }; const configuration: Partial = { From ba36acbc4b4986771805056247d8513c7ca6c2dc Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Thu, 30 Oct 2025 11:22:56 +0000 Subject: [PATCH 07/10] Always create a slot on a new room. --- src/utils/matrix.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 0a2b5c1a5..f87327cbd 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -13,6 +13,7 @@ import { MemoryStore, Preset, Visibility, + EventType, } from "matrix-js-sdk"; import { type ISyncStateData, type SyncState } from "matrix-js-sdk/lib/sync"; import { logger } from "matrix-js-sdk/lib/logger"; @@ -28,6 +29,10 @@ import { type EncryptionSystem, saveKeyForRoom, } from "../e2ee/sharedKeyManagement"; +import { + DefaultCallApplicationSlot, + RtcSlotEventContent, +} from "matrix-js-sdk/lib/matrixrtc"; export const fallbackICEServerAllowed = import.meta.env.VITE_FALLBACK_STUN_ALLOWED === "true"; @@ -236,6 +241,15 @@ export async function createRoom( preset: Preset.PublicChat, name, room_alias_name: e2ee ? undefined : roomAliasLocalpartFromRoomName(name), + initial_state: [ + // Always create a slot + { + type: EventType.RTCSlot, + content: { + application: { ...DefaultCallApplicationSlot.application }, + } satisfies RtcSlotEventContent, + }, + ], power_level_content_override: { invite: 100, kick: 100, From 33469f992d6d1a8a02016cd97d6ed42fa14204b1 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 7 Nov 2025 10:39:24 +0000 Subject: [PATCH 08/10] Use sticky events synapse --- dev-backend-docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 50498c7a4..27b22b4ee 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -88,7 +88,7 @@ services: synapse: hostname: homeserver - image: docker.io/matrixdotorg/synapse:latest + image: ghcr.io/element-hq/synapse:msc4354-5 pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml @@ -106,7 +106,7 @@ services: synapse-1: hostname: homeserver-1 - image: docker.io/matrixdotorg/synapse:latest + image: ghcr.io/element-hq/synapse:msc4354-5 pull_policy: always environment: - SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml From dc293e9f9707a37b57190126bae477b8e7bf9e63 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 7 Nov 2025 10:39:30 +0000 Subject: [PATCH 09/10] Update branch --- package.json | 2 +- yarn.lock | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 35468c21f..795e5fe4d 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "livekit-client": "^2.13.0", "lodash-es": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=hs/rtc-slots&commit=c56fe525aa4dcb8c3b8629dc303b95f4dd4988bd", "matrix-widget-api": "^1.13.0", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/yarn.lock b/yarn.lock index e78dbbf21..0185e6687 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7545,7 +7545,7 @@ __metadata: livekit-client: "npm:^2.13.0" lodash-es: "npm:^4.17.21" loglevel: "npm:^1.9.1" - matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21" + matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=hs/rtc-slots&commit=c56fe525aa4dcb8c3b8629dc303b95f4dd4988bd" matrix-widget-api: "npm:^1.13.0" normalize.css: "npm:^8.0.1" observable-hooks: "npm:^4.2.3" @@ -10343,9 +10343,9 @@ __metadata: languageName: node linkType: hard -"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21": - version: 38.4.0 - resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=e7f5bec51b6f70501a025b79fe5021c933385b21" +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=hs/rtc-slots&commit=c56fe525aa4dcb8c3b8629dc303b95f4dd4988bd": + version: 39.0.0 + resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=c56fe525aa4dcb8c3b8629dc303b95f4dd4988bd" dependencies: "@babel/runtime": "npm:^7.12.5" "@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0" @@ -10358,10 +10358,10 @@ __metadata: matrix-widget-api: "npm:^1.10.0" oidc-client-ts: "npm:^3.0.1" p-retry: "npm:7" - sdp-transform: "npm:^2.14.1" + sdp-transform: "npm:^3.0.0" unhomoglyph: "npm:^1.0.6" uuid: "npm:13" - checksum: 10c0/7adffdc183affd2d3ee1e8497cad6ca7904a37f98328ff7bc15aa6c1829dc9f9a92f8e1bd6260432a33626ff2a839644de938270163e73438b7294675cd954e4 + checksum: 10c0/f8267c2a1fac67076400f886259d9b58b597f975865ce49aefe774d2f56a5c7caef66307f02fe24dcad35829e747bd7e563ecbb9a694d1f80b2439eebe0724fa languageName: node linkType: hard @@ -12544,7 +12544,7 @@ __metadata: languageName: node linkType: hard -"sdp-transform@npm:^2.14.1, sdp-transform@npm:^2.15.0": +"sdp-transform@npm:^2.15.0": version: 2.15.0 resolution: "sdp-transform@npm:2.15.0" bin: @@ -12553,6 +12553,15 @@ __metadata: languageName: node linkType: hard +"sdp-transform@npm:^3.0.0": + version: 3.0.0 + resolution: "sdp-transform@npm:3.0.0" + bin: + sdp-verify: checker.js + checksum: 10c0/828a4595041ba64c86b29075aa4007ab384519b1fa29882db59ccb83b54b2b2a33b60848293f8da537fe151c52f5844fc17c8325396cac309fb19e2e81ec5bf4 + languageName: node + linkType: hard + "sdp@npm:^3.2.0": version: 3.2.0 resolution: "sdp@npm:3.2.0" From 8268e7527c031086c7716c14ef0a0487912ec8e7 Mon Sep 17 00:00:00 2001 From: Half-Shot Date: Fri, 7 Nov 2025 12:31:19 +0000 Subject: [PATCH 10/10] Ensure we set the slot_id --- src/utils/matrix.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index f87327cbd..2292c3061 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -246,6 +246,7 @@ export async function createRoom( { type: EventType.RTCSlot, content: { + slot_id: DefaultCallApplicationSlot.slot_id, application: { ...DefaultCallApplicationSlot.application }, } satisfies RtcSlotEventContent, },