diff --git a/src/state/Connection.test.ts b/src/state/Connection.test.ts index 14c42206..41be4e39 100644 --- a/src/state/Connection.test.ts +++ b/src/state/Connection.test.ts @@ -35,6 +35,7 @@ import type { import { type ConnectionOpts, type FocusConnectionState, + type PublishingParticipant, RemoteConnection, } from "./Connection.ts"; import { ObservableScope } from "./ObservableScope.ts"; @@ -454,22 +455,19 @@ describe("Publishing participants observations", () => { const bobIsAPublisher = Promise.withResolvers(); const danIsAPublisher = Promise.withResolvers(); - const observedPublishers: { - participant: RemoteParticipant; - membership: CallMembership; - }[][] = []; + const observedPublishers: PublishingParticipant[][] = []; const s = connection.publishingParticipants$.subscribe((publishers) => { observedPublishers.push(publishers); if ( publishers.some( - (p) => p.participant.identity === "@bob:example.org:DEV111", + (p) => p.participant?.identity === "@bob:example.org:DEV111", ) ) { bobIsAPublisher.resolve(); } if ( publishers.some( - (p) => p.participant.identity === "@dan:example.org:DEV333", + (p) => p.participant?.identity === "@dan:example.org:DEV333", ) ) { danIsAPublisher.resolve(); @@ -529,7 +527,7 @@ describe("Publishing participants observations", () => { await bobIsAPublisher.promise; const publishers = observedPublishers.pop(); expect(publishers?.length).toEqual(1); - expect(publishers?.[0].participant.identity).toEqual( + expect(publishers?.[0].participant?.identity).toEqual( "@bob:example.org:DEV111", ); @@ -546,12 +544,12 @@ describe("Publishing participants observations", () => { expect(twoPublishers?.length).toEqual(2); expect( twoPublishers?.some( - (p) => p.participant.identity === "@bob:example.org:DEV111", + (p) => p.participant?.identity === "@bob:example.org:DEV111", ), ).toBeTruthy(); expect( twoPublishers?.some( - (p) => p.participant.identity === "@dan:example.org:DEV333", + (p) => p.participant?.identity === "@dan:example.org:DEV333", ), ).toBeTruthy(); @@ -568,12 +566,25 @@ describe("Publishing participants observations", () => { ); const updatedPublishers = observedPublishers.pop(); - expect(updatedPublishers?.length).toEqual(1); + // Bob is not connected to the room but he is still in the rtc memberships declaring that + // he is using that focus to publish, so he should still appear as a publisher + expect(updatedPublishers?.length).toEqual(2); + const pp = updatedPublishers?.find( + (p) => p.membership.sender == "@bob:example.org", + ); + expect(pp).toBeDefined(); + expect(pp!.participant).not.toBeDefined(); expect( updatedPublishers?.some( - (p) => p.participant.identity === "@dan:example.org:DEV333", + (p) => p.participant?.identity === "@dan:example.org:DEV333", ), ).toBeTruthy(); + // Now if bob is not in the rtc memberships, he should disappear + const noBob = rtcMemberships.filter( + ({ membership }) => membership.sender !== "@bob:example.org", + ); + fakeMembershipsFocusMap$.next(noBob); + expect(observedPublishers.pop()?.length).toEqual(1); }); it("should be scoped to parent scope", (): void => { @@ -581,10 +592,7 @@ describe("Publishing participants observations", () => { const connection = setupRemoteConnection(); - let observedPublishers: { - participant: RemoteParticipant; - membership: CallMembership; - }[][] = []; + let observedPublishers: PublishingParticipant[][] = []; const s = connection.publishingParticipants$.subscribe((publishers) => { observedPublishers.push(publishers); }); @@ -619,7 +627,7 @@ describe("Publishing participants observations", () => { // We should have bob has a publisher now const publishers = observedPublishers.pop(); expect(publishers?.length).toEqual(1); - expect(publishers?.[0].participant.identity).toEqual( + expect(publishers?.[0].participant?.identity).toEqual( "@bob:example.org:DEV111", ); diff --git a/src/state/Connection.ts b/src/state/Connection.ts index bdb77ed8..5750eaaa 100644 --- a/src/state/Connection.ts +++ b/src/state/Connection.ts @@ -13,6 +13,7 @@ import { ConnectionError, type ConnectionState, type E2EEOptions, + type RemoteParticipant, Room as LivekitRoom, type RoomOptions, } from "livekit-client"; @@ -64,6 +65,21 @@ export type FocusConnectionState = } | { state: "Stopped"; focus: LivekitTransport }; +/** + * Represents participant publishing or expected to publish on the connection. + * It is paired with its associated rtc membership. + */ +export type PublishingParticipant = { + /** + * The LiveKit participant publishing on this connection, or undefined if the participant is not currently (yet) connected to the livekit room. + */ + participant: RemoteParticipant | undefined; + /** + * The rtc call membership associated with this participant. + */ + membership: CallMembership; +}; + /** * A connection to a Matrix RTC LiveKit backend. * @@ -183,7 +199,7 @@ export class 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. */ - public readonly publishingParticipants$; + public readonly publishingParticipants$: Behavior; /** * The focus server to connect to. @@ -226,10 +242,10 @@ export class Connection { ) // Pair with their associated LiveKit participant (if any) // Uses flatMap to filter out memberships with no associated rtc participant ([]) - .flatMap((membership) => { + .map((membership) => { const id = `${membership.sender}:${membership.deviceId}`; const participant = participants.find((p) => p.identity === id); - return participant ? [{ participant, membership }] : []; + return { participant, membership }; }), ), [],