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.
Files changed (142) hide show
  1. package/README.md +10 -10
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +6 -1
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2588 -1056
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +6 -1
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/room/PCTransport.d.ts +49 -2
  17. package/dist/src/room/PCTransport.d.ts.map +1 -1
  18. package/dist/src/room/RTCEngine.d.ts +48 -65
  19. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  20. package/dist/src/room/Room.d.ts +6 -0
  21. package/dist/src/room/Room.d.ts.map +1 -1
  22. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  23. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  24. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  25. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  26. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  27. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  28. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  29. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  30. package/dist/src/room/data-channel/types.d.ts +12 -0
  31. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  32. package/dist/src/room/data-stream/compression.d.ts +49 -0
  33. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  34. package/dist/src/room/data-stream/constants.d.ts +18 -0
  35. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  36. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  37. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  39. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  40. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  41. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  42. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  43. package/dist/src/room/errors.d.ts +3 -1
  44. package/dist/src/room/errors.d.ts.map +1 -1
  45. package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
  46. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  47. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  48. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  49. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  50. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  51. package/dist/src/room/token-source/TokenSource.d.ts +19 -10
  52. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  53. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  54. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/RemoteTrack.d.ts +3 -0
  56. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/Track.d.ts.map +1 -1
  58. package/dist/src/room/types.d.ts +17 -0
  59. package/dist/src/room/types.d.ts.map +1 -1
  60. package/dist/src/room/utils.d.ts +28 -0
  61. package/dist/src/room/utils.d.ts.map +1 -1
  62. package/dist/src/test/promiseState.d.ts +12 -0
  63. package/dist/src/test/promiseState.d.ts.map +1 -0
  64. package/dist/src/test/signalToken.d.ts.map +1 -1
  65. package/dist/src/utils/AsyncQueue.d.ts +3 -3
  66. package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
  67. package/dist/src/version.d.ts +5 -1
  68. package/dist/src/version.d.ts.map +1 -1
  69. package/dist/ts4.2/index.d.ts +1 -1
  70. package/dist/ts4.2/room/PCTransport.d.ts +49 -2
  71. package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
  72. package/dist/ts4.2/room/Room.d.ts +6 -0
  73. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  74. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  75. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  76. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  77. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  78. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  79. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  80. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  81. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  82. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  83. package/dist/ts4.2/room/errors.d.ts +3 -1
  84. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
  85. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  86. package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
  87. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  88. package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
  89. package/dist/ts4.2/room/types.d.ts +17 -0
  90. package/dist/ts4.2/room/utils.d.ts +28 -0
  91. package/dist/ts4.2/test/promiseState.d.ts +12 -0
  92. package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
  93. package/dist/ts4.2/version.d.ts +5 -1
  94. package/package.json +14 -13
  95. package/src/api/SignalClient.e2e.test.ts +16 -9
  96. package/src/e2ee/utils.ts +1 -1
  97. package/src/index.ts +2 -0
  98. package/src/room/PCTransport.test.ts +243 -1
  99. package/src/room/PCTransport.ts +181 -80
  100. package/src/room/RTCEngine.test.ts +397 -101
  101. package/src/room/RTCEngine.ts +244 -545
  102. package/src/room/Room.test.ts +138 -4
  103. package/src/room/Room.ts +92 -25
  104. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  105. package/src/room/data-channel/DataChannelManager.ts +237 -0
  106. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  107. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  108. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  109. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  110. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  111. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  112. package/src/room/data-channel/types.ts +30 -0
  113. package/src/room/data-stream/compression.ts +117 -0
  114. package/src/room/data-stream/constants.ts +18 -0
  115. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  116. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  117. package/src/room/data-stream/incoming/StreamReader.ts +9 -3
  118. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  119. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  120. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  121. package/src/room/errors.ts +7 -2
  122. package/src/room/participant/LocalParticipant.ts +13 -0
  123. package/src/room/participant/RemoteParticipant.ts +19 -5
  124. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  125. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  126. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  127. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  128. package/src/room/token-source/TokenSource.ts +25 -12
  129. package/src/room/track/LocalVideoTrack.test.ts +105 -2
  130. package/src/room/track/LocalVideoTrack.ts +36 -10
  131. package/src/room/track/RemoteTrack.test.ts +144 -0
  132. package/src/room/track/RemoteTrack.ts +39 -12
  133. package/src/room/track/Track.ts +2 -1
  134. package/src/room/types.ts +18 -0
  135. package/src/room/utils.test.ts +71 -2
  136. package/src/room/utils.ts +119 -0
  137. package/src/test/promiseState.ts +23 -0
  138. package/src/test/signalServerSetup.ts +2 -1
  139. package/src/test/signalToken.ts +17 -13
  140. package/src/type-polyfills/header-extensions.d.ts +13 -0
  141. package/src/utils/AsyncQueue.ts +3 -3
  142. package/src/version.ts +5 -1
@@ -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
- // TODO: handle when ICE restart is needed but we don't have a remote description
372
- // the best thing to do is to recreate the peerconnection
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 (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
462
+ if (!trackbr.cid) {
400
463
  return false;
401
464
  }
402
465
 
403
- let codecPayload = 0;
404
- media.rtp.some((rtp): boolean => {
405
- if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
406
- codecPayload = rtp.payload;
407
- return true;
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.ensureVideoDDExtensionForSVC(media, sdpParsed);
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
- private ensureVideoDDExtensionForSVC(
686
- media: {
687
- type: string;
688
- port: number;
689
- protocol: string;
690
- payloads?: string | undefined;
691
- } & MediaDescription,
692
- sdp: SessionDescription,
693
- ) {
694
- const ddFound = media.ext?.some((ext): boolean => {
695
- if (ext.uri === ddExtensionURI) {
696
- return true;
697
- }
698
- return false;
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
- if (!ddFound) {
702
- if (this.ddExtID === 0) {
703
- let maxID = 0;
704
- sdp.media.forEach((m) => {
705
- m.ext?.forEach((ext) => {
706
- if (ext.value > maxID) {
707
- maxID = ext.value;
708
- }
709
- });
710
- });
711
- this.ddExtID = maxID + 1;
712
- }
713
- media.ext?.push({
714
- value: this.ddExtID,
715
- uri: ddExtensionURI,
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
- function ensureAudioNackAndStereo(
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
- if (rtp.codec === 'opus') {
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.includes('stereo=1')) {
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
- function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
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
- if (rtp.codec === 'opus') {
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.includes('sprop-stereo=1')) {
980
+ if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
880
981
  stereoMids.push(mid);
881
982
  }
882
983
  return true;