Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
9011ae4
temp
toger5 Aug 27, 2025
35319dd
Fix some errors in CallViewModel
robintown Aug 27, 2025
d9b6302
Fix crash?
robintown Aug 27, 2025
376a4b4
initial compiling version
toger5 Aug 27, 2025
c6e8c94
Fix makeFocus
robintown Aug 27, 2025
cb91f1a
Make it actually join the session
robintown Aug 27, 2025
7b88420
first video!
toger5 Aug 27, 2025
a10489d
publish audio in remote rooms
toger5 Aug 27, 2025
6bdfd7f
add comment
toger5 Aug 27, 2025
55b46b3
introduce publishingParticipants$
toger5 Aug 27, 2025
8ffb360
add local storage + more readable + remoteParticipants + use publishi…
toger5 Aug 28, 2025
33bc78e
add logging
toger5 Aug 28, 2025
a617a92
make it work
toger5 Aug 28, 2025
e4a54e3
refactor connnection class
toger5 Aug 28, 2025
802ebf8
refactor connection
toger5 Aug 28, 2025
598371b
lots of work. noone knows if it works.
toger5 Aug 28, 2025
02f4c73
Add my own local storage SFU config stuff too
robintown Aug 28, 2025
d46fe55
Import unfinished mute states refactor
robintown Aug 28, 2025
386dc6c
temp before holiday
toger5 Aug 28, 2025
e08f16f
All my Friday work. Demoable!
robintown Aug 29, 2025
c809873
one e2ee worker per session
toger5 Sep 15, 2025
cc870c3
enable encryption in per sender case
toger5 Sep 16, 2025
38d78dd
make audio work
toger5 Sep 16, 2025
ccfd32c
move leave logic into view model
toger5 Sep 16, 2025
41e152f
dont throw disconnected error at start of the call
toger5 Sep 17, 2025
d9fe310
start fixing CallViewModel tests.
toger5 Sep 19, 2025
02f23e2
remove todo from matrix audio renderer
toger5 Sep 22, 2025
dddda70
add todo comments and who works on them
toger5 Sep 22, 2025
8bf2489
TODO: settings modal with multiple connections
toger5 Sep 22, 2025
78e9521
Make track processor work
toger5 Sep 23, 2025
7777179
cleanup (delete files useLivekit) now covered by Connection.ts
toger5 Sep 23, 2025
96e96a5
fix leaving
toger5 Sep 23, 2025
6b44f3b
a tiny bit of tests lint fixes.
toger5 Sep 23, 2025
8e21ea6
Merge branch 'livekit' into voip-team/rebased-multiSFU
robintown Sep 24, 2025
f99a256
Reset matrix-js-sdk to multi SFU branch
robintown Sep 24, 2025
edd3eb8
Implement screen sharing
robintown Sep 24, 2025
6cf0207
Make UI react instantly to hanging up but also wait for leave sound
robintown Sep 25, 2025
530fbaf
Clear up the room membership confusion around reading session members
robintown Sep 25, 2025
4980d8a
Merge branch 'livekit' into voip-team/rebased-multiSFU
robintown Sep 25, 2025
0759f9b
Don't render audio from participants that aren't meant to be publishing
robintown Sep 26, 2025
dbdf853
Stop connections on view model destroy
robintown Sep 26, 2025
a4a0a58
Remove the option to show non-member ("ghost") participants
robintown Sep 26, 2025
2819c79
use updated multi sfu js-sdk
toger5 Sep 30, 2025
71127a4
Merge pull request #3516 from element-hq/toger5/voip-team/rebased-mul…
robintown Sep 30, 2025
68aae4a
fix another rename + another js-sdk bump
toger5 Oct 2, 2025
86fb026
Turn multi-SFU media transport into a developer option
robintown Oct 3, 2025
1820cac
Create media items for session members not joined to LiveKit
robintown Oct 3, 2025
1fff71a
Actually leave the MatrixRTC session again
robintown Oct 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@
"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)",
"show_connection_stats": "Show connection statistics",
"show_non_member_tiles": "Show tiles for non-member media",
"url_params": "URL parameters",
"use_new_membership_manager": "Use the new implementation of the call MembershipManager",
"use_to_device_key_transport": "Use to device key transport. This will fallback to room key transport when another call member sent a room key"
Expand All @@ -92,7 +92,7 @@
"generic_description": "Submitting debug logs will help us track down the problem.",
"insufficient_capacity": "Insufficient capacity",
"insufficient_capacity_description": "The server has reached its maximum capacity and you cannot join the call at this time. Try again later, or contact your server admin if the problem persists.",
"matrix_rtc_focus_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
"matrix_rtc_transport_missing": "The server is not configured to work with {{brand}}. Please contact your server admin (Domain: {{domain}}, Error Code: {{ errorCode }}).",
"open_elsewhere": "Opened in another tab",
"open_elsewhere_description": "{{brand}} has been opened in another tab. If that doesn't sound right, try reloading the page.",
"room_creation_restricted": "Failed to create call",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,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=develop",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=voip-team/multi-SFU",
"matrix-widget-api": "^1.13.0",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
Expand Down
10 changes: 5 additions & 5 deletions src/livekit/MatrixAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useTracks } from "@livekit/components-react";

import { testAudioContext } from "../useAudioContext.test";
import * as MediaDevicesContext from "../MediaDevicesContext";
import { MatrixAudioRenderer } from "./MatrixAudioRenderer";
import { LivekitRoomAudioRenderer } from "./MatrixAudioRenderer";
import { mockMediaDevices, mockTrack } from "../utils/test";

export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
Expand Down Expand Up @@ -54,7 +54,7 @@ vi.mocked(useTracks).mockReturnValue(tracks);
it("should render for member", () => {
const { container, queryAllByTestId } = render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<MatrixAudioRenderer
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
/>
</MediaDevicesProvider>,
Expand All @@ -69,7 +69,7 @@ it("should not render without member", () => {
] as CallMembership[];
const { container, queryAllByTestId } = render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<MatrixAudioRenderer members={memberships} />
<LivekitRoomAudioRenderer members={memberships} />
</MediaDevicesProvider>,
);
expect(container).toBeTruthy();
Expand All @@ -79,7 +79,7 @@ it("should not render without member", () => {
it("should not setup audioContext gain and pan if there is no need to.", () => {
render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<MatrixAudioRenderer
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
/>
</MediaDevicesProvider>,
Expand All @@ -102,7 +102,7 @@ it("should setup audioContext gain and pan", () => {
});
render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<MatrixAudioRenderer
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
/>
</MediaDevicesProvider>,
Expand Down
49 changes: 32 additions & 17 deletions src/livekit/MatrixAudioRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,36 @@
*/

import { getTrackReferenceId } from "@livekit/components-core";
import { type Room as LivekitRoom, type Participant } from "livekit-client";
import { type RemoteAudioTrack, Track } from "livekit-client";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import {
useTracks,
AudioTrack,
type AudioTrackProps,
} from "@livekit/components-react";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { type RoomMember } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";

import { useEarpieceAudioConfig } from "../MediaDevicesContext";
import { useReactiveState } from "../useReactiveState";
import * as controls from "../controls";

import {} from "@livekit/components-core";
export interface MatrixAudioRendererProps {
/**
* The service URL of the LiveKit room.
*/
url: string;
livekitRoom: LivekitRoom;
/**
* The list of participants to render audio for.
* This list needs to be composed based on the matrixRTC members so that we do not play audio from users
* that are not expected to be in the rtc session.
*/
members: CallMembership[];
participants: {
participant: Participant;
member: RoomMember;
}[];
/**
* If set to `true`, mutes all audio tracks rendered by the component.
* @remarks
Expand All @@ -49,14 +58,15 @@
* ```
* @public
*/
export function MatrixAudioRenderer({
members,
export function LivekitRoomAudioRenderer({
url,
livekitRoom,
participants,
muted,
}: MatrixAudioRendererProps): ReactNode {
const validIdentities = useMemo(
() =>
new Set(members?.map((member) => `${member.sender}:${member.deviceId}`)),
[members],
const participantSet = useMemo(
() => new Set(participants.map(({ participant }) => participant)),

Check failure on line 68 in src/livekit/MatrixAudioRenderer.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests

src/livekit/MatrixAudioRenderer.test.tsx > should render for member

TypeError: Cannot read properties of undefined (reading 'map') ❯ src/livekit/MatrixAudioRenderer.tsx:68:32 ❯ mountMemo node_modules/react-dom/cjs/react-dom-client.development.js:6603:23 ❯ Object.useMemo node_modules/react-dom/cjs/react-dom-client.development.js:22924:18 ❯ process.env.NODE_ENV.exports.useMemo node_modules/react/cjs/react.development.js:1209:34 ❯ LivekitRoomAudioRenderer src/livekit/MatrixAudioRenderer.tsx:67:26 ❯ Object.react-stack-bottom-frame node_modules/react-dom/cjs/react-dom-client.development.js:23863:20 ❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:5529:22 ❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:8897:19 ❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:10522:18 ❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:1522:13
[participants],
);

const loggedInvalidIdentities = useRef(new Set<string>());
Expand All @@ -68,11 +78,11 @@
* @param identity The identity of the track that is invalid
* @param validIdentities The list of valid identities
*/
const logInvalid = (identity: string, validIdentities: Set<string>): void => {
const logInvalid = (identity: string): void => {
if (loggedInvalidIdentities.current.has(identity)) return;
logger.warn(
`[MatrixAudioRenderer] Audio track ${identity} has no matching matrix call member`,
`current members: ${Array.from(validIdentities.values())}`,
`[MatrixAudioRenderer] Audio track ${identity} from ${url} has no matching matrix call member`,
`current members: ${participants.map((p) => p.participant.identity)}`,
`track will not get rendered`,
);
loggedInvalidIdentities.current.add(identity);
Expand All @@ -87,25 +97,30 @@
{
updateOnlyOn: [],
onlySubscribed: true,
room: livekitRoom,
},
).filter((ref) => {
const isValid = validIdentities?.has(ref.participant.identity);
const isValid = participantSet?.has(ref.participant);
if (!isValid && !ref.participant.isLocal)
logInvalid(ref.participant.identity, validIdentities);
logInvalid(ref.participant.identity);
return (
!ref.participant.isLocal &&
ref.publication.kind === Track.Kind.Audio &&
isValid
);
});

useEffect(() => {
if (!tracks.some((t) => !validIdentities.has(t.participant.identity))) {
if (
loggedInvalidIdentities.current.size &&
tracks.every((t) => participantSet.has(t.participant))
) {
logger.debug(
`[MatrixAudioRenderer] All audio tracks have a matching matrix call member identity.`,
`[MatrixAudioRenderer] All audio tracks from ${url} have a matching matrix call member identity.`,
);
loggedInvalidIdentities.current.clear();
}
}, [tracks, validIdentities]);
}, [tracks, participantSet, url]);

// This component is also (in addition to the "only play audio for connected members" logic above)
// responsible for mimicking earpiece audio on iPhones.
Expand Down
42 changes: 41 additions & 1 deletion src/livekit/TrackProcessorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ import {
useMemo,
} from "react";
import { type LocalVideoTrack } from "livekit-client";
import { combineLatest, map, type Observable } from "rxjs";
import { useObservable } from "observable-hooks";

import {
backgroundBlur as backgroundBlurSettings,
useSetting,
} from "../settings/settings";
import { BlurBackgroundTransformer } from "./BlurBackgroundTransformer";
import { type Behavior } from "../state/Behavior";

type ProcessorState = {
//TODO-MULTI-SFU: This is not yet fully there.
// it is a combination of exposing observable and react hooks.
// preferably we should not make this a context anymore and instead just a vm?

export type ProcessorState = {
supported: boolean | undefined;
processor: undefined | ProcessorWrapper<BackgroundOptions>;
};
Expand All @@ -42,6 +49,39 @@ export function useTrackProcessor(): ProcessorState {
return state;
}

export function useTrackProcessorObservable$(): Observable<ProcessorState> {
const state = use(ProcessorContext);
if (state === undefined)
throw new Error(
"useTrackProcessor must be used within a ProcessorProvider",
);
const state$ = useObservable(
(init$) => init$.pipe(map(([init]) => init)),
[state],
);

return state$;
}

export const trackProcessorSync = (
videoTrack$: Behavior<LocalVideoTrack | null>,
processor$: Behavior<ProcessorState>,
): void => {
combineLatest([videoTrack$, processor$]).subscribe(
([videoTrack, processorState]) => {
if (!processorState) return;
if (!videoTrack) return;
const { processor } = processorState;
if (processor && !videoTrack.getProcessor()) {
void videoTrack.setProcessor(processor);
}
if (!processor && videoTrack.getProcessor()) {
void videoTrack.stopProcessor();
}
},
);
};

export const useTrackProcessorSync = (
videoTrack: LocalVideoTrack | null,
): void => {
Expand Down
66 changes: 12 additions & 54 deletions src/livekit/openIDSFU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,7 @@ Please see LICENSE in the repository root for full details.

import { type IOpenIDToken, type MatrixClient } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import { useEffect, useState } from "react";
import { type LivekitFocus } from "matrix-js-sdk/lib/matrixrtc";

import { useActiveLivekitFocus } from "../room/useActiveFocus";
import { useErrorBoundary } from "../useErrorBoundary";
import { FailToGetOpenIdToken } from "../utils/errors";
import { doNetworkOperationWithRetry } from "../utils/matrix";

Expand All @@ -34,38 +29,11 @@ export type OpenIDClientParts = Pick<
"getOpenIdToken" | "getDeviceId"
>;

export function useOpenIDSFU(
client: OpenIDClientParts,
rtcSession: MatrixRTCSession,
): SFUConfig | undefined {
const [sfuConfig, setSFUConfig] = useState<SFUConfig | undefined>(undefined);

const activeFocus = useActiveLivekitFocus(rtcSession);
const { showErrorBoundary } = useErrorBoundary();

useEffect(() => {
if (activeFocus) {
getSFUConfigWithOpenID(client, activeFocus).then(
(sfuConfig) => {
setSFUConfig(sfuConfig);
},
(e) => {
showErrorBoundary(new FailToGetOpenIdToken(e));
logger.error("Failed to get SFU config", e);
},
);
} else {
setSFUConfig(undefined);
}
}, [client, activeFocus, showErrorBoundary]);

return sfuConfig;
}

export async function getSFUConfigWithOpenID(
client: OpenIDClientParts,
activeFocus: LivekitFocus,
): Promise<SFUConfig | undefined> {
serviceUrl: string,
livekitAlias: string,
): Promise<SFUConfig> {
let openIdToken: IOpenIDToken;
try {
openIdToken = await doNetworkOperationWithRetry(async () =>
Expand All @@ -78,26 +46,16 @@ export async function getSFUConfigWithOpenID(
}
logger.debug("Got openID token", openIdToken);

try {
logger.info(
`Trying to get JWT from call's active focus URL of ${activeFocus.livekit_service_url}...`,
);
const sfuConfig = await getLiveKitJWT(
client,
activeFocus.livekit_service_url,
activeFocus.livekit_alias,
openIdToken,
);
logger.info(`Got JWT from call's active focus URL.`);
logger.info(`Trying to get JWT for focus ${serviceUrl}...`);
const sfuConfig = await getLiveKitJWT(
client,
serviceUrl,
livekitAlias,
openIdToken,
);
logger.info(`Got JWT from call's active focus URL.`);

return sfuConfig;
} catch (e) {
logger.warn(
`Failed to get JWT from RTC session's active focus URL of ${activeFocus.livekit_service_url}.`,
e,
);
return undefined;
}
return sfuConfig;
}

async function getLiveKitJWT(
Expand Down
1 change: 1 addition & 0 deletions src/livekit/useECConnectionState.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// TODO not used anymore - remove
/*
Copyright 2023, 2024 New Vector Ltd.

Expand Down
Loading
Loading