livekit-client 2.20.2 → 2.22.0
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/README.md +10 -10
- 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 +6 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2588 -1056
- 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 +6 -1
- 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/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +48 -65
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
- package/dist/ts4.2/room/Room.d.ts +6 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +28 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +14 -13
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +397 -101
- package/src/room/RTCEngine.ts +244 -545
- package/src/room/Room.test.ts +138 -4
- package/src/room/Room.ts +92 -25
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +9 -3
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/errors.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +119 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/version.ts +5 -1
package/src/room/PCTransport.ts
CHANGED
|
@@ -36,6 +36,63 @@ const maxStartBitrateKbps = 1000;
|
|
|
36
36
|
|
|
37
37
|
const debounceInterval = 20;
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Applies the configured start bitrate when this media section belongs to `cid`.
|
|
41
|
+
* This SDP munging is used for a bitrate setting that cannot be applied through
|
|
42
|
+
* `RTCRtpEncodingParameters`.
|
|
43
|
+
*
|
|
44
|
+
* Returns `undefined` when the section does not belong to the track, `0` when
|
|
45
|
+
* it does but does not offer the requested codec, and the codec payload when the
|
|
46
|
+
* requested codec is present (whether the bitrate was added or already set).
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export function applyVideoStartBitrate(
|
|
51
|
+
media: MediaDescription,
|
|
52
|
+
cid: string,
|
|
53
|
+
codec: string,
|
|
54
|
+
maxbr: number,
|
|
55
|
+
isScreenShare = false,
|
|
56
|
+
): number | undefined {
|
|
57
|
+
if (!media.msid?.includes(cid)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const codecPayload =
|
|
62
|
+
media.rtp.find((rtp) => rtp.codec.toUpperCase() === codec.toUpperCase())?.payload ?? 0;
|
|
63
|
+
if (codecPayload === 0) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
|
|
68
|
+
// from starting too aggressively. Screen share is not capped since text/UI
|
|
69
|
+
// clarity requires high bitrate from the start.
|
|
70
|
+
// TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
|
|
71
|
+
const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
|
|
72
|
+
const startBitrate = isScreenShare
|
|
73
|
+
? calculatedStartBitrate
|
|
74
|
+
: Math.min(calculatedStartBitrate, maxStartBitrateKbps);
|
|
75
|
+
|
|
76
|
+
const fmtp = media.fmtp.find((entry) => entry.payload === codecPayload);
|
|
77
|
+
if (fmtp) {
|
|
78
|
+
// If another track's fmtp already has a start bitrate, it cannot be
|
|
79
|
+
// overridden here because the payload type is shared across the bundle.
|
|
80
|
+
// This forces every track sharing that payload to use the initial track's
|
|
81
|
+
// start bitrate.
|
|
82
|
+
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
83
|
+
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
// VP8 and some codecs may not have an existing fmtp line.
|
|
87
|
+
media.fmtp.push({
|
|
88
|
+
payload: codecPayload,
|
|
89
|
+
config: `x-google-start-bitrate=${startBitrate}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return codecPayload;
|
|
94
|
+
}
|
|
95
|
+
|
|
39
96
|
export const PCEvents = {
|
|
40
97
|
NegotiationStarted: 'negotiationStarted',
|
|
41
98
|
NegotiationComplete: 'negotiationComplete',
|
|
@@ -368,9 +425,15 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
368
425
|
// the only exception to this is when ICE restart is needed
|
|
369
426
|
const currentSD = this._pc.remoteDescription;
|
|
370
427
|
if (options?.iceRestart && currentSD) {
|
|
371
|
-
//
|
|
372
|
-
//
|
|
428
|
+
// roll the remote description back in so createOffer produces a valid
|
|
429
|
+
// ICE-restart offer on top of the already-negotiated state
|
|
373
430
|
await this._pc.setRemoteDescription(currentSD);
|
|
431
|
+
} else if (options?.iceRestart) {
|
|
432
|
+
// ICE restart with no remote description to restart on: `renegotiate` would stall
|
|
433
|
+
// (the pending offer is never answered), so throw for the caller to recreate the PC.
|
|
434
|
+
throw new NegotiationError(
|
|
435
|
+
'ICE restart requested without a remote description, peer connection must be recreated',
|
|
436
|
+
);
|
|
374
437
|
} else {
|
|
375
438
|
this.renegotiate = true;
|
|
376
439
|
this.log.debug('requesting renegotiation');
|
|
@@ -396,56 +459,25 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
396
459
|
ensureAudioNackAndStereo(media, ['all'], []);
|
|
397
460
|
} else if (media.type === 'video') {
|
|
398
461
|
this.trackBitrates.some((trackbr): boolean => {
|
|
399
|
-
if (!
|
|
462
|
+
if (!trackbr.cid) {
|
|
400
463
|
return false;
|
|
401
464
|
}
|
|
402
465
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
466
|
+
const codecPayload = applyVideoStartBitrate(
|
|
467
|
+
media,
|
|
468
|
+
trackbr.cid,
|
|
469
|
+
trackbr.codec,
|
|
470
|
+
trackbr.maxbr,
|
|
471
|
+
trackbr.isScreenShare,
|
|
472
|
+
);
|
|
473
|
+
if (codecPayload === undefined) {
|
|
409
474
|
return false;
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
if (codecPayload === 0) {
|
|
413
|
-
return true;
|
|
414
475
|
}
|
|
415
476
|
|
|
416
|
-
if (isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
417
|
-
this.
|
|
477
|
+
if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
|
|
478
|
+
this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
|
|
418
479
|
}
|
|
419
480
|
|
|
420
|
-
// mung sdp for bitrate setting that can't apply by sendEncoding
|
|
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);
|
|
428
|
-
|
|
429
|
-
let fmtpFound = false;
|
|
430
|
-
for (const fmtp of media.fmtp) {
|
|
431
|
-
if (fmtp.payload === codecPayload) {
|
|
432
|
-
fmtpFound = true;
|
|
433
|
-
// if another track's fmtp already is set, we cannot override the bitrate
|
|
434
|
-
// this has the unfortunate consequence of being forced to use the
|
|
435
|
-
// initial track's bitrate for all tracks
|
|
436
|
-
if (!fmtp.config.includes('x-google-start-bitrate')) {
|
|
437
|
-
fmtp.config += `;x-google-start-bitrate=${startBitrate}`;
|
|
438
|
-
}
|
|
439
|
-
break;
|
|
440
|
-
}
|
|
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
|
-
}
|
|
449
481
|
return true;
|
|
450
482
|
});
|
|
451
483
|
}
|
|
@@ -681,44 +713,110 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
681
713
|
throw new NegotiationError(msg);
|
|
682
714
|
}
|
|
683
715
|
}
|
|
716
|
+
}
|
|
684
717
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
|
|
720
|
+
* returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
|
|
721
|
+
* been chosen yet).
|
|
722
|
+
*
|
|
723
|
+
* A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
|
|
724
|
+
* `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
|
|
725
|
+
* sections it can send on, and an earlier offer may have munged it into others.
|
|
726
|
+
* @internal
|
|
727
|
+
*/
|
|
728
|
+
export function ensureVideoDDExtension(
|
|
729
|
+
media: {
|
|
730
|
+
type: string;
|
|
731
|
+
port: number;
|
|
732
|
+
protocol: string;
|
|
733
|
+
payloads?: string | undefined;
|
|
734
|
+
} & MediaDescription,
|
|
735
|
+
sdp: SessionDescription,
|
|
736
|
+
ddExtID: number,
|
|
737
|
+
): number {
|
|
738
|
+
const id = ddExtensionIDFor(sdp, ddExtID);
|
|
739
|
+
if (id === undefined) {
|
|
740
|
+
return ddExtID;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!media.ext?.some((ext) => ext.uri === ddExtensionURI)) {
|
|
744
|
+
media.ext ??= [];
|
|
745
|
+
media.ext.push({
|
|
746
|
+
value: id,
|
|
747
|
+
uri: ddExtensionURI,
|
|
699
748
|
});
|
|
749
|
+
}
|
|
750
|
+
return id;
|
|
751
|
+
}
|
|
700
752
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
753
|
+
/**
|
|
754
|
+
* The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
|
|
755
|
+
* consistent for the whole bundle and the extension therefore has to be left out.
|
|
756
|
+
*/
|
|
757
|
+
function ddExtensionIDFor(sdp: SessionDescription, cachedID: number): number | undefined {
|
|
758
|
+
const mapped = mappedExtensionID(sdp, ddExtensionURI);
|
|
759
|
+
if (mapped !== undefined) {
|
|
760
|
+
// Adopting an id that also stands for another URI is what the browser rejects the bundle
|
|
761
|
+
// over, and its own half of the map is not ours to renumber, so give up on this offer.
|
|
762
|
+
return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
|
|
763
|
+
}
|
|
764
|
+
// Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
|
|
765
|
+
// while nothing else has taken it: the browser assigns ids to its own extensions without
|
|
766
|
+
// knowing about ours, so an id that was free when we picked it can since have been claimed —
|
|
767
|
+
// typically by the fuller extension set that arrives with the first section we send on.
|
|
768
|
+
if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
|
|
769
|
+
return cachedID;
|
|
770
|
+
}
|
|
771
|
+
return unusedExtensionID(sdp);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** The id `uri` is mapped to in `sdp`, if any section maps it. */
|
|
775
|
+
function mappedExtensionID(sdp: SessionDescription, uri: string): number | undefined {
|
|
776
|
+
for (const media of sdp.media) {
|
|
777
|
+
const ext = media.ext?.find((candidate) => candidate.uri === uri);
|
|
778
|
+
if (ext) {
|
|
779
|
+
return ext.value;
|
|
717
780
|
}
|
|
718
781
|
}
|
|
782
|
+
return undefined;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** Whether `id` stands for anything in `sdp` other than `uri`. */
|
|
786
|
+
function usedForOtherURI(sdp: SessionDescription, id: number, uri: string): boolean {
|
|
787
|
+
return sdp.media.some((media) => media.ext?.some((ext) => ext.value === id && ext.uri !== uri));
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
|
|
792
|
+
* is less likely to be an id the browser goes on to allocate to another extension, and steps
|
|
793
|
+
* over 15, which RFC 8285 reserves.
|
|
794
|
+
*/
|
|
795
|
+
function unusedExtensionID(sdp: SessionDescription): number {
|
|
796
|
+
let maxID = 0;
|
|
797
|
+
sdp.media.forEach((media) => {
|
|
798
|
+
media.ext?.forEach((ext) => {
|
|
799
|
+
if (ext.value > maxID) {
|
|
800
|
+
maxID = ext.value;
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
return maxID + 1 === 15 ? 16 : maxID + 1;
|
|
719
805
|
}
|
|
720
806
|
|
|
721
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Checks whether an fmtp config declares `param` as an exact, `;`-delimited
|
|
809
|
+
* token. A plain substring check conflates distinct opus parameters — e.g.
|
|
810
|
+
* `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
|
|
811
|
+
* parameter, not appear anywhere within the config string.
|
|
812
|
+
* @internal
|
|
813
|
+
*/
|
|
814
|
+
export function fmtpConfigHasParam(config: string, param: string): boolean {
|
|
815
|
+
return config.split(';').some((entry) => entry.trim() === param);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** @internal */
|
|
819
|
+
export function ensureAudioNackAndStereo(
|
|
722
820
|
media: {
|
|
723
821
|
type: string;
|
|
724
822
|
port: number;
|
|
@@ -734,7 +832,8 @@ function ensureAudioNackAndStereo(
|
|
|
734
832
|
// found opus codec to add nack fb
|
|
735
833
|
let opusPayload = 0;
|
|
736
834
|
media.rtp.some((rtp): boolean => {
|
|
737
|
-
|
|
835
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
836
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
738
837
|
opusPayload = rtp.payload;
|
|
739
838
|
return true;
|
|
740
839
|
}
|
|
@@ -760,7 +859,7 @@ function ensureAudioNackAndStereo(
|
|
|
760
859
|
if (stereoMids.includes(mid) || (stereoMids.length === 1 && stereoMids[0] === 'all')) {
|
|
761
860
|
media.fmtp.some((fmtp): boolean => {
|
|
762
861
|
if (fmtp.payload === opusPayload) {
|
|
763
|
-
if (!fmtp.config
|
|
862
|
+
if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
|
|
764
863
|
fmtp.config += ';stereo=1';
|
|
765
864
|
}
|
|
766
865
|
return true;
|
|
@@ -851,7 +950,8 @@ export function conformBundledCodecFmtp(
|
|
|
851
950
|
}
|
|
852
951
|
}
|
|
853
952
|
|
|
854
|
-
|
|
953
|
+
/** @internal */
|
|
954
|
+
export function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
855
955
|
stereoMids: string[];
|
|
856
956
|
nackMids: string[];
|
|
857
957
|
} {
|
|
@@ -863,7 +963,8 @@ function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
|
863
963
|
const mid = getMidString(media.mid!);
|
|
864
964
|
if (media.type === 'audio') {
|
|
865
965
|
media.rtp.some((rtp): boolean => {
|
|
866
|
-
|
|
966
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
967
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
867
968
|
opusPayload = rtp.payload;
|
|
868
969
|
return true;
|
|
869
970
|
}
|
|
@@ -876,7 +977,7 @@ function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
|
876
977
|
|
|
877
978
|
media.fmtp.some((fmtp): boolean => {
|
|
878
979
|
if (fmtp.payload === opusPayload) {
|
|
879
|
-
if (fmtp.config
|
|
980
|
+
if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
|
|
880
981
|
stereoMids.push(mid);
|
|
881
982
|
}
|
|
882
983
|
return true;
|