werift 0.14.4-debug0 → 0.15.0-alpha.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 +6 -5
  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/media/receiver/nack.js +2 -2
  32. package/lib/webrtc/src/media/receiver/nack.js.map +1 -1
  33. package/lib/webrtc/src/media/rtpReceiver.d.ts +3 -2
  34. package/lib/webrtc/src/media/rtpReceiver.js +4 -2
  35. package/lib/webrtc/src/media/rtpReceiver.js.map +1 -1
  36. package/lib/webrtc/src/media/rtpSender.d.ts +4 -2
  37. package/lib/webrtc/src/media/rtpSender.js +15 -7
  38. package/lib/webrtc/src/media/rtpSender.js.map +1 -1
  39. package/lib/webrtc/src/media/rtpTransceiver.d.ts +7 -6
  40. package/lib/webrtc/src/media/rtpTransceiver.js +8 -3
  41. package/lib/webrtc/src/media/rtpTransceiver.js.map +1 -1
  42. package/lib/webrtc/src/nonstandard/recorder/writer/webm.js +1 -1
  43. package/lib/webrtc/src/nonstandard/recorder/writer/webm.js.map +1 -1
  44. package/lib/webrtc/src/peerConnection.d.ts +10 -10
  45. package/lib/webrtc/src/peerConnection.js +324 -188
  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/media/receiver/nack.ts +1 -1
  57. package/src/media/rtpReceiver.ts +6 -5
  58. package/src/media/rtpSender.ts +18 -9
  59. package/src/media/rtpTransceiver.ts +13 -6
  60. package/src/nonstandard/recorder/writer/webm.ts +1 -1
  61. package/src/peerConnection.ts +386 -223
  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];
@@ -337,7 +343,8 @@ export class RTCPeerConnection extends EventTarget {
337
343
  protocol: settings.protocol,
338
344
  });
339
345
 
340
- return new RTCDataChannel(this.sctpTransport, parameters);
346
+ const channel = new RTCDataChannel(this.sctpTransport, parameters);
347
+ return channel;
341
348
  }
342
349
 
343
350
  removeTrack(sender: RTCRtpSender) {
@@ -380,20 +387,31 @@ export class RTCPeerConnection extends EventTarget {
380
387
  };
381
388
 
382
389
  private createTransport(srtpProfiles: Profile[] = []) {
390
+ const [existing] = this.iceTransports;
391
+
392
+ if (this.configuration.bundlePolicy === "max-bundle") {
393
+ if (existing) {
394
+ return this.dtlsTransports[0];
395
+ }
396
+ }
397
+
383
398
  const iceGatherer = new RTCIceGatherer({
384
399
  ...parseIceServers(this.configuration.iceServers),
385
400
  forceTurn: this.configuration.iceTransportPolicy === "relay",
386
401
  portRange: this.configuration.icePortRange,
387
402
  });
388
- iceGatherer.onGatheringStateChange.subscribe((state) => {
389
- this.updateIceGatheringState(state);
403
+ if (existing) {
404
+ iceGatherer.connection.localUserName = existing.connection.localUserName;
405
+ iceGatherer.connection.localPassword = existing.connection.localPassword;
406
+ }
407
+ iceGatherer.onGatheringStateChange.subscribe(() => {
408
+ this.updateIceGatheringState();
390
409
  });
391
- this.updateIceGatheringState(iceGatherer.gatheringState);
410
+ this.updateIceGatheringState();
392
411
  const iceTransport = new RTCIceTransport(iceGatherer);
393
- iceTransport.onStateChange.subscribe((state) => {
394
- this.updateIceConnectionState(state);
412
+ iceTransport.onStateChange.subscribe(() => {
413
+ this.updateIceConnectionState();
395
414
  });
396
- this.updateIceConnectionState(iceTransport.state);
397
415
 
398
416
  iceTransport.iceGather.onIceCandidate = (candidate) => {
399
417
  if (!this.localDescription) return;
@@ -405,7 +423,6 @@ export class RTCPeerConnection extends EventTarget {
405
423
  }
406
424
  candidate.sdpMLineIndex = 0;
407
425
  candidate.sdpMid = media.rtp.muxId;
408
- // for chrome & firefox & maybe others
409
426
  candidate.foundation = "candidate:" + candidate.foundation;
410
427
 
411
428
  this.onIceCandidate.execute(candidate.toJSON());
@@ -421,11 +438,16 @@ export class RTCPeerConnection extends EventTarget {
421
438
  srtpProfiles
422
439
  );
423
440
 
424
- return { dtlsTransport, iceTransport };
441
+ return dtlsTransport;
425
442
  }
426
443
 
427
444
  private createSctpTransport() {
428
- const sctp = new RTCSctpTransport(this.dtlsTransport);
445
+ const dtlsTransport = this.createTransport([
446
+ SRTP_PROFILE.SRTP_AEAD_AES_128_GCM, // prefer
447
+ SRTP_PROFILE.SRTP_AES128_CM_HMAC_SHA1_80,
448
+ ]);
449
+ const sctp = new RTCSctpTransport();
450
+ sctp.setDtlsTransport(dtlsTransport);
429
451
  sctp.mid = undefined;
430
452
 
431
453
  sctp.onDataChannel.subscribe((channel) => {
@@ -436,6 +458,8 @@ export class RTCPeerConnection extends EventTarget {
436
458
  this.emit("datachannel", event);
437
459
  });
438
460
 
461
+ this.updateIceConnectionState();
462
+
439
463
  return sctp;
440
464
  }
441
465
 
@@ -457,10 +481,7 @@ export class RTCPeerConnection extends EventTarget {
457
481
 
458
482
  // # assign MID
459
483
  description.media.forEach((media, i) => {
460
- const mid = media.rtp.muxId;
461
- if (!mid) {
462
- throw new Error("mid not exist");
463
- }
484
+ const mid = media.rtp.muxId!;
464
485
  this.seenMid.add(mid);
465
486
  if (["audio", "video"].includes(media.kind)) {
466
487
  const transceiver = this.getTransceiverByMLineIndex(i);
@@ -473,26 +494,31 @@ export class RTCPeerConnection extends EventTarget {
473
494
  }
474
495
  });
475
496
 
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
- }
497
+ const setupRole = (dtlsTransport: RTCDtlsTransport) => {
498
+ const iceTransport = dtlsTransport.iceTransport;
487
499
 
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;
500
+ // # set ICE role
501
+ if (description.type === "offer") {
502
+ iceTransport.connection.iceControlling = true;
503
+ } else {
504
+ iceTransport.connection.iceControlling = false;
494
505
  }
495
- }
506
+ // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
507
+ // RFC 8445 S6.1.1
508
+ if (iceTransport.connection.remoteIsLite) {
509
+ iceTransport.connection.iceControlling = true;
510
+ }
511
+
512
+ // # set DTLS role for mediasoup
513
+ if (description.type === "answer") {
514
+ const role = description.media.find((media) => media.dtlsParams)
515
+ ?.dtlsParams?.role;
516
+ if (role) {
517
+ dtlsTransport.role = role;
518
+ }
519
+ }
520
+ };
521
+ this.dtlsTransports.forEach((d) => setupRole(d));
496
522
 
497
523
  // # configure direction
498
524
  this.transceivers.forEach((t) => {
@@ -506,10 +532,18 @@ export class RTCPeerConnection extends EventTarget {
506
532
  this.setLocal(description);
507
533
 
508
534
  // # gather candidates
509
- await this.iceTransport.iceGather.gather();
510
- description.media.map((media) => {
511
- addTransportDescription(media, this.dtlsTransport);
512
- });
535
+ for (const iceTransport of this.iceTransports) {
536
+ await iceTransport.iceGather.gather();
537
+ }
538
+ description.media
539
+ .filter((m) => ["audio", "video"].includes(m.kind))
540
+ .forEach((m, i) => {
541
+ addTransportDescription(m, this.transceivers[i].dtlsTransport);
542
+ });
543
+ const sctpMedia = description.media.find((m) => m.kind === "application");
544
+ if (this.sctpTransport && sctpMedia) {
545
+ addTransportDescription(sctpMedia, this.sctpTransport.dtlsTransport);
546
+ }
513
547
 
514
548
  this.setLocal(description);
515
549
 
@@ -540,39 +574,41 @@ export class RTCPeerConnection extends EventTarget {
540
574
 
541
575
  async addIceCandidate(candidateMessage: RTCIceCandidate) {
542
576
  const candidate = IceCandidate.fromJSON(candidateMessage);
543
- await this.iceTransport.addRemoteCandidate(candidate);
577
+ for (const iceTransport of this.iceTransports) {
578
+ await iceTransport.addRemoteCandidate(candidate);
579
+ }
544
580
  }
545
581
 
546
582
  private async connect() {
547
583
  if (this.masterTransportEstablished) return;
548
584
 
549
- const dtlsTransport = this.dtlsTransport;
550
- const iceTransport = dtlsTransport.iceTransport;
551
-
552
585
  this.setConnectionState("connecting");
553
586
 
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");
587
+ await Promise.all(
588
+ this.dtlsTransports.map(async (dtlsTransport) => {
589
+ const { iceTransport } = dtlsTransport;
590
+ await iceTransport.start().catch((err) => {
591
+ log("iceTransport.start failed", err);
592
+ throw err;
593
+ });
594
+ await dtlsTransport.start().catch((err) => {
595
+ log("dtlsTransport.start failed", err);
596
+ throw err;
597
+ });
598
+ })
599
+ );
564
600
 
565
601
  if (this.sctpTransport && this.sctpRemotePort) {
566
602
  await this.sctpTransport.start(this.sctpRemotePort);
567
603
  await this.sctpTransport.sctp.stateChanged.connected.asPromise();
604
+ log("sctp connected");
568
605
  }
569
- log("sctp connected");
570
606
 
571
607
  this.masterTransportEstablished = true;
572
608
  this.setConnectionState("connected");
573
609
  }
574
610
 
575
- private localRtp(transceiver: RTCRtpTransceiver): RTCRtpParameters {
611
+ private getLocalRtpParams(transceiver: RTCRtpTransceiver): RTCRtpParameters {
576
612
  if (transceiver.mid == undefined) throw new Error("mid not assigned");
577
613
 
578
614
  const rtp: RTCRtpParameters = {
@@ -584,15 +620,10 @@ export class RTCPeerConnection extends EventTarget {
584
620
  return rtp;
585
621
  }
586
622
 
587
- private remoteRtp(
588
- remoteDescription: SessionDescription,
623
+ private getRemoteRtpParams(
624
+ media: MediaDescription,
589
625
  transceiver: RTCRtpTransceiver
590
626
  ): 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
627
  const receiveParameters: RTCRtpReceiveParameters = {
597
628
  muxId: media.rtp.muxId,
598
629
  rtcp: media.rtp.rtcp,
@@ -632,152 +663,83 @@ export class RTCPeerConnection extends EventTarget {
632
663
  remoteSdp.type = sessionDescription.type;
633
664
  this.validateDescription(remoteSdp, false);
634
665
 
666
+ const bundle = remoteSdp.group.find((g) => g.semantic === "BUNDLE");
667
+ let bundleTransport: RTCDtlsTransport | undefined;
668
+
635
669
  // # apply description
636
670
  for (const [i, remoteMedia] of enumerate(remoteSdp.media)) {
637
- if (["audio", "video"].includes(remoteMedia.kind)) {
638
- const transceiver =
639
- this.transceivers.find(
640
- (t) =>
641
- t.kind === remoteMedia.kind &&
642
- [undefined, remoteMedia.rtp.muxId].includes(t.mid)
643
- ) ||
644
- (() => {
645
- // create remote transceiver
646
- const transceiver = this.addTransceiver(remoteMedia.kind, {
647
- direction: "recvonly",
648
- });
649
-
650
- this.onRemoteTransceiverAdded.execute(transceiver);
651
- this.onTransceiver.execute(transceiver);
671
+ let dtlsTransport: RTCDtlsTransport | undefined;
652
672
 
653
- return transceiver;
654
- })();
655
-
656
- if (!transceiver.mid) {
657
- transceiver.mid = remoteMedia.rtp.muxId;
658
- transceiver.mLineIndex = i;
659
- }
660
-
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);
678
- }
679
-
680
- return true;
681
- });
682
-
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)
673
+ if (["audio", "video"].includes(remoteMedia.kind)) {
674
+ let transceiver = this.transceivers.find(
675
+ (t) =>
676
+ t.kind === remoteMedia.kind &&
677
+ [undefined, remoteMedia.rtp.muxId].includes(t.mid)
694
678
  );
695
-
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;
701
- } else {
702
- transceiver.offerDirection = direction;
703
- }
704
-
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);
710
-
711
- // register simulcast receiver
712
- remoteMedia.simulcastParameters.forEach((param) => {
713
- this.router.registerRtpReceiverByRid(
714
- transceiver,
715
- param,
716
- remotePrams
717
- );
679
+ if (!transceiver) {
680
+ // create remote transceiver
681
+ transceiver = this.addTransceiver(remoteMedia.kind, {
682
+ direction: "recvonly",
718
683
  });
719
-
720
- transceiver.receiver.prepareReceive(remotePrams);
721
- // register ssrc receiver
722
- this.router.registerRtpReceiverBySsrc(transceiver, remotePrams);
684
+ this.onRemoteTransceiverAdded.execute(transceiver);
723
685
  }
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
- );
686
+
687
+ if (bundle) {
688
+ if (!bundleTransport) {
689
+ bundleTransport = transceiver.dtlsTransport;
690
+ } else {
691
+ transceiver.setDtlsTransport(bundleTransport);
739
692
  }
740
693
  }
741
694
 
742
- transceiver.receiver.setupTWCC(remoteMedia.ssrc[0]?.ssrc);
695
+ dtlsTransport = transceiver.dtlsTransport;
696
+
697
+ this.setRemoteRTP(transceiver, remoteMedia, remoteSdp.type, i);
743
698
  } 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
699
  if (!this.sctpTransport) {
750
700
  this.sctpTransport = this.createSctpTransport();
751
701
  }
752
- this.sctpTransport.setRemotePort(this.sctpRemotePort);
753
- if (!this.sctpTransport.mid) {
754
- this.sctpTransport.mid = remoteMedia.rtp.muxId;
702
+
703
+ if (bundle) {
704
+ if (!bundleTransport) {
705
+ bundleTransport = this.sctpTransport.dtlsTransport;
706
+ } else {
707
+ this.sctpTransport.setDtlsTransport(bundleTransport);
708
+ }
755
709
  }
710
+
711
+ dtlsTransport = this.sctpTransport.dtlsTransport;
712
+
713
+ this.setRemoteSCTP(remoteMedia, this.sctpTransport);
714
+ } else {
715
+ throw new Error("invalid media kind");
756
716
  }
757
717
 
718
+ const iceTransport = dtlsTransport.iceTransport;
719
+
758
720
  if (remoteMedia.iceParams && remoteMedia.dtlsParams) {
759
- this.iceTransport.setRemoteParams(remoteMedia.iceParams);
760
- this.dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
721
+ iceTransport.setRemoteParams(remoteMedia.iceParams);
722
+ dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
761
723
 
762
724
  // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
763
725
  // RFC 8445 S6.1.1
764
726
  if (remoteMedia.iceParams?.iceLite) {
765
- this.iceTransport.connection.iceControlling = true;
727
+ iceTransport.connection.iceControlling = true;
766
728
  }
767
729
  }
768
730
 
769
731
  // # add ICE candidates
770
- remoteMedia.iceCandidates.forEach(this.iceTransport.addRemoteCandidate);
732
+ remoteMedia.iceCandidates.forEach(iceTransport.addRemoteCandidate);
771
733
 
772
- await this.iceTransport.iceGather.gather();
734
+ await iceTransport.iceGather.gather();
773
735
 
774
736
  if (remoteMedia.iceCandidatesComplete) {
775
- await this.iceTransport.addRemoteCandidate(undefined);
737
+ await iceTransport.addRemoteCandidate(undefined);
776
738
  }
777
739
 
778
740
  // # set DTLS role
779
741
  if (remoteSdp.type === "answer" && remoteMedia.dtlsParams?.role) {
780
- this.dtlsTransport.role =
742
+ dtlsTransport.role =
781
743
  remoteMedia.dtlsParams.role === "client" ? "server" : "client";
782
744
  }
783
745
  }
@@ -810,6 +772,109 @@ export class RTCPeerConnection extends EventTarget {
810
772
  }
811
773
  }
812
774
 
775
+ private setRemoteRTP(
776
+ transceiver: RTCRtpTransceiver,
777
+ remoteMedia: MediaDescription,
778
+ type: "offer" | "answer",
779
+ mLineIndex: number
780
+ ) {
781
+ if (!transceiver.mid) {
782
+ transceiver.mid = remoteMedia.rtp.muxId;
783
+ transceiver.mLineIndex = mLineIndex;
784
+ }
785
+
786
+ // # negotiate codecs
787
+ transceiver.codecs = remoteMedia.rtp.codecs.filter((remoteCodec) => {
788
+ const localCodecs = this.configuration.codecs[remoteMedia.kind] || [];
789
+
790
+ const existCodec = findCodecByMimeType(localCodecs, remoteCodec);
791
+ if (!existCodec) return false;
792
+
793
+ if (existCodec?.name.toLowerCase() === "rtx") {
794
+ const params = codecParametersFromString(existCodec.parameters ?? "");
795
+ const pt = params["apt"];
796
+ const origin = remoteMedia.rtp.codecs.find((c) => c.payloadType === pt);
797
+ if (!origin) return false;
798
+ return !!findCodecByMimeType(localCodecs, origin);
799
+ }
800
+
801
+ return true;
802
+ });
803
+
804
+ log("negotiated codecs", transceiver.codecs);
805
+ if (transceiver.codecs.length === 0) {
806
+ throw new Error("negotiate codecs failed.");
807
+ }
808
+ transceiver.headerExtensions = remoteMedia.rtp.headerExtensions.filter(
809
+ (extension) =>
810
+ (
811
+ this.configuration.headerExtensions[
812
+ remoteMedia.kind as "video" | "audio"
813
+ ] || []
814
+ ).find((v) => v.uri === extension.uri)
815
+ );
816
+
817
+ // # configure direction
818
+ const mediaDirection = remoteMedia.direction || "inactive";
819
+ const direction = reverseDirection(mediaDirection);
820
+ if (["answer", "pranswer"].includes(type)) {
821
+ transceiver.currentDirection = direction;
822
+ } else {
823
+ transceiver.offerDirection = direction;
824
+ }
825
+
826
+ const localParams = this.getLocalRtpParams(transceiver);
827
+ transceiver.sender.prepareSend(localParams);
828
+
829
+ if (["recvonly", "sendrecv"].includes(transceiver.direction)) {
830
+ const remotePrams = this.getRemoteRtpParams(remoteMedia, transceiver);
831
+
832
+ // register simulcast receiver
833
+ remoteMedia.simulcastParameters.forEach((param) => {
834
+ this.router.registerRtpReceiverByRid(transceiver, param, remotePrams);
835
+ });
836
+
837
+ transceiver.receiver.prepareReceive(remotePrams);
838
+ // register ssrc receiver
839
+ this.router.registerRtpReceiverBySsrc(transceiver, remotePrams);
840
+ }
841
+ if (["sendonly", "sendrecv"].includes(mediaDirection)) {
842
+ // assign msid
843
+ if (remoteMedia.msid != undefined) {
844
+ const [streamId, trackId] = remoteMedia.msid.split(" ");
845
+ transceiver.receiver.remoteStreamId = streamId;
846
+ transceiver.receiver.remoteTrackId = trackId;
847
+
848
+ this.fireOnTrack(
849
+ transceiver.receiver.track,
850
+ transceiver,
851
+ new MediaStream({
852
+ id: streamId,
853
+ tracks: [transceiver.receiver.track],
854
+ })
855
+ );
856
+ }
857
+ }
858
+
859
+ transceiver.receiver.setupTWCC(remoteMedia.ssrc[0]?.ssrc);
860
+ }
861
+
862
+ private setRemoteSCTP(
863
+ remoteMedia: MediaDescription,
864
+ sctpTransport: RTCSctpTransport
865
+ ) {
866
+ // # configure sctp
867
+ this.sctpRemotePort = remoteMedia.sctpPort;
868
+ if (!this.sctpRemotePort) {
869
+ throw new Error("sctpRemotePort not exist");
870
+ }
871
+
872
+ sctpTransport.setRemotePort(this.sctpRemotePort);
873
+ if (!sctpTransport.mid) {
874
+ sctpTransport.mid = remoteMedia.rtp.muxId;
875
+ }
876
+ }
877
+
813
878
  private validateDescription(
814
879
  description: SessionDescription,
815
880
  isLocal: boolean
@@ -888,14 +953,19 @@ export class RTCPeerConnection extends EventTarget {
888
953
 
889
954
  const direction = options.direction || "sendrecv";
890
955
 
891
- const sender = new RTCRtpSender(trackOrKind, this.dtlsTransport);
892
- const receiver = new RTCRtpReceiver(kind, this.dtlsTransport, sender.ssrc);
956
+ const dtlsTransport = this.createTransport([
957
+ SRTP_PROFILE.SRTP_AEAD_AES_128_GCM, // prefer
958
+ SRTP_PROFILE.SRTP_AES128_CM_HMAC_SHA1_80,
959
+ ]);
960
+
961
+ const sender = new RTCRtpSender(trackOrKind);
962
+ const receiver = new RTCRtpReceiver(kind, sender.ssrc);
893
963
  const transceiver = new RTCRtpTransceiver(
894
964
  kind,
965
+ dtlsTransport,
895
966
  receiver,
896
967
  sender,
897
- direction,
898
- this.dtlsTransport
968
+ direction
899
969
  );
900
970
  transceiver.options = options;
901
971
  this.router.registerRtpSender(transceiver.sender);
@@ -903,6 +973,7 @@ export class RTCPeerConnection extends EventTarget {
903
973
  this.transceivers.push(transceiver);
904
974
  this.onTransceiverAdded.execute(transceiver);
905
975
 
976
+ this.updateIceConnectionState();
906
977
  this.needNegotiation();
907
978
 
908
979
  return transceiver;
@@ -971,58 +1042,81 @@ export class RTCPeerConnection extends EventTarget {
971
1042
  }
972
1043
  }
973
1044
 
1045
+ private async ensureCerts() {
1046
+ const ensureCert = async (dtlsTransport: RTCDtlsTransport) => {
1047
+ if (this.certificates.length === 0) {
1048
+ const localCertificate = await dtlsTransport.setupCertificate();
1049
+ this.certificates.push(localCertificate);
1050
+ } else {
1051
+ dtlsTransport.localCertificate = this.certificates[0];
1052
+ }
1053
+ };
1054
+
1055
+ for (const dtlsTransport of this.dtlsTransports) {
1056
+ await ensureCert(dtlsTransport);
1057
+ }
1058
+ }
1059
+
974
1060
  async createAnswer() {
975
1061
  this.assertNotClosed();
976
1062
  if (
977
1063
  !["have-remote-offer", "have-local-pranswer"].includes(
978
1064
  this.signalingState
979
- ) ||
980
- !this.dtlsTransport
981
- )
1065
+ )
1066
+ ) {
982
1067
  throw new Error("createAnswer failed");
983
-
984
- if (this.certificates.length === 0) {
985
- await this.dtlsTransport.setupCertificate();
1068
+ }
1069
+ if (!this._remoteDescription) {
1070
+ throw new Error("wrong state");
986
1071
  }
987
1072
 
1073
+ await this.ensureCerts();
1074
+
988
1075
  const description = new SessionDescription();
989
1076
  addSDPHeader("answer", description);
990
1077
 
991
- this._remoteDescription?.media.forEach((remoteM) => {
1078
+ this._remoteDescription.media.forEach((remoteMedia) => {
992
1079
  let dtlsTransport!: RTCDtlsTransport;
993
1080
  let media: MediaDescription;
994
1081
 
995
- if (["audio", "video"].includes(remoteM.kind)) {
996
- const transceiver = this.getTransceiverByMid(remoteM.rtp.muxId!)!;
1082
+ if (["audio", "video"].includes(remoteMedia.kind)) {
1083
+ const transceiver = this.getTransceiverByMid(remoteMedia.rtp.muxId!)!;
997
1084
  media = createMediaDescriptionForTransceiver(
998
1085
  transceiver,
999
1086
  this.cname,
1000
1087
  andDirection(transceiver.direction, transceiver.offerDirection),
1001
1088
  transceiver.mid!
1002
1089
  );
1003
- if (!transceiver.dtlsTransport) throw new Error();
1004
1090
  dtlsTransport = transceiver.dtlsTransport;
1005
- } else if (remoteM.kind === "application") {
1006
- if (!this.sctpTransport || !this.sctpTransport.mid) throw new Error();
1091
+ } else if (remoteMedia.kind === "application") {
1092
+ if (!this.sctpTransport || !this.sctpTransport.mid) {
1093
+ throw new Error("sctpTransport not found");
1094
+ }
1007
1095
  media = createMediaDescriptionForSctp(
1008
1096
  this.sctpTransport,
1009
1097
  this.sctpTransport.mid
1010
1098
  );
1011
1099
 
1012
1100
  dtlsTransport = this.sctpTransport.dtlsTransport;
1013
- } else throw new Error();
1101
+ } else {
1102
+ throw new Error("invalid kind");
1103
+ }
1014
1104
 
1015
1105
  // # determine DTLS role, or preserve the currently configured role
1016
- if (!media.dtlsParams) throw new Error();
1106
+ if (!media.dtlsParams) {
1107
+ throw new Error("dtlsParams missing");
1108
+ }
1017
1109
  if (dtlsTransport.role === "auto") {
1018
1110
  media.dtlsParams.role = "client";
1019
1111
  } else {
1020
1112
  media.dtlsParams.role = dtlsTransport.role;
1021
1113
  }
1022
- media.simulcastParameters = remoteM.simulcastParameters.map((v) => ({
1114
+
1115
+ media.simulcastParameters = remoteMedia.simulcastParameters.map((v) => ({
1023
1116
  ...v,
1024
1117
  direction: reverseSimulcastDirection(v.direction),
1025
1118
  }));
1119
+
1026
1120
  description.media.push(media);
1027
1121
  });
1028
1122
 
@@ -1050,29 +1144,94 @@ export class RTCPeerConnection extends EventTarget {
1050
1144
  if (this.sctpTransport) {
1051
1145
  await this.sctpTransport.stop();
1052
1146
  }
1053
- await this.dtlsTransport.stop();
1054
- await this.iceTransport.stop();
1147
+ for (const dtlsTransport of this.dtlsTransports) {
1148
+ await dtlsTransport.stop();
1149
+ await dtlsTransport.iceTransport.stop();
1150
+ }
1055
1151
 
1056
1152
  this.dispose();
1057
1153
  log("peerConnection closed");
1058
1154
  }
1059
1155
 
1060
1156
  private assertNotClosed() {
1061
- if (this.isClosed) throw new Error("RTCPeerConnection is closed");
1157
+ if (this.isClosed) {
1158
+ throw new Error("RTCPeerConnection is closed");
1159
+ }
1062
1160
  }
1063
1161
 
1064
- private updateIceGatheringState(state: IceGathererState) {
1065
- log("iceGatheringStateChange", state);
1066
- this.iceGatheringState = state;
1067
- this.iceGatheringStateChange.execute(state);
1068
- this.emit("icegatheringstatechange", state);
1162
+ // https://w3c.github.io/webrtc-pc/#dom-rtcicegatheringstate
1163
+ private updateIceGatheringState() {
1164
+ const all = this.iceTransports;
1165
+
1166
+ function allMatch(...state: IceGathererState[]) {
1167
+ return (
1168
+ all.filter((check) => state.includes(check.iceGather.gatheringState))
1169
+ .length === all.length
1170
+ );
1171
+ }
1172
+
1173
+ let newState: IceGathererState;
1174
+
1175
+ if (all.length && allMatch("complete")) {
1176
+ newState = "complete";
1177
+ } else if (!all.length || allMatch("new", "complete")) {
1178
+ newState = "new";
1179
+ } else if (
1180
+ all.map((check) => check.iceGather.gatheringState).includes("gathering")
1181
+ ) {
1182
+ newState = "gathering";
1183
+ } else {
1184
+ newState = "new";
1185
+ }
1186
+
1187
+ if (this.iceGatheringState === newState) {
1188
+ return;
1189
+ }
1190
+
1191
+ log("iceGatheringStateChange", newState);
1192
+ this.iceGatheringState = newState;
1193
+ this.iceGatheringStateChange.execute(newState);
1194
+ this.emit("icegatheringstatechange", newState);
1069
1195
  }
1070
1196
 
1071
- private updateIceConnectionState(state: RTCIceConnectionState) {
1072
- log("iceConnectionStateChange", state);
1073
- this.iceConnectionState = state;
1074
- this.iceConnectionStateChange.execute(state);
1075
- this.emit("iceconnectionstatechange", state);
1197
+ // https://w3c.github.io/webrtc-pc/#dom-rtciceconnectionstate
1198
+ private updateIceConnectionState() {
1199
+ const all = this.iceTransports;
1200
+ let newState: RTCIceConnectionState;
1201
+
1202
+ function allMatch(...state: RTCIceConnectionState[]) {
1203
+ return (
1204
+ all.filter((check) => state.includes(check.state)).length === all.length
1205
+ );
1206
+ }
1207
+
1208
+ if (this.connectionState === "closed") {
1209
+ newState = "closed";
1210
+ } else if (allMatch("failed")) {
1211
+ newState = "failed";
1212
+ } else if (allMatch("disconnected")) {
1213
+ newState = "disconnected";
1214
+ } else if (allMatch("new", "closed")) {
1215
+ newState = "new";
1216
+ } else if (allMatch("new", "checking")) {
1217
+ newState = "checking";
1218
+ } else if (allMatch("completed", "closed")) {
1219
+ newState = "completed";
1220
+ } else if (allMatch("connected", "completed", "closed")) {
1221
+ newState = "connected";
1222
+ } else {
1223
+ // unreachable?
1224
+ newState = "new";
1225
+ }
1226
+
1227
+ if (this.iceConnectionState === newState) {
1228
+ return;
1229
+ }
1230
+
1231
+ log("iceConnectionStateChange", newState);
1232
+ this.iceConnectionState = newState;
1233
+ this.iceConnectionStateChange.execute(newState);
1234
+ this.emit("iceconnectionstatechange", newState);
1076
1235
  }
1077
1236
 
1078
1237
  private setSignalingState(state: RTCSignalingState) {
@@ -1209,6 +1368,8 @@ export function allocateMid(mids: Set<string>) {
1209
1368
  return mid;
1210
1369
  }
1211
1370
 
1371
+ export type BundlePolicy = "max-compat" | "max-bundle";
1372
+
1212
1373
  export interface PeerConfig {
1213
1374
  codecs: Partial<{
1214
1375
  audio: RTCRtpCodecParameters[];
@@ -1225,6 +1386,7 @@ export interface PeerConfig {
1225
1386
  dtls: Partial<{
1226
1387
  keys: DtlsKeys;
1227
1388
  }>;
1389
+ bundlePolicy: BundlePolicy;
1228
1390
  }
1229
1391
 
1230
1392
  export const findCodecByMimeType = (
@@ -1276,6 +1438,7 @@ export const defaultPeerConfig: PeerConfig = {
1276
1438
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
1277
1439
  icePortRange: undefined,
1278
1440
  dtls: {},
1441
+ bundlePolicy: "max-compat",
1279
1442
  };
1280
1443
 
1281
1444
  export interface RTCTrackEvent {