werift 0.14.5 → 0.15.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 (64) hide show
  1. package/lib/common/src/binary.d.ts +0 -1
  2. package/lib/common/src/binary.js +1 -2
  3. package/lib/common/src/binary.js.map +1 -1
  4. package/lib/dtls/src/context/cipher.js +5 -2
  5. package/lib/dtls/src/context/cipher.js.map +1 -1
  6. package/lib/dtls/src/handshake/extensions/useSrtp.js +5 -2
  7. package/lib/dtls/src/handshake/extensions/useSrtp.js.map +1 -1
  8. package/lib/ice/src/candidate.js +5 -2
  9. package/lib/ice/src/candidate.js.map +1 -1
  10. package/lib/ice/src/ice.d.ts +2 -2
  11. package/lib/ice/src/ice.js +19 -8
  12. package/lib/ice/src/ice.js.map +1 -1
  13. package/lib/ice/src/stun/attributes.js +5 -2
  14. package/lib/ice/src/stun/attributes.js.map +1 -1
  15. package/lib/rtp/src/rtcp/rr.js +5 -2
  16. package/lib/rtp/src/rtcp/rr.js.map +1 -1
  17. package/lib/rtp/src/rtcp/rtpfb/nack.js +6 -3
  18. package/lib/rtp/src/rtcp/rtpfb/nack.js.map +1 -1
  19. package/lib/rtp/src/rtcp/rtpfb/twcc.js +6 -6
  20. package/lib/rtp/src/rtcp/rtpfb/twcc.js.map +1 -1
  21. package/lib/rtp/src/rtcp/sr.js +5 -2
  22. package/lib/rtp/src/rtcp/sr.js.map +1 -1
  23. package/lib/rtp/src/srtp/cipher/ctr.js +5 -2
  24. package/lib/rtp/src/srtp/cipher/ctr.js.map +1 -1
  25. package/lib/rtp/src/srtp/cipher/gcm.js +6 -3
  26. package/lib/rtp/src/srtp/cipher/gcm.js.map +1 -1
  27. package/lib/sctp/src/param.js +5 -2
  28. package/lib/sctp/src/param.js.map +1 -1
  29. package/lib/sctp/src/sctp.js +5 -5
  30. package/lib/sctp/src/sctp.js.map +1 -1
  31. package/lib/webrtc/src/dataChannel.js +3 -2
  32. package/lib/webrtc/src/dataChannel.js.map +1 -1
  33. package/lib/webrtc/src/media/receiver/nack.js +2 -2
  34. package/lib/webrtc/src/media/receiver/nack.js.map +1 -1
  35. package/lib/webrtc/src/media/rtpReceiver.d.ts +3 -2
  36. package/lib/webrtc/src/media/rtpReceiver.js +4 -2
  37. package/lib/webrtc/src/media/rtpReceiver.js.map +1 -1
  38. package/lib/webrtc/src/media/rtpSender.d.ts +4 -2
  39. package/lib/webrtc/src/media/rtpSender.js +15 -7
  40. package/lib/webrtc/src/media/rtpSender.js.map +1 -1
  41. package/lib/webrtc/src/media/rtpTransceiver.d.ts +7 -6
  42. package/lib/webrtc/src/media/rtpTransceiver.js +10 -3
  43. package/lib/webrtc/src/media/rtpTransceiver.js.map +1 -1
  44. package/lib/webrtc/src/peerConnection.d.ts +10 -10
  45. package/lib/webrtc/src/peerConnection.js +356 -211
  46. package/lib/webrtc/src/peerConnection.js.map +1 -1
  47. package/lib/webrtc/src/sdp.js +5 -2
  48. package/lib/webrtc/src/sdp.js.map +1 -1
  49. package/lib/webrtc/src/transport/dtls.d.ts +3 -2
  50. package/lib/webrtc/src/transport/dtls.js +3 -0
  51. package/lib/webrtc/src/transport/dtls.js.map +1 -1
  52. package/lib/webrtc/src/transport/sctp.d.ts +5 -3
  53. package/lib/webrtc/src/transport/sctp.js +46 -34
  54. package/lib/webrtc/src/transport/sctp.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/dataChannel.ts +4 -2
  57. package/src/media/receiver/nack.ts +1 -1
  58. package/src/media/rtpReceiver.ts +6 -5
  59. package/src/media/rtpSender.ts +18 -9
  60. package/src/media/rtpTransceiver.ts +17 -6
  61. package/src/peerConnection.ts +431 -253
  62. package/src/sdp.ts +1 -1
  63. package/src/transport/dtls.ts +4 -1
  64. package/src/transport/sctp.ts +53 -33
@@ -1,5 +1,6 @@
1
1
  import debug from "debug";
2
- import { cloneDeep, isEqual } from "lodash";
2
+ import cloneDeep from "lodash/cloneDeep";
3
+ import isEqual from "lodash/isEqual";
3
4
  import Event from "rx.mini";
4
5
  import * as uuid from "uuid";
5
6
 
@@ -61,8 +62,6 @@ const log = debug("werift:packages/webrtc/src/peerConnection.ts");
61
62
 
62
63
  export class RTCPeerConnection extends EventTarget {
63
64
  readonly cname = uuid.v4();
64
- iceTransport: RTCIceTransport;
65
- dtlsTransport: RTCDtlsTransport;
66
65
  sctpTransport?: RTCSctpTransport;
67
66
  masterTransportEstablished = false;
68
67
  configuration: Required<PeerConfig> =
@@ -73,17 +72,13 @@ export class RTCPeerConnection extends EventTarget {
73
72
  signalingState: RTCSignalingState = "stable";
74
73
  negotiationneeded = false;
75
74
  readonly transceivers: RTCRtpTransceiver[] = [];
75
+
76
76
  readonly iceGatheringStateChange = new Event<[IceGathererState]>();
77
77
  readonly iceConnectionStateChange = new Event<[RTCIceConnectionState]>();
78
78
  readonly signalingStateChange = new Event<[RTCSignalingState]>();
79
79
  readonly connectionStateChange = new Event<[ConnectionState]>();
80
80
  readonly onDataChannel = new Event<[RTCDataChannel]>();
81
81
  readonly onRemoteTransceiverAdded = new Event<[RTCRtpTransceiver]>();
82
- /**
83
- * should use onRemoteTransceiverAdded
84
- * @deprecated
85
- */
86
- readonly onTransceiver = new Event<[RTCRtpTransceiver]>();
87
82
  readonly onTransceiverAdded = new Event<[RTCRtpTransceiver]>();
88
83
  readonly onIceCandidate = new Event<[RTCIceCandidate]>();
89
84
  readonly onNegotiationneeded = new Event<[]>();
@@ -106,6 +101,22 @@ export class RTCPeerConnection extends EventTarget {
106
101
  private isClosed = false;
107
102
  private shouldNegotiationneeded = false;
108
103
 
104
+ get dtlsTransports() {
105
+ const transports = this.transceivers.map((t) => t.dtlsTransport);
106
+ if (this.sctpTransport) {
107
+ transports.push(this.sctpTransport.dtlsTransport);
108
+ }
109
+ return transports.reduce((acc: RTCDtlsTransport[], cur) => {
110
+ if (!acc.map((d) => d.id).includes(cur.id)) {
111
+ acc.push(cur);
112
+ }
113
+ return acc;
114
+ }, []);
115
+ }
116
+ get iceTransports() {
117
+ return this.dtlsTransports.map((d) => d.iceTransport);
118
+ }
119
+
109
120
  constructor({
110
121
  codecs,
111
122
  headerExtensions,
@@ -113,6 +124,7 @@ export class RTCPeerConnection extends EventTarget {
113
124
  iceTransportPolicy,
114
125
  icePortRange,
115
126
  dtls,
127
+ bundlePolicy,
116
128
  }: Partial<PeerConfig> = {}) {
117
129
  super();
118
130
 
@@ -179,13 +191,9 @@ export class RTCPeerConnection extends EventTarget {
179
191
  break;
180
192
  }
181
193
  });
182
-
183
- const { iceTransport, dtlsTransport } = this.createTransport([
184
- SRTP_PROFILE.SRTP_AEAD_AES_128_GCM, // prefer
185
- SRTP_PROFILE.SRTP_AES128_CM_HMAC_SHA1_80,
186
- ]);
187
- this.iceTransport = iceTransport;
188
- this.dtlsTransport = dtlsTransport;
194
+ if (bundlePolicy) {
195
+ this.configuration.bundlePolicy = bundlePolicy;
196
+ }
189
197
  }
190
198
 
191
199
  get localDescription() {
@@ -217,9 +225,7 @@ export class RTCPeerConnection extends EventTarget {
217
225
  }
218
226
 
219
227
  async createOffer() {
220
- if (this.certificates.length === 0) {
221
- await this.dtlsTransport.setupCertificate();
222
- }
228
+ await this.ensureCerts();
223
229
 
224
230
  this.transceivers.forEach((transceiver) => {
225
231
  transceiver.codecs = this.configuration.codecs[transceiver.kind];
@@ -291,11 +297,13 @@ export class RTCPeerConnection extends EventTarget {
291
297
  );
292
298
  }
293
299
 
294
- const mids = description.media
295
- .map((m) => m.rtp.muxId)
296
- .filter((v) => v) as string[];
297
- const bundle = new GroupDescription("BUNDLE", mids);
298
- description.group.push(bundle);
300
+ if (this.configuration.bundlePolicy !== "disable") {
301
+ const mids = description.media
302
+ .map((m) => m.rtp.muxId)
303
+ .filter((v) => v) as string[];
304
+ const bundle = new GroupDescription("BUNDLE", mids);
305
+ description.group.push(bundle);
306
+ }
299
307
 
300
308
  return description.toJSON();
301
309
  }
@@ -337,7 +345,8 @@ export class RTCPeerConnection extends EventTarget {
337
345
  protocol: settings.protocol,
338
346
  });
339
347
 
340
- return new RTCDataChannel(this.sctpTransport, parameters);
348
+ const channel = new RTCDataChannel(this.sctpTransport, parameters);
349
+ return channel;
341
350
  }
342
351
 
343
352
  removeTrack(sender: RTCRtpSender) {
@@ -380,20 +389,33 @@ export class RTCPeerConnection extends EventTarget {
380
389
  };
381
390
 
382
391
  private createTransport(srtpProfiles: Profile[] = []) {
392
+ const [existing] = this.iceTransports;
393
+
394
+ // Gather ICE candidates for only one track. If the remote endpoint is not bundle-aware, negotiate only one media track.
395
+ // https://w3c.github.io/webrtc-pc/#rtcbundlepolicy-enum
396
+ if (this.configuration.bundlePolicy === "max-bundle") {
397
+ if (existing) {
398
+ return this.dtlsTransports[0];
399
+ }
400
+ }
401
+
383
402
  const iceGatherer = new RTCIceGatherer({
384
403
  ...parseIceServers(this.configuration.iceServers),
385
404
  forceTurn: this.configuration.iceTransportPolicy === "relay",
386
405
  portRange: this.configuration.icePortRange,
387
406
  });
388
- iceGatherer.onGatheringStateChange.subscribe((state) => {
389
- this.updateIceGatheringState(state);
407
+ if (existing) {
408
+ iceGatherer.connection.localUserName = existing.connection.localUserName;
409
+ iceGatherer.connection.localPassword = existing.connection.localPassword;
410
+ }
411
+ iceGatherer.onGatheringStateChange.subscribe(() => {
412
+ this.updateIceGatheringState();
390
413
  });
391
- this.updateIceGatheringState(iceGatherer.gatheringState);
414
+ this.updateIceGatheringState();
392
415
  const iceTransport = new RTCIceTransport(iceGatherer);
393
- iceTransport.onStateChange.subscribe((state) => {
394
- this.updateIceConnectionState(state);
416
+ iceTransport.onStateChange.subscribe(() => {
417
+ this.updateIceConnectionState();
395
418
  });
396
- this.updateIceConnectionState(iceTransport.state);
397
419
 
398
420
  iceTransport.iceGather.onIceCandidate = (candidate) => {
399
421
  if (!this.localDescription) return;
@@ -405,7 +427,6 @@ export class RTCPeerConnection extends EventTarget {
405
427
  }
406
428
  candidate.sdpMLineIndex = 0;
407
429
  candidate.sdpMid = media.rtp.muxId;
408
- // for chrome & firefox & maybe others
409
430
  candidate.foundation = "candidate:" + candidate.foundation;
410
431
 
411
432
  this.onIceCandidate.execute(candidate.toJSON());
@@ -421,11 +442,16 @@ export class RTCPeerConnection extends EventTarget {
421
442
  srtpProfiles
422
443
  );
423
444
 
424
- return { dtlsTransport, iceTransport };
445
+ return dtlsTransport;
425
446
  }
426
447
 
427
448
  private createSctpTransport() {
428
- const sctp = new RTCSctpTransport(this.dtlsTransport);
449
+ const dtlsTransport = this.createTransport([
450
+ SRTP_PROFILE.SRTP_AEAD_AES_128_GCM, // prefer
451
+ SRTP_PROFILE.SRTP_AES128_CM_HMAC_SHA1_80,
452
+ ]);
453
+ const sctp = new RTCSctpTransport();
454
+ sctp.setDtlsTransport(dtlsTransport);
429
455
  sctp.mid = undefined;
430
456
 
431
457
  sctp.onDataChannel.subscribe((channel) => {
@@ -436,6 +462,9 @@ export class RTCPeerConnection extends EventTarget {
436
462
  this.emit("datachannel", event);
437
463
  });
438
464
 
465
+ this.sctpTransport = sctp;
466
+ this.updateIceConnectionState();
467
+
439
468
  return sctp;
440
469
  }
441
470
 
@@ -457,10 +486,7 @@ export class RTCPeerConnection extends EventTarget {
457
486
 
458
487
  // # assign MID
459
488
  description.media.forEach((media, i) => {
460
- const mid = media.rtp.muxId;
461
- if (!mid) {
462
- throw new Error("mid not exist");
463
- }
489
+ const mid = media.rtp.muxId!;
464
490
  this.seenMid.add(mid);
465
491
  if (["audio", "video"].includes(media.kind)) {
466
492
  const transceiver = this.getTransceiverByMLineIndex(i);
@@ -473,26 +499,31 @@ export class RTCPeerConnection extends EventTarget {
473
499
  }
474
500
  });
475
501
 
476
- // # set ICE role
477
- if (description.type === "offer") {
478
- this.iceTransport.connection.iceControlling = true;
479
- } else {
480
- this.iceTransport.connection.iceControlling = false;
481
- }
482
- // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
483
- // RFC 8445 S6.1.1
484
- if (this.iceTransport.connection.remoteIsLite) {
485
- this.iceTransport.connection.iceControlling = true;
486
- }
502
+ const setupRole = (dtlsTransport: RTCDtlsTransport) => {
503
+ const iceTransport = dtlsTransport.iceTransport;
487
504
 
488
- // # set DTLS role for mediasoup
489
- if (description.type === "answer") {
490
- const role = description.media.find((media) => media.dtlsParams)
491
- ?.dtlsParams?.role;
492
- if (role) {
493
- this.dtlsTransport.role = role;
505
+ // # set ICE role
506
+ if (description.type === "offer") {
507
+ iceTransport.connection.iceControlling = true;
508
+ } else {
509
+ iceTransport.connection.iceControlling = false;
510
+ }
511
+ // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
512
+ // RFC 8445 S6.1.1
513
+ if (iceTransport.connection.remoteIsLite) {
514
+ iceTransport.connection.iceControlling = true;
494
515
  }
495
- }
516
+
517
+ // # set DTLS role for mediasoup
518
+ if (description.type === "answer") {
519
+ const role = description.media.find((media) => media.dtlsParams)
520
+ ?.dtlsParams?.role;
521
+ if (role) {
522
+ dtlsTransport.role = role;
523
+ }
524
+ }
525
+ };
526
+ this.dtlsTransports.forEach((d) => setupRole(d));
496
527
 
497
528
  // # configure direction
498
529
  this.transceivers.forEach((t) => {
@@ -506,10 +537,18 @@ export class RTCPeerConnection extends EventTarget {
506
537
  this.setLocal(description);
507
538
 
508
539
  // # gather candidates
509
- await this.iceTransport.iceGather.gather();
510
- description.media.map((media) => {
511
- addTransportDescription(media, this.dtlsTransport);
512
- });
540
+ for (const iceTransport of this.iceTransports) {
541
+ await iceTransport.iceGather.gather();
542
+ }
543
+ description.media
544
+ .filter((m) => ["audio", "video"].includes(m.kind))
545
+ .forEach((m, i) => {
546
+ addTransportDescription(m, this.transceivers[i].dtlsTransport);
547
+ });
548
+ const sctpMedia = description.media.find((m) => m.kind === "application");
549
+ if (this.sctpTransport && sctpMedia) {
550
+ addTransportDescription(sctpMedia, this.sctpTransport.dtlsTransport);
551
+ }
513
552
 
514
553
  this.setLocal(description);
515
554
 
@@ -540,39 +579,44 @@ export class RTCPeerConnection extends EventTarget {
540
579
 
541
580
  async addIceCandidate(candidateMessage: RTCIceCandidate) {
542
581
  const candidate = IceCandidate.fromJSON(candidateMessage);
543
- await this.iceTransport.addRemoteCandidate(candidate);
582
+ for (const iceTransport of this.iceTransports) {
583
+ await iceTransport.addRemoteCandidate(candidate);
584
+ }
544
585
  }
545
586
 
546
587
  private async connect() {
547
588
  if (this.masterTransportEstablished) return;
548
589
 
549
- const dtlsTransport = this.dtlsTransport;
550
- const iceTransport = dtlsTransport.iceTransport;
551
-
552
590
  this.setConnectionState("connecting");
553
591
 
554
- await iceTransport.start().catch((err) => {
555
- log("iceTransport.start failed", err);
556
- throw err;
557
- });
558
- log("ice connected");
559
- await dtlsTransport.start().catch((err) => {
560
- log("dtlsTransport.start failed", err);
561
- throw err;
562
- });
563
- log("dtls connected");
564
-
565
- if (this.sctpTransport && this.sctpRemotePort) {
566
- await this.sctpTransport.start(this.sctpRemotePort);
567
- await this.sctpTransport.sctp.stateChanged.connected.asPromise();
568
- }
569
- log("sctp connected");
592
+ await Promise.all(
593
+ this.dtlsTransports.map(async (dtlsTransport) => {
594
+ const { iceTransport } = dtlsTransport;
595
+ await iceTransport.start().catch((err) => {
596
+ log("iceTransport.start failed", err);
597
+ throw err;
598
+ });
599
+ await dtlsTransport.start().catch((err) => {
600
+ log("dtlsTransport.start failed", err);
601
+ throw err;
602
+ });
603
+ if (
604
+ this.sctpTransport &&
605
+ this.sctpRemotePort &&
606
+ this.sctpTransport.dtlsTransport.id === dtlsTransport.id
607
+ ) {
608
+ await this.sctpTransport.start(this.sctpRemotePort);
609
+ await this.sctpTransport.sctp.stateChanged.connected.asPromise();
610
+ log("sctp connected");
611
+ }
612
+ })
613
+ );
570
614
 
571
615
  this.masterTransportEstablished = true;
572
616
  this.setConnectionState("connected");
573
617
  }
574
618
 
575
- private localRtp(transceiver: RTCRtpTransceiver): RTCRtpParameters {
619
+ private getLocalRtpParams(transceiver: RTCRtpTransceiver): RTCRtpParameters {
576
620
  if (transceiver.mid == undefined) throw new Error("mid not assigned");
577
621
 
578
622
  const rtp: RTCRtpParameters = {
@@ -584,15 +628,10 @@ export class RTCPeerConnection extends EventTarget {
584
628
  return rtp;
585
629
  }
586
630
 
587
- private remoteRtp(
588
- remoteDescription: SessionDescription,
631
+ private getRemoteRtpParams(
632
+ media: MediaDescription,
589
633
  transceiver: RTCRtpTransceiver
590
634
  ): RTCRtpReceiveParameters {
591
- if (transceiver.mLineIndex == undefined)
592
- throw new Error("mLineIndex not assigned");
593
- const media = remoteDescription.media[transceiver.mLineIndex];
594
- if (!media) throw new Error("media line not exist");
595
-
596
635
  const receiveParameters: RTCRtpReceiveParameters = {
597
636
  muxId: media.rtp.muxId,
598
637
  rtcp: media.rtp.rtcp,
@@ -632,155 +671,98 @@ export class RTCPeerConnection extends EventTarget {
632
671
  remoteSdp.type = sessionDescription.type;
633
672
  this.validateDescription(remoteSdp, false);
634
673
 
674
+ if (remoteSdp.type === "answer") {
675
+ this.currentRemoteDescription = remoteSdp;
676
+ this.pendingRemoteDescription = undefined;
677
+ } else {
678
+ this.pendingRemoteDescription = remoteSdp;
679
+ }
680
+
681
+ const bundle = remoteSdp.group.find(
682
+ (g) =>
683
+ g.semantic === "BUNDLE" && this.configuration.bundlePolicy !== "disable"
684
+ );
685
+ let bundleTransport: RTCDtlsTransport | undefined;
686
+
635
687
  // # apply description
636
- for (const [i, remoteMedia] of enumerate(remoteSdp.media)) {
637
- if (["audio", "video"].includes(remoteMedia.kind)) {
638
- const transceiver =
639
- this.transceivers.find(
688
+ await Promise.all(
689
+ enumerate(remoteSdp.media).map(async ([i, remoteMedia]) => {
690
+ let dtlsTransport: RTCDtlsTransport | undefined;
691
+
692
+ if (["audio", "video"].includes(remoteMedia.kind)) {
693
+ let transceiver = this.transceivers.find(
640
694
  (t) =>
641
695
  t.kind === remoteMedia.kind &&
642
696
  [undefined, remoteMedia.rtp.muxId].includes(t.mid)
643
- ) ||
644
- (() => {
697
+ );
698
+ if (!transceiver) {
645
699
  // create remote transceiver
646
- const transceiver = this.addTransceiver(remoteMedia.kind, {
700
+ transceiver = this.addTransceiver(remoteMedia.kind, {
647
701
  direction: "recvonly",
648
702
  });
649
-
650
703
  this.onRemoteTransceiverAdded.execute(transceiver);
651
- this.onTransceiver.execute(transceiver);
704
+ }
652
705
 
653
- return transceiver;
654
- })();
706
+ if (bundle) {
707
+ if (!bundleTransport) {
708
+ bundleTransport = transceiver.dtlsTransport;
709
+ } else {
710
+ transceiver.setDtlsTransport(bundleTransport);
711
+ }
712
+ }
655
713
 
656
- if (!transceiver.mid) {
657
- transceiver.mid = remoteMedia.rtp.muxId;
658
- transceiver.mLineIndex = i;
659
- }
714
+ dtlsTransport = transceiver.dtlsTransport;
660
715
 
661
- // # negotiate codecs
662
- transceiver.codecs = remoteMedia.rtp.codecs.filter((remoteCodec) => {
663
- const localCodecs = this.configuration.codecs[remoteMedia.kind] || [];
664
-
665
- const existCodec = findCodecByMimeType(localCodecs, remoteCodec);
666
- if (!existCodec) return false;
667
-
668
- if (existCodec?.name.toLowerCase() === "rtx") {
669
- const params = codecParametersFromString(
670
- existCodec.parameters ?? ""
671
- );
672
- const pt = params["apt"];
673
- const origin = remoteMedia.rtp.codecs.find(
674
- (c) => c.payloadType === pt
675
- );
676
- if (!origin) return false;
677
- return !!findCodecByMimeType(localCodecs, origin);
716
+ this.setRemoteRTP(transceiver, remoteMedia, remoteSdp.type, i);
717
+ } else if (remoteMedia.kind === "application") {
718
+ if (!this.sctpTransport) {
719
+ this.sctpTransport = this.createSctpTransport();
678
720
  }
679
721
 
680
- return true;
681
- });
722
+ if (bundle) {
723
+ if (!bundleTransport) {
724
+ bundleTransport = this.sctpTransport.dtlsTransport;
725
+ } else {
726
+ this.sctpTransport.setDtlsTransport(bundleTransport);
727
+ }
728
+ }
682
729
 
683
- log("negotiated codecs", transceiver.codecs);
684
- if (transceiver.codecs.length === 0) {
685
- throw new Error("negotiate codecs failed.");
686
- }
687
- transceiver.headerExtensions = remoteMedia.rtp.headerExtensions.filter(
688
- (extension) =>
689
- (
690
- this.configuration.headerExtensions[
691
- remoteMedia.kind as "video" | "audio"
692
- ] || []
693
- ).find((v) => v.uri === extension.uri)
694
- );
730
+ dtlsTransport = this.sctpTransport.dtlsTransport;
695
731
 
696
- // # configure direction
697
- const mediaDirection = remoteMedia.direction || "inactive";
698
- const direction = reverseDirection(mediaDirection);
699
- if (["answer", "pranswer"].includes(remoteSdp.type)) {
700
- transceiver.currentDirection = direction;
732
+ this.setRemoteSCTP(remoteMedia, this.sctpTransport);
701
733
  } else {
702
- transceiver.offerDirection = direction;
734
+ throw new Error("invalid media kind");
703
735
  }
704
736
 
705
- const localParams = this.localRtp(transceiver);
706
- transceiver.sender.prepareSend(localParams);
707
-
708
- if (["recvonly", "sendrecv"].includes(transceiver.direction)) {
709
- const remotePrams = this.remoteRtp(remoteSdp, transceiver);
737
+ const iceTransport = dtlsTransport.iceTransport;
710
738
 
711
- // register simulcast receiver
712
- remoteMedia.simulcastParameters.forEach((param) => {
713
- this.router.registerRtpReceiverByRid(
714
- transceiver,
715
- param,
716
- remotePrams
717
- );
718
- });
739
+ if (remoteMedia.iceParams && remoteMedia.dtlsParams) {
740
+ iceTransport.setRemoteParams(remoteMedia.iceParams);
741
+ dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
719
742
 
720
- transceiver.receiver.prepareReceive(remotePrams);
721
- // register ssrc receiver
722
- this.router.registerRtpReceiverBySsrc(transceiver, remotePrams);
723
- }
724
- if (["sendonly", "sendrecv"].includes(mediaDirection)) {
725
- // assign msid
726
- if (remoteMedia.msid != undefined) {
727
- const [streamId, trackId] = remoteMedia.msid.split(" ");
728
- transceiver.receiver.remoteStreamId = streamId;
729
- transceiver.receiver.remoteTrackId = trackId;
730
-
731
- this.fireOnTrack(
732
- transceiver.receiver.track,
733
- transceiver,
734
- new MediaStream({
735
- id: streamId,
736
- tracks: [transceiver.receiver.track],
737
- })
738
- );
743
+ // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
744
+ // RFC 8445 S6.1.1
745
+ if (remoteMedia.iceParams?.iceLite) {
746
+ iceTransport.connection.iceControlling = true;
739
747
  }
740
748
  }
741
749
 
742
- transceiver.receiver.setupTWCC(remoteMedia.ssrc[0]?.ssrc);
743
- } else if (remoteMedia.kind === "application") {
744
- // # configure sctp
745
- this.sctpRemotePort = remoteMedia.sctpPort;
746
- if (!this.sctpRemotePort) {
747
- throw new Error("sctpRemotePort not exist");
748
- }
749
- if (!this.sctpTransport) {
750
- this.sctpTransport = this.createSctpTransport();
751
- }
752
- this.sctpTransport.setRemotePort(this.sctpRemotePort);
753
- if (!this.sctpTransport.mid) {
754
- this.sctpTransport.mid = remoteMedia.rtp.muxId;
755
- }
756
- }
750
+ // # add ICE candidates
751
+ remoteMedia.iceCandidates.forEach(iceTransport.addRemoteCandidate);
757
752
 
758
- if (remoteMedia.iceParams && remoteMedia.dtlsParams) {
759
- this.iceTransport.setRemoteParams(remoteMedia.iceParams);
760
- this.dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
753
+ await iceTransport.iceGather.gather();
761
754
 
762
- // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
763
- // RFC 8445 S6.1.1
764
- if (remoteMedia.iceParams?.iceLite) {
765
- this.iceTransport.connection.iceControlling = true;
755
+ if (remoteMedia.iceCandidatesComplete) {
756
+ await iceTransport.addRemoteCandidate(undefined);
766
757
  }
767
- }
768
-
769
- // # add ICE candidates
770
- remoteMedia.iceCandidates.forEach(this.iceTransport.addRemoteCandidate);
771
-
772
- await this.iceTransport.iceGather.gather();
773
758
 
774
- if (remoteMedia.iceCandidatesComplete) {
775
- await this.iceTransport.addRemoteCandidate(undefined);
776
- }
777
-
778
- // # set DTLS role
779
- if (remoteSdp.type === "answer" && remoteMedia.dtlsParams?.role) {
780
- this.dtlsTransport.role =
781
- remoteMedia.dtlsParams.role === "client" ? "server" : "client";
782
- }
783
- }
759
+ // # set DTLS role
760
+ if (remoteSdp.type === "answer" && remoteMedia.dtlsParams?.role) {
761
+ dtlsTransport.role =
762
+ remoteMedia.dtlsParams.role === "client" ? "server" : "client";
763
+ }
764
+ })
765
+ );
784
766
 
785
767
  // connect transports
786
768
  if (remoteSdp.type === "answer") {
@@ -797,19 +779,115 @@ export class RTCPeerConnection extends EventTarget {
797
779
  this.setSignalingState("stable");
798
780
  }
799
781
 
800
- if (remoteSdp.type === "answer") {
801
- this.currentRemoteDescription = remoteSdp;
802
- this.pendingRemoteDescription = undefined;
803
- } else {
804
- this.pendingRemoteDescription = remoteSdp;
805
- }
806
-
807
782
  this.negotiationneeded = false;
808
783
  if (this.shouldNegotiationneeded) {
809
784
  this.needNegotiation();
810
785
  }
811
786
  }
812
787
 
788
+ private setRemoteRTP(
789
+ transceiver: RTCRtpTransceiver,
790
+ remoteMedia: MediaDescription,
791
+ type: "offer" | "answer",
792
+ mLineIndex: number
793
+ ) {
794
+ if (!transceiver.mid) {
795
+ transceiver.mid = remoteMedia.rtp.muxId;
796
+ transceiver.mLineIndex = mLineIndex;
797
+ }
798
+
799
+ // # negotiate codecs
800
+ transceiver.codecs = remoteMedia.rtp.codecs.filter((remoteCodec) => {
801
+ const localCodecs = this.configuration.codecs[remoteMedia.kind] || [];
802
+
803
+ const existCodec = findCodecByMimeType(localCodecs, remoteCodec);
804
+ if (!existCodec) return false;
805
+
806
+ if (existCodec?.name.toLowerCase() === "rtx") {
807
+ const params = codecParametersFromString(existCodec.parameters ?? "");
808
+ const pt = params["apt"];
809
+ const origin = remoteMedia.rtp.codecs.find((c) => c.payloadType === pt);
810
+ if (!origin) return false;
811
+ return !!findCodecByMimeType(localCodecs, origin);
812
+ }
813
+
814
+ return true;
815
+ });
816
+
817
+ log("negotiated codecs", transceiver.codecs);
818
+ if (transceiver.codecs.length === 0) {
819
+ throw new Error("negotiate codecs failed.");
820
+ }
821
+ transceiver.headerExtensions = remoteMedia.rtp.headerExtensions.filter(
822
+ (extension) =>
823
+ (
824
+ this.configuration.headerExtensions[
825
+ remoteMedia.kind as "video" | "audio"
826
+ ] || []
827
+ ).find((v) => v.uri === extension.uri)
828
+ );
829
+
830
+ // # configure direction
831
+ const mediaDirection = remoteMedia.direction || "inactive";
832
+ const direction = reverseDirection(mediaDirection);
833
+ if (["answer", "pranswer"].includes(type)) {
834
+ transceiver.currentDirection = direction;
835
+ } else {
836
+ transceiver.offerDirection = direction;
837
+ }
838
+
839
+ const localParams = this.getLocalRtpParams(transceiver);
840
+ transceiver.sender.prepareSend(localParams);
841
+
842
+ if (["recvonly", "sendrecv"].includes(transceiver.direction)) {
843
+ const remotePrams = this.getRemoteRtpParams(remoteMedia, transceiver);
844
+
845
+ // register simulcast receiver
846
+ remoteMedia.simulcastParameters.forEach((param) => {
847
+ this.router.registerRtpReceiverByRid(transceiver, param, remotePrams);
848
+ });
849
+
850
+ transceiver.receiver.prepareReceive(remotePrams);
851
+ // register ssrc receiver
852
+ this.router.registerRtpReceiverBySsrc(transceiver, remotePrams);
853
+ }
854
+ if (["sendonly", "sendrecv"].includes(mediaDirection)) {
855
+ // assign msid
856
+ if (remoteMedia.msid != undefined) {
857
+ const [streamId, trackId] = remoteMedia.msid.split(" ");
858
+ transceiver.receiver.remoteStreamId = streamId;
859
+ transceiver.receiver.remoteTrackId = trackId;
860
+
861
+ this.fireOnTrack(
862
+ transceiver.receiver.track,
863
+ transceiver,
864
+ new MediaStream({
865
+ id: streamId,
866
+ tracks: [transceiver.receiver.track],
867
+ })
868
+ );
869
+ }
870
+ }
871
+
872
+ transceiver.receiver.setupTWCC(remoteMedia.ssrc[0]?.ssrc);
873
+ }
874
+
875
+ private setRemoteSCTP(
876
+ remoteMedia: MediaDescription,
877
+ sctpTransport: RTCSctpTransport
878
+ ) {
879
+ // # configure sctp
880
+ this.sctpRemotePort = remoteMedia.sctpPort;
881
+ if (!this.sctpRemotePort) {
882
+ throw new Error("sctpRemotePort not exist");
883
+ }
884
+
885
+ sctpTransport.setRemotePort(this.sctpRemotePort);
886
+ if (!sctpTransport.mid) {
887
+ sctpTransport.mid = remoteMedia.rtp.muxId;
888
+ }
889
+ }
890
+
813
891
  private validateDescription(
814
892
  description: SessionDescription,
815
893
  isLocal: boolean
@@ -888,14 +966,19 @@ export class RTCPeerConnection extends EventTarget {
888
966
 
889
967
  const direction = options.direction || "sendrecv";
890
968
 
891
- const sender = new RTCRtpSender(trackOrKind, this.dtlsTransport);
892
- const receiver = new RTCRtpReceiver(kind, this.dtlsTransport, sender.ssrc);
969
+ const dtlsTransport = this.createTransport([
970
+ SRTP_PROFILE.SRTP_AEAD_AES_128_GCM, // prefer
971
+ SRTP_PROFILE.SRTP_AES128_CM_HMAC_SHA1_80,
972
+ ]);
973
+
974
+ const sender = new RTCRtpSender(trackOrKind);
975
+ const receiver = new RTCRtpReceiver(kind, sender.ssrc);
893
976
  const transceiver = new RTCRtpTransceiver(
894
977
  kind,
978
+ dtlsTransport,
895
979
  receiver,
896
980
  sender,
897
- direction,
898
- this.dtlsTransport
981
+ direction
899
982
  );
900
983
  transceiver.options = options;
901
984
  this.router.registerRtpSender(transceiver.sender);
@@ -903,6 +986,7 @@ export class RTCPeerConnection extends EventTarget {
903
986
  this.transceivers.push(transceiver);
904
987
  this.onTransceiverAdded.execute(transceiver);
905
988
 
989
+ this.updateIceConnectionState();
906
990
  this.needNegotiation();
907
991
 
908
992
  return transceiver;
@@ -971,66 +1055,91 @@ export class RTCPeerConnection extends EventTarget {
971
1055
  }
972
1056
  }
973
1057
 
1058
+ private async ensureCerts() {
1059
+ const ensureCert = async (dtlsTransport: RTCDtlsTransport) => {
1060
+ if (this.certificates.length === 0) {
1061
+ const localCertificate = await dtlsTransport.setupCertificate();
1062
+ this.certificates.push(localCertificate);
1063
+ } else {
1064
+ dtlsTransport.localCertificate = this.certificates[0];
1065
+ }
1066
+ };
1067
+
1068
+ for (const dtlsTransport of this.dtlsTransports) {
1069
+ await ensureCert(dtlsTransport);
1070
+ }
1071
+ }
1072
+
974
1073
  async createAnswer() {
975
1074
  this.assertNotClosed();
976
1075
  if (
977
1076
  !["have-remote-offer", "have-local-pranswer"].includes(
978
1077
  this.signalingState
979
- ) ||
980
- !this.dtlsTransport
981
- )
1078
+ )
1079
+ ) {
982
1080
  throw new Error("createAnswer failed");
983
-
984
- if (this.certificates.length === 0) {
985
- await this.dtlsTransport.setupCertificate();
986
1081
  }
1082
+ if (!this._remoteDescription) {
1083
+ throw new Error("wrong state");
1084
+ }
1085
+
1086
+ await this.ensureCerts();
987
1087
 
988
1088
  const description = new SessionDescription();
989
1089
  addSDPHeader("answer", description);
990
1090
 
991
- this._remoteDescription?.media.forEach((remoteM) => {
1091
+ this._remoteDescription.media.forEach((remoteMedia) => {
992
1092
  let dtlsTransport!: RTCDtlsTransport;
993
1093
  let media: MediaDescription;
994
1094
 
995
- if (["audio", "video"].includes(remoteM.kind)) {
996
- const transceiver = this.getTransceiverByMid(remoteM.rtp.muxId!)!;
1095
+ if (["audio", "video"].includes(remoteMedia.kind)) {
1096
+ const transceiver = this.getTransceiverByMid(remoteMedia.rtp.muxId!)!;
997
1097
  media = createMediaDescriptionForTransceiver(
998
1098
  transceiver,
999
1099
  this.cname,
1000
1100
  andDirection(transceiver.direction, transceiver.offerDirection),
1001
1101
  transceiver.mid!
1002
1102
  );
1003
- if (!transceiver.dtlsTransport) throw new Error();
1004
1103
  dtlsTransport = transceiver.dtlsTransport;
1005
- } else if (remoteM.kind === "application") {
1006
- if (!this.sctpTransport || !this.sctpTransport.mid) throw new Error();
1104
+ } else if (remoteMedia.kind === "application") {
1105
+ if (!this.sctpTransport || !this.sctpTransport.mid) {
1106
+ throw new Error("sctpTransport not found");
1107
+ }
1007
1108
  media = createMediaDescriptionForSctp(
1008
1109
  this.sctpTransport,
1009
1110
  this.sctpTransport.mid
1010
1111
  );
1011
1112
 
1012
1113
  dtlsTransport = this.sctpTransport.dtlsTransport;
1013
- } else throw new Error();
1114
+ } else {
1115
+ throw new Error("invalid kind");
1116
+ }
1014
1117
 
1015
1118
  // # determine DTLS role, or preserve the currently configured role
1016
- if (!media.dtlsParams) throw new Error();
1119
+ if (!media.dtlsParams) {
1120
+ throw new Error("dtlsParams missing");
1121
+ }
1017
1122
  if (dtlsTransport.role === "auto") {
1018
1123
  media.dtlsParams.role = "client";
1019
1124
  } else {
1020
1125
  media.dtlsParams.role = dtlsTransport.role;
1021
1126
  }
1022
- media.simulcastParameters = remoteM.simulcastParameters.map((v) => ({
1127
+
1128
+ media.simulcastParameters = remoteMedia.simulcastParameters.map((v) => ({
1023
1129
  ...v,
1024
1130
  direction: reverseSimulcastDirection(v.direction),
1025
1131
  }));
1132
+
1026
1133
  description.media.push(media);
1027
1134
  });
1028
1135
 
1029
- const bundle = new GroupDescription("BUNDLE", []);
1030
- description.media.forEach((media) => {
1031
- bundle.items.push(media.rtp.muxId!);
1032
- });
1033
- description.group.push(bundle);
1136
+ if (this.configuration.bundlePolicy !== "disable") {
1137
+ const bundle = new GroupDescription("BUNDLE", []);
1138
+ description.media.forEach((media) => {
1139
+ bundle.items.push(media.rtp.muxId!);
1140
+ });
1141
+ description.group.push(bundle);
1142
+ }
1034
1143
 
1035
1144
  return description.toJSON();
1036
1145
  }
@@ -1050,29 +1159,94 @@ export class RTCPeerConnection extends EventTarget {
1050
1159
  if (this.sctpTransport) {
1051
1160
  await this.sctpTransport.stop();
1052
1161
  }
1053
- await this.dtlsTransport.stop();
1054
- await this.iceTransport.stop();
1162
+ for (const dtlsTransport of this.dtlsTransports) {
1163
+ await dtlsTransport.stop();
1164
+ await dtlsTransport.iceTransport.stop();
1165
+ }
1055
1166
 
1056
1167
  this.dispose();
1057
1168
  log("peerConnection closed");
1058
1169
  }
1059
1170
 
1060
1171
  private assertNotClosed() {
1061
- if (this.isClosed) throw new Error("RTCPeerConnection is closed");
1172
+ if (this.isClosed) {
1173
+ throw new Error("RTCPeerConnection is closed");
1174
+ }
1062
1175
  }
1063
1176
 
1064
- private updateIceGatheringState(state: IceGathererState) {
1065
- log("iceGatheringStateChange", state);
1066
- this.iceGatheringState = state;
1067
- this.iceGatheringStateChange.execute(state);
1068
- this.emit("icegatheringstatechange", state);
1177
+ // https://w3c.github.io/webrtc-pc/#dom-rtcicegatheringstate
1178
+ private updateIceGatheringState() {
1179
+ const all = this.iceTransports;
1180
+
1181
+ function allMatch(...state: IceGathererState[]) {
1182
+ return (
1183
+ all.filter((check) => state.includes(check.iceGather.gatheringState))
1184
+ .length === all.length
1185
+ );
1186
+ }
1187
+
1188
+ let newState: IceGathererState;
1189
+
1190
+ if (all.length && allMatch("complete")) {
1191
+ newState = "complete";
1192
+ } else if (!all.length || allMatch("new", "complete")) {
1193
+ newState = "new";
1194
+ } else if (
1195
+ all.map((check) => check.iceGather.gatheringState).includes("gathering")
1196
+ ) {
1197
+ newState = "gathering";
1198
+ } else {
1199
+ newState = "new";
1200
+ }
1201
+
1202
+ if (this.iceGatheringState === newState) {
1203
+ return;
1204
+ }
1205
+
1206
+ log("iceGatheringStateChange", newState);
1207
+ this.iceGatheringState = newState;
1208
+ this.iceGatheringStateChange.execute(newState);
1209
+ this.emit("icegatheringstatechange", newState);
1069
1210
  }
1070
1211
 
1071
- private updateIceConnectionState(state: RTCIceConnectionState) {
1072
- log("iceConnectionStateChange", state);
1073
- this.iceConnectionState = state;
1074
- this.iceConnectionStateChange.execute(state);
1075
- this.emit("iceconnectionstatechange", state);
1212
+ // https://w3c.github.io/webrtc-pc/#dom-rtciceconnectionstate
1213
+ private updateIceConnectionState() {
1214
+ const all = this.iceTransports;
1215
+ let newState: RTCIceConnectionState;
1216
+
1217
+ function allMatch(...state: RTCIceConnectionState[]) {
1218
+ return (
1219
+ all.filter((check) => state.includes(check.state)).length === all.length
1220
+ );
1221
+ }
1222
+
1223
+ if (this.connectionState === "closed") {
1224
+ newState = "closed";
1225
+ } else if (allMatch("failed")) {
1226
+ newState = "failed";
1227
+ } else if (allMatch("disconnected")) {
1228
+ newState = "disconnected";
1229
+ } else if (allMatch("new", "closed")) {
1230
+ newState = "new";
1231
+ } else if (allMatch("new", "checking")) {
1232
+ newState = "checking";
1233
+ } else if (allMatch("completed", "closed")) {
1234
+ newState = "completed";
1235
+ } else if (allMatch("connected", "completed", "closed")) {
1236
+ newState = "connected";
1237
+ } else {
1238
+ // unreachable?
1239
+ newState = "new";
1240
+ }
1241
+
1242
+ if (this.iceConnectionState === newState) {
1243
+ return;
1244
+ }
1245
+
1246
+ log("iceConnectionStateChange", newState);
1247
+ this.iceConnectionState = newState;
1248
+ this.iceConnectionStateChange.execute(newState);
1249
+ this.emit("iceconnectionstatechange", newState);
1076
1250
  }
1077
1251
 
1078
1252
  private setSignalingState(state: RTCSignalingState) {
@@ -1209,6 +1383,8 @@ export function allocateMid(mids: Set<string>) {
1209
1383
  return mid;
1210
1384
  }
1211
1385
 
1386
+ export type BundlePolicy = "max-compat" | "max-bundle" | "disable";
1387
+
1212
1388
  export interface PeerConfig {
1213
1389
  codecs: Partial<{
1214
1390
  audio: RTCRtpCodecParameters[];
@@ -1225,6 +1401,7 @@ export interface PeerConfig {
1225
1401
  dtls: Partial<{
1226
1402
  keys: DtlsKeys;
1227
1403
  }>;
1404
+ bundlePolicy: BundlePolicy;
1228
1405
  }
1229
1406
 
1230
1407
  export const findCodecByMimeType = (
@@ -1276,6 +1453,7 @@ export const defaultPeerConfig: PeerConfig = {
1276
1453
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
1277
1454
  icePortRange: undefined,
1278
1455
  dtls: {},
1456
+ bundlePolicy: "max-compat",
1279
1457
  };
1280
1458
 
1281
1459
  export interface RTCTrackEvent {