Don't render audio from participants that aren't meant to be publishing
This commit is contained in:
@@ -6,7 +6,7 @@ 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 } from "livekit-client";
|
import { type Room as LivekitRoom, type Participant } 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,7 +14,7 @@ import {
|
|||||||
AudioTrack,
|
AudioTrack,
|
||||||
type AudioTrackProps,
|
type AudioTrackProps,
|
||||||
} from "@livekit/components-react";
|
} 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 { logger } from "matrix-js-sdk/lib/logger";
|
||||||
|
|
||||||
import { useEarpieceAudioConfig } from "../MediaDevicesContext";
|
import { useEarpieceAudioConfig } from "../MediaDevicesContext";
|
||||||
@@ -22,13 +22,20 @@ 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.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
livekitRoom: LivekitRoom;
|
||||||
/**
|
/**
|
||||||
* The list of participants to render audio for.
|
* 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
|
* 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.
|
||||||
*/
|
*/
|
||||||
members: CallMembership[];
|
participants: {
|
||||||
livekitRoom: LivekitRoom;
|
participant: Participant;
|
||||||
|
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
|
||||||
@@ -52,14 +59,14 @@ export interface MatrixAudioRendererProps {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export function LivekitRoomAudioRenderer({
|
export function LivekitRoomAudioRenderer({
|
||||||
members,
|
url,
|
||||||
muted,
|
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
|
participants,
|
||||||
|
muted,
|
||||||
}: MatrixAudioRendererProps): ReactNode {
|
}: MatrixAudioRendererProps): ReactNode {
|
||||||
const validIdentities = useMemo(
|
const participantSet = useMemo(
|
||||||
() =>
|
() => new Set(participants.map(({ participant }) => participant)),
|
||||||
new Set(members?.map((member) => `${member.sender}:${member.deviceId}`)),
|
[participants],
|
||||||
[members],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const loggedInvalidIdentities = useRef(new Set<string>());
|
const loggedInvalidIdentities = useRef(new Set<string>());
|
||||||
@@ -71,11 +78,11 @@ export function LivekitRoomAudioRenderer({
|
|||||||
* @param identity The identity of the track that is invalid
|
* @param identity The identity of the track that is invalid
|
||||||
* @param validIdentities The list of valid identities
|
* @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;
|
if (loggedInvalidIdentities.current.has(identity)) return;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`[MatrixAudioRenderer] Audio track ${identity} has no matching matrix call member`,
|
`[MatrixAudioRenderer] Audio track ${identity} from ${url} has no matching matrix call member`,
|
||||||
`current members: ${Array.from(validIdentities.values())}`,
|
`current members: ${participants.map((p) => p.participant.identity)}`,
|
||||||
`track will not get rendered`,
|
`track will not get rendered`,
|
||||||
);
|
);
|
||||||
loggedInvalidIdentities.current.add(identity);
|
loggedInvalidIdentities.current.add(identity);
|
||||||
@@ -93,23 +100,27 @@ export function LivekitRoomAudioRenderer({
|
|||||||
room: livekitRoom,
|
room: livekitRoom,
|
||||||
},
|
},
|
||||||
).filter((ref) => {
|
).filter((ref) => {
|
||||||
const isValid = validIdentities?.has(ref.participant.identity);
|
const isValid = participantSet?.has(ref.participant);
|
||||||
if (!isValid && !ref.participant.isLocal)
|
if (!isValid && !ref.participant.isLocal)
|
||||||
logInvalid(ref.participant.identity, validIdentities);
|
logInvalid(ref.participant.identity);
|
||||||
return (
|
return (
|
||||||
!ref.participant.isLocal &&
|
!ref.participant.isLocal &&
|
||||||
ref.publication.kind === Track.Kind.Audio &&
|
ref.publication.kind === Track.Kind.Audio &&
|
||||||
isValid
|
isValid
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tracks.some((t) => !validIdentities.has(t.participant.identity))) {
|
if (
|
||||||
|
loggedInvalidIdentities.current.size &&
|
||||||
|
tracks.every((t) => participantSet.has(t.participant))
|
||||||
|
) {
|
||||||
logger.debug(
|
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();
|
loggedInvalidIdentities.current.clear();
|
||||||
}
|
}
|
||||||
}, [tracks, validIdentities]);
|
}, [tracks, participantSet, url]);
|
||||||
|
|
||||||
// This component is also (in addition to the "only play audio for connected members" logic above)
|
// This component is also (in addition to the "only play audio for connected members" logic above)
|
||||||
// responsible for mimicking earpiece audio on iPhones.
|
// responsible for mimicking earpiece audio on iPhones.
|
||||||
|
|||||||
@@ -286,6 +286,8 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
() => void toggleRaisedHand(),
|
() => void toggleRaisedHand(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const allLivekitRooms = useBehavior(vm.allLivekitRooms$);
|
||||||
|
const participantsByRoom = useBehavior(vm.participantsByRoom$);
|
||||||
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$);
|
||||||
@@ -739,9 +741,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
matrixRoom.roomId,
|
matrixRoom.roomId,
|
||||||
);
|
);
|
||||||
|
|
||||||
const allLivekitRooms = useBehavior(vm.allLivekitRooms$);
|
|
||||||
const memberships = useBehavior(vm.memberships$);
|
|
||||||
|
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
@@ -862,11 +861,12 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{allLivekitRooms.map((roomItem) => (
|
{participantsByRoom.map(({ livekitRoom, url, participants }) => (
|
||||||
<LivekitRoomAudioRenderer
|
<LivekitRoomAudioRenderer
|
||||||
key={roomItem.url}
|
key={url}
|
||||||
livekitRoom={roomItem.room}
|
url={url}
|
||||||
members={memberships}
|
livekitRoom={livekitRoom}
|
||||||
|
participants={participants}
|
||||||
muted={muteAllAudio}
|
muted={muteAllAudio}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
*/
|
*/
|
||||||
// Note that MatrixRTCSession already filters the call memberships by users
|
// Note that MatrixRTCSession already filters the call memberships by users
|
||||||
// that are joined to the room; we don't need to perform extra filtering here.
|
// that are joined to the room; we don't need to perform extra filtering here.
|
||||||
public readonly memberships$ = this.scope.behavior(
|
private readonly memberships$ = this.scope.behavior(
|
||||||
fromEvent(
|
fromEvent(
|
||||||
this.matrixRTCSession,
|
this.matrixRTCSession,
|
||||||
MatrixRTCSessionEvent.MembershipsChanged,
|
MatrixRTCSessionEvent.MembershipsChanged,
|
||||||
@@ -679,16 +679,19 @@ export class CallViewModel extends ViewModel {
|
|||||||
// in a split-brained state.
|
// in a split-brained state.
|
||||||
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
private readonly pretendToBeDisconnected$ = this.reconnecting$;
|
||||||
|
|
||||||
private readonly participants$ = this.scope.behavior<
|
public readonly participantsByRoom$ = this.scope.behavior<
|
||||||
{
|
{
|
||||||
participant: LocalParticipant | RemoteParticipant;
|
|
||||||
member: RoomMember;
|
|
||||||
livekitRoom: LivekitRoom;
|
livekitRoom: LivekitRoom;
|
||||||
|
url: string;
|
||||||
|
participants: {
|
||||||
|
participant: LocalParticipant | RemoteParticipant;
|
||||||
|
member: RoomMember;
|
||||||
|
}[];
|
||||||
}[]
|
}[]
|
||||||
>(
|
>(
|
||||||
from(this.localConnection)
|
combineLatest([this.localConnection, this.localFocus])
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((localConnection) => {
|
switchMap(([localConnection, localFocus]) => {
|
||||||
const memberError = (): never => {
|
const memberError = (): never => {
|
||||||
throw new Error("No room member for call membership");
|
throw new Error("No room member for call membership");
|
||||||
};
|
};
|
||||||
@@ -696,32 +699,41 @@ export class CallViewModel extends ViewModel {
|
|||||||
participant: localConnection.livekitRoom.localParticipant,
|
participant: localConnection.livekitRoom.localParticipant,
|
||||||
member:
|
member:
|
||||||
this.matrixRoom.getMember(this.userId ?? "") ?? memberError(),
|
this.matrixRoom.getMember(this.userId ?? "") ?? memberError(),
|
||||||
livekitRoom: localConnection.livekitRoom,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.remoteConnections$.pipe(
|
return this.remoteConnections$.pipe(
|
||||||
switchMap((connections) =>
|
switchMap((connections) =>
|
||||||
combineLatest(
|
combineLatest(
|
||||||
[localConnection, ...connections.values()].map((c) =>
|
[
|
||||||
|
[localFocus.livekit_service_url, localConnection] as const,
|
||||||
|
...connections,
|
||||||
|
].map(([url, c]) =>
|
||||||
c.publishingParticipants$.pipe(
|
c.publishingParticipants$.pipe(
|
||||||
map((ps) =>
|
map((ps) => {
|
||||||
ps.map(({ participant, membership }) => ({
|
const participants: {
|
||||||
|
participant: LocalParticipant | RemoteParticipant;
|
||||||
|
member: RoomMember;
|
||||||
|
}[] = ps.map(({ participant, membership }) => ({
|
||||||
participant,
|
participant,
|
||||||
member:
|
member:
|
||||||
getRoomMemberFromRtcMember(
|
getRoomMemberFromRtcMember(
|
||||||
membership,
|
membership,
|
||||||
this.matrixRoom,
|
this.matrixRoom,
|
||||||
)?.member ?? memberError(),
|
)?.member ?? memberError(),
|
||||||
|
}));
|
||||||
|
if (c === localConnection)
|
||||||
|
participants.push(localParticipant);
|
||||||
|
|
||||||
|
return {
|
||||||
livekitRoom: c.livekitRoom,
|
livekitRoom: c.livekitRoom,
|
||||||
})),
|
url,
|
||||||
),
|
participants,
|
||||||
|
};
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
map((remoteParticipants) => [
|
|
||||||
localParticipant,
|
|
||||||
...remoteParticipants.flat(1),
|
|
||||||
]),
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -798,7 +810,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
*/
|
*/
|
||||||
private readonly mediaItems$ = this.scope.behavior<MediaItem[]>(
|
private readonly mediaItems$ = this.scope.behavior<MediaItem[]>(
|
||||||
combineLatest([
|
combineLatest([
|
||||||
this.participants$,
|
this.participantsByRoom$,
|
||||||
duplicateTiles.value$,
|
duplicateTiles.value$,
|
||||||
this.memberships$,
|
this.memberships$,
|
||||||
showNonMemberTiles.value$,
|
showNonMemberTiles.value$,
|
||||||
@@ -806,71 +818,75 @@ export class CallViewModel extends ViewModel {
|
|||||||
scan(
|
scan(
|
||||||
(
|
(
|
||||||
prevItems,
|
prevItems,
|
||||||
[participants, duplicateTiles, memberships, showNonMemberTiles],
|
[participantsByRoom, duplicateTiles, memberships, showNonMemberTiles],
|
||||||
) => {
|
) => {
|
||||||
const newItems: Map<string, UserMedia | ScreenShare> = new Map(
|
const newItems: Map<string, UserMedia | ScreenShare> = new Map(
|
||||||
function* (this: CallViewModel): Iterable<[string, MediaItem]> {
|
function* (this: CallViewModel): Iterable<[string, MediaItem]> {
|
||||||
for (const { participant, member, livekitRoom } of participants) {
|
for (const { livekitRoom, participants } of participantsByRoom) {
|
||||||
const matrixId = participant.isLocal
|
for (const { participant, member } of participants) {
|
||||||
? "local"
|
const matrixId = participant.isLocal
|
||||||
: participant.identity;
|
? "local"
|
||||||
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
: participant.identity;
|
||||||
const mediaId = `${matrixId}:${i}`;
|
|
||||||
let prevMedia = prevItems.get(mediaId);
|
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||||
if (prevMedia && prevMedia instanceof UserMedia) {
|
const mediaId = `${matrixId}:${i}`;
|
||||||
prevMedia.updateParticipant(participant);
|
let prevMedia = prevItems.get(mediaId);
|
||||||
if (prevMedia.vm.member === undefined) {
|
if (prevMedia && prevMedia instanceof UserMedia) {
|
||||||
// We have a previous media created because of the `debugShowNonMember` flag.
|
prevMedia.updateParticipant(participant);
|
||||||
// In this case we actually replace the media item.
|
if (prevMedia.vm.member === undefined) {
|
||||||
// This "hack" never occurs if we do not use the `debugShowNonMember` debugging
|
// We have a previous media created because of the `debugShowNonMember` flag.
|
||||||
// option and if we always find a room member for each rtc member (which also
|
// In this case we actually replace the media item.
|
||||||
// only fails if we have a fundamental problem)
|
// This "hack" never occurs if we do not use the `debugShowNonMember` debugging
|
||||||
prevMedia = undefined;
|
// option and if we always find a room member for each rtc member (which also
|
||||||
}
|
// only fails if we have a fundamental problem)
|
||||||
}
|
prevMedia = undefined;
|
||||||
yield [
|
}
|
||||||
mediaId,
|
}
|
||||||
// We create UserMedia with or without a participant.
|
|
||||||
// This will be the initial value of a BehaviourSubject.
|
|
||||||
// Once a participant appears we will update the BehaviourSubject. (see above)
|
|
||||||
prevMedia ??
|
|
||||||
new UserMedia(
|
|
||||||
mediaId,
|
|
||||||
member,
|
|
||||||
participant,
|
|
||||||
this.options.encryptionSystem,
|
|
||||||
livekitRoom,
|
|
||||||
this.mediaDevices,
|
|
||||||
this.pretendToBeDisconnected$,
|
|
||||||
this.memberDisplaynames$.pipe(
|
|
||||||
map((m) => m.get(matrixId) ?? "[👻]"),
|
|
||||||
),
|
|
||||||
this.handsRaised$.pipe(
|
|
||||||
map((v) => v[matrixId]?.time ?? null),
|
|
||||||
),
|
|
||||||
this.reactions$.pipe(
|
|
||||||
map((v) => v[matrixId] ?? undefined),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (participant?.isScreenShareEnabled) {
|
|
||||||
const screenShareId = `${mediaId}:screen-share`;
|
|
||||||
yield [
|
yield [
|
||||||
screenShareId,
|
mediaId,
|
||||||
prevItems.get(screenShareId) ??
|
// We create UserMedia with or without a participant.
|
||||||
new ScreenShare(
|
// This will be the initial value of a BehaviourSubject.
|
||||||
screenShareId,
|
// Once a participant appears we will update the BehaviourSubject. (see above)
|
||||||
|
prevMedia ??
|
||||||
|
new UserMedia(
|
||||||
|
mediaId,
|
||||||
member,
|
member,
|
||||||
participant,
|
participant,
|
||||||
this.options.encryptionSystem,
|
this.options.encryptionSystem,
|
||||||
livekitRoom,
|
livekitRoom,
|
||||||
|
this.mediaDevices,
|
||||||
this.pretendToBeDisconnected$,
|
this.pretendToBeDisconnected$,
|
||||||
this.memberDisplaynames$.pipe(
|
this.memberDisplaynames$.pipe(
|
||||||
map((m) => m.get(matrixId) ?? "[👻]"),
|
map((m) => m.get(matrixId) ?? "[👻]"),
|
||||||
),
|
),
|
||||||
|
this.handsRaised$.pipe(
|
||||||
|
map((v) => v[matrixId]?.time ?? null),
|
||||||
|
),
|
||||||
|
this.reactions$.pipe(
|
||||||
|
map((v) => v[matrixId] ?? undefined),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (participant?.isScreenShareEnabled) {
|
||||||
|
const screenShareId = `${mediaId}:screen-share`;
|
||||||
|
yield [
|
||||||
|
screenShareId,
|
||||||
|
prevItems.get(screenShareId) ??
|
||||||
|
new ScreenShare(
|
||||||
|
screenShareId,
|
||||||
|
member,
|
||||||
|
participant,
|
||||||
|
this.options.encryptionSystem,
|
||||||
|
livekitRoom,
|
||||||
|
this.pretendToBeDisconnected$,
|
||||||
|
this.memberDisplaynames$.pipe(
|
||||||
|
map((m) => m.get(matrixId) ?? "[👻]"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,11 +93,9 @@ export class Connection {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.publishingParticipants$ = this.scope.behavior(
|
this.publishingParticipants$ = this.scope.behavior(
|
||||||
combineLatest([
|
combineLatest(
|
||||||
this.participantsIncludingSubscribers$,
|
[this.participantsIncludingSubscribers$, this.membershipsFocusMap$],
|
||||||
this.membershipsFocusMap$,
|
(participants, membershipsFocusMap) =>
|
||||||
]).pipe(
|
|
||||||
map(([participants, membershipsFocusMap]) =>
|
|
||||||
membershipsFocusMap
|
membershipsFocusMap
|
||||||
// Find all members that claim to publish on this connection
|
// Find all members that claim to publish on this connection
|
||||||
.flatMap(({ membership, focus }) =>
|
.flatMap(({ membership, focus }) =>
|
||||||
@@ -113,7 +111,6 @@ export class Connection {
|
|||||||
);
|
);
|
||||||
return participant ? [{ participant, membership }] : [];
|
return participant ? [{ participant, membership }] : [];
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user