Skip to content

Conversation

@BillCarsonFr
Copy link
Member

Problem:

Livekit's LocalParticipant kind of offer two different levels of API.

  • A Low-Level one: With createTracks and publishTrack, then accessing directly the tracks to call mute/unmute etc..
  • A High-Level one: With enableCameraAndMicrophone/setCameraEnabled/setMicrophoneEnabled. These one will automatically creates the tracks, manage them, remember if there is any pending publications. For example calling setCameraEnabled(false) will check if there is already a track or if there is a pending publication and waits for it to complete.

Our Publisher.ts wants control on track creation and publication, so it is using the Low-Level APIs to createTracks.
But it is also using the High-Level ones in the mutestates handler with setCameraEnabled/setMicrophoneEnabled.
This mixed usage of API levels cause problems. For example the Publisher will create a track, but meanwhile the mutestate will call setCameraEnabled(true), the high level is not aware of the tracks that the Publisher have created so it will create a new one. Then later the Publisher will publish the one it manages.
This can be observed in rage-shake as per this log publishing a second track with the same source:camera

Proposal

We have to stop mixing high-level and low-level.
This PR is an attempt at using only the High-Level API.
It is sometimes a bit odd because the Publisher want to have more control than what the API allows (like create track and wait to publish)
We could eventually create an alternative PR to use the low-level API, but this will then require us to re-built a proper track management system (like tracking pending publications)

@BillCarsonFr BillCarsonFr added the PR-Task Release note category. A PR that is hidden from release note. label Dec 12, 2025
* This starts audio and video tracks. They will be reused when calling `requestPublish`.
*/
startTracks: () => Behavior<LocalTrack[]>;
startTracks: () => Behavior<void>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does it return in the first place if it is a void behavior?
Would a void promise or just a void return make more sense?

Comment on lines 278 to 281
const startTracks = (): Behavior<void> => {
trackStartRequested.resolve();
return tracks$;
return constant(undefined);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes lets remove the return type entirely or add a comment that this should return tracks$

Comment on lines 351 to 358
// XXX Why is that?
// else {
// try {
// await publisher?.stopPublishing();
// } catch (error) {
// setLivekitError(new UnknownCallError(error as Error));
// }
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

The commented section now is redundant with else if (isPublishing)
But the whole stopPublishing block is for when shouldJoinAndPublish becomes false. (I think we dont need this case now) but i would expect, that setting the subject to false stops the publications.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thx I had questions about that. Like do we really want to stop / restart publishing? or even start/stop tracks.

Or is it more that we need a destroy publisher that should clean itself?

Comment on lines 410 to 413
// const hasTracks = tracks.length > 0;
// let trackState: TrackState = TrackState.WaitingForUser;
// if (hasTracks && shouldStartTracks) trackState = TrackState.Ready;
// if (!hasTracks && shouldStartTracks) trackState = TrackState.Creating;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this also can get a generic comment.

But I prefer just removing the core entirely. Maybe linking this PR instead as reference.

Comment on lines 144 to 154
const enableTracks = async (): Promise<void> => {
if (audio && video) {
// Enable both at once in order to have a single permission prompt!
await lkRoom.localParticipant.enableCameraAndMicrophone();
} else if (audio) {
await lkRoom.localParticipant.setMicrophoneEnabled(true);
} else if (video) {
await lkRoom.localParticipant.setCameraEnabled(true);
}
return;
};
Copy link
Contributor

Choose a reason for hiding this comment

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

    const enableTracks = ():void => {
      if (audio && video) {
        // Enable both at once in order to have a single permission prompt!
        void lkRoom.localParticipant.enableCameraAndMicrophone();
      } else if (audio) {
        void lkRoom.localParticipant.setMicrophoneEnabled(true);
      } else if (video) {
        void lkRoom.localParticipant.setCameraEnabled(true);
      }
    };

We can just explicitly void the promises.

Copy link
Member Author

Choose a reason for hiding this comment

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

right there used to have a .then() block on the enable track but was removed here 610af43
I guess I can just voids

@BillCarsonFr
Copy link
Member Author

@toger5 I have addressed your comments and they were not blocking so I am merging now as you are OOO and I'd like it to sit a bit on dev.

@BillCarsonFr BillCarsonFr merged commit 6cfa95b into livekit Dec 17, 2025
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR-Task Release note category. A PR that is hidden from release note.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants