Don't show 'waiting for media' on connected participants

We would show 'waiting for media' on participants that were connected but had no published tracks, because we were filtering them out of the remote participants list on connections. I believe this was done in an attempt to limit our view to only the participants that have a matching MatrixRTC membership. But that's fully redundant to the "Matrix-LiveKit members" module, which actually has the right information to do this (the MatrixRTC memberships).
This commit is contained in:
Robin
2025-12-10 15:09:40 -05:00
parent 92bcc52e87
commit 2c54263b2f
4 changed files with 46 additions and 96 deletions

View File

@@ -39,6 +39,7 @@ import {
ElementCallError, ElementCallError,
FailToGetOpenIdToken, FailToGetOpenIdToken,
} from "../../../utils/errors.ts"; } from "../../../utils/errors.ts";
import { mockRemoteParticipant } from "../../../utils/test.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
@@ -376,46 +377,32 @@ describe("Start connection states", () => {
}); });
}); });
function fakeRemoteLivekitParticipant( describe("remote participants", () => {
id: string, it("emits the list of remote participants", () => {
publications: number = 1,
): RemoteParticipant {
return {
identity: id,
getTrackPublications: () => Array(publications),
} as unknown as RemoteParticipant;
}
describe("Publishing participants observations", () => {
it("should emit the list of publishing participants", () => {
setupTest(); setupTest();
const connection = setupRemoteConnection(); const connection = setupRemoteConnection();
const bobIsAPublisher = Promise.withResolvers<void>(); const observedParticipants: RemoteParticipant[][] = [];
const danIsAPublisher = Promise.withResolvers<void>(); const s = connection.remoteParticipants$.subscribe((participants) => {
const observedPublishers: RemoteParticipant[][] = []; observedParticipants.push(participants);
const s = connection.remoteParticipantsWithTracks$.subscribe( });
(publishers) => {
observedPublishers.push(publishers);
if (publishers.some((p) => p.identity === "@bob:example.org:DEV111")) {
bobIsAPublisher.resolve();
}
if (publishers.some((p) => p.identity === "@dan:example.org:DEV333")) {
danIsAPublisher.resolve();
}
},
);
onTestFinished(() => s.unsubscribe()); onTestFinished(() => s.unsubscribe());
// The remoteParticipants$ observable is derived from the current members of the // The remoteParticipants$ observable is derived from the current members of the
// livekitRoom and the rtc membership in order to publish the members that are publishing // livekitRoom and the rtc membership in order to publish the members that are publishing
// on this connection. // on this connection.
let participants: RemoteParticipant[] = [ const participants: RemoteParticipant[] = [
fakeRemoteLivekitParticipant("@alice:example.org:DEV000", 0), mockRemoteParticipant({ identity: "@alice:example.org:DEV000" }),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 0), mockRemoteParticipant({ identity: "@bob:example.org:DEV111" }),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222", 0), mockRemoteParticipant({ identity: "@carol:example.org:DEV222" }),
fakeRemoteLivekitParticipant("@dan:example.org:DEV333", 0), // Mock Dan to have no published tracks. We want him to still show show up
// in the participants list.
mockRemoteParticipant({
identity: "@dan:example.org:DEV333",
getTrackPublication: () => undefined,
getTrackPublications: () => [],
}),
]; ];
// Let's simulate 3 members on the livekitRoom // Let's simulate 3 members on the livekitRoom
@@ -427,21 +414,8 @@ describe("Publishing participants observations", () => {
fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, p), fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, p),
); );
// At this point there should be no publishers // All remote participants should be present
expect(observedPublishers.pop()!.length).toEqual(0); expect(observedParticipants.pop()!.length).toEqual(4);
participants = [
fakeRemoteLivekitParticipant("@alice:example.org:DEV000", 1),
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 1),
fakeRemoteLivekitParticipant("@carol:example.org:DEV222", 1),
fakeRemoteLivekitParticipant("@dan:example.org:DEV333", 2),
];
participants.forEach((p) =>
fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, p),
);
// At this point there should be no publishers
expect(observedPublishers.pop()!.length).toEqual(4);
}); });
it("should be scoped to parent scope", (): void => { it("should be scoped to parent scope", (): void => {
@@ -449,16 +423,14 @@ describe("Publishing participants observations", () => {
const connection = setupRemoteConnection(); const connection = setupRemoteConnection();
let observedPublishers: RemoteParticipant[][] = []; let observedParticipants: RemoteParticipant[][] = [];
const s = connection.remoteParticipantsWithTracks$.subscribe( const s = connection.remoteParticipants$.subscribe((participants) => {
(publishers) => { observedParticipants.push(participants);
observedPublishers.push(publishers); });
},
);
onTestFinished(() => s.unsubscribe()); onTestFinished(() => s.unsubscribe());
let participants: RemoteParticipant[] = [ let participants: RemoteParticipant[] = [
fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 0), mockRemoteParticipant({ identity: "@bob:example.org:DEV111" }),
]; ];
// Let's simulate 3 members on the livekitRoom // Let's simulate 3 members on the livekitRoom
@@ -470,35 +442,26 @@ describe("Publishing participants observations", () => {
fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, participant); fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, participant);
} }
// At this point there should be no publishers // We should have bob as a participant now
expect(observedPublishers.pop()!.length).toEqual(0); const ps = observedParticipants.pop();
expect(ps?.length).toEqual(1);
participants = [fakeRemoteLivekitParticipant("@bob:example.org:DEV111", 1)]; expect(ps?.[0]?.identity).toEqual("@bob:example.org:DEV111");
for (const participant of participants) {
fakeLivekitRoom.emit(RoomEvent.ParticipantConnected, participant);
}
// We should have bob has a publisher now
const publishers = observedPublishers.pop();
expect(publishers?.length).toEqual(1);
expect(publishers?.[0]?.identity).toEqual("@bob:example.org:DEV111");
// end the parent scope // end the parent scope
testScope.end(); testScope.end();
observedPublishers = []; observedParticipants = [];
// SHOULD NOT emit any more publishers as the scope is ended // SHOULD NOT emit any more participants as the scope is ended
participants = participants.filter( participants = participants.filter(
(p) => p.identity !== "@bob:example.org:DEV111", (p) => p.identity !== "@bob:example.org:DEV111",
); );
fakeLivekitRoom.emit( fakeLivekitRoom.emit(
RoomEvent.ParticipantDisconnected, RoomEvent.ParticipantDisconnected,
fakeRemoteLivekitParticipant("@bob:example.org:DEV111"), mockRemoteParticipant({ identity: "@bob:example.org:DEV111" }),
); );
expect(observedPublishers.length).toEqual(0); expect(observedParticipants.length).toEqual(0);
}); });
}); });

View File

@@ -14,7 +14,6 @@ import {
ConnectionError, ConnectionError,
type Room as LivekitRoom, type Room as LivekitRoom,
type RemoteParticipant, type RemoteParticipant,
RoomEvent,
} from "livekit-client"; } from "livekit-client";
import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc"; import { type LivekitTransport } from "matrix-js-sdk/lib/matrixrtc";
import { BehaviorSubject, map } from "rxjs"; import { BehaviorSubject, map } from "rxjs";
@@ -96,11 +95,13 @@ export class Connection {
private scope: ObservableScope; private scope: ObservableScope;
/** /**
* An observable of the participants that are publishing on this connection. (Excluding our local participant) * The remote LiveKit participants that are visible on this connection.
* This is derived from `participantsIncludingSubscribers$` and `remoteTransports$`. *
* It filters the participants to only those that are associated with a membership that claims to publish on this connection. * Note that this may include participants that are connected only to
* subscribe, or publishers that are otherwise unattested in MatrixRTC state.
* It is therefore more low-level than what should be presented to the user.
*/ */
public readonly remoteParticipantsWithTracks$: Behavior<RemoteParticipant[]>; public readonly remoteParticipants$: Behavior<RemoteParticipant[]>;
/** /**
* Whether the connection has been stopped. * Whether the connection has been stopped.
@@ -231,23 +232,9 @@ export class Connection {
this.transport = transport; this.transport = transport;
this.client = client; this.client = client;
// REMOTE participants with track!!! this.remoteParticipants$ = scope.behavior(
// this.remoteParticipantsWithTracks$ // Only tracks remote participants
this.remoteParticipantsWithTracks$ = scope.behavior( connectedParticipantsObserver(this.livekitRoom),
// only tracks remote participants
connectedParticipantsObserver(this.livekitRoom, {
additionalRoomEvents: [
RoomEvent.TrackPublished,
RoomEvent.TrackUnpublished,
],
}).pipe(
map((participants) => {
return participants.filter(
(participant) => participant.getTrackPublications().length > 0,
);
}),
),
[],
); );
scope.onEnd(() => { scope.onEnd(() => {

View File

@@ -52,7 +52,7 @@ beforeEach(() => {
(transport: LivekitTransport, scope: ObservableScope) => { (transport: LivekitTransport, scope: ObservableScope) => {
const mockConnection = { const mockConnection = {
transport, transport,
remoteParticipantsWithTracks$: new BehaviorSubject([]), remoteParticipants$: new BehaviorSubject([]),
} as unknown as Connection; } as unknown as Connection;
vi.mocked(mockConnection).start = vi.fn(); vi.mocked(mockConnection).start = vi.fn();
vi.mocked(mockConnection).stop = vi.fn(); vi.mocked(mockConnection).stop = vi.fn();
@@ -200,7 +200,7 @@ describe("connections$ stream", () => {
}); });
describe("connectionManagerData$ stream", () => { describe("connectionManagerData$ stream", () => {
// Used in test to control fake connections' remoteParticipantsWithTracks$ streams // Used in test to control fake connections' remoteParticipants$ streams
let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>; let fakeRemoteParticipantsStreams: Map<string, Behavior<RemoteParticipant[]>>;
function keyForTransport(transport: LivekitTransport): string { function keyForTransport(transport: LivekitTransport): string {
@@ -229,7 +229,7 @@ describe("connectionManagerData$ stream", () => {
>([]); >([]);
const mockConnection = { const mockConnection = {
transport, transport,
remoteParticipantsWithTracks$: getRemoteParticipantsFor(transport), remoteParticipants$: getRemoteParticipantsFor(transport),
} as unknown as Connection; } as unknown as Connection;
vi.mocked(mockConnection).start = vi.fn(); vi.mocked(mockConnection).start = vi.fn();
vi.mocked(mockConnection).stop = vi.fn(); vi.mocked(mockConnection).stop = vi.fn();

View File

@@ -152,7 +152,7 @@ export function createConnectionManager$({
// Map the connections to list of {connection, participants}[] // Map the connections to list of {connection, participants}[]
const listOfConnectionsWithRemoteParticipants = connections.value.map( const listOfConnectionsWithRemoteParticipants = connections.value.map(
(connection) => { (connection) => {
return connection.remoteParticipantsWithTracks$.pipe( return connection.remoteParticipants$.pipe(
map((participants) => ({ map((participants) => ({
connection, connection,
participants, participants,