mediasoup-client 3.24.1 → 3.24.4

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.
@@ -238,115 +238,131 @@ class Safari12 extends enhancedEvents_1.EnhancedEventEmitter {
238
238
  this.assertNotClosed();
239
239
  this.assertSendDirection();
240
240
  logger.debug('send() [kind:%s, track.id:%s, streamId:%s]', track.kind, track.id, streamId);
241
+ if (encodings && encodings.length > 1) {
242
+ // Set rid and verify scalabilityMode in each encoding.
243
+ // NOTE: Even if WebRTC allows different scalabilityMode (different number
244
+ // of temporal layers) per simulcast stream, we need that those are the
245
+ // same in all them, so let's pick up the highest value.
246
+ // NOTE: If scalabilityMode is not given, Safari will use L1T3.
247
+ let maxTemporalLayers = 1;
248
+ for (const encoding of encodings) {
249
+ const temporalLayers = encoding.scalabilityMode
250
+ ? (0, scalabilityModes_1.parse)(encoding.scalabilityMode).temporalLayers
251
+ : 3;
252
+ if (temporalLayers > maxTemporalLayers) {
253
+ maxTemporalLayers = temporalLayers;
254
+ }
255
+ }
256
+ encodings.forEach((encoding, idx) => {
257
+ encoding.rid = `r${idx}`;
258
+ encoding.scalabilityMode = `L1T${maxTemporalLayers}`;
259
+ });
260
+ }
241
261
  const mediaSectionIdx = this._remoteSdp.getNextMediaSectionIdx();
242
262
  const transceiver = this._pc.addTransceiver(track, {
243
263
  direction: 'sendonly',
244
264
  streams: [this._sendStream],
245
265
  sendEncodings: encodings,
246
266
  });
247
- if (this._forcedRtpExtensions) {
248
- ortcUtils.applyForcedRtpExtensions(transceiver, this._forcedRtpExtensions);
249
- }
250
- if (onRtpSender) {
251
- onRtpSender(transceiver.sender);
252
- }
253
- let offer = await this._pc.createOffer();
254
- let localSdpObject = sdpTransform.parse(offer.sdp);
255
- if (localSdpObject.extmapAllowMixed) {
256
- this._remoteSdp.setSessionExtmapAllowMixed();
257
- }
258
- const nativeRtpCapabilities = Safari12.getLocalRtpCapabilities(localSdpObject);
259
- const sendExtendedRtpCapabilities = this._getSendExtendedRtpCapabilities(nativeRtpCapabilities);
260
- // Generic sending RTP parameters.
261
- const sendingRtpParameters = ortc.getSendingRtpParameters(track.kind, sendExtendedRtpCapabilities);
262
- // This may throw.
263
- sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec);
264
- // Generic sending RTP parameters suitable for the SDP remote answer.
265
- const sendingRemoteRtpParameters = ortc.getSendingRemoteRtpParameters(track.kind, sendExtendedRtpCapabilities);
266
- // This may throw.
267
- sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec);
268
- let offerMediaObject;
269
- if (!this._transportReady) {
270
- await this.setupTransport({
271
- localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
272
- localSdpObject,
267
+ try {
268
+ if (this._forcedRtpExtensions) {
269
+ ortcUtils.applyForcedRtpExtensions(transceiver, this._forcedRtpExtensions);
270
+ }
271
+ if (onRtpSender) {
272
+ onRtpSender(transceiver.sender);
273
+ }
274
+ const offer = await this._pc.createOffer();
275
+ let localSdpObject = sdpTransform.parse(offer.sdp);
276
+ if (localSdpObject.extmapAllowMixed) {
277
+ this._remoteSdp.setSessionExtmapAllowMixed();
278
+ }
279
+ const nativeRtpCapabilities = Safari12.getLocalRtpCapabilities(localSdpObject);
280
+ const sendExtendedRtpCapabilities = this._getSendExtendedRtpCapabilities(nativeRtpCapabilities);
281
+ // Generic sending RTP parameters.
282
+ const sendingRtpParameters = ortc.getSendingRtpParameters(track.kind, sendExtendedRtpCapabilities);
283
+ // This may throw.
284
+ sendingRtpParameters.codecs = ortc.reduceCodecs(sendingRtpParameters.codecs, codec);
285
+ // Generic sending RTP parameters suitable for the SDP remote answer.
286
+ const sendingRemoteRtpParameters = ortc.getSendingRemoteRtpParameters(track.kind, sendExtendedRtpCapabilities);
287
+ // This may throw.
288
+ sendingRemoteRtpParameters.codecs = ortc.reduceCodecs(sendingRemoteRtpParameters.codecs, codec);
289
+ if (!this._transportReady) {
290
+ await this.setupTransport({
291
+ localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
292
+ localSdpObject,
293
+ });
294
+ }
295
+ logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer);
296
+ await this._pc.setLocalDescription(offer);
297
+ // We can now get the transceiver.mid.
298
+ const localId = transceiver.mid;
299
+ // Set MID.
300
+ sendingRtpParameters.mid = localId;
301
+ localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
302
+ const offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];
303
+ // Set RTCP CNAME.
304
+ sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({
305
+ offerMediaObject,
273
306
  });
274
- }
275
- const layers = (0, scalabilityModes_1.parse)((encodings ?? [{}])[0].scalabilityMode);
276
- if (encodings && encodings.length > 1) {
277
- logger.debug('send() | enabling legacy simulcast');
278
- localSdpObject = sdpTransform.parse(offer.sdp);
279
- offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];
280
- sdpUnifiedPlanUtils.addLegacySimulcast({
307
+ // Set msid.
308
+ sendingRtpParameters.msid = `${streamId ?? this._sendStream.id} ${track.id}`;
309
+ // Set RTP encodings by parsing the SDP offer if no encodings are given.
310
+ if (!encodings) {
311
+ sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({
312
+ offerMediaObject,
313
+ codecs: sendingRtpParameters.codecs,
314
+ });
315
+ }
316
+ // Set RTP encodings by parsing the SDP offer and complete them with given
317
+ // one if just a single encoding has been given.
318
+ else if (encodings.length === 1) {
319
+ const newEncodings = sdpUnifiedPlanUtils.getRtpEncodings({
320
+ offerMediaObject,
321
+ codecs: sendingRtpParameters.codecs,
322
+ });
323
+ Object.assign(newEncodings[0], encodings[0]);
324
+ sendingRtpParameters.encodings = newEncodings;
325
+ }
326
+ // Otherwise if more than 1 encoding are given use them verbatim.
327
+ else {
328
+ sendingRtpParameters.encodings = encodings;
329
+ }
330
+ this._remoteSdp.send({
281
331
  offerMediaObject,
282
- numStreams: encodings.length,
332
+ reuseMid: mediaSectionIdx.reuseMid,
333
+ offerRtpParameters: sendingRtpParameters,
334
+ answerRtpParameters: sendingRemoteRtpParameters,
335
+ codecOptions,
283
336
  });
284
- offer = {
285
- type: 'offer',
286
- sdp: sdpTransform.write(localSdpObject),
337
+ const answer = {
338
+ type: 'answer',
339
+ sdp: this._remoteSdp.getSdp(),
340
+ };
341
+ logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer);
342
+ await this._pc.setRemoteDescription(answer);
343
+ // Store in the map.
344
+ this._mapMidTransceiver.set(localId, transceiver);
345
+ return {
346
+ localId,
347
+ rtpParameters: sendingRtpParameters,
348
+ rtpSender: transceiver.sender,
287
349
  };
288
350
  }
289
- logger.debug('send() | calling pc.setLocalDescription() [offer:%o]', offer);
290
- await this._pc.setLocalDescription(offer);
291
- // We can now get the transceiver.mid.
292
- const localId = transceiver.mid;
293
- // Set MID.
294
- sendingRtpParameters.mid = localId;
295
- localSdpObject = sdpTransform.parse(this._pc.localDescription.sdp);
296
- offerMediaObject = localSdpObject.media[mediaSectionIdx.idx];
297
- // Set RTCP CNAME.
298
- sendingRtpParameters.rtcp.cname = sdpCommonUtils.getCname({
299
- offerMediaObject,
300
- });
301
- // Set msid.
302
- sendingRtpParameters.msid = `${streamId ?? this._sendStream.id} ${track.id}`;
303
- // Set RTP encodings.
304
- sendingRtpParameters.encodings = sdpUnifiedPlanUtils.getRtpEncodings({
305
- offerMediaObject,
306
- codecs: sendingRtpParameters.codecs,
307
- });
308
- // Complete encodings with given values.
309
- if (encodings) {
310
- for (let idx = 0; idx < sendingRtpParameters.encodings.length; ++idx) {
311
- if (encodings[idx]) {
312
- Object.assign(sendingRtpParameters.encodings[idx], encodings[idx]);
313
- }
314
- }
315
- }
316
- // If VP8 or H264 and there is effective simulcast, add scalabilityMode to
317
- // each encoding.
318
- if (sendingRtpParameters.encodings.length > 1 &&
319
- (sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/vp8' ||
320
- sendingRtpParameters.codecs[0].mimeType.toLowerCase() === 'video/h264')) {
321
- for (const encoding of sendingRtpParameters.encodings) {
322
- if (encoding.scalabilityMode) {
323
- encoding.scalabilityMode = `L1T${layers.temporalLayers}`;
324
- }
325
- else {
326
- encoding.scalabilityMode = 'L1T3';
351
+ catch (error) {
352
+ // Undo the changes done in the PeerConnection, otherwise it gets out of
353
+ // sync with the remote SDP and later calls to send() fail.
354
+ try {
355
+ if (this._pc.signalingState === 'have-local-offer') {
356
+ try {
357
+ await this._pc.setLocalDescription({ type: 'rollback' });
358
+ }
359
+ catch (error2) { }
327
360
  }
361
+ transceiver.stop();
328
362
  }
363
+ catch (error3) { }
364
+ throw error;
329
365
  }
330
- this._remoteSdp.send({
331
- offerMediaObject,
332
- reuseMid: mediaSectionIdx.reuseMid,
333
- offerRtpParameters: sendingRtpParameters,
334
- answerRtpParameters: sendingRemoteRtpParameters,
335
- codecOptions,
336
- });
337
- const answer = {
338
- type: 'answer',
339
- sdp: this._remoteSdp.getSdp(),
340
- };
341
- logger.debug('send() | calling pc.setRemoteDescription() [answer:%o]', answer);
342
- await this._pc.setRemoteDescription(answer);
343
- // Store in the map.
344
- this._mapMidTransceiver.set(localId, transceiver);
345
- return {
346
- localId,
347
- rtpParameters: sendingRtpParameters,
348
- rtpSender: transceiver.sender,
349
- };
350
366
  }
351
367
  async stopSending(localId) {
352
368
  this.assertSendDirection();
@@ -514,25 +530,43 @@ class Safari12 extends enhancedEvents_1.EnhancedEventEmitter {
514
530
  // If this is the first DataChannel we need to create the SDP answer with
515
531
  // m=application section.
516
532
  if (!this._hasDataChannelMediaSection) {
517
- const offer = await this._pc.createOffer();
518
- const localSdpObject = sdpTransform.parse(offer.sdp);
519
- const offerMediaObject = localSdpObject.media.find(m => m.type === 'application');
520
- if (!this._transportReady) {
521
- await this.setupTransport({
522
- localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
523
- localSdpObject,
524
- });
533
+ try {
534
+ const offer = await this._pc.createOffer();
535
+ const localSdpObject = sdpTransform.parse(offer.sdp);
536
+ const offerMediaObject = localSdpObject.media.find(m => m.type === 'application');
537
+ if (!this._transportReady) {
538
+ await this.setupTransport({
539
+ localDtlsRole: this._forcedLocalDtlsRole ?? 'client',
540
+ localSdpObject,
541
+ });
542
+ }
543
+ logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer);
544
+ await this._pc.setLocalDescription(offer);
545
+ this._remoteSdp.sendSctpAssociation({ offerMediaObject });
546
+ const answer = {
547
+ type: 'answer',
548
+ sdp: this._remoteSdp.getSdp(),
549
+ };
550
+ logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer);
551
+ await this._pc.setRemoteDescription(answer);
552
+ this._hasDataChannelMediaSection = true;
553
+ }
554
+ catch (error) {
555
+ // Undo the changes done in the PeerConnection, otherwise it gets out of
556
+ // sync with the remote SDP and later calls to send() and
557
+ // sendDataChannel() fail.
558
+ try {
559
+ if (this._pc.signalingState === 'have-local-offer') {
560
+ try {
561
+ await this._pc.setLocalDescription({ type: 'rollback' });
562
+ }
563
+ catch (error2) { }
564
+ }
565
+ dataChannel.close();
566
+ }
567
+ catch (error3) { }
568
+ throw error;
525
569
  }
526
- logger.debug('sendDataChannel() | calling pc.setLocalDescription() [offer:%o]', offer);
527
- await this._pc.setLocalDescription(offer);
528
- this._remoteSdp.sendSctpAssociation({ offerMediaObject });
529
- const answer = {
530
- type: 'answer',
531
- sdp: this._remoteSdp.getSdp(),
532
- };
533
- logger.debug('sendDataChannel() | calling pc.setRemoteDescription() [answer:%o]', answer);
534
- await this._pc.setRemoteDescription(answer);
535
- this._hasDataChannelMediaSection = true;
536
570
  }
537
571
  const newSctpStreamParameters = {
538
572
  streamId: options.id,
package/lib/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export * as types from './types';
6
6
  /**
7
7
  * Expose mediasoup-client version.
8
8
  */
9
- export declare const version = "3.24.1";
9
+ export declare const version = "3.24.4";
10
10
  /**
11
11
  * Expose Device class and device detector helpers.
12
12
  */
package/lib/index.js CHANGED
@@ -46,7 +46,7 @@ exports.types = __importStar(require("./types"));
46
46
  /**
47
47
  * Expose mediasoup-client version.
48
48
  */
49
- exports.version = '3.24.1';
49
+ exports.version = '3.24.4';
50
50
  /**
51
51
  * Expose Device class and device detector helpers.
52
52
  */
package/lib/ortc.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ortc.d.ts","sourceRoot":"","sources":["../src/ortc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,eAAe,EACf,SAAS,EACT,kBAAkB,EAElB,aAAa,EACb,kBAAkB,EAKlB,uBAAuB,EAGvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAO7D;;;;GAIG;AACH,wBAAgB,mCAAmC,CAClD,IAAI,EAAE,eAAe,GACnB,IAAI,CA0BN;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAiD7E;AAED;;;;GAIG;AACH,wBAAgB,wCAAwC,CACvD,MAAM,EAAE,oBAAoB,GAC1B,IAAI,CA8DN;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACzC,SAAS,EAAE,eAAe,EAC1B,UAAU,EAAE,eAAe,EAC3B,sBAAsB,EAAE,OAAO,GAC7B,uBAAuB,CAiJzB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,uBAAuB,EAAE,uBAAuB,GAC9C,eAAe,CAEjB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,uBAAuB,EAAE,uBAAuB,GAC9C,eAAe,CAEjB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,SAAS,EACf,uBAAuB,EAAE,uBAAuB,GAC9C,aAAa,CA8Df;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,SAAS,EACf,uBAAuB,EAAE,uBAAuB,GAC9C,aAAa,CA+Ff;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,QAAQ,CAAC,EAAE,kBAAkB,GAC3B,kBAAkB,EAAE,CA+BtB;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,kBAAkB,EAAE,aAAa,GAC/B,aAAa,CAoBf;AAED;;GAEG;AACH,wBAAgB,OAAO,CACtB,IAAI,EAAE,SAAS,EACf,eAAe,EAAE,eAAe,GAC9B,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACzB,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,eAAe,GAC9B,OAAO,CAaT"}
1
+ {"version":3,"file":"ortc.d.ts","sourceRoot":"","sources":["../src/ortc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,eAAe,EACf,SAAS,EACT,kBAAkB,EAElB,aAAa,EACb,kBAAkB,EAKlB,uBAAuB,EAGvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAO7D;;;;GAIG;AACH,wBAAgB,mCAAmC,CAClD,IAAI,EAAE,eAAe,GACnB,IAAI,CA0BN;AAED;;;;GAIG;AACH,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAiD7E;AAED;;;;GAIG;AACH,wBAAgB,wCAAwC,CACvD,MAAM,EAAE,oBAAoB,GAC1B,IAAI,CAsEN;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACzC,SAAS,EAAE,eAAe,EAC1B,UAAU,EAAE,eAAe,EAC3B,sBAAsB,EAAE,OAAO,GAC7B,uBAAuB,CAiJzB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,uBAAuB,EAAE,uBAAuB,GAC9C,eAAe,CAEjB;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACrC,uBAAuB,EAAE,uBAAuB,GAC9C,eAAe,CAEjB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,IAAI,EAAE,SAAS,EACf,uBAAuB,EAAE,uBAAuB,GAC9C,aAAa,CA8Df;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,IAAI,EAAE,SAAS,EACf,uBAAuB,EAAE,uBAAuB,GAC9C,aAAa,CA+Ff;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,kBAAkB,EAAE,EAC5B,QAAQ,CAAC,EAAE,kBAAkB,GAC3B,kBAAkB,EAAE,CA+BtB;AAED;;GAEG;AACH,wBAAgB,6BAA6B,CAC5C,kBAAkB,EAAE,aAAa,GAC/B,aAAa,CAoBf;AAED;;GAEG;AACH,wBAAgB,OAAO,CACtB,IAAI,EAAE,SAAS,EACf,eAAe,EAAE,eAAe,GAC9B,OAAO,CAET;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACzB,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,eAAe,GAC9B,OAAO,CAaT"}
package/lib/ortc.js CHANGED
@@ -151,24 +151,28 @@ function validateAndNormalizeSctpStreamParameters(params) {
151
151
  params.ordered = true;
152
152
  }
153
153
  // maxPacketLifeTime is optional.
154
- if (params.maxPacketLifeTime &&
154
+ if (params.maxPacketLifeTime !== undefined &&
155
155
  typeof params.maxPacketLifeTime !== 'number') {
156
156
  throw new TypeError('invalid params.maxPacketLifeTime');
157
157
  }
158
158
  // maxRetransmits is optional.
159
- if (params.maxRetransmits && typeof params.maxRetransmits !== 'number') {
159
+ if (params.maxRetransmits !== undefined &&
160
+ typeof params.maxRetransmits !== 'number') {
160
161
  throw new TypeError('invalid params.maxRetransmits');
161
162
  }
162
- if (params.maxPacketLifeTime && params.maxRetransmits) {
163
+ if (params.maxPacketLifeTime !== undefined &&
164
+ params.maxRetransmits !== undefined) {
163
165
  throw new TypeError('cannot provide both maxPacketLifeTime and maxRetransmits');
164
166
  }
165
167
  if (orderedGiven &&
166
168
  params.ordered &&
167
- (params.maxPacketLifeTime || params.maxRetransmits)) {
169
+ (params.maxPacketLifeTime !== undefined ||
170
+ params.maxRetransmits !== undefined)) {
168
171
  throw new TypeError('cannot be ordered with maxPacketLifeTime or maxRetransmits');
169
172
  }
170
173
  else if (!orderedGiven &&
171
- (params.maxPacketLifeTime || params.maxRetransmits)) {
174
+ (params.maxPacketLifeTime !== undefined ||
175
+ params.maxRetransmits !== undefined)) {
172
176
  params.ordered = false;
173
177
  }
174
178
  // label is optional.
package/lib/test/test.js CHANGED
@@ -761,6 +761,31 @@ test('transport.produceData() succeeds', async () => {
761
761
  expect(dataProducer.label).toBe('FOO');
762
762
  expect(dataProducer.protocol).toBe('BAR');
763
763
  }, 500);
764
+ test('transport.produceData() with maxRetransmits 0 or maxPacketLifeTime 0 creates an unordered DataProducer', async () => {
765
+ const dataProducer1 = await ctx.connectedSendTransport.produceData({
766
+ maxRetransmits: 0,
767
+ });
768
+ expect(dataProducer1.sctpStreamParameters.ordered).toBe(false);
769
+ expect(dataProducer1.sctpStreamParameters.maxRetransmits).toBe(0);
770
+ const dataProducer2 = await ctx.connectedSendTransport.produceData({
771
+ maxPacketLifeTime: 0,
772
+ });
773
+ expect(dataProducer2.sctpStreamParameters.ordered).toBe(false);
774
+ expect(dataProducer2.sctpStreamParameters.maxPacketLifeTime).toBe(0);
775
+ }, 500);
776
+ test('transport.produceData() closes the DataChannel if the "producedata" listener rejects', async () => {
777
+ const handlerSpy = jest.spyOn(ctx.connectedSendTransport.handler, 'sendDataChannel');
778
+ const error = new Error('produceData() failed');
779
+ const listener = (parameters, callback, errback) => {
780
+ errback(error);
781
+ };
782
+ ctx.connectedSendTransport.prependListener('producedata', listener);
783
+ await expect(ctx.connectedSendTransport.produceData()).rejects.toThrow(error);
784
+ const { dataChannel } = await handlerSpy.mock.results[0].value;
785
+ expect(dataChannel.readyState).toBe('closed');
786
+ ctx.connectedSendTransport.off('producedata', listener);
787
+ handlerSpy.mockRestore();
788
+ }, 500);
764
789
  test('transport.produceData() in a receiving Transport rejects with UnsupportedError', async () => {
765
790
  await expect(ctx.recvTransport.produceData({})).rejects.toThrow(errors_1.UnsupportedError);
766
791
  }, 500);
@@ -904,7 +929,7 @@ test('producer.replaceTrack() with a new track succeeds', async () => {
904
929
  const videoProducerPreviousTrack = ctx.videoProducer.track;
905
930
  const newVideoTrack = new fake_mediastreamtrack_1.FakeMediaStreamTrack({ kind: 'video' });
906
931
  await expect(ctx.videoProducer.replaceTrack({ track: newVideoTrack })).resolves.toBe(undefined);
907
- // Previous track must be 'ended' due to stopTracks: true.
932
+ // Previous track must be 'ended' due to stopTracks: true (default value).
908
933
  expect(videoProducerPreviousTrack?.readyState).toBe('ended');
909
934
  expect(ctx.videoProducer.track).not.toBe(videoProducerPreviousTrack);
910
935
  expect(ctx.videoProducer.track).toBe(newVideoTrack);
@@ -949,6 +974,34 @@ test('producer.setMaxSpatialLayer() succeeds', async () => {
949
974
  await expect(ctx.videoProducer.setMaxSpatialLayer(0)).resolves.toBe(undefined);
950
975
  expect(ctx.videoProducer.maxSpatialLayer).toBe(0);
951
976
  }, 500);
977
+ test('producer.replaceTrack() rejects with InvalidStateError and stops the new track if the Producer is closed while replacing', async () => {
978
+ const previousTrack = ctx.videoProducer.track;
979
+ const newTrack = new fake_mediastreamtrack_1.FakeMediaStreamTrack({ kind: 'video' });
980
+ const replacePromise = ctx.videoProducer.replaceTrack({ track: newTrack });
981
+ ctx.videoProducer.close();
982
+ await expect(replacePromise).rejects.toThrow(errors_1.InvalidStateError);
983
+ expect(previousTrack?.readyState).toBe('ended');
984
+ // The new track must be 'ended' due to stopTracks: true (default value).
985
+ expect(newTrack.readyState).toBe('ended');
986
+ expect(ctx.videoProducer.track).toBe(previousTrack);
987
+ }, 500);
988
+ test('producer.replaceTrack() stops the new track if the Transport is closed while replacing', async () => {
989
+ const newTrack = new fake_mediastreamtrack_1.FakeMediaStreamTrack({ kind: 'video' });
990
+ const replacePromise = ctx.videoProducer.replaceTrack({ track: newTrack });
991
+ ctx.connectedSendTransport.close();
992
+ await expect(replacePromise).rejects.toThrow();
993
+ // The new track must be 'ended' due to stopTracks: true (default value).
994
+ expect(newTrack.readyState).toBe('ended');
995
+ }, 500);
996
+ test('producer.setMaxSpatialLayer() rejects and keeps the previous value if the handler fails', async () => {
997
+ const previousMaxSpatialLayer = ctx.videoProducer.maxSpatialLayer;
998
+ const error = new Error('setParameters() failed');
999
+ const handlerSpy = jest.spyOn(ctx.connectedSendTransport.handler, 'setMaxSpatialLayer');
1000
+ handlerSpy.mockRejectedValueOnce(error);
1001
+ await expect(ctx.videoProducer.setMaxSpatialLayer(1)).rejects.toThrow(error);
1002
+ expect(ctx.videoProducer.maxSpatialLayer).toBe(previousMaxSpatialLayer);
1003
+ handlerSpy.mockRestore();
1004
+ }, 500);
952
1005
  test('producer.setMaxSpatialLayer() in an audio Producer rejects with UnsupportedError', async () => {
953
1006
  await expect(ctx.audioProducer.setMaxSpatialLayer(1)).rejects.toThrow(errors_1.UnsupportedError);
954
1007
  expect(ctx.audioProducer.maxSpatialLayer).toBe(undefined);
@@ -1083,6 +1136,12 @@ test('transport.produce() rejects with InvalidStateError if closed', async () =>
1083
1136
  // The track must be 'live' due to stopTracks: false.
1084
1137
  expect(track.readyState).toBe('live');
1085
1138
  }, 500);
1139
+ test('transport.produce() stops the track if the Transport is closed', async () => {
1140
+ const track = new fake_mediastreamtrack_1.FakeMediaStreamTrack({ kind: 'audio' });
1141
+ ctx.connectedSendTransport.close();
1142
+ await expect(ctx.connectedSendTransport.produce({ track })).rejects.toThrow(errors_1.InvalidStateError);
1143
+ expect(track.readyState).toBe('ended');
1144
+ }, 500);
1086
1145
  test('transport.consume() rejects with InvalidStateError if closed', async () => {
1087
1146
  ctx.connectedRecvTransport.close();
1088
1147
  // @ts-expect-error --- On purpose.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasoup-client",
3
- "version": "3.24.1",
3
+ "version": "3.24.4",
4
4
  "description": "mediasoup client side TypeScript library",
5
5
  "contributors": [
6
6
  "Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)",
@@ -91,13 +91,13 @@
91
91
  "eslint-config-prettier": "^10.1.8",
92
92
  "eslint-plugin-jest": "^29.16.6",
93
93
  "eslint-plugin-prettier": "^5.5.6",
94
- "globals": "^17.12.0",
94
+ "globals": "^17.13.0",
95
95
  "jest": "^30.5.2",
96
- "knip": "^6.38.0",
96
+ "knip": "^6.39.0",
97
97
  "open-cli": "^9.0.0",
98
98
  "prettier": "^3.9.9",
99
- "ts-jest": "^29.4.13",
99
+ "ts-jest": "^29.4.14",
100
100
  "typescript": "^6.0.3",
101
- "typescript-eslint": "^8.70.1"
101
+ "typescript-eslint": "^8.71.0"
102
102
  }
103
103
  }