Skip to content

Conversation

BillCarsonFr
Copy link
Member

@BillCarsonFr BillCarsonFr commented Oct 1, 2025

Better reviewed commit per commit

Updated the test because now name will be the userId instead of default display name
Copy link
Member

@robintown robintown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice work with fixing things up as you go. For the sake of getting our two branches merged into one as soon as possible, I went ahead and applied some changes directly for things which seemed hopefully uncontrovertial - if you like you can have a look through them. The rest of my comments are mostly about things from my branch that seem to have been lost in the previous merge conflicts.

I did also confirm that the app runs and connects to an SFU successfully.

* 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.
*/
// TODO: Why do we have this structure? looks like we only need the valid/active participants (not the room member or id)?
Copy link
Member

@robintown robintown Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just laziness, really. The CallViewModel needs the ID and room member internally to create the media tiles, so I took the same data and made it public for the purpose of audio rendering without making the public type any stricter. I've remedied this in: c96e81b

// dependency references.
import "matrix-js-sdk/lib/browser-index";

import { StrictMode } from "react";
Copy link
Member

@robintown robintown Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, I didn't mean to commit my temporary disabling of strict mode. e346c8c

) {}
}

// TODO there is another MuteStates in src/room/MuteStates.tsx ?? why
Copy link
Member

@robintown robintown Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead code, we can remove it. 5da780e

import { catchError, from, map, type Observable, of, startWith } from "rxjs";

// TODO where are all the comments? ::cry::
// There used to be an unitialized state!, a state might not start in loading
Copy link
Member

@robintown robintown Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the "uninitialized" state doesn't belong here. In cases where it might be tempting to use "uninitialized", I'd like it if we explicitly add such a case into our types as needed (Async<T>Async<T | undefined>) or keep an an eye out for better ways to model the problem. With just these three states, there's a very clear mapping between async values and Promises.

I added hopefully a decent amount of comments: b1d1437

Comment on lines +156 to +159
/**
* The focus server to connect to.
*/
public readonly localTransport: LivekitTransport;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this called localTransport and not transport? It's often remote.

Comment on lines 201 to 207
// Pair with their associated LiveKit participant (if any)
.map((membership) => {
// Uses flatMap to filter out memberships with no associated rtc participant ([])
.flatMap((membership) => {
const id = `${membership.sender}:${membership.deviceId}`;
const participant = participants.find((p) => p.identity === id);
return { participant, membership };
return participant ? [{ participant, membership }] : [];
}),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think something went wrong here with the merge. If you take a look at 1820cac I was trying to make sure that even RTC memberships with no associated LiveKit participant are included in publishingParticipants$. (One could add them back in CallViewModel, but it seemed way easier and more efficient to just avoid filtering them out here.)

The type that I was aiming for was:

Connection.publishingParticipants$: Behavior<{
  participant: RemoteParticipant | undefined;
  membership: CallMembership;
}[]>

Once we get the CallViewModel tests back up and running we'll be back to a state where this can't regress without a red flag.

Comment on lines 212 to 222
scope
.behavior<ConnectionState>(connectionStateObserver(this.livekitRoom))
.subscribe((connectionState) => {
const current = this._focusedConnectionState$.value;
// Only update the state if we are already connected to the LiveKit room.
if (current.state === "ConnectedToLkRoom") {
this._focusedConnectionState$.next({
state: "ConnectedToLkRoom",
connectionState,
focus: current.focus,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps a more elegant way to do this would be to change the type to:

 {
   state: "ConnectedToLkRoom";
-  connectionState: ConnectionState;
+  connectionState: Observable<ConnectionState>;
   focus: LivekitTransport;
 }

Then (on line 111) you can just write:

this._focusedConnectionState$.next({
  state: "ConnectedToLkRoom",
  focus: this.localTransport,
  connectionState: connectionStateObserver(this.livekitRoom),
});

and remove this line 212 code.

Comment on lines +79 to +86
// Setup track processor syncing (blur)
this.observeTrackProcessors(scope, room, trackerProcessorState$);
// Observe mute state changes and update LiveKit microphone/camera states accordingly
this.observeMuteStates(scope);
// Observe media device changes and update LiveKit active devices accordingly
this.observeMediaDevices(scope, devices, controlledAudioDevices);

this.workaroundRestartAudioInputTrackChrome(devices, scope);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this cleaner constructor is also a welcome change

Comment on lines +226 to +229
scope.onEnd(() => {
this.muteStates.audio.unsetHandler();
this.muteStates.video.unsetHandler();
});
Copy link
Member

@robintown robintown Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 86fb026 I moved these calls to unsetHandler to the stop method as scope.onEnd happens much later than what we need for SFU-hopping. I believe they should stay there in stop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, scratch that, we should just give the Connection class a proper ObservableScope of its own that ends at the right time (when CallViewModel is about to drop the Connection object).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can work on that.

Comment on lines +105 to +117
// TODO this can throw errors? It will also prompt for permissions if not already granted
const tracks = await this.livekitRoom.localParticipant.createTracks({
audio: this.muteStates.audio.enabled$.value,
video: this.muteStates.video.enabled$.value,
});
if (this.stopped) return;
for (const track of tracks) {
// TODO: handle errors? Needs the signaling connection to be up, but it has some retries internally
// with a timeout.
await this.livekitRoom.localParticipant.publishTrack(track);
if (this.stopped) return;
// TODO: check if the connection is still active? and break the loop if not?
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should restore this diff from 86fb026:

@@ -130,23 +131,35 @@ export class PublishConnection extends Connection {
     if (!this.stopped) await this.livekitRoom.connect(url, jwt);
 
     if (!this.stopped) {
-      const tracks = await this.livekitRoom.localParticipant.createTracks({
-        audio: this.muteStates.audio.enabled$.value,
-        video: this.muteStates.video.enabled$.value,
-      });
-      for (const track of tracks) {
-        await this.livekitRoom.localParticipant.publishTrack(track);
+      // TODO-MULTI-SFU: Prepublish a microphone track
+      const audio = this.muteStates.audio.enabled$.value;
+      const video = this.muteStates.video.enabled$.value;
+      // createTracks throws if called with audio=false and video=false
+      if (audio || video) {
+        const tracks = await this.livekitRoom.localParticipant.createTracks({
+          audio,
+          video,
+        });
+        for (const track of tracks) {
+          await this.livekitRoom.localParticipant.publishTrack(track);
+        }
       }
     }
   }

(by which I mean just the TODO and the audio || video guard.)

@robintown robintown force-pushed the voip-team/rebased-multiSFU branch from 0fd4143 to de5f519 Compare October 10, 2025 13:52
@BillCarsonFr BillCarsonFr marked this pull request as ready for review October 10, 2025 14:53
@BillCarsonFr BillCarsonFr requested a review from a team as a code owner October 10, 2025 14:53
@BillCarsonFr BillCarsonFr requested review from Half-Shot and removed request for a team October 10, 2025 14:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants