livekit-client 2.21.0 → 2.22.1
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 +189 -2
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2243 -270
- 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 +189 -2
- 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 +25 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/src/api/SignalClientStateMachine.d.ts.map +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +2 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +22 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +51 -3
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +27 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +10 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/src/room/statsSummary.d.ts +13 -0
- package/dist/src/room/statsSummary.d.ts.map +1 -0
- 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/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.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 +10 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +15 -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/utils/machineInspector.d.ts +54 -0
- package/dist/src/utils/machineInspector.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +25 -1
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -0
- package/dist/ts4.2/index.d.ts +3 -2
- package/dist/ts4.2/logger.d.ts +2 -1
- package/dist/ts4.2/options.d.ts +22 -0
- package/dist/ts4.2/room/PCTransport.d.ts +51 -3
- package/dist/ts4.2/room/RTCEngine.d.ts +27 -3
- package/dist/ts4.2/room/Room.d.ts +10 -1
- package/dist/ts4.2/room/events.d.ts +1 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -1
- package/dist/ts4.2/room/statsSummary.d.ts +13 -0
- 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/track/Track.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +15 -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/utils/machineInspector.d.ts +54 -0
- package/package.json +19 -12
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/api/SignalClient.test.ts +320 -8
- package/src/api/SignalClient.ts +260 -82
- package/src/api/SignalClientStateMachine.test.ts +472 -0
- package/src/api/SignalClientStateMachine.ts +180 -0
- package/src/api/WebSocketStream.ts +19 -3
- package/src/api/utils.test.ts +20 -1
- package/src/api/utils.ts +5 -0
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +5 -0
- package/src/logger.ts +1 -0
- package/src/options.ts +24 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +183 -81
- package/src/room/RTCEngine.test.ts +339 -2
- package/src/room/RTCEngine.ts +168 -18
- package/src/room/Room.test.ts +134 -3
- package/src/room/Room.ts +122 -22
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +1 -1
- package/src/room/data-stream/incoming/StreamReader.ts +1 -1
- package/src/room/errors.ts +1 -2
- package/src/room/events.ts +1 -1
- package/src/room/statsSummary.ts +187 -0
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/token-source/test-tokens.ts +20 -0
- package/src/room/token-source/utils.test.ts +27 -0
- package/src/room/token-source/utils.ts +12 -5
- package/src/room/track/LocalAudioTrack.ts +9 -3
- 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 +29 -1
- package/src/room/utils.test.ts +94 -2
- package/src/room/utils.ts +53 -1
- 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/utils/machineInspector.ts +90 -0
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
|
}
|
|
@@ -565,8 +597,9 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
565
597
|
return this.pc?.remoteDescription;
|
|
566
598
|
}
|
|
567
599
|
|
|
600
|
+
/** stats of the underlying connection, `undefined` when there is none */
|
|
568
601
|
getStats() {
|
|
569
|
-
return this.
|
|
602
|
+
return this._pc?.getStats();
|
|
570
603
|
}
|
|
571
604
|
|
|
572
605
|
getMaxMessageSize() {
|
|
@@ -681,44 +714,110 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
681
714
|
throw new NegotiationError(msg);
|
|
682
715
|
}
|
|
683
716
|
}
|
|
717
|
+
}
|
|
684
718
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
719
|
+
/**
|
|
720
|
+
* Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
|
|
721
|
+
* returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
|
|
722
|
+
* been chosen yet).
|
|
723
|
+
*
|
|
724
|
+
* A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
|
|
725
|
+
* `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
|
|
726
|
+
* sections it can send on, and an earlier offer may have munged it into others.
|
|
727
|
+
* @internal
|
|
728
|
+
*/
|
|
729
|
+
export function ensureVideoDDExtension(
|
|
730
|
+
media: {
|
|
731
|
+
type: string;
|
|
732
|
+
port: number;
|
|
733
|
+
protocol: string;
|
|
734
|
+
payloads?: string | undefined;
|
|
735
|
+
} & MediaDescription,
|
|
736
|
+
sdp: SessionDescription,
|
|
737
|
+
ddExtID: number,
|
|
738
|
+
): number {
|
|
739
|
+
const id = ddExtensionIDFor(sdp, ddExtID);
|
|
740
|
+
if (id === undefined) {
|
|
741
|
+
return ddExtID;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (!media.ext?.some((ext) => ext.uri === ddExtensionURI)) {
|
|
745
|
+
media.ext ??= [];
|
|
746
|
+
media.ext.push({
|
|
747
|
+
value: id,
|
|
748
|
+
uri: ddExtensionURI,
|
|
699
749
|
});
|
|
750
|
+
}
|
|
751
|
+
return id;
|
|
752
|
+
}
|
|
700
753
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
754
|
+
/**
|
|
755
|
+
* The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
|
|
756
|
+
* consistent for the whole bundle and the extension therefore has to be left out.
|
|
757
|
+
*/
|
|
758
|
+
function ddExtensionIDFor(sdp: SessionDescription, cachedID: number): number | undefined {
|
|
759
|
+
const mapped = mappedExtensionID(sdp, ddExtensionURI);
|
|
760
|
+
if (mapped !== undefined) {
|
|
761
|
+
// Adopting an id that also stands for another URI is what the browser rejects the bundle
|
|
762
|
+
// over, and its own half of the map is not ours to renumber, so give up on this offer.
|
|
763
|
+
return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
|
|
764
|
+
}
|
|
765
|
+
// Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
|
|
766
|
+
// while nothing else has taken it: the browser assigns ids to its own extensions without
|
|
767
|
+
// knowing about ours, so an id that was free when we picked it can since have been claimed —
|
|
768
|
+
// typically by the fuller extension set that arrives with the first section we send on.
|
|
769
|
+
if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
|
|
770
|
+
return cachedID;
|
|
771
|
+
}
|
|
772
|
+
return unusedExtensionID(sdp);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** The id `uri` is mapped to in `sdp`, if any section maps it. */
|
|
776
|
+
function mappedExtensionID(sdp: SessionDescription, uri: string): number | undefined {
|
|
777
|
+
for (const media of sdp.media) {
|
|
778
|
+
const ext = media.ext?.find((candidate) => candidate.uri === uri);
|
|
779
|
+
if (ext) {
|
|
780
|
+
return ext.value;
|
|
717
781
|
}
|
|
718
782
|
}
|
|
783
|
+
return undefined;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** Whether `id` stands for anything in `sdp` other than `uri`. */
|
|
787
|
+
function usedForOtherURI(sdp: SessionDescription, id: number, uri: string): boolean {
|
|
788
|
+
return sdp.media.some((media) => media.ext?.some((ext) => ext.value === id && ext.uri !== uri));
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
|
|
793
|
+
* is less likely to be an id the browser goes on to allocate to another extension, and steps
|
|
794
|
+
* over 15, which RFC 8285 reserves.
|
|
795
|
+
*/
|
|
796
|
+
function unusedExtensionID(sdp: SessionDescription): number {
|
|
797
|
+
let maxID = 0;
|
|
798
|
+
sdp.media.forEach((media) => {
|
|
799
|
+
media.ext?.forEach((ext) => {
|
|
800
|
+
if (ext.value > maxID) {
|
|
801
|
+
maxID = ext.value;
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
return maxID + 1 === 15 ? 16 : maxID + 1;
|
|
719
806
|
}
|
|
720
807
|
|
|
721
|
-
|
|
808
|
+
/**
|
|
809
|
+
* Checks whether an fmtp config declares `param` as an exact, `;`-delimited
|
|
810
|
+
* token. A plain substring check conflates distinct opus parameters — e.g.
|
|
811
|
+
* `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
|
|
812
|
+
* parameter, not appear anywhere within the config string.
|
|
813
|
+
* @internal
|
|
814
|
+
*/
|
|
815
|
+
export function fmtpConfigHasParam(config: string, param: string): boolean {
|
|
816
|
+
return config.split(';').some((entry) => entry.trim() === param);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** @internal */
|
|
820
|
+
export function ensureAudioNackAndStereo(
|
|
722
821
|
media: {
|
|
723
822
|
type: string;
|
|
724
823
|
port: number;
|
|
@@ -734,7 +833,8 @@ function ensureAudioNackAndStereo(
|
|
|
734
833
|
// found opus codec to add nack fb
|
|
735
834
|
let opusPayload = 0;
|
|
736
835
|
media.rtp.some((rtp): boolean => {
|
|
737
|
-
|
|
836
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
837
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
738
838
|
opusPayload = rtp.payload;
|
|
739
839
|
return true;
|
|
740
840
|
}
|
|
@@ -760,7 +860,7 @@ function ensureAudioNackAndStereo(
|
|
|
760
860
|
if (stereoMids.includes(mid) || (stereoMids.length === 1 && stereoMids[0] === 'all')) {
|
|
761
861
|
media.fmtp.some((fmtp): boolean => {
|
|
762
862
|
if (fmtp.payload === opusPayload) {
|
|
763
|
-
if (!fmtp.config
|
|
863
|
+
if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
|
|
764
864
|
fmtp.config += ';stereo=1';
|
|
765
865
|
}
|
|
766
866
|
return true;
|
|
@@ -851,7 +951,8 @@ export function conformBundledCodecFmtp(
|
|
|
851
951
|
}
|
|
852
952
|
}
|
|
853
953
|
|
|
854
|
-
|
|
954
|
+
/** @internal */
|
|
955
|
+
export function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
855
956
|
stereoMids: string[];
|
|
856
957
|
nackMids: string[];
|
|
857
958
|
} {
|
|
@@ -863,7 +964,8 @@ function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
|
863
964
|
const mid = getMidString(media.mid!);
|
|
864
965
|
if (media.type === 'audio') {
|
|
865
966
|
media.rtp.some((rtp): boolean => {
|
|
866
|
-
|
|
967
|
+
// rtpmap encoding names are case-insensitive (RFC 4855)
|
|
968
|
+
if (rtp.codec.toLowerCase() === 'opus') {
|
|
867
969
|
opusPayload = rtp.payload;
|
|
868
970
|
return true;
|
|
869
971
|
}
|
|
@@ -876,7 +978,7 @@ function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
|
876
978
|
|
|
877
979
|
media.fmtp.some((fmtp): boolean => {
|
|
878
980
|
if (fmtp.payload === opusPayload) {
|
|
879
|
-
if (fmtp.config
|
|
981
|
+
if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
|
|
880
982
|
stereoMids.push(mid);
|
|
881
983
|
}
|
|
882
984
|
return true;
|