Simplify type of audio participants exposed from CallViewModel

This commit is contained in:
Robin
2025-10-08 16:40:06 -04:00
parent e346c8c148
commit c96e81bfd3
4 changed files with 36 additions and 39 deletions

View File

@@ -23,12 +23,7 @@ import { useTracks } from "@livekit/components-react";
import { testAudioContext } from "../useAudioContext.test"; import { testAudioContext } from "../useAudioContext.test";
import * as MediaDevicesContext from "../MediaDevicesContext"; import * as MediaDevicesContext from "../MediaDevicesContext";
import { LivekitRoomAudioRenderer } from "./MatrixAudioRenderer"; import { LivekitRoomAudioRenderer } from "./MatrixAudioRenderer";
import { import { mockMediaDevices, mockTrack } from "../utils/test";
mockMatrixRoomMember,
mockMediaDevices,
mockRtcMembership,
mockTrack,
} from "../utils/test";
export const TestAudioContextConstructor = vi.fn(() => testAudioContext); export const TestAudioContextConstructor = vi.fn(() => testAudioContext);
@@ -80,17 +75,11 @@ function renderTestComponent(
isLocal, isLocal,
} as unknown as RemoteParticipant); } as unknown as RemoteParticipant);
}); });
const participants = rtcMembers.map(({ userId, deviceId }) => { const participants = rtcMembers.flatMap(({ userId, deviceId }) => {
const p = liveKitParticipants.find( const p = liveKitParticipants.find(
(p) => p.identity === `${userId}:${deviceId}`, (p) => p.identity === `${userId}:${deviceId}`,
); );
const localRtcMember = mockRtcMembership(userId, deviceId); return p === undefined ? [] : [p];
const member = mockMatrixRoomMember(localRtcMember);
return {
id: `${userId}:${deviceId}`,
participant: p,
member,
};
}); });
const livekitRoom = vi.mocked<Room>({ const livekitRoom = vi.mocked<Room>({
remoteParticipants: new Map<string, Participant>( remoteParticipants: new Map<string, Participant>(
@@ -98,9 +87,7 @@ function renderTestComponent(
), ),
} as unknown as Room); } as unknown as Room);
tracks = participants tracks = participants.map((p) => mockTrack(p));
.filter((p) => p.participant)
.map((p) => mockTrack(p.participant!)) as TrackReference[];
vi.mocked(useTracks).mockReturnValue(tracks); vi.mocked(useTracks).mockReturnValue(tracks);
return render( return render(

View File

@@ -6,7 +6,10 @@ Please see LICENSE in the repository root for full details.
*/ */
import { getTrackReferenceId } from "@livekit/components-core"; import { getTrackReferenceId } from "@livekit/components-core";
import { type Room as LivekitRoom, type Participant } from "livekit-client"; import {
type RemoteParticipant,
type Room as LivekitRoom,
} from "livekit-client";
import { type RemoteAudioTrack, Track } from "livekit-client"; import { type RemoteAudioTrack, Track } from "livekit-client";
import { useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { import {
@@ -14,13 +17,13 @@ import {
AudioTrack, AudioTrack,
type AudioTrackProps, type AudioTrackProps,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { type RoomMember } from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { useEarpieceAudioConfig } from "../MediaDevicesContext"; import { useEarpieceAudioConfig } from "../MediaDevicesContext";
import { useReactiveState } from "../useReactiveState"; import { useReactiveState } from "../useReactiveState";
import * as controls from "../controls"; import * as controls from "../controls";
import {} from "@livekit/components-core"; import {} from "@livekit/components-core";
export interface MatrixAudioRendererProps { export interface MatrixAudioRendererProps {
/** /**
* The service URL of the LiveKit room. * The service URL of the LiveKit room.
@@ -32,13 +35,7 @@ export interface MatrixAudioRendererProps {
* This list needs to be composed based on the matrixRTC members so that we do not play audio from users * 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. * that are not expected to be in the rtc session.
*/ */
// TODO: Why do we have this structure? looks like we only need the valid/active participants (not the room member or id)? participants: RemoteParticipant[];
participants: {
id: string;
// TODO it appears to be optional as per InCallView? but what does that mean here? a rtc member not yet joined in livekit?
participant: Participant | undefined;
member: RoomMember;
}[];
/** /**
* If set to `true`, mutes all audio tracks rendered by the component. * If set to `true`, mutes all audio tracks rendered by the component.
* @remarks * @remarks
@@ -48,8 +45,7 @@ export interface MatrixAudioRendererProps {
} }
/** /**
* The `MatrixAudioRenderer` component is a drop-in solution for adding audio to your LiveKit app. * Takes care of handling remote participants audio tracks and makes sure that microphones and screen share are audible.
* It takes care of handling remote participants audio tracks and makes sure that microphones and screen share are audible.
* *
* It also takes care of the earpiece audio configuration for iOS devices. * It also takes care of the earpiece audio configuration for iOS devices.
* This is done by using the WebAudio API to create a stereo pan effect that mimics the earpiece audio. * This is done by using the WebAudio API to create a stereo pan effect that mimics the earpiece audio.
@@ -70,12 +66,7 @@ export function LivekitRoomAudioRenderer({
// This is the list of valid identities that are allowed to play audio. // This is the list of valid identities that are allowed to play audio.
// It is derived from the list of matrix rtc members. // It is derived from the list of matrix rtc members.
const validIdentities = useMemo( const validIdentities = useMemo(
() => () => new Set(participants.map((p) => p.identity)),
new Set(
participants
.filter(({ participant }) => participant) // filter out participants that are not yet joined in livekit
.map(({ participant }) => participant!.identity),
),
[participants], [participants],
); );
@@ -92,7 +83,7 @@ export function LivekitRoomAudioRenderer({
if (loggedInvalidIdentities.current.has(identity)) return; if (loggedInvalidIdentities.current.has(identity)) return;
logger.warn( logger.warn(
`[MatrixAudioRenderer] Audio track ${identity} from ${url} has no matching matrix call member`, `[MatrixAudioRenderer] Audio track ${identity} from ${url} has no matching matrix call member`,
`current members: ${participants.map((p) => p.participant?.identity)}`, `current members: ${participants.map((p) => p.identity)}`,
`track will not get rendered`, `track will not get rendered`,
); );
loggedInvalidIdentities.current.add(identity); loggedInvalidIdentities.current.add(identity);

View File

@@ -286,7 +286,7 @@ export const InCallView: FC<InCallViewProps> = ({
); );
const allLivekitRooms = useBehavior(vm.allLivekitRooms$); const allLivekitRooms = useBehavior(vm.allLivekitRooms$);
const participantsByRoom = useBehavior(vm.participantsByRoom$); const audioParticipants = useBehavior(vm.audioParticipants$);
const participantCount = useBehavior(vm.participantCount$); const participantCount = useBehavior(vm.participantCount$);
const reconnecting = useBehavior(vm.reconnecting$); const reconnecting = useBehavior(vm.reconnecting$);
const windowMode = useBehavior(vm.windowMode$); const windowMode = useBehavior(vm.windowMode$);
@@ -860,7 +860,7 @@ export const InCallView: FC<InCallViewProps> = ({
</Text> </Text>
) )
} }
{participantsByRoom.map(({ livekitRoom, url, participants }) => ( {audioParticipants.map(({ livekitRoom, url, participants }) => (
<LivekitRoomAudioRenderer <LivekitRoomAudioRenderer
key={url} key={url}
url={url} url={url}

View File

@@ -14,7 +14,7 @@ import {
type Room as LivekitRoom, type Room as LivekitRoom,
type LocalParticipant, type LocalParticipant,
ParticipantEvent, ParticipantEvent,
type RemoteParticipant, RemoteParticipant,
type Participant, type Participant,
} from "livekit-client"; } from "livekit-client";
import E2EEWorker from "livekit-client/e2ee-worker?worker"; import E2EEWorker from "livekit-client/e2ee-worker?worker";
@@ -793,7 +793,7 @@ export class CallViewModel extends ViewModel {
* Lists, for each LiveKit room, the LiveKit participants whose media should * Lists, for each LiveKit room, the LiveKit participants whose media should
* be presented. * be presented.
*/ */
public readonly participantsByRoom$ = this.scope.behavior< private readonly participantsByRoom$ = this.scope.behavior<
{ {
livekitRoom: LivekitRoom; livekitRoom: LivekitRoom;
url: string; url: string;
@@ -861,6 +861,25 @@ export class CallViewModel extends ViewModel {
.pipe(startWith([]), pauseWhen(this.pretendToBeDisconnected$)), .pipe(startWith([]), pauseWhen(this.pretendToBeDisconnected$)),
); );
/**
* Lists, for each LiveKit room, the LiveKit participants whose audio should
* be rendered.
*/
// (This is effectively just participantsByRoom$ with a stricter type)
public readonly audioParticipants$ = this.scope.behavior(
this.participantsByRoom$.pipe(
map((data) =>
data.map(({ livekitRoom, url, participants }) => ({
livekitRoom,
url,
participants: participants.flatMap(({ participant }) =>
participant instanceof RemoteParticipant ? [participant] : [],
),
})),
),
),
);
/** /**
* Displaynames for each member of the call. This will disambiguate * Displaynames for each member of the call. This will disambiguate
* any displaynames that clashes with another member. Only members * any displaynames that clashes with another member. Only members