livekit-client 2.20.0 → 2.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +41 -11
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +744 -178
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +41 -7
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +1 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +1 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -1
- package/dist/src/frameMetadata/types.d.ts +1 -0
- package/dist/src/frameMetadata/types.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +42 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +46 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +1 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +1 -0
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/record.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +19 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -0
- package/dist/ts4.2/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/e2ee/utils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/types.d.ts +1 -0
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +42 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +46 -4
- package/dist/ts4.2/room/Room.d.ts +1 -0
- package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/utils.d.ts +1 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +19 -0
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +35 -2
- package/src/e2ee/E2eeManager.ts +3 -1
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/utils.ts +1 -0
- package/src/e2ee/worker/DataCryptor.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +1 -0
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
- package/src/e2ee/worker/e2ee.worker.ts +1 -0
- package/src/e2ee/worker/naluUtils.ts +1 -0
- package/src/e2ee/worker/sifPayload.ts +1 -0
- package/src/frameMetadata/frameMetadata.test.ts +83 -0
- package/src/frameMetadata/frameMetadata.ts +8 -0
- package/src/frameMetadata/types.ts +1 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.test.ts +150 -0
- package/src/room/PCTransport.ts +179 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +275 -2
- package/src/room/RTCEngine.ts +277 -71
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.ts +8 -1
- package/src/room/data-track/depacketizer.ts +1 -0
- package/src/room/data-track/frame.ts +1 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/data-track/outgoing/types.ts +1 -0
- package/src/room/data-track/packet/extensions.ts +1 -0
- package/src/room/data-track/packet/index.ts +1 -0
- package/src/room/data-track/packet/serializable.ts +1 -0
- package/src/room/errors.ts +4 -4
- package/src/room/participant/LocalParticipant.ts +17 -6
- package/src/room/participant/Participant.ts +6 -0
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +11 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/track/record.ts +1 -0
- package/src/room/utils.ts +1 -0
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +48 -1
package/src/room/PCTransport.ts
CHANGED
|
@@ -15,15 +15,24 @@ interface TrackBitrateInfo {
|
|
|
15
15
|
transceiver?: RTCRtpTransceiver;
|
|
16
16
|
codec: string;
|
|
17
17
|
maxbr: number;
|
|
18
|
+
isScreenShare?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
/*
|
|
22
|
+
* Video codecs use a very low bitrate at the beginning and increase slowly by
|
|
23
|
+
* the bandwidth estimator until they reach the target bitrate. The process commonly
|
|
24
|
+
* costs more than 10 seconds causing subscribers to get blurry video at the first
|
|
25
|
+
* few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
|
|
26
|
+
*
|
|
27
|
+
* Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
|
|
28
|
+
* Why same for all codecs: Target bitrate already accounts for codec efficiency
|
|
29
|
+
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
|
|
30
|
+
* Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
|
|
31
|
+
*/
|
|
32
|
+
const startBitrateMultiplier = 0.9;
|
|
33
|
+
|
|
34
|
+
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
35
|
+
const maxStartBitrateKbps = 1000;
|
|
27
36
|
|
|
28
37
|
const debounceInterval = 20;
|
|
29
38
|
|
|
@@ -53,6 +62,8 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
53
62
|
|
|
54
63
|
private log = log;
|
|
55
64
|
|
|
65
|
+
private iceLog = log;
|
|
66
|
+
|
|
56
67
|
private loggerOptions: LoggerOptions;
|
|
57
68
|
|
|
58
69
|
private ddExtID = 0;
|
|
@@ -95,8 +106,12 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
95
106
|
|
|
96
107
|
constructor(config?: RTCConfiguration, loggerOptions: LoggerOptions = {}) {
|
|
97
108
|
super();
|
|
98
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCTransport);
|
|
99
109
|
this.loggerOptions = loggerOptions;
|
|
110
|
+
this.log = getLogger(
|
|
111
|
+
loggerOptions.loggerName ?? LoggerNames.PCTransport,
|
|
112
|
+
() => this.logContext,
|
|
113
|
+
);
|
|
114
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
100
115
|
this.config = config;
|
|
101
116
|
this._pc = this.createPC();
|
|
102
117
|
this.offerLock = new Mutex();
|
|
@@ -107,24 +122,33 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
107
122
|
|
|
108
123
|
pc.onicecandidate = (ev) => {
|
|
109
124
|
if (!ev.candidate) return;
|
|
125
|
+
this.iceLog.debug('local ICE candidate gathered', { candidate: ev.candidate.candidate });
|
|
110
126
|
this.onIceCandidate?.(ev.candidate);
|
|
111
127
|
};
|
|
112
128
|
pc.onicecandidateerror = (ev) => {
|
|
129
|
+
this.iceLog.debug('ICE candidate error', { event: ev });
|
|
113
130
|
this.onIceCandidateError?.(ev);
|
|
114
131
|
};
|
|
115
132
|
|
|
116
133
|
pc.oniceconnectionstatechange = () => {
|
|
134
|
+
this.iceLog.debug(`ICE connection state: ${pc.iceConnectionState}`);
|
|
117
135
|
this.onIceConnectionStateChange?.(pc.iceConnectionState);
|
|
118
136
|
};
|
|
119
137
|
|
|
120
138
|
pc.onsignalingstatechange = () => {
|
|
139
|
+
this.log.debug(`signaling state: ${pc.signalingState}`);
|
|
121
140
|
this.onSignalingStatechange?.(pc.signalingState);
|
|
122
141
|
};
|
|
123
142
|
|
|
124
143
|
pc.onconnectionstatechange = () => {
|
|
144
|
+
this.log.debug(`connection state: ${pc.connectionState}`);
|
|
125
145
|
this.onConnectionStateChange?.(pc.connectionState);
|
|
126
146
|
};
|
|
127
147
|
pc.ondatachannel = (ev) => {
|
|
148
|
+
this.log.debug('data channel opened by peer', {
|
|
149
|
+
label: ev.channel.label,
|
|
150
|
+
id: ev.channel.id,
|
|
151
|
+
});
|
|
128
152
|
this.onDataChannel?.(ev);
|
|
129
153
|
};
|
|
130
154
|
pc.ontrack = (ev) => {
|
|
@@ -150,6 +174,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
150
174
|
if (this.pc.remoteDescription && !this.restartingIce) {
|
|
151
175
|
return this.pc.addIceCandidate(candidate);
|
|
152
176
|
}
|
|
177
|
+
this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
|
|
178
|
+
pendingCount: this.pendingCandidates.length + 1,
|
|
179
|
+
});
|
|
153
180
|
this.pendingCandidates.push(candidate);
|
|
154
181
|
}
|
|
155
182
|
|
|
@@ -161,7 +188,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
161
188
|
offerId !== this.latestOfferId
|
|
162
189
|
) {
|
|
163
190
|
this.log.warn('ignoring answer for old offer', {
|
|
164
|
-
...this.logContext,
|
|
165
191
|
offerId,
|
|
166
192
|
latestOfferId: this.latestOfferId,
|
|
167
193
|
});
|
|
@@ -180,7 +206,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
180
206
|
sdpParsed.media.forEach((media) => {
|
|
181
207
|
ensureIPAddrMatchVersion(media);
|
|
182
208
|
});
|
|
183
|
-
this.log.debug('setting pending initial offer before processing answer'
|
|
209
|
+
this.log.debug('setting pending initial offer before processing answer');
|
|
184
210
|
await this.setMungedSDP(initialOffer, write(sdpParsed));
|
|
185
211
|
}
|
|
186
212
|
const sdpParsed = parse(sd.sdp ?? '');
|
|
@@ -234,10 +260,26 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
234
260
|
});
|
|
235
261
|
}
|
|
236
262
|
});
|
|
263
|
+
// The server's answer sets per-track codec params (e.g. opus `usedtx`) on
|
|
264
|
+
// the sections mapped to a published track, but leaves the pre-populated
|
|
265
|
+
// recvonly placeholder sections with defaults. Conform the placeholders so
|
|
266
|
+
// each shared payload type is consistent across the bundle, otherwise
|
|
267
|
+
// libwebrtc flags a "bundled payload type collision".
|
|
268
|
+
const placeholderMids = this.getPlaceholderMids();
|
|
269
|
+
if (placeholderMids.size > 0) {
|
|
270
|
+
conformBundledCodecFmtp(sdpParsed.media, (media) =>
|
|
271
|
+
placeholderMids.has(getMidString(media.mid!)),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
237
274
|
mungedSDP = write(sdpParsed);
|
|
238
275
|
}
|
|
239
276
|
await this.setMungedSDP(sd, mungedSDP, true);
|
|
240
277
|
|
|
278
|
+
if (this.pendingCandidates.length > 0) {
|
|
279
|
+
this.iceLog.debug('flushing queued ICE candidates', {
|
|
280
|
+
count: this.pendingCandidates.length,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
241
283
|
this.pendingCandidates.forEach((candidate) => {
|
|
242
284
|
this.pc.addIceCandidate(candidate);
|
|
243
285
|
});
|
|
@@ -287,10 +329,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
287
329
|
const unlock = await this.offerLock.lock();
|
|
288
330
|
try {
|
|
289
331
|
if (this.pc.signalingState !== 'stable') {
|
|
290
|
-
this.log.warn(
|
|
291
|
-
'signaling state is not stable, cannot create initial offer',
|
|
292
|
-
this.logContext,
|
|
293
|
-
);
|
|
332
|
+
this.log.warn('signaling state is not stable, cannot create initial offer');
|
|
294
333
|
return;
|
|
295
334
|
}
|
|
296
335
|
const offerId = this.latestOfferId + 1;
|
|
@@ -317,7 +356,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
317
356
|
}
|
|
318
357
|
|
|
319
358
|
if (options?.iceRestart) {
|
|
320
|
-
this.
|
|
359
|
+
this.iceLog.debug('restarting ICE');
|
|
321
360
|
this.restartingIce = true;
|
|
322
361
|
}
|
|
323
362
|
|
|
@@ -334,21 +373,21 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
334
373
|
await this._pc.setRemoteDescription(currentSD);
|
|
335
374
|
} else {
|
|
336
375
|
this.renegotiate = true;
|
|
337
|
-
this.log.debug('requesting renegotiation'
|
|
376
|
+
this.log.debug('requesting renegotiation');
|
|
338
377
|
return;
|
|
339
378
|
}
|
|
340
379
|
} else if (!this._pc || this._pc.signalingState === 'closed') {
|
|
341
|
-
this.log.warn('could not createOffer with closed peer connection'
|
|
380
|
+
this.log.warn('could not createOffer with closed peer connection');
|
|
342
381
|
return;
|
|
343
382
|
}
|
|
344
383
|
|
|
345
384
|
// actually negotiate
|
|
346
|
-
this.log.debug('starting to negotiate'
|
|
385
|
+
this.log.debug('starting to negotiate');
|
|
347
386
|
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
|
348
387
|
const offerId = this.latestOfferId + 1;
|
|
349
388
|
this.latestOfferId = offerId;
|
|
350
389
|
const offer = await this.pc.createOffer(options);
|
|
351
|
-
this.log.debug('original offer', { sdp: offer.sdp
|
|
390
|
+
this.log.debug('original offer', { sdp: offer.sdp });
|
|
352
391
|
|
|
353
392
|
const sdpParsed = parse(offer.sdp ?? '');
|
|
354
393
|
sdpParsed.media.forEach((media) => {
|
|
@@ -379,14 +418,18 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
379
418
|
}
|
|
380
419
|
|
|
381
420
|
// mung sdp for bitrate setting that can't apply by sendEncoding
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const startBitrate =
|
|
421
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
|
|
422
|
+
// Screen share is not capped since text/UI clarity requires high bitrate from the start
|
|
423
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
|
|
424
|
+
const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
425
|
+
const startBitrate = trackbr.isScreenShare
|
|
426
|
+
? calculatedStartBitrate
|
|
427
|
+
: Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
387
428
|
|
|
429
|
+
let fmtpFound = false;
|
|
388
430
|
for (const fmtp of media.fmtp) {
|
|
389
431
|
if (fmtp.payload === codecPayload) {
|
|
432
|
+
fmtpFound = true;
|
|
390
433
|
// if another track's fmtp already is set, we cannot override the bitrate
|
|
391
434
|
// this has the unfortunate consequence of being forced to use the
|
|
392
435
|
// initial track's bitrate for all tracks
|
|
@@ -396,13 +439,30 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
396
439
|
break;
|
|
397
440
|
}
|
|
398
441
|
}
|
|
442
|
+
// VP8 and some codecs may not have an existing fmtp line - create one
|
|
443
|
+
if (!fmtpFound) {
|
|
444
|
+
media.fmtp.push({
|
|
445
|
+
payload: codecPayload,
|
|
446
|
+
config: `x-google-start-bitrate=${startBitrate}`,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
399
449
|
return true;
|
|
400
450
|
});
|
|
401
451
|
}
|
|
402
452
|
});
|
|
453
|
+
// Conform the placeholder sections (pre-populated, or reverted after an
|
|
454
|
+
// unpublish) so every shared payload type carries identical fmtp across the
|
|
455
|
+
// bundle, otherwise libwebrtc flags a "bundled payload type collision".
|
|
456
|
+
// Detection is by transceiver (mids are stable across renegotiations) since
|
|
457
|
+
// an unpublished section keeps its `a=msid`.
|
|
458
|
+
const placeholderMids = this.getPlaceholderMids();
|
|
459
|
+
if (placeholderMids.size > 0) {
|
|
460
|
+
conformBundledCodecFmtp(sdpParsed.media, (media) =>
|
|
461
|
+
placeholderMids.has(getMidString(media.mid!)),
|
|
462
|
+
);
|
|
463
|
+
}
|
|
403
464
|
if (this.latestOfferId > offerId) {
|
|
404
465
|
this.log.warn('latestOfferId mismatch', {
|
|
405
|
-
...this.logContext,
|
|
406
466
|
latestOfferId: this.latestOfferId,
|
|
407
467
|
offerId,
|
|
408
468
|
});
|
|
@@ -428,6 +488,17 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
428
488
|
return answer;
|
|
429
489
|
}
|
|
430
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Returns the mids of transceivers that carry no outgoing track on this
|
|
493
|
+
* (publisher) connection: the pre-populated placeholders added by
|
|
494
|
+
* `RTCEngine.applyInitialPublisherLayout`, plus any transceiver that was used
|
|
495
|
+
* for a track and reverted on unpublish. Their codec fmtp is conformed to the
|
|
496
|
+
* published tracks so a shared payload type stays consistent across the bundle.
|
|
497
|
+
*/
|
|
498
|
+
private getPlaceholderMids(): Set<string> {
|
|
499
|
+
return placeholderMidsFromTransceivers(this._pc?.getTransceivers() ?? []);
|
|
500
|
+
}
|
|
501
|
+
|
|
431
502
|
createDataChannel(label: string, dataChannelDict: RTCDataChannelInit) {
|
|
432
503
|
return this.pc.createDataChannel(label, dataChannelDict);
|
|
433
504
|
}
|
|
@@ -543,6 +614,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
543
614
|
if (!this._pc) {
|
|
544
615
|
return;
|
|
545
616
|
}
|
|
617
|
+
this.log.debug('closing peer connection');
|
|
546
618
|
this.pendingInitialOffer = undefined;
|
|
547
619
|
this._pc.close();
|
|
548
620
|
this._pc.onconnectionstatechange = null;
|
|
@@ -564,10 +636,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
564
636
|
if (munged) {
|
|
565
637
|
sd.sdp = munged;
|
|
566
638
|
try {
|
|
567
|
-
this.log.debug(
|
|
568
|
-
`setting munged ${remote ? 'remote' : 'local'} description`,
|
|
569
|
-
this.logContext,
|
|
570
|
-
);
|
|
639
|
+
this.log.debug(`setting munged ${remote ? 'remote' : 'local'} description`);
|
|
571
640
|
if (remote) {
|
|
572
641
|
await this.pc.setRemoteDescription(sd);
|
|
573
642
|
} else {
|
|
@@ -576,7 +645,6 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
576
645
|
return;
|
|
577
646
|
} catch (e) {
|
|
578
647
|
this.log.warn(`not able to set ${sd.type}, falling back to unmodified sdp`, {
|
|
579
|
-
...this.logContext,
|
|
580
648
|
error: e,
|
|
581
649
|
mungedSdp: munged,
|
|
582
650
|
originalSdp,
|
|
@@ -609,7 +677,7 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
609
677
|
if (!remote && this.pc.remoteDescription) {
|
|
610
678
|
fields.remoteSdp = this.pc.remoteDescription;
|
|
611
679
|
}
|
|
612
|
-
this.log.error(`unable to set ${sd.type}`, {
|
|
680
|
+
this.log.error(`unable to set ${sd.type}`, { fields });
|
|
613
681
|
throw new NegotiationError(msg);
|
|
614
682
|
}
|
|
615
683
|
}
|
|
@@ -703,6 +771,86 @@ function ensureAudioNackAndStereo(
|
|
|
703
771
|
}
|
|
704
772
|
}
|
|
705
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Returns the mids of transceivers that carry no outgoing track: the
|
|
776
|
+
* pre-populated placeholders added by `RTCEngine.applyInitialPublisherLayout`,
|
|
777
|
+
* plus any transceiver that was used for a track and reverted on unpublish. The
|
|
778
|
+
* `sender.track` check is the reliable signal — an unpublished section keeps its
|
|
779
|
+
* `a=msid` (and its stale send-derived fmtp), so it can't be told apart from a
|
|
780
|
+
* real send by SDP alone. Transceiver mids are stable across renegotiations, so
|
|
781
|
+
* this works for every offer/answer after the first.
|
|
782
|
+
* @internal
|
|
783
|
+
*/
|
|
784
|
+
export function placeholderMidsFromTransceivers(
|
|
785
|
+
transceivers: readonly RTCRtpTransceiver[],
|
|
786
|
+
): Set<string> {
|
|
787
|
+
const mids = new Set<string>();
|
|
788
|
+
for (const transceiver of transceivers) {
|
|
789
|
+
if (transceiver.mid && !transceiver.sender.track) {
|
|
790
|
+
mids.add(transceiver.mid);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return mids;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Within a BUNDLE group a payload type must map to identical codec parameters
|
|
798
|
+
* across every m-line. When the same payload type carries different fmtp between
|
|
799
|
+
* sections — e.g. opus `usedtx=1` on the published microphone but not on the
|
|
800
|
+
* pre-populated recvonly placeholders, or H.265 with different `level-id` between
|
|
801
|
+
* a published video track and a placeholder — libwebrtc flags a "bundled payload
|
|
802
|
+
* type collision".
|
|
803
|
+
*
|
|
804
|
+
* Rewrite the placeholder sections so every shared payload type carries the
|
|
805
|
+
* same fmtp. Real (non-placeholder) sections always win the canonical value, so
|
|
806
|
+
* a published track's encoder parameters are never altered. When no real
|
|
807
|
+
* section declares a payload type — e.g. a placeholder that was reused for a
|
|
808
|
+
* track and then reverted to recvonly keeps its send-derived `level-id` while
|
|
809
|
+
* fresh placeholders use the default — the placeholders still converge on the
|
|
810
|
+
* first value seen, so two placeholders can't disagree either. Only placeholder
|
|
811
|
+
* sections are ever rewritten, and it is codec-agnostic (opus, H.265, ...).
|
|
812
|
+
* `isPlaceholder` identifies the sections to conform.
|
|
813
|
+
* @internal
|
|
814
|
+
*/
|
|
815
|
+
export function conformBundledCodecFmtp(
|
|
816
|
+
media: MediaDescription[],
|
|
817
|
+
isPlaceholder: (media: MediaDescription) => boolean,
|
|
818
|
+
) {
|
|
819
|
+
// Canonical fmtp per payload type. Payload types are unique within a BUNDLE
|
|
820
|
+
// group, so keying by payload alone (across audio and video) is safe. A real
|
|
821
|
+
// section's value always takes precedence; otherwise the first placeholder
|
|
822
|
+
// value seen is used so divergent placeholders still converge.
|
|
823
|
+
const canonicalByPayload = new Map<number, string>();
|
|
824
|
+
const fromRealSection = new Set<number>();
|
|
825
|
+
for (const m of media) {
|
|
826
|
+
const placeholder = isPlaceholder(m);
|
|
827
|
+
for (const fmtp of m.fmtp ?? []) {
|
|
828
|
+
if (!placeholder) {
|
|
829
|
+
canonicalByPayload.set(fmtp.payload, fmtp.config);
|
|
830
|
+
fromRealSection.add(fmtp.payload);
|
|
831
|
+
} else if (!canonicalByPayload.has(fmtp.payload)) {
|
|
832
|
+
canonicalByPayload.set(fmtp.payload, fmtp.config);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (canonicalByPayload.size === 0) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Conform placeholder sections to the canonical fmtp for each shared payload.
|
|
841
|
+
for (const m of media) {
|
|
842
|
+
if (!isPlaceholder(m)) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
for (const fmtp of m.fmtp ?? []) {
|
|
846
|
+
const config = canonicalByPayload.get(fmtp.payload);
|
|
847
|
+
if (config !== undefined && fmtp.config !== config) {
|
|
848
|
+
fmtp.config = config;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
706
854
|
function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
707
855
|
stereoMids: string[];
|
|
708
856
|
nackMids: string[];
|
|
@@ -65,6 +65,8 @@ export class PCTransportManager {
|
|
|
65
65
|
|
|
66
66
|
private log = log;
|
|
67
67
|
|
|
68
|
+
private iceLog = log;
|
|
69
|
+
|
|
68
70
|
private loggerOptions: LoggerOptions;
|
|
69
71
|
|
|
70
72
|
private _mode: PCMode;
|
|
@@ -74,8 +76,9 @@ export class PCTransportManager {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
constructor(mode: PCMode, loggerOptions: LoggerOptions, rtcConfig?: RTCConfiguration) {
|
|
77
|
-
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager);
|
|
78
79
|
this.loggerOptions = loggerOptions;
|
|
80
|
+
this.log = getLogger(loggerOptions.loggerName ?? LoggerNames.PCManager, () => this.logContext);
|
|
81
|
+
this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
|
|
79
82
|
|
|
80
83
|
this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
|
|
81
84
|
this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
|
|
@@ -151,7 +154,7 @@ export class PCTransportManager {
|
|
|
151
154
|
publisher.removeTrack(sender);
|
|
152
155
|
}
|
|
153
156
|
} catch (e) {
|
|
154
|
-
this.log.warn('could not removeTrack', {
|
|
157
|
+
this.log.warn('could not removeTrack', { error: e });
|
|
155
158
|
}
|
|
156
159
|
}
|
|
157
160
|
}
|
|
@@ -160,6 +163,7 @@ export class PCTransportManager {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
async triggerIceRestart() {
|
|
166
|
+
this.iceLog.warn('triggering ICE restart');
|
|
163
167
|
if (this.subscriber) {
|
|
164
168
|
this.subscriber.restartingIce = true;
|
|
165
169
|
}
|
|
@@ -170,6 +174,7 @@ export class PCTransportManager {
|
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
async addIceCandidate(candidate: RTCIceCandidateInit, target: SignalTarget) {
|
|
177
|
+
this.iceLog.debug('adding remote ICE candidate', { target, candidate });
|
|
173
178
|
if (target === SignalTarget.PUBLISHER) {
|
|
174
179
|
await this.publisher.addIceCandidate(candidate);
|
|
175
180
|
} else {
|
|
@@ -179,7 +184,6 @@ export class PCTransportManager {
|
|
|
179
184
|
|
|
180
185
|
async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number) {
|
|
181
186
|
this.log.debug('received server offer', {
|
|
182
|
-
...this.logContext,
|
|
183
187
|
RTCSdpType: sd.type,
|
|
184
188
|
sdp: sd.sdp,
|
|
185
189
|
signalingState: this.subscriber?.getSignallingState().toString(),
|
|
@@ -200,6 +204,7 @@ export class PCTransportManager {
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
updateConfiguration(config: RTCConfiguration, iceRestart?: boolean) {
|
|
207
|
+
this.log.debug('updating rtc configuration', { iceRestart });
|
|
203
208
|
this.publisher.setConfiguration(config);
|
|
204
209
|
this.subscriber?.setConfiguration(config);
|
|
205
210
|
if (iceRestart) {
|
|
@@ -215,7 +220,7 @@ export class PCTransportManager {
|
|
|
215
220
|
this.publisher.getConnectionState() !== 'connected' &&
|
|
216
221
|
this.publisher.getConnectionState() !== 'connecting'
|
|
217
222
|
) {
|
|
218
|
-
this.log.debug('negotiation required, start negotiating'
|
|
223
|
+
this.log.debug('negotiation required, start negotiating');
|
|
219
224
|
this.publisher.negotiate();
|
|
220
225
|
}
|
|
221
226
|
await Promise.all(
|
|
@@ -368,7 +373,6 @@ export class PCTransportManager {
|
|
|
368
373
|
`pc state change: from ${PCTransportState[previousState]} to ${
|
|
369
374
|
PCTransportState[this.state]
|
|
370
375
|
}`,
|
|
371
|
-
this.logContext,
|
|
372
376
|
);
|
|
373
377
|
this.onStateChange?.(
|
|
374
378
|
this.state,
|
|
@@ -390,7 +394,7 @@ export class PCTransportManager {
|
|
|
390
394
|
|
|
391
395
|
return new Promise<void>(async (resolve, reject) => {
|
|
392
396
|
const abortHandler = () => {
|
|
393
|
-
this.log.warn('abort transport connection'
|
|
397
|
+
this.log.warn('abort transport connection');
|
|
394
398
|
CriticalTimers.clearTimeout(connectTimeout);
|
|
395
399
|
|
|
396
400
|
reject(ConnectionError.cancelled('room connection has been cancelled'));
|