Skip to content

Commit 68c8376

Browse files
lukasIOboks1971
andauthored
Single peer connection support (#1682)
Co-authored-by: boks1971 <raja.gobi@tutanota.com>
1 parent 4b432e5 commit 68c8376

File tree

15 files changed

+190
-67
lines changed

15 files changed

+190
-67
lines changed

.changeset/yellow-birds-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
Single peer connection support

examples/demo/demo.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ const appActions = {
131131
encryption: e2eeEnabled
132132
? { keyProvider: state.e2eeKeyProvider, worker: new E2EEWorker() }
133133
: undefined,
134+
singlePeerConnection: true,
134135
};
135136
if (
136137
roomOpts.publishDefaults?.videoCodec === 'av1' ||
@@ -826,16 +827,16 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
826827
};
827828
}
828829
const videoElm = <HTMLVideoElement>container.querySelector(`#video-${identity}`);
829-
const audioELm = <HTMLAudioElement>container.querySelector(`#audio-${identity}`);
830+
const audioElm = <HTMLAudioElement>container.querySelector(`#audio-${identity}`);
830831
if (remove) {
831832
div?.remove();
832833
if (videoElm) {
833834
videoElm.srcObject = null;
834835
videoElm.src = '';
835836
}
836-
if (audioELm) {
837-
audioELm.srcObject = null;
838-
audioELm.src = '';
837+
if (audioElm) {
838+
audioElm.srcObject = null;
839+
audioElm.src = '';
839840
}
840841
return;
841842
}
@@ -899,13 +900,13 @@ function renderParticipant(participant: Participant, remove: boolean = false) {
899900
if (micEnabled) {
900901
if (!isLocalParticipant(participant)) {
901902
// don't attach local audio
902-
audioELm.onloadeddata = () => {
903+
audioElm.onloadeddata = () => {
903904
if (participant.joinedAt && participant.joinedAt.getTime() < startTime) {
904905
const fromJoin = Date.now() - startTime;
905906
appendLog(`RemoteAudioTrack ${micPub?.trackSid} played ${fromJoin}ms from start`);
906907
}
907908
};
908-
micPub?.audioTrack?.attach(audioELm);
909+
micPub?.audioTrack?.attach(audioElm);
909910
}
910911
micElm.className = 'mic-on';
911912
micElm.innerHTML = '<i class="fas fa-microphone"></i>';

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,5 @@
108108
"vite": "7.1.8",
109109
"vitest": "^3.0.0"
110110
},
111-
"packageManager": "pnpm@10.14.0"
111+
"packageManager": "pnpm@10.15.0"
112112
}

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/api/SignalClient.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import {
44
AudioTrackFeature,
55
ClientInfo,
66
ConnectionQualityUpdate,
7+
ConnectionSettings,
78
DisconnectReason,
9+
JoinRequest,
810
JoinResponse,
911
LeaveRequest,
1012
LeaveRequest_Action,
13+
MediaSectionsRequirement,
1114
MuteTrackRequest,
1215
ParticipantInfo,
1316
Ping,
@@ -38,6 +41,7 @@ import {
3841
UpdateTrackSettings,
3942
UpdateVideoLayers,
4043
VideoLayer,
44+
WrappedJoinRequest,
4145
protoInt64,
4246
} from '@livekit/protocol';
4347
import log, { LoggerNames, getLogger } from '../logger';
@@ -66,6 +70,7 @@ export interface SignalOptions {
6670
maxRetries: number;
6771
e2eeEnabled: boolean;
6872
websocketTimeout: number;
73+
singlePeerConnection: boolean;
6974
}
7075

7176
type SignalMessage = SignalRequest['message'];
@@ -155,6 +160,8 @@ export class SignalClient {
155160

156161
onRoomMoved?: (res: RoomMovedResponse) => void;
157162

163+
onMediaSectionsRequirement?: (requirement: MediaSectionsRequirement) => void;
164+
158165
connectOptions?: ConnectOpts;
159166

160167
ws?: WebSocketStream;
@@ -271,7 +278,9 @@ export class SignalClient {
271278

272279
this.connectOptions = opts;
273280
const clientInfo = getClientInfo();
274-
const params = createConnectionParams(token, clientInfo, opts);
281+
const params = opts.singlePeerConnection
282+
? createJoinRequestConnectionParams(token, clientInfo, opts)
283+
: createConnectionParams(token, clientInfo, opts);
275284
const rtcUrl = createRtcUrl(url, params);
276285
const validateUrl = createValidateUrl(rtcUrl);
277286

@@ -459,6 +468,7 @@ export class SignalClient {
459468
this.onTokenRefresh = undefined;
460469
this.onTrickle = undefined;
461470
this.onClose = undefined;
471+
this.onMediaSectionsRequirement = undefined;
462472
};
463473

464474
async close(updateState: boolean = true) {
@@ -772,6 +782,10 @@ export class SignalClient {
772782
if (this.onRoomMoved) {
773783
this.onRoomMoved(msg.value);
774784
}
785+
} else if (msg.case === 'mediaSectionsRequirement') {
786+
if (this.onMediaSectionsRequirement) {
787+
this.onMediaSectionsRequirement(msg.value);
788+
}
775789
} else {
776790
this.log.debug('unsupported message', { ...this.logContext, msgCase: msg.case });
777791
}
@@ -1063,3 +1077,31 @@ function createConnectionParams(
10631077

10641078
return params;
10651079
}
1080+
1081+
function createJoinRequestConnectionParams(
1082+
token: string,
1083+
info: ClientInfo,
1084+
opts: ConnectOpts,
1085+
): URLSearchParams {
1086+
const params = new URLSearchParams();
1087+
params.set('access_token', token);
1088+
1089+
const joinRequest = new JoinRequest({
1090+
clientInfo: info,
1091+
connectionSettings: new ConnectionSettings({
1092+
autoSubscribe: !!opts.autoSubscribe,
1093+
adaptiveStream: !!opts.adaptiveStream,
1094+
}),
1095+
reconnect: !!opts.reconnect,
1096+
participantSid: opts.sid ? opts.sid : undefined,
1097+
});
1098+
if (opts.reconnectReason) {
1099+
joinRequest.reconnectReason = opts.reconnectReason;
1100+
}
1101+
const wrappedJoinRequest = new WrappedJoinRequest({
1102+
joinRequest: joinRequest.toBinary(),
1103+
});
1104+
params.set('join_request', btoa(new TextDecoder('utf-8').decode(wrappedJoinRequest.toBinary())));
1105+
1106+
return params;
1107+
}

src/connectionHelper/checks/turn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class TURNCheck extends Checker {
1313
maxRetries: 0,
1414
e2eeEnabled: false,
1515
websocketTimeout: 15_000,
16+
singlePeerConnection: false,
1617
});
1718

1819
let hasTLS = false;

src/connectionHelper/checks/webrtc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class WebRTCCheck extends Checker {
3838
}
3939
};
4040

41-
if (this.room.engine.pcManager) {
41+
if (this.room.engine.pcManager?.subscriber) {
4242
this.room.engine.pcManager.subscriber.onIceCandidateError = (ev) => {
4343
if (ev instanceof RTCPeerConnectionIceErrorEvent) {
4444
this.appendWarning(

src/connectionHelper/checks/websocket.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class WebSocketCheck extends Checker {
1818
maxRetries: 0,
1919
e2eeEnabled: false,
2020
websocketTimeout: 15_000,
21+
singlePeerConnection: false,
2122
});
2223
this.appendMessage(`Connected to server, version ${joinRes.serverVersion}.`);
2324
if (joinRes.serverInfo?.edition === ServerInfo_Edition.Cloud && joinRes.serverInfo?.region) {

src/options.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ export interface InternalRoomOptions {
9999
encryption?: E2EEOptions;
100100

101101
loggerName?: string;
102+
103+
/**
104+
* @experimental
105+
* only supported on LiveKit Cloud
106+
* and LiveKit OSS >= 1.9.2
107+
*/
108+
singlePeerConnection: boolean;
102109
}
103110

104111
/**

src/room/PCTransport.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export default class PCTransport extends EventEmitter {
167167
sdpParsed.media.forEach((media) => {
168168
const mid = getMidString(media.mid!);
169169
if (media.type === 'audio') {
170-
// mung sdp for opus bitrate settings
170+
// munge sdp for opus bitrate settings
171171
this.trackBitrates.some((trackbr): boolean => {
172172
if (!trackbr.transceiver || mid != trackbr.transceiver.mid) {
173173
return false;
@@ -297,7 +297,7 @@ export default class PCTransport extends EventEmitter {
297297
sdpParsed.media.forEach((media) => {
298298
ensureIPAddrMatchVersion(media);
299299
if (media.type === 'audio') {
300-
ensureAudioNackAndStereo(media, [], []);
300+
ensureAudioNackAndStereo(media, ['all'], []);
301301
} else if (media.type === 'video') {
302302
this.trackBitrates.some((trackbr): boolean => {
303303
if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
@@ -380,6 +380,10 @@ export default class PCTransport extends EventEmitter {
380380
return this.pc.addTransceiver(mediaStreamTrack, transceiverInit);
381381
}
382382

383+
addTransceiverOfKind(kind: 'audio' | 'video', transceiverInit: RTCRtpTransceiverInit) {
384+
return this.pc.addTransceiver(kind, transceiverInit);
385+
}
386+
383387
addTrack(track: MediaStreamTrack) {
384388
if (!this._pc) {
385389
throw new UnexpectedConnectionState('PC closed, cannot add track');
@@ -623,7 +627,7 @@ function ensureAudioNackAndStereo(
623627
});
624628
}
625629

626-
if (stereoMids.includes(mid)) {
630+
if (stereoMids.includes(mid) || (stereoMids.length === 1 && stereoMids[0] === 'all')) {
627631
media.fmtp.some((fmtp): boolean => {
628632
if (fmtp.payload === opusPayload) {
629633
if (!fmtp.config.includes('stereo=1')) {

0 commit comments

Comments
 (0)