Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
edf68d1
refactoring: prep work extract to file + documentation
BillCarsonFr Sep 30, 2025
b00f7d5
refactor: Remote / Publish Connection and constructor
BillCarsonFr Sep 30, 2025
879a1d4
Connection: add Connection state and handle error on start
BillCarsonFr Oct 1, 2025
3d8639d
Connection states tests
BillCarsonFr Oct 1, 2025
47c876f
lint fixes
BillCarsonFr Oct 1, 2025
2290016
extract common test setup
BillCarsonFr Oct 1, 2025
6a1f7dd
ConnectionState: test livekit connection states
BillCarsonFr Oct 1, 2025
e8bf817
tests: end scope tests
BillCarsonFr Oct 1, 2025
dfaa6a3
fix lint errors
BillCarsonFr Oct 1, 2025
0502f66
tests: Add publisher observable tests
BillCarsonFr Oct 2, 2025
84f95be
test: Ensure scope for publishers observer
BillCarsonFr Oct 2, 2025
00401ca
refactor: PublishConnection extract from giant constructor
BillCarsonFr Oct 2, 2025
91a366f
tests: Publish connection states
BillCarsonFr Oct 6, 2025
597e678
Merge branch 'voip-team/rebased-multiSFU' into valere/multi-sfu/conne…
BillCarsonFr Oct 7, 2025
c3c0516
Lint: fix all the lint errors
BillCarsonFr Oct 7, 2025
c820ba3
build: update lock file
BillCarsonFr Oct 7, 2025
7437961
lint: fix import order
BillCarsonFr Oct 7, 2025
529cb8a
prettier !
BillCarsonFr Oct 7, 2025
18ba02c
knip: remove dead code
BillCarsonFr Oct 7, 2025
05e7b5a
fixup MediaView tests
BillCarsonFr Oct 7, 2025
13fb466
test: Fix mediaView test, ,member is not optional anymore
BillCarsonFr Oct 8, 2025
f5ea734
esLint fix
BillCarsonFr Oct 8, 2025
afe004c
Remove un-necessary transport field, already accessible from connection
BillCarsonFr Oct 8, 2025
427a8dd
test: Fix Audio render tests and added more
BillCarsonFr Oct 8, 2025
e346c8c
Re-enable React strict mode
robintown Oct 8, 2025
c96e81b
Simplify type of audio participants exposed from CallViewModel
robintown Oct 8, 2025
5da780e
Remove dead MuteStates file
robintown Oct 8, 2025
b1d1437
Add comments to Async
robintown Oct 8, 2025
e884744
Correct / document some missing bits in tests
robintown Oct 8, 2025
8778be8
Fix doc comment typo
robintown Oct 8, 2025
3691e71
Restore a hidden 'null' state for the local transport/connection
robintown Oct 8, 2025
dee06a4
Remove unused useIsEarpiece hook
robintown Oct 8, 2025
dcc3ab6
Remove MockedObject from mockMediaDevices type signature
robintown Oct 8, 2025
00daf83
Remove local participant case (now enforced by types) from audio tests
robintown Oct 8, 2025
5be3b91
Fix focus connection state typo, simplify its initialization
robintown Oct 8, 2025
64c2e59
Update outdated comment
robintown Oct 8, 2025
2c576a7
Clean up subscriptions in Connection tests
robintown Oct 8, 2025
85ffe68
Remove outdated comment
robintown Oct 8, 2025
4c6b960
fix: use correct TestEachFunction
BillCarsonFr Oct 9, 2025
7cbb1ec
Simplify AudioRenderer and add more tests
BillCarsonFr Oct 9, 2025
a500915
test: Fix mute test, behavior change from setMuted to setAudioEnabled
BillCarsonFr Oct 9, 2025
6710f4c
Test: Fix mocking to fix failing tests
BillCarsonFr Oct 10, 2025
1ab081d
test: MISSING_MATRIX_RTC_FOCUS renamed as MISSING_MATRIX_RTC_TRANSPORT
BillCarsonFr Oct 10, 2025
0fd4143
Merge branch 'livekit' into voip-team/rebased-multiSFU
toger5 Oct 10, 2025
4608d68
Merge branch 'voip-team/rebased-multiSFU' into valere/multi-sfu/conne…
BillCarsonFr Oct 10, 2025
4a8f5bc
post merge lint fixes
BillCarsonFr Oct 10, 2025
1e75f9a
test: fix additional test with proper mutestate fix
BillCarsonFr Oct 10, 2025
cca46bd
Merge branch 'voip-team/rebased-multiSFU' into valere/multi-sfu/conne…
BillCarsonFr Oct 10, 2025
2fc7f11
prettier fix
BillCarsonFr Oct 10, 2025
fc2384e
post merge fixes (js-sdk changes)
BillCarsonFr Oct 10, 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-rxjs": "^5.0.3",
"eslint-plugin-unicorn": "^56.0.0",
"fetch-mock": "11.1.5",
"global-jsdom": "^26.0.0",
"i18next": "^24.0.0",
"i18next-browser-languagedetector": "^8.0.0",
Expand Down
8 changes: 0 additions & 8 deletions src/MediaDevicesContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ export function useMediaDevices(): MediaDevices {
return mediaDevices;
}

export const useIsEarpiece = (): boolean => {
const devices = useMediaDevices();
const audioOutput = useObservableEagerState(devices.audioOutput.selected$);
const available = useObservableEagerState(devices.audioOutput.available$);
if (!audioOutput?.id) return false;
return available.get(audioOutput.id)?.type === "earpiece";
};

/**
* A convenience hook to get the audio node configuration for the earpiece.
* It will check the `useAsEarpiece` of the `audioOutput` device and return
Expand Down
3 changes: 1 addition & 2 deletions src/button/ReactionToggleButton.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { expect, test } from "vitest";
import { TooltipProvider } from "@vector-im/compound-web";
import { userEvent } from "@testing-library/user-event";
import { type ReactNode } from "react";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";

import { ReactionToggleButton } from "./ReactionToggleButton";
import { ElementCallReactionEventType } from "../reactions";
Expand All @@ -33,7 +32,7 @@ function TestComponent({
<TooltipProvider>
<ReactionsSenderProvider
vm={vm}
rtcSession={rtcSession as unknown as MatrixRTCSession}
rtcSession={rtcSession.asMockedSession()}
>
<ReactionToggleButton vm={vm} identifier={localIdent} />
</ReactionsSenderProvider>
Expand Down
224 changes: 194 additions & 30 deletions src/livekit/MatrixAudioRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,28 @@ Please see LICENSE in the repository root for full details.
*/

import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { type CallMembership } from "matrix-js-sdk/lib/matrixrtc";
import { render, type RenderResult } from "@testing-library/react";
import {
getTrackReferenceId,
type TrackReference,
} from "@livekit/components-core";
import { type RemoteAudioTrack } from "livekit-client";
import {
type Participant,
type RemoteAudioTrack,
type Room,
Track,
} from "livekit-client";
import { type ReactNode } from "react";
import { useTracks } from "@livekit/components-react";

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

export const TestAudioContextConstructor = vi.fn(() => testAudioContext);

Expand Down Expand Up @@ -48,42 +56,203 @@ vi.mock("@livekit/components-react", async (importOriginal) => {
};
});

const tracks = [mockTrack("test:123")];
vi.mocked(useTracks).mockReturnValue(tracks);
let tracks: TrackReference[] = [];

it("should render for member", () => {
const { container, queryAllByTestId } = render(
/**
* Render the test component with given rtc members and livekit participant identities.
*
* It is possible to have rtc members that are not in livekit (e.g. not yet joined) and vice versa.
*
* @param rtcMembers - Array of active rtc members with userId and deviceId.
* @param livekitParticipantIdentities - Array of livekit participant (that are publishing).
* @param explicitTracks - Array of tracks available in livekit, if not provided, one audio track per livekitParticipantIdentities will be created.
* */

function renderTestComponent(
rtcMembers: { userId: string; deviceId: string }[],
livekitParticipantIdentities: string[],
explicitTracks?: {
participantId: string;
kind: Track.Kind;
source: Track.Source;
}[],
): RenderResult {
const liveKitParticipants = livekitParticipantIdentities.map((identity) =>
mockRemoteParticipant({ identity }),
);
const participants = rtcMembers.flatMap(({ userId, deviceId }) => {
const p = liveKitParticipants.find(
(p) => p.identity === `${userId}:${deviceId}`,
);
return p === undefined ? [] : [p];
});
const livekitRoom = {
remoteParticipants: new Map<string, Participant>(
liveKitParticipants.map((p) => [p.identity, p]),
),
} as unknown as Room;

if (explicitTracks?.length ?? 0 > 0) {
tracks = explicitTracks!.map(({ participantId, source, kind }) => {
const participant =
liveKitParticipants.find((p) => p.identity === participantId) ??
mockRemoteParticipant({ identity: participantId });
return mockTrack(participant, kind, source);
});
} else {
tracks = participants.map((p) => mockTrack(p));
}

vi.mocked(useTracks).mockReturnValue(tracks);
return render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
validIdentities={participants.map((p) => p.identity)}
livekitRoom={livekitRoom}
url={""}
/>
</MediaDevicesProvider>,
);
}

it("should render for member", () => {
const { container, queryAllByTestId } = renderTestComponent(
[{ userId: "@alice", deviceId: "DEV0" }],
["@alice:DEV0"],
);
expect(container).toBeTruthy();
expect(queryAllByTestId("audio")).toHaveLength(1);
});

it("should not render without member", () => {
const memberships = [
{ sender: "othermember", deviceId: "123" },
] as CallMembership[];
const { container, queryAllByTestId } = render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<LivekitRoomAudioRenderer members={memberships} />
</MediaDevicesProvider>,
const { container, queryAllByTestId } = renderTestComponent(
[{ userId: "@bob", deviceId: "DEV0" }],
["@alice:DEV0"],
);
expect(container).toBeTruthy();
expect(queryAllByTestId("audio")).toHaveLength(0);
});

const TEST_CASES: {
name: string;
rtcUsers: { userId: string; deviceId: string }[];
livekitParticipantIdentities: string[];
explicitTracks?: {
participantId: string;
kind: Track.Kind;
source: Track.Source;
}[];
expectedAudioTracks: number;
}[] = [
{
name: "single user single device",
rtcUsers: [
{ userId: "@alice", deviceId: "DEV0" },
{ userId: "@alice", deviceId: "DEV1" },
{ userId: "@bob", deviceId: "DEV0" },
],
livekitParticipantIdentities: ["@alice:DEV0", "@bob:DEV0", "@alice:DEV1"],
expectedAudioTracks: 3,
},
// Charlie is a rtc member but not in livekit
{
name: "Charlie is rtc member but not in livekit",
rtcUsers: [
{ userId: "@alice", deviceId: "DEV0" },
{ userId: "@bob", deviceId: "DEV0" },
{ userId: "@charlie", deviceId: "DEV0" },
],
livekitParticipantIdentities: ["@alice:DEV0", "@bob:DEV0"],
expectedAudioTracks: 2,
},
// Charlie is in livekit but not rtc member
{
name: "Charlie is in livekit but not rtc member",
rtcUsers: [
{ userId: "@alice", deviceId: "DEV0" },
{ userId: "@bob", deviceId: "DEV0" },
],
livekitParticipantIdentities: ["@alice:DEV0", "@bob:DEV0", "@charlie:DEV0"],
expectedAudioTracks: 2,
},
{
name: "no audio track, only video track",
rtcUsers: [{ userId: "@alice", deviceId: "DEV0" }],
livekitParticipantIdentities: ["@alice:DEV0"],
explicitTracks: [
{
participantId: "@alice:DEV0",
kind: Track.Kind.Video,
source: Track.Source.Camera,
},
],
expectedAudioTracks: 0,
},
{
name: "Audio track from unknown source",
rtcUsers: [{ userId: "@alice", deviceId: "DEV0" }],
livekitParticipantIdentities: ["@alice:DEV0"],
explicitTracks: [
{
participantId: "@alice:DEV0",
kind: Track.Kind.Audio,
source: Track.Source.Unknown,
},
],
expectedAudioTracks: 1,
},
{
name: "Audio track from other device",
rtcUsers: [{ userId: "@alice", deviceId: "DEV0" }],
livekitParticipantIdentities: ["@alice:DEV0"],
explicitTracks: [
{
participantId: "@alice:DEV1",
kind: Track.Kind.Audio,
source: Track.Source.Microphone,
},
],
expectedAudioTracks: 0,
},
{
name: "two audio tracks, microphone and screenshare",
rtcUsers: [{ userId: "@alice", deviceId: "DEV0" }],
livekitParticipantIdentities: ["@alice:DEV0"],
explicitTracks: [
{
participantId: "@alice:DEV0",
kind: Track.Kind.Audio,
source: Track.Source.Microphone,
},
{
participantId: "@alice:DEV0",
kind: Track.Kind.Audio,
source: Track.Source.ScreenShareAudio,
},
],
expectedAudioTracks: 2,
},
];

it.each(TEST_CASES)(
`should render sound test cases $name`,
({
rtcUsers,
livekitParticipantIdentities,
explicitTracks,
expectedAudioTracks,
}) => {
const { queryAllByTestId } = renderTestComponent(
rtcUsers,
livekitParticipantIdentities,
explicitTracks,
);
expect(queryAllByTestId("audio")).toHaveLength(expectedAudioTracks);
},
);

it("should not setup audioContext gain and pan if there is no need to.", () => {
render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
/>
</MediaDevicesProvider>,
);
renderTestComponent([{ userId: "@bob", deviceId: "DEV0" }], ["@bob:DEV0"]);
const audioTrack = tracks[0].publication.track! as RemoteAudioTrack;

expect(audioTrack.setAudioContext).toHaveBeenCalledTimes(1);
Expand All @@ -100,13 +269,8 @@ it("should setup audioContext gain and pan", () => {
pan: 1,
volume: 0.1,
});
render(
<MediaDevicesProvider value={mockMediaDevices({})}>
<LivekitRoomAudioRenderer
members={[{ sender: "test", deviceId: "123" }] as CallMembership[]}
/>
</MediaDevicesProvider>,
);

renderTestComponent([{ userId: "@bob", deviceId: "DEV0" }], ["@bob:DEV0"]);

const audioTrack = tracks[0].publication.track! as RemoteAudioTrack;
expect(audioTrack.setAudioContext).toHaveBeenCalled();
Expand Down
Loading
Loading