Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preconnect-buffer-publish-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'livekit-client': patch
---

Read `preConnectBuffer` from the publish options passed to `setMicrophoneEnabled` instead of the capture options.
52 changes: 51 additions & 1 deletion src/room/participant/LocalParticipant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PacketTrailerFeature } from '@livekit/protocol';
import { describe, expect, it, vi } from 'vitest';
import type LocalTrack from '../track/LocalTrack';
import { Track } from '../track/Track';
import type { TrackPublishOptions } from '../track/options';
import type { AudioCaptureOptions, TrackPublishOptions } from '../track/options';
import LocalParticipant from './LocalParticipant';

type FrameMetadataTestParticipant = {
Expand Down Expand Up @@ -79,3 +79,53 @@ describe('LocalParticipant frame metadata publish options', () => {
expect(participant.log.warn).toHaveBeenCalledOnce();
});
});

type PreConnectBufferTestParticipant = Pick<LocalParticipant, 'setMicrophoneEnabled'> & {
log: { debug: ReturnType<typeof vi.fn>; info: ReturnType<typeof vi.fn> };
roomOptions: { publishDefaults?: TrackPublishOptions };
pendingPublishing: Set<Track.Source>;
getTrackPublication: ReturnType<typeof vi.fn>;
createTracks: ReturnType<typeof vi.fn>;
publishTrack: ReturnType<typeof vi.fn>;
};

function makeAudioTrackWithBufferSpy() {
const track = makeTrack(Track.Kind.Audio) as LocalTrack & {
startPreConnectBuffer: ReturnType<typeof vi.fn>;
};
track.startPreConnectBuffer = vi.fn();
return track;
}

function makePreConnectParticipant(track: LocalTrack) {
const participant = Object.create(LocalParticipant.prototype) as PreConnectBufferTestParticipant;
participant.log = { debug: vi.fn(), info: vi.fn() };
participant.roomOptions = { publishDefaults: {} };
participant.pendingPublishing = new Set();
participant.getTrackPublication = vi.fn().mockReturnValue(undefined);
participant.createTracks = vi.fn().mockResolvedValue([track]);
participant.publishTrack = vi.fn().mockResolvedValue(undefined);
return participant;
}

describe('LocalParticipant preconnect buffer', () => {
it('starts the preconnect buffer when preConnectBuffer is set in the publish options', async () => {
const track = makeAudioTrackWithBufferSpy();
const participant = makePreConnectParticipant(track);

await participant.setMicrophoneEnabled(true, undefined, { preConnectBuffer: true });

expect(track.startPreConnectBuffer).toHaveBeenCalledOnce();
});

it('does not start the preconnect buffer when preConnectBuffer is set in the capture options', async () => {
const track = makeAudioTrackWithBufferSpy();
const participant = makePreConnectParticipant(track);

await participant.setMicrophoneEnabled(true, {
preConnectBuffer: true,
} as AudioCaptureOptions);

expect(track.startPreConnectBuffer).not.toHaveBeenCalled();
});
});
8 changes: 4 additions & 4 deletions src/room/participant/LocalParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,11 +583,11 @@ export default class LocalParticipant extends Participant {
throw e;
}

const opts: TrackPublishOptions = {
...this.roomOptions.publishDefaults,
...publishOptions,
};
for (const localTrack of localTracks) {
const opts: TrackPublishOptions = {
...this.roomOptions.publishDefaults,
...options,
};
if (
Comment on lines +586 to 591

@1egoman 1egoman Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

thought: Would it be worth a preConnectBuffer: never in publishOptions options or similar to ensure that a user doesn't pass options.publishDefaults by accident anymore? My understanding is that while this is a breaking change, it's actually a fix for a regression here so it's probably worth it.

I suppose another option could be to add publishDefaults in both places but I suspect that's probably more trouble than it is worth.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

preConnectBuffer: never in publishOptions

not sure I fully understand the proposal. preConnectBuffer is a field on TrackPublishOptions and it should stay that way.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What I am referring to is that in the old version, options was being merged into the object:

const opts: TrackPublishOptions = {
  ...this.roomOptions.publishDefaults,
  ...options,
};

But in the new version, publishOptions is instead:

const opts: TrackPublishOptions = {
  ...this.roomOptions.publishDefaults,
  ...publishOptions,
};

(I am assuming this switch from options -> publishOptions was intentional, maybe this was a mistake and that's where the confusion is coming from?)

Seeing this made me wonder if there should be something added to options (like my proposed preConnectBuffer: never) to ensure that a user doesn't pass that parameter in options by accident, given that is how it previously was (incorrectly) working? But maybe it doesn't matter if preConnectBuffer passed to options would have been exposed as a type error to a user anyway.

source === Track.Source.Microphone &&
isAudioTrack(localTrack) &&
Expand Down
Loading