twilio-video 2.32.0 → 2.32.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/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@ The Twilio Programmable Video SDKs use [Semantic Versioning](http://www.semver.o
2
2
 
3
3
  **Version 1.x reached End of Life on September 8th, 2021.** See the changelog entry [here](https://www.twilio.com/changelog/end-of-life-complete-for-unsupported-versions-of-the-programmable-video-sdk). Support for the 1.x version ended on December 4th, 2020.
4
4
 
5
+ 2.32.1 (August 1, 2025)
6
+ ====================
7
+
8
+ Bug Fixes
9
+ ---------
10
+ - Fixed an issue where video tracks would freeze when the video element was offscreen in a document Picture-in-Picture window.
11
+ - Fixed an issue where `LocalVideoTrack.restart()` would fail on some devices when a video processor was active.
12
+ - Fixed an issue where SDP munging would generate duplicated payload types, causing SDP negotiation failures with Chrome 137+.
13
+
5
14
  2.32.0 (July 23, 2025)
6
15
  ====================
7
16
 
package/README.md CHANGED
@@ -74,7 +74,7 @@ Releases of twilio-video.js are hosted on a CDN, and you can include these
74
74
  directly in your web app using a <script> tag.
75
75
 
76
76
  ```html
77
- <script src="//sdk.twilio.com/js/video/releases/2.32.0/twilio-video.min.js"></script>
77
+ <script src="//sdk.twilio.com/js/video/releases/2.32.1/twilio-video.min.js"></script>
78
78
  ```
79
79
 
80
80
  Using this method, twilio-video.js will set a browser global:
@@ -1,4 +1,4 @@
1
- /*! twilio-video.js 2.32.0
1
+ /*! twilio-video.js 2.32.1
2
2
 
3
3
  The following license applies to all parts of this software except as
4
4
  documented below.
@@ -4338,6 +4338,10 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4338
4338
  _workaroundSilentLocalVideoCleanup: {
4339
4339
  value: null,
4340
4340
  writable: true
4341
+ },
4342
+ _pipIntersectionObserver: {
4343
+ value: null,
4344
+ writable: true
4341
4345
  }
4342
4346
  });
4343
4347
  // NOTE(mmalavalli): In iOS Safari, we work around a bug where local video
@@ -4351,6 +4355,96 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4351
4355
  LocalVideoTrack.prototype.toString = function () {
4352
4356
  return "[LocalVideoTrack #" + this._instanceId + ": " + this.id + "]";
4353
4357
  };
4358
+ /**
4359
+ * Handle document PiP enter event - set up intersection observer for PiP-specific visibility tracking
4360
+ *
4361
+ * NOTE(lrivas): This is only created if we are in document PiP mode and should be considered deprecated
4362
+ * if Chrome's native PiP fixes the issue in the future.
4363
+ * @private
4364
+ */
4365
+ LocalVideoTrack.prototype._onDocumentPipEnter = function () {
4366
+ this._log.debug('LocalVideoTrack: Setting up PiP intersection observer');
4367
+ this._setupPipIntersectionObserver();
4368
+ };
4369
+ /**
4370
+ * Handle document PiP exit event - clean up intersection observer
4371
+ * NOTE(lrivas): This is only created if we are in document PiP mode and should be considered deprecated
4372
+ * if Chrome's native PiP fixes the issue in the future.
4373
+ * @private
4374
+ */
4375
+ LocalVideoTrack.prototype._onDocumentPipExit = function () {
4376
+ this._log.debug('LocalVideoTrack: Cleaning up PiP intersection observer');
4377
+ this._cleanupPipIntersectionObserver();
4378
+ };
4379
+ /**
4380
+ * Set up an IntersectionObserver to track visibility of elements attached in document PiP.
4381
+ * This ensures that videos in PiP play when visible.
4382
+ *
4383
+ * NOTE(lrivas): This is only created if we are in document PiP mode and should be considered deprecated
4384
+ * if Chrome's native PiP fixes the issue in the future.
4385
+ * @private
4386
+ */
4387
+ LocalVideoTrack.prototype._setupPipIntersectionObserver = function () {
4388
+ var _this = this;
4389
+ if (!this._pipIntersectionObserver && this._documentPipWindow) {
4390
+ this._log.debug('LocalVideoTrack: Creating PiP intersection observer');
4391
+ this._pipIntersectionObserver = new IntersectionObserver(function (entries) {
4392
+ var hasVisibleElements = entries.some(function (entry) { return entry.isIntersecting; });
4393
+ if (hasVisibleElements) {
4394
+ _this._log.debug('LocalVideoTrack: Element visible in PiP, ensuring videos are playing');
4395
+ VideoTrack._ensureDocumentPipVideosPlaying(_this);
4396
+ }
4397
+ }, { threshold: 0.25 });
4398
+ this._attachments.forEach(function (el) {
4399
+ _this._log.debug('LocalVideoTrack: Observing element for PiP visibility');
4400
+ _this._pipIntersectionObserver.observe(el);
4401
+ });
4402
+ }
4403
+ };
4404
+ /**
4405
+ * Clean up PiP-specific intersection observer
4406
+ *
4407
+ * NOTE(lrivas): This is only created if we are in document PiP mode and should be considered deprecated
4408
+ * if Chrome's native PiP fixes the issue in the future.
4409
+ * @private
4410
+ */
4411
+ LocalVideoTrack.prototype._cleanupPipIntersectionObserver = function () {
4412
+ if (this._pipIntersectionObserver) {
4413
+ this._log.debug('LocalVideoTrack: Disconnecting PiP intersection observer');
4414
+ this._pipIntersectionObserver.disconnect();
4415
+ this._pipIntersectionObserver = null;
4416
+ }
4417
+ };
4418
+ /**
4419
+ * Attach the {@link LocalVideoTrack} to an HTMLMediaElement.
4420
+ * @returns {HTMLMediaElement} mediaElement
4421
+ */
4422
+ LocalVideoTrack.prototype.attach = function () {
4423
+ var result = _super.prototype.attach.apply(this, arguments);
4424
+ // If we're in document PiP, new elements should be observed for visibility
4425
+ if (this._pipIntersectionObserver) {
4426
+ this._log.debug('LocalVideoTrack: Observing newly attached element for PiP visibility');
4427
+ this._pipIntersectionObserver.observe(result);
4428
+ }
4429
+ return result;
4430
+ };
4431
+ /**
4432
+ * Detach the {@link LocalVideoTrack} from HTMLMediaElement(s).
4433
+ * @returns {HTMLMediaElement|Array<HTMLMediaElement>} mediaElement(s)
4434
+ */
4435
+ LocalVideoTrack.prototype.detach = function () {
4436
+ var _this = this;
4437
+ var result = _super.prototype.detach.apply(this, arguments);
4438
+ var elements = Array.isArray(result) ? result : [result];
4439
+ // Stop observing detached elements if we're in document PiP
4440
+ if (this._pipIntersectionObserver) {
4441
+ elements.forEach(function (el) {
4442
+ _this._log.debug('LocalVideoTrack: Unobserving detached element from PiP visibility');
4443
+ _this._pipIntersectionObserver.unobserve(el);
4444
+ });
4445
+ }
4446
+ return result;
4447
+ };
4354
4448
  /**
4355
4449
  * @private
4356
4450
  */
@@ -4364,6 +4458,8 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4364
4458
  return _super.prototype._end.apply(this, arguments);
4365
4459
  };
4366
4460
  /**
4461
+ * @param {boolean} useProcessed - Whether to use the processed track or the unprocessed track.
4462
+ * @returns {Promise<void>}
4367
4463
  * @private
4368
4464
  */
4369
4465
  LocalVideoTrack.prototype._setSenderMediaStreamTrack = function (useProcessed) {
@@ -4487,12 +4583,18 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4487
4583
  * the MediaStreamTrack directly, please do so in the "started" event handler. Also,
4488
4584
  * the {@link LocalVideoTrack}'s ID is no longer guaranteed to be the same as the
4489
4585
  * underlying MediaStreamTrack's ID.
4586
+ *
4587
+ * If a {@link VideoProcessor} is applied to this track, it will be automatically
4588
+ * restarted after the new MediaStreamTrack is created to ensure proper processing
4589
+ * continues with the new video source.
4590
+ *
4490
4591
  * @param {MediaTrackConstraints} [constraints] - The optional <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints" target="_blank">MediaTrackConstraints</a>
4491
4592
  * for restarting the {@link LocalVideoTrack}; If not specified, then the current MediaTrackConstraints
4492
4593
  * will be used; If <code>{}</code> (empty object) is specified, then the default MediaTrackConstraints
4493
4594
  * will be used
4494
- * @returns {Promise<void>} Rejects with a TypeError if the {@link LocalVideoTrack} was not created
4495
- * using an one of <code>createLocalVideoTrack</code>, <code>createLocalTracks</code> or <code>connect</code>;
4595
+ * @returns {Promise<void>} Resolves when the restart is complete, including any processor restart operations.
4596
+ * Rejects with a TypeError if the {@link LocalVideoTrack} was not created
4597
+ * using one of <code>createLocalVideoTrack</code>, <code>createLocalTracks</code> or <code>connect</code>;
4496
4598
  * Also rejects with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Exceptions" target="_blank">DOMException</a>
4497
4599
  * raised by <code>getUserMedia</code> when it fails
4498
4600
  * @fires LocalVideoTrack#stopped
@@ -4520,9 +4622,7 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4520
4622
  }
4521
4623
  var promise = _super.prototype.restart.apply(this, arguments);
4522
4624
  if (this.processor) {
4523
- promise.then(function () {
4524
- _this._restartProcessor();
4525
- });
4625
+ promise = promise.then(function () { return _this._restartProcessor(); });
4526
4626
  }
4527
4627
  if (this._workaroundSilentLocalVideo) {
4528
4628
  promise.finally(function () {
@@ -4531,6 +4631,26 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4531
4631
  }
4532
4632
  return promise;
4533
4633
  };
4634
+ /**
4635
+ * Restarts the video processor by removing and re-adding it with the same options,
4636
+ * then updates the sender to use the newly processed MediaStreamTrack.
4637
+ *
4638
+ * This operations are done synchronously to ensure the processor is restarted before the
4639
+ * sender is updated to avoid race conditions.
4640
+ *
4641
+ * @returns {Promise<void>}
4642
+ * @private
4643
+ */
4644
+ LocalVideoTrack.prototype._restartProcessor = function () {
4645
+ var processor = this.processor;
4646
+ var processorOptions = Object.assign({}, this._processorOptions);
4647
+ // Local video track's removeProcessor and addProcessor call setSenderMediaStreamTrack async,
4648
+ // but we need to wait for that operation to finish for a successful restart.
4649
+ // So we call the parent's removeProcessor and addProcessor, then setSenderMediaStreamTrack right after.
4650
+ _super.prototype.removeProcessor.apply(this, [processor]);
4651
+ _super.prototype.addProcessor.apply(this, [processor, processorOptions]);
4652
+ return this._setSenderMediaStreamTrack(true);
4653
+ };
4534
4654
  /**
4535
4655
  * Calls stop on the underlying MediaStreamTrack. If you choose to stop a
4536
4656
  * {@link LocalVideoTrack}, you should unpublish it after stopping.
@@ -4542,6 +4662,8 @@ var LocalVideoTrack = /** @class */ (function (_super) {
4542
4662
  this._workaroundSilentLocalVideoCleanup();
4543
4663
  this._workaroundSilentLocalVideoCleanup = null;
4544
4664
  }
4665
+ // Clean up PiP intersection observer when stopping
4666
+ this._cleanupPipIntersectionObserver();
4545
4667
  return _super.prototype.stop.apply(this, arguments);
4546
4668
  };
4547
4669
  return LocalVideoTrack;
@@ -6313,7 +6435,7 @@ var documentVisibilityMonitor = require('../../util/documentvisibilitymonitor.js
6313
6435
  var NullObserver = require('../../util/nullobserver.js').NullObserver;
6314
6436
  var Timeout = require('../../util/timeout');
6315
6437
  var RemoteMediaVideoTrack = mixinRemoteMediaTrack(VideoTrack);
6316
- var TRACK_TURN_OF_DELAY_MS = 50;
6438
+ var TRACK_TURN_OFF_DELAY_MS = 50;
6317
6439
  /**
6318
6440
  * A {@link RemoteVideoTrack} represents a {@link VideoTrack} published to a
6319
6441
  * {@link Room} by a {@link RemoteParticipant}.
@@ -6380,7 +6502,7 @@ var RemoteVideoTrack = /** @class */ (function (_super) {
6380
6502
  _turnOffTimer: {
6381
6503
  value: new Timeout(function () {
6382
6504
  _this._setRenderHint({ enabled: false });
6383
- }, TRACK_TURN_OF_DELAY_MS, false),
6505
+ }, TRACK_TURN_OFF_DELAY_MS, false),
6384
6506
  },
6385
6507
  _resizeObserver: {
6386
6508
  value: new options.ResizeObserver(function (entries) {
@@ -6646,6 +6768,8 @@ function maybeUpdateEnabledHint(remoteVideoTrack) {
6646
6768
  if (enabled === true) {
6647
6769
  remoteVideoTrack._turnOffTimer.clear();
6648
6770
  remoteVideoTrack._setRenderHint({ enabled: true });
6771
+ // Check if videos in document PiP are actually playing
6772
+ VideoTrack._ensureDocumentPipVideosPlaying(remoteVideoTrack);
6649
6773
  }
6650
6774
  else if (!remoteVideoTrack._turnOffTimer.isSet) {
6651
6775
  // set the track to be turned off after some delay.
@@ -7398,6 +7522,18 @@ var VideoTrack = /** @class */ (function (_super) {
7398
7522
  enumerable: true,
7399
7523
  value: null,
7400
7524
  writable: true
7525
+ },
7526
+ _documentPipWindow: {
7527
+ value: null,
7528
+ writable: true
7529
+ },
7530
+ _documentPipEnterListener: {
7531
+ value: null,
7532
+ writable: true
7533
+ },
7534
+ _documentPipExitListener: {
7535
+ value: null,
7536
+ writable: true
7401
7537
  }
7402
7538
  });
7403
7539
  _this._processorEventObserver = new (options.VideoProcessorEventObserver || VideoProcessorEventObserver)(_this._log);
@@ -7720,6 +7856,10 @@ var VideoTrack = /** @class */ (function (_super) {
7720
7856
  */
7721
7857
  VideoTrack.prototype.attach = function () {
7722
7858
  var result = _super.prototype.attach.apply(this, arguments);
7859
+ // Set up document PiP listener on first attach
7860
+ if (this._attachments.size === 1) {
7861
+ setupDocumentPipListener(this);
7862
+ }
7723
7863
  if (this.processor) {
7724
7864
  this._captureFrames();
7725
7865
  }
@@ -7749,7 +7889,12 @@ var VideoTrack = /** @class */ (function (_super) {
7749
7889
  * videoTrack.detach('#my-video-element').remove();
7750
7890
  */
7751
7891
  VideoTrack.prototype.detach = function () {
7752
- return _super.prototype.detach.apply(this, arguments);
7892
+ var result = _super.prototype.detach.apply(this, arguments);
7893
+ // Clean up document PiP listener when no attachments remain
7894
+ if (this._attachments.size === 0) {
7895
+ cleanupDocumentPipListener(this);
7896
+ }
7897
+ return result;
7753
7898
  };
7754
7899
  /**
7755
7900
  * Remove the previously added {@link VideoProcessor} using `addProcessor` API.
@@ -7805,6 +7950,87 @@ function dimensionsChanged(track, elem) {
7805
7950
  return track.dimensions.width !== elem.videoWidth
7806
7951
  || track.dimensions.height !== elem.videoHeight;
7807
7952
  }
7953
+ /**
7954
+ * Set up document PiP listener for a VideoTrack.
7955
+ *
7956
+ * The received VideoTrack can define `_onDocumentPipEnter` and `_onDocumentPipExit`
7957
+ * to handle PiP-specific setup and cleanup.
7958
+ * @param {VideoTrack} videoTrack - The VideoTrack to set up the listener for
7959
+ * @private
7960
+ */
7961
+ function setupDocumentPipListener(videoTrack) {
7962
+ if (!videoTrack._documentPipEnterListener && 'documentPictureInPicture' in globalThis &&
7963
+ globalThis.documentPictureInPicture &&
7964
+ typeof globalThis.documentPictureInPicture.addEventListener === 'function') {
7965
+ videoTrack._documentPipEnterListener = function (event) {
7966
+ videoTrack._log.debug('document pip entered');
7967
+ videoTrack._documentPipWindow = event.window;
7968
+ // Set up exit listener on the PiP window
7969
+ videoTrack._documentPipExitListener = function () {
7970
+ videoTrack._log.debug('document pip exited');
7971
+ videoTrack._documentPipWindow = null;
7972
+ if (typeof videoTrack._onDocumentPipExit === 'function') {
7973
+ videoTrack._onDocumentPipExit();
7974
+ }
7975
+ };
7976
+ // Listen for window close on the PiP window
7977
+ if (event.window && typeof event.window.addEventListener === 'function') {
7978
+ event.window.addEventListener('pagehide', videoTrack._documentPipExitListener);
7979
+ }
7980
+ // Immediate fix for any paused videos
7981
+ ensureDocumentPipVideosPlaying(videoTrack);
7982
+ if (typeof videoTrack._onDocumentPipEnter === 'function') {
7983
+ videoTrack._onDocumentPipEnter(event);
7984
+ }
7985
+ };
7986
+ globalThis.documentPictureInPicture.addEventListener('enter', videoTrack._documentPipEnterListener);
7987
+ }
7988
+ }
7989
+ /**
7990
+ * Clean up document PiP listener for VideoTrack
7991
+ * @private
7992
+ */
7993
+ function cleanupDocumentPipListener(videoTrack) {
7994
+ if (videoTrack._documentPipEnterListener) {
7995
+ if (globalThis.documentPictureInPicture && typeof globalThis.documentPictureInPicture.removeEventListener === 'function') {
7996
+ globalThis.documentPictureInPicture.removeEventListener('enter', videoTrack._documentPipEnterListener);
7997
+ }
7998
+ // Clean up exit listener from PiP window if it exists
7999
+ if (videoTrack._documentPipWindow && videoTrack._documentPipExitListener &&
8000
+ typeof videoTrack._documentPipWindow.removeEventListener === 'function') {
8001
+ videoTrack._documentPipWindow.removeEventListener('pagehide', videoTrack._documentPipExitListener);
8002
+ }
8003
+ videoTrack._documentPipEnterListener = null;
8004
+ videoTrack._documentPipExitListener = null;
8005
+ videoTrack._documentPipWindow = null;
8006
+ }
8007
+ }
8008
+ /**
8009
+ * This is a workaround to ensure that videos rendered in a document PIP continue playing after being enabled
8010
+ * by the Intersection Observer. It appears to be a bug in Chrome, and we should revisit this issue once the
8011
+ * Document Picture-in-Picture feature is more reliably supported.
8012
+ * @private
8013
+ */
8014
+ function ensureDocumentPipVideosPlaying(videoTrack) {
8015
+ if (!videoTrack._documentPipWindow) {
8016
+ return;
8017
+ }
8018
+ try {
8019
+ var pipEls_1 = new WeakSet(videoTrack._documentPipWindow.document.querySelectorAll('video'));
8020
+ videoTrack._attachments.forEach(function (el) {
8021
+ if (pipEls_1.has(el) && el.paused) {
8022
+ el.play().then(function () {
8023
+ videoTrack._log.debug('Successfully played inadvertently paused video element in document PiP window');
8024
+ }).catch(function (error) {
8025
+ videoTrack._log.debug('Failed to play inadvertently paused video element in document PiP window', error);
8026
+ });
8027
+ }
8028
+ });
8029
+ }
8030
+ catch (error) {
8031
+ videoTrack._log.debug('Error checking document PiP video playback:', error);
8032
+ }
8033
+ }
7808
8034
  /**
7809
8035
  * A {@link VideoTrack}'s width and height.
7810
8036
  * @typedef {object} VideoTrack.Dimensions
@@ -7892,6 +8118,7 @@ function dimensionsChanged(track, elem) {
7892
8118
  * @param {VideoTrack} track - The {@link VideoTrack} that started
7893
8119
  * @event VideoTrack#started
7894
8120
  */
8121
+ VideoTrack._ensureDocumentPipVideosPlaying = ensureDocumentPipVideosPlaying;
7895
8122
  module.exports = VideoTrack;
7896
8123
 
7897
8124
  },{"../../util/constants":127,"../../webrtc/util":172,"./capturevideoframes":15,"./mediatrack":29,"./videoprocessoreventobserver":43}],45:[function(require,module,exports){
@@ -23995,6 +24222,11 @@ var __read = (this && this.__read) || function (o, n) {
23995
24222
  }
23996
24223
  return ar;
23997
24224
  };
24225
+ var __spreadArray = (this && this.__spreadArray) || function (to, from) {
24226
+ for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
24227
+ to[j] = from[i];
24228
+ return to;
24229
+ };
23998
24230
  var _a = require('../'), difference = _a.difference, flatMap = _a.flatMap;
23999
24231
  var setSimulcastInMediaSection = require('./simulcast');
24000
24232
  var ptToFixedBitrateAudioCodecName = {
@@ -24040,6 +24272,11 @@ function createMidToMediaSectionMap(sdp) {
24040
24272
  */
24041
24273
  function createPtToCodecName(mediaSection) {
24042
24274
  return getPayloadTypesInMediaSection(mediaSection).reduce(function (ptToCodecName, pt) {
24275
+ // NOTE(lrivas): Ignore repeated PTs to prevent RFC non‑compliant SDP generation.
24276
+ // See, https://github.com/twilio/twilio-video.js/issues/2122.
24277
+ if (ptToCodecName.has(pt)) {
24278
+ return ptToCodecName;
24279
+ }
24043
24280
  var rtpmapPattern = new RegExp("a=rtpmap:" + pt + " ([^/]+)");
24044
24281
  var matches = mediaSection.match(rtpmapPattern);
24045
24282
  var codecName = matches
@@ -24261,10 +24498,13 @@ function filterCodecsInMediaSection(section, peerMidsToMediaSections, codecsToRe
24261
24498
  // Payload Type if present.
24262
24499
  var rtxPts = codecsToPts.get('rtx') || [];
24263
24500
  // In "a=fmtp:<rtxPt> apt=<apt>", extract the codec PT <apt> associated with rtxPt.
24264
- pts = pts.concat(rtxPts.filter(function (rtxPt) {
24501
+ var additionalRtxPts = rtxPts.filter(function (rtxPt) {
24265
24502
  var fmtpAttrs = getFmtpAttributesForPt(rtxPt, section);
24266
24503
  return fmtpAttrs && pts.includes(fmtpAttrs.apt);
24267
- }));
24504
+ });
24505
+ // NOTE(lrivas): Retain rtx Payload Types that reference a codec already in `pts`.
24506
+ // Using an intermediate Set to prevent duplicates from being reintroduced after concatenation.
24507
+ pts = Array.from(new Set(__spreadArray(__spreadArray([], __read(pts)), __read(additionalRtxPts))));
24268
24508
  // Filter out the below mentioned attribute lines in the m= section that do not
24269
24509
  // belong to one of the Payload Types that are to be retained.
24270
24510
  // 1. "a=rtpmap:<pt> <codec>"
@@ -24277,7 +24517,9 @@ function filterCodecsInMediaSection(section, peerMidsToMediaSections, codecsToRe
24277
24517
  });
24278
24518
  // Filter the list of Payload Types in the first line of the m= section.
24279
24519
  var orderedPts = getPayloadTypesInMediaSection(section).filter(function (pt) { return pts.includes(pt); });
24280
- return setPayloadTypesInMediaSection(orderedPts, lines.join('\r\n'));
24520
+ // Ensure only unique Payload Types are retained.
24521
+ var uniquePayloadTypes = Array.from(new Set(orderedPts));
24522
+ return setPayloadTypesInMediaSection(uniquePayloadTypes, lines.join('\r\n'));
24281
24523
  }
24282
24524
  /**
24283
24525
  * Filter local codecs based on the remote SDP.
@@ -24561,6 +24803,7 @@ function sdpWorkaround(sdp) {
24561
24803
  function mediaSectionWorkaround(mediaSection) {
24562
24804
  var ptToCodecName = createPtToCodecName(mediaSection);
24563
24805
  mediaSection = deleteDuplicateRtxPts(mediaSection, ptToCodecName);
24806
+ mediaSection = resolvePayloadTypeConflicts(mediaSection);
24564
24807
  var codecNameToPts = createCodecNameToPts(ptToCodecName);
24565
24808
  var rtxPts = codecNameToPts.get('rtx') || new Set();
24566
24809
  var invalidRtxPts = new Set();
@@ -24588,6 +24831,42 @@ function mediaSectionWorkaround(mediaSection) {
24588
24831
  });
24589
24832
  return mediaSection;
24590
24833
  }
24834
+ /**
24835
+ * Resolve payload type conflicts where same PT maps to different codecs
24836
+ * @param {string} mediaSection
24837
+ * @returns {string} resolvedMediaSection
24838
+ */
24839
+ function resolvePayloadTypeConflicts(mediaSection) {
24840
+ var lines = mediaSection.split('\r\n');
24841
+ var seenPts = new Map(); // PT -> first codec seen
24842
+ var nextAvailablePt = 96; // Start from 96, which is the first dynamic PT. See https://datatracker.ietf.org/doc/html/rfc3551#section-3
24843
+ for (var i = 0; i < lines.length; i++) {
24844
+ var rtpmapMatch = lines[i].match(/^a=rtpmap:(\d+) ([^/]+)/);
24845
+ if (rtpmapMatch) {
24846
+ var pt = parseInt(rtpmapMatch[1], 10);
24847
+ var codec = rtpmapMatch[2].toLowerCase();
24848
+ if (seenPts.has(pt) && seenPts.get(pt) !== codec) {
24849
+ // Conflict detected - reassign this PT
24850
+ while (seenPts.has(nextAvailablePt) || nextAvailablePt === pt) {
24851
+ nextAvailablePt++;
24852
+ }
24853
+ var newPt = nextAvailablePt;
24854
+ seenPts.set(newPt, codec);
24855
+ lines[i] = lines[i].replace("a=rtpmap:" + pt, "a=rtpmap:" + newPt);
24856
+ for (var j = i + 1; j < lines.length; j++) {
24857
+ // eslint-disable-next-line max-depth
24858
+ if (lines[j].match(new RegExp("^a=(fmtp|rtcp-fb):" + pt + "\\b"))) {
24859
+ lines[j] = lines[j].replace(new RegExp("^a=(fmtp|rtcp-fb):" + pt + "\\b"), "a=$1:" + newPt);
24860
+ }
24861
+ }
24862
+ }
24863
+ else {
24864
+ seenPts.set(pt, codec);
24865
+ }
24866
+ }
24867
+ }
24868
+ return lines.join('\r\n');
24869
+ }
24591
24870
  /**
24592
24871
  * @param {string} mediaSection
24593
24872
  * @param {Map<PT, Codec>} ptToCodecName
@@ -30942,7 +31221,7 @@ module.exports={
30942
31221
  "name": "twilio-video",
30943
31222
  "title": "Twilio Video",
30944
31223
  "description": "Twilio Video JavaScript Library",
30945
- "version": "2.32.0",
31224
+ "version": "2.32.1",
30946
31225
  "homepage": "https://twilio.com",
30947
31226
  "author": "Mark Andrus Roberts <mroberts@twilio.com>",
30948
31227
  "contributors": [
@@ -31032,7 +31311,7 @@ module.exports={
31032
31311
  "webrtc-adapter": "^7.7.1"
31033
31312
  },
31034
31313
  "engines": {
31035
- "node": ">=0.12"
31314
+ "node": ">=22"
31036
31315
  },
31037
31316
  "license": "BSD-3-Clause",
31038
31317
  "main": "./es5/index.js",
@@ -31092,12 +31371,10 @@ module.exports={
31092
31371
  "dependencies": {
31093
31372
  "events": "^3.3.0",
31094
31373
  "util": "^0.12.4",
31095
- "ws": "^7.4.6",
31096
- "xmlhttprequest": "^1.8.0"
31374
+ "ws": "^7.4.6"
31097
31375
  },
31098
31376
  "browser": {
31099
- "ws": "./src/ws.js",
31100
- "xmlhttprequest": "./src/xmlhttprequest.js"
31377
+ "ws": "./src/ws.js"
31101
31378
  }
31102
31379
  }
31103
31380