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/webrtc-single-peer-connection-option.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@elevenlabs/client": minor
---

Add an optional `webRtc.singlePeerConnection` session option for WebRTC connections. Set it to `false` to force LiveKit's dual peer connection path, which restores the microphone request timing used before livekit-client began defaulting to the v1 join protocol. Leaving it unset keeps livekit-client's own default.
8 changes: 8 additions & 0 deletions packages/client/src/utils/BaseConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export type BaseSessionConfig = {
* Defaults to "all".
*/
iceTransportPolicy?: "all" | "relay";
/**
* Whether to negotiate over a single peer connection (LiveKit's v1 join
* protocol, which bundles the publisher offer in the JoinRequest). Set to
* false to force the dual peer connection path, e.g. on platforms that
* reject the microphone request at the point v1 issues it. Defaults to
* livekit-client's own default, currently true.
*/
singlePeerConnection?: boolean;
};
overrides?: {
agent?: {
Expand Down
91 changes: 91 additions & 0 deletions packages/client/src/utils/WebRTCConnection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,97 @@ describe("WebRTCConnection", () => {
connection.close();
});

describe("webRtc.singlePeerConnection", () => {
// Options the Room constructor was last called with, if any.
function lastRoomOptions(): { singlePeerConnection?: boolean } | undefined {
return (Room as unknown as ReturnType<typeof vi.fn>).mock.calls.at(
-1
)?.[0];
}

// Returns a room mock whose connect sequence resolves, so create() runs to
// completion and we can inspect how the Room itself was constructed.
function mockConnectingRoom() {
const mockRoom = new Room() as any;
(mockRoom.on as ReturnType<typeof vi.fn>).mockImplementation(
(event: string, callback: () => void) => {
if (event === "connected") {
queueMicrotask(callback);
}
}
);
(mockRoom.once as ReturnType<typeof vi.fn>).mockImplementation(
(event: string, callback: () => void) => {
if (event === "signalConnected") {
queueMicrotask(callback);
}
}
);
return mockRoom;
}

it("forces the dual peer connection path when set to false", async () => {
mockConnectingRoom();

const connection = await WebRTCConnection.create({
conversationToken: "test-token",
connectionType: "webrtc",
webRtc: { singlePeerConnection: false },
});

expect(Room).toHaveBeenLastCalledWith({ singlePeerConnection: false });

connection.close();
});

it("passes the option through when set to true", async () => {
mockConnectingRoom();

const connection = await WebRTCConnection.create({
conversationToken: "test-token",
connectionType: "webrtc",
webRtc: { singlePeerConnection: true },
});

expect(Room).toHaveBeenLastCalledWith({ singlePeerConnection: true });

connection.close();
});

// The back-compat control: an unset option must leave livekit-client's own
// default in place rather than pinning it from here. Asserts that no mode
// was pinned rather than the exact call arity, since `new Room()` and
// `new Room(undefined)` are the same thing to LiveKit.
it("leaves the LiveKit default alone when the option is omitted", async () => {
mockConnectingRoom();

const connection = await WebRTCConnection.create({
conversationToken: "test-token",
connectionType: "webrtc",
});

expect(lastRoomOptions()?.singlePeerConnection).toBeUndefined();

connection.close();
});

// The two webRtc options are independent: setting the sibling must not
// start pinning the peer connection mode.
it("does not pin the peer connection mode when only iceTransportPolicy is set", async () => {
mockConnectingRoom();

const connection = await WebRTCConnection.create({
conversationToken: "test-token",
connectionType: "webrtc",
webRtc: { iceTransportPolicy: "relay" },
});

expect(lastRoomOptions()?.singlePeerConnection).toBeUndefined();

connection.close();
});
});

it("reconnects input analyser after unmuting", async () => {
const mockRoom = new Room() as any;

Expand Down
10 changes: 9 additions & 1 deletion packages/client/src/utils/WebRTCConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,15 @@ export class WebRTCConnection extends BaseConnection {
);
}

const room = new Room();
// livekit-client defaults `singlePeerConnection` to true, which bundles the
// publisher offer in the JoinRequest and moves the microphone request
// earlier in the connection sequence. Callers that need the dual peer
// connection path can opt back into it. Left undefined so the LiveKit
// default applies unless the caller says otherwise.
const singlePeerConnection = config.webRtc?.singlePeerConnection;
const room = new Room(
singlePeerConnection === undefined ? undefined : { singlePeerConnection }
);

try {
// Create connection instance first to set up event listeners
Expand Down