werift 0.24.4 → 0.25.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 (166) hide show
  1. package/README.md +116 -113
  2. package/lib/common/src/transport.d.ts +22 -2
  3. package/lib/common/src/transport.js +84 -26
  4. package/lib/common/src/transport.js.map +1 -1
  5. package/lib/ice/src/exceptions.d.ts +9 -0
  6. package/lib/ice/src/exceptions.js +23 -1
  7. package/lib/ice/src/exceptions.js.map +1 -1
  8. package/lib/ice/src/ice.d.ts +40 -0
  9. package/lib/ice/src/ice.js +194 -28
  10. package/lib/ice/src/ice.js.map +1 -1
  11. package/lib/ice/src/iceBase.d.ts +26 -0
  12. package/lib/ice/src/iceBase.js.map +1 -1
  13. package/lib/ice/src/stun/const.d.ts +7 -0
  14. package/lib/ice/src/stun/const.js +8 -0
  15. package/lib/ice/src/stun/const.js.map +1 -1
  16. package/lib/ice/src/stun/protocol.js +1 -4
  17. package/lib/ice/src/stun/protocol.js.map +1 -1
  18. package/lib/ice/src/stun/tcpProtocol.d.ts +7 -1
  19. package/lib/ice/src/stun/tcpProtocol.js +65 -14
  20. package/lib/ice/src/stun/tcpProtocol.js.map +1 -1
  21. package/lib/ice/src/stun/transaction.d.ts +12 -1
  22. package/lib/ice/src/stun/transaction.js +37 -4
  23. package/lib/ice/src/stun/transaction.js.map +1 -1
  24. package/lib/ice/src/turn/protocol.d.ts +40 -15
  25. package/lib/ice/src/turn/protocol.js +141 -32
  26. package/lib/ice/src/turn/protocol.js.map +1 -1
  27. package/lib/ice/src/types/model.d.ts +8 -0
  28. package/lib/ice/src/types/model.js.map +1 -1
  29. package/lib/index.mjs +6175 -4073
  30. package/lib/nonstandard/index.mjs +286 -1003
  31. package/lib/polyfill/dom.mjs +23794 -0
  32. package/lib/polyfill/index.mjs +23794 -0
  33. package/lib/rtp/src/extra/container/mp4/container.js +12 -4
  34. package/lib/rtp/src/extra/container/mp4/container.js.map +1 -1
  35. package/lib/rtp/src/extra/processor/mp4.js +8 -3
  36. package/lib/rtp/src/extra/processor/mp4.js.map +1 -1
  37. package/lib/rtp/src/extra/processor/mp4Callback.js +3 -1
  38. package/lib/rtp/src/extra/processor/mp4Callback.js.map +1 -1
  39. package/lib/sctp/src/chunk.d.ts +5 -0
  40. package/lib/sctp/src/chunk.js +16 -11
  41. package/lib/sctp/src/chunk.js.map +1 -1
  42. package/lib/sctp/src/index.d.ts +3 -1
  43. package/lib/sctp/src/index.js +10 -1
  44. package/lib/sctp/src/index.js.map +1 -1
  45. package/lib/sctp/src/sctp.d.ts +12 -3
  46. package/lib/sctp/src/sctp.js +51 -18
  47. package/lib/sctp/src/sctp.js.map +1 -1
  48. package/lib/webrtc/src/const.js +2 -0
  49. package/lib/webrtc/src/const.js.map +1 -1
  50. package/lib/webrtc/src/errors.d.ts +9 -1
  51. package/lib/webrtc/src/errors.js +19 -0
  52. package/lib/webrtc/src/errors.js.map +1 -1
  53. package/lib/webrtc/src/media/codec.d.ts +5 -0
  54. package/lib/webrtc/src/media/codec.js +21 -0
  55. package/lib/webrtc/src/media/codec.js.map +1 -1
  56. package/lib/webrtc/src/media/codecCompatibility.d.ts +23 -0
  57. package/lib/webrtc/src/media/codecCompatibility.js +138 -0
  58. package/lib/webrtc/src/media/codecCompatibility.js.map +1 -0
  59. package/lib/webrtc/src/media/index.d.ts +1 -0
  60. package/lib/webrtc/src/media/index.js +1 -0
  61. package/lib/webrtc/src/media/index.js.map +1 -1
  62. package/lib/webrtc/src/media/router.d.ts +2 -0
  63. package/lib/webrtc/src/media/router.js +23 -3
  64. package/lib/webrtc/src/media/router.js.map +1 -1
  65. package/lib/webrtc/src/media/rtpReceiver.d.ts +12 -0
  66. package/lib/webrtc/src/media/rtpReceiver.js +35 -0
  67. package/lib/webrtc/src/media/rtpReceiver.js.map +1 -1
  68. package/lib/webrtc/src/media/rtpSender.d.ts +65 -4
  69. package/lib/webrtc/src/media/rtpSender.js +264 -35
  70. package/lib/webrtc/src/media/rtpSender.js.map +1 -1
  71. package/lib/webrtc/src/media/rtpTransceiver.d.ts +42 -0
  72. package/lib/webrtc/src/media/rtpTransceiver.js +157 -4
  73. package/lib/webrtc/src/media/rtpTransceiver.js.map +1 -1
  74. package/lib/webrtc/src/media/track.d.ts +29 -1
  75. package/lib/webrtc/src/media/track.js +164 -5
  76. package/lib/webrtc/src/media/track.js.map +1 -1
  77. package/lib/webrtc/src/nonstandard/index.d.ts +0 -1
  78. package/lib/webrtc/src/nonstandard/index.js +0 -1
  79. package/lib/webrtc/src/nonstandard/index.js.map +1 -1
  80. package/lib/webrtc/src/nonstandard/recorder/writer/webm.js +2 -1
  81. package/lib/webrtc/src/nonstandard/recorder/writer/webm.js.map +1 -1
  82. package/lib/webrtc/src/nonstandard/userMedia.d.ts +1 -2
  83. package/lib/webrtc/src/nonstandard/userMedia.js +30 -14
  84. package/lib/webrtc/src/nonstandard/userMedia.js.map +1 -1
  85. package/lib/webrtc/src/peerConnection.d.ts +126 -5
  86. package/lib/webrtc/src/peerConnection.js +609 -136
  87. package/lib/webrtc/src/peerConnection.js.map +1 -1
  88. package/lib/webrtc/src/polyfill/api.d.ts +14 -0
  89. package/lib/webrtc/src/polyfill/api.js +23 -0
  90. package/lib/webrtc/src/polyfill/api.js.map +1 -0
  91. package/lib/webrtc/src/polyfill/browserIdentity.d.ts +11 -0
  92. package/lib/webrtc/src/polyfill/browserIdentity.js +59 -0
  93. package/lib/webrtc/src/polyfill/browserIdentity.js.map +1 -0
  94. package/lib/webrtc/src/polyfill/dom.d.ts +11 -0
  95. package/lib/webrtc/src/polyfill/dom.js +18 -0
  96. package/lib/webrtc/src/polyfill/dom.js.map +1 -0
  97. package/lib/webrtc/src/polyfill/existingMediaDevices.d.ts +2 -0
  98. package/lib/webrtc/src/polyfill/existingMediaDevices.js +22 -0
  99. package/lib/webrtc/src/polyfill/existingMediaDevices.js.map +1 -0
  100. package/lib/webrtc/src/polyfill/globals.d.ts +28 -0
  101. package/lib/webrtc/src/polyfill/globals.js +3 -0
  102. package/lib/webrtc/src/polyfill/globals.js.map +1 -0
  103. package/lib/webrtc/src/polyfill/index.d.ts +2 -0
  104. package/lib/webrtc/src/polyfill/index.js +19 -0
  105. package/lib/webrtc/src/polyfill/index.js.map +1 -0
  106. package/lib/webrtc/src/polyfill/install.d.ts +13 -0
  107. package/lib/webrtc/src/polyfill/install.js +266 -0
  108. package/lib/webrtc/src/polyfill/install.js.map +1 -0
  109. package/lib/webrtc/src/polyfill/mediaDevices.d.ts +38 -0
  110. package/lib/webrtc/src/polyfill/mediaDevices.js +228 -0
  111. package/lib/webrtc/src/polyfill/mediaDevices.js.map +1 -0
  112. package/lib/webrtc/src/polyfill/mediaRegister.d.ts +51 -0
  113. package/lib/webrtc/src/polyfill/mediaRegister.js +14 -0
  114. package/lib/webrtc/src/polyfill/mediaRegister.js.map +1 -0
  115. package/lib/webrtc/src/polyfill/peerConnectionConfig.d.ts +3 -0
  116. package/lib/webrtc/src/polyfill/peerConnectionConfig.js +52 -0
  117. package/lib/webrtc/src/polyfill/peerConnectionConfig.js.map +1 -0
  118. package/lib/webrtc/src/polyfill/registers/callback.d.ts +16 -0
  119. package/lib/webrtc/src/polyfill/registers/callback.js +57 -0
  120. package/lib/webrtc/src/polyfill/registers/callback.js.map +1 -0
  121. package/lib/webrtc/src/polyfill/registers/empty.d.ts +2 -0
  122. package/lib/webrtc/src/polyfill/registers/empty.js +28 -0
  123. package/lib/webrtc/src/polyfill/registers/empty.js.map +1 -0
  124. package/lib/webrtc/src/polyfill/registers/mp4Webm.d.ts +36 -0
  125. package/lib/webrtc/src/polyfill/registers/mp4Webm.js +242 -0
  126. package/lib/webrtc/src/polyfill/registers/mp4Webm.js.map +1 -0
  127. package/lib/webrtc/src/polyfill/registers/rtpRtcp.d.ts +17 -0
  128. package/lib/webrtc/src/polyfill/registers/rtpRtcp.js +143 -0
  129. package/lib/webrtc/src/polyfill/registers/rtpRtcp.js.map +1 -0
  130. package/lib/webrtc/src/polyfill/rtcSessionDescription.d.ts +18 -0
  131. package/lib/webrtc/src/polyfill/rtcSessionDescription.js +36 -0
  132. package/lib/webrtc/src/polyfill/rtcSessionDescription.js.map +1 -0
  133. package/lib/webrtc/src/polyfill/rtpCodec.d.ts +17 -0
  134. package/lib/webrtc/src/polyfill/rtpCodec.js +93 -0
  135. package/lib/webrtc/src/polyfill/rtpCodec.js.map +1 -0
  136. package/lib/webrtc/src/polyfill/selectSettings.d.ts +31 -0
  137. package/lib/webrtc/src/polyfill/selectSettings.js +156 -0
  138. package/lib/webrtc/src/polyfill/selectSettings.js.map +1 -0
  139. package/lib/webrtc/src/polyfill/sourceIo.d.ts +21 -0
  140. package/lib/webrtc/src/polyfill/sourceIo.js +298 -0
  141. package/lib/webrtc/src/polyfill/sourceIo.js.map +1 -0
  142. package/lib/webrtc/src/polyfill/trackStop.d.ts +4 -0
  143. package/lib/webrtc/src/polyfill/trackStop.js +16 -0
  144. package/lib/webrtc/src/polyfill/trackStop.js.map +1 -0
  145. package/lib/webrtc/src/sctpManager.d.ts +2 -1
  146. package/lib/webrtc/src/sctpManager.js +5 -2
  147. package/lib/webrtc/src/sctpManager.js.map +1 -1
  148. package/lib/webrtc/src/sdp.d.ts +1 -1
  149. package/lib/webrtc/src/sdp.js +5 -6
  150. package/lib/webrtc/src/sdp.js.map +1 -1
  151. package/lib/webrtc/src/sdpManager.d.ts +38 -1
  152. package/lib/webrtc/src/sdpManager.js +236 -30
  153. package/lib/webrtc/src/sdpManager.js.map +1 -1
  154. package/lib/webrtc/src/secureTransportManager.d.ts +43 -12
  155. package/lib/webrtc/src/secureTransportManager.js +123 -47
  156. package/lib/webrtc/src/secureTransportManager.js.map +1 -1
  157. package/lib/webrtc/src/transceiverManager.d.ts +91 -6
  158. package/lib/webrtc/src/transceiverManager.js +477 -62
  159. package/lib/webrtc/src/transceiverManager.js.map +1 -1
  160. package/lib/webrtc/src/transport/ice.d.ts +8 -0
  161. package/lib/webrtc/src/transport/ice.js +17 -1
  162. package/lib/webrtc/src/transport/ice.js.map +1 -1
  163. package/lib/webrtc/src/transport/sctp.d.ts +3 -2
  164. package/lib/webrtc/src/transport/sctp.js +8 -2
  165. package/lib/webrtc/src/transport/sctp.js.map +1 -1
  166. package/package.json +13 -3
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RTCTrackEvent = exports.defaultPeerConfig = exports.findCodecByMimeType = exports.RTCPeerConnection = void 0;
4
4
  const crypto_1 = require("crypto");
5
+ const src_1 = require("../../sctp/src");
5
6
  const errors_1 = require("./errors");
6
7
  const helper_1 = require("./helper");
7
8
  const common_1 = require("./imports/common");
@@ -124,6 +125,40 @@ class RTCPeerConnection extends helper_1.EventTarget {
124
125
  writable: true,
125
126
  value: false
126
127
  });
128
+ /**同じ tick の複数の変更で negotiationneeded を重複発火しないための予約フラグ */
129
+ Object.defineProperty(this, "negotiationneededScheduled", {
130
+ enumerable: true,
131
+ configurable: true,
132
+ writable: true,
133
+ value: false
134
+ });
135
+ /**交渉が必要な変更の通し番号 */
136
+ Object.defineProperty(this, "negotiationChangeSeq", {
137
+ enumerable: true,
138
+ configurable: true,
139
+ writable: true,
140
+ value: 0
141
+ });
142
+ /**answer まで確定した local offer が反映している変更の通し番号 */
143
+ Object.defineProperty(this, "negotiatedChangeSeq", {
144
+ enumerable: true,
145
+ configurable: true,
146
+ writable: true,
147
+ value: 0
148
+ });
149
+ /**適用中の local offer が反映している変更の通し番号 (rollback で破棄) */
150
+ Object.defineProperty(this, "pendingOfferChangeSeq", {
151
+ enumerable: true,
152
+ configurable: true,
153
+ writable: true,
154
+ value: void 0
155
+ });
156
+ Object.defineProperty(this, "lastCreatedOfferChangeSeq", {
157
+ enumerable: true,
158
+ configurable: true,
159
+ writable: true,
160
+ value: 0
161
+ });
127
162
  Object.defineProperty(this, "needRestart", {
128
163
  enumerable: true,
129
164
  configurable: true,
@@ -256,31 +291,21 @@ class RTCPeerConnection extends helper_1.EventTarget {
256
291
  writable: true,
257
292
  value: {}
258
293
  });
294
+ /**交渉が必要な変更を記録し、negotiationneeded を予約する */
259
295
  Object.defineProperty(this, "needNegotiation", {
260
296
  enumerable: true,
261
297
  configurable: true,
262
298
  writable: true,
263
- value: async () => {
264
- this.invalidateLastCreatedDescriptions();
265
- this.shouldNegotiationneeded = true;
266
- if (this.negotiationneeded || this.signalingState !== "stable") {
267
- return;
268
- }
269
- this.shouldNegotiationneeded = false;
270
- setImmediate(() => {
271
- this.negotiationneeded = true;
272
- this.onNegotiationneeded.execute();
273
- if (this.onnegotiationneeded) {
274
- this.onnegotiationneeded(new globalThis.Event("negotiationneeded"));
275
- }
276
- this.emit("negotiationneeded");
277
- });
299
+ value: () => {
300
+ this.negotiationChangeSeq++;
301
+ this.scheduleNegotiationneeded();
278
302
  }
279
303
  });
280
304
  this.setConfiguration(config);
281
305
  this.sdpManager = new sdpManager_1.SDPManager({
282
306
  cname: this.cname,
283
307
  bundlePolicy: this.config.bundlePolicy,
308
+ mLineReuse: this.config.mLineReuse,
284
309
  });
285
310
  this.transceiverManager = new media_1.TransceiverManager(this.cname, this.config, this.router);
286
311
  this.transceiverManager.onTransceiverAdded.pipe(this.onTransceiverAdded);
@@ -441,6 +466,15 @@ class RTCPeerConnection extends helper_1.EventTarget {
441
466
  normalizedConfig.bundlePolicy !== this.config.bundlePolicy) {
442
467
  throw new Error("bundlePolicy cannot be changed");
443
468
  }
469
+ if (normalizedConfig.mLineReuse !== undefined &&
470
+ !MLineReuseModes.includes(normalizedConfig.mLineReuse)) {
471
+ throw (0, errors_1.createWebRtcTypeError)(`mLineReuse must be one of ${MLineReuseModes.join(", ")}`);
472
+ }
473
+ if (isReconfiguration &&
474
+ normalizedConfig.mLineReuse !== undefined &&
475
+ normalizedConfig.mLineReuse !== this.config.mLineReuse) {
476
+ throw (0, errors_1.createWebRtcDomException)("InvalidModificationError", "mLineReuse cannot be changed");
477
+ }
444
478
  if (isReconfiguration &&
445
479
  normalizedConfig.rtcpMuxPolicy !== undefined &&
446
480
  normalizedConfig.rtcpMuxPolicy !== this.config.rtcpMuxPolicy) {
@@ -460,7 +494,18 @@ class RTCPeerConnection extends helper_1.EventTarget {
460
494
  if ((normalizedConfig.iceCandidatePoolSize ?? 0) > 0) {
461
495
  throw new Error("iceCandidatePoolSize > 0 is not supported");
462
496
  }
497
+ if (normalizedConfig.sctp !== undefined) {
498
+ const requestedSctpMtu = normalizedConfig.sctp.mtu ?? src_1.DEFAULT_SCTP_MTU;
499
+ (0, src_1.validateSctpMtu)(requestedSctpMtu);
500
+ if (this.sctpManager?.sctpTransport &&
501
+ requestedSctpMtu !== this.config.sctp.mtu) {
502
+ throw new Error("sctp.mtu cannot be changed after SCTP transport creation");
503
+ }
504
+ }
463
505
  (0, utils_1.deepMerge)(this.config, normalizedConfig);
506
+ this.config.sctp = {
507
+ mtu: this.config.sctp?.mtu ?? src_1.DEFAULT_SCTP_MTU,
508
+ };
464
509
  if (this.config.icePortRange) {
465
510
  const [min, max] = this.config.icePortRange;
466
511
  if (min === max)
@@ -476,37 +521,26 @@ class RTCPeerConnection extends helper_1.EventTarget {
476
521
  this.sctpManager.sctpTransport.maxMessageSize =
477
522
  this.config.maxMessageSize;
478
523
  }
479
- for (const [i, codecParams] of (0, helper_1.enumerate)([
480
- ...(this.config.codecs.audio || []),
481
- ...(this.config.codecs.video || []),
482
- ])) {
483
- if (codecParams.payloadType != undefined) {
484
- continue;
485
- }
486
- codecParams.payloadType = 96 + i;
487
- switch (codecParams.name.toLowerCase()) {
488
- case "rtx":
489
- {
490
- codecParams.parameters = `apt=${codecParams.payloadType - 1}`;
491
- }
492
- break;
493
- case "red":
494
- {
495
- if (codecParams.contentType === "audio") {
496
- const redundant = codecParams.payloadType + 1;
497
- codecParams.parameters = `${redundant}/${redundant}`;
498
- codecParams.payloadType = 63;
499
- }
500
- }
501
- break;
502
- }
503
- }
524
+ assignDynamicPayloadTypes(this.config);
504
525
  [
505
526
  ...(this.config.headerExtensions.audio || []),
506
527
  ...(this.config.headerExtensions.video || []),
507
528
  ].forEach((v, i) => {
508
529
  v.id = 1 + i;
509
530
  });
531
+ // Propagate ICE server changes only to transports still in gathering
532
+ // state "new". JSEP (RFC 8829 §4.1.18): STUN/TURN changes affect the next
533
+ // gathering phase; once gathering has started or finished, nothing is
534
+ // applied to the live Connection (WHIP: createOffer → setConfiguration →
535
+ // setLocalDescription). Per-transport state is used so a gatherer that
536
+ // was reset to "new" (e.g. after ICE restart) is not blocked by a stale
537
+ // manager aggregate of "complete".
538
+ if (isReconfiguration &&
539
+ this.secureManager &&
540
+ (normalizedConfig.iceServers !== undefined ||
541
+ normalizedConfig.turnUdpFamily !== undefined)) {
542
+ this.secureManager.updateIceServers();
543
+ }
510
544
  }
511
545
  getConfiguration() {
512
546
  return clonePeerConfiguration(this.config);
@@ -518,7 +552,8 @@ class RTCPeerConnection extends helper_1.EventTarget {
518
552
  }
519
553
  await this.secureManager.ensureCerts();
520
554
  for (const transceiver of this.transceiverManager.getTransceivers()) {
521
- if (transceiver.codecs.length === 0) {
555
+ if (transceiver.codecs.length === 0 ||
556
+ transceiver.codecPreferencesNeedResolution) {
522
557
  this.transceiverManager.assignTransceiverCodecs(transceiver);
523
558
  }
524
559
  if (transceiver.headerExtensions.length === 0) {
@@ -529,10 +564,11 @@ class RTCPeerConnection extends helper_1.EventTarget {
529
564
  const description = this.sdpManager.buildOfferSdp(this.transceiverManager.getTransceivers(), this.sctpTransport);
530
565
  const createdOffer = description.toJSON();
531
566
  this.lastCreatedOffer = createdOffer;
567
+ this.lastCreatedOfferChangeSeq = this.negotiationChangeSeq;
532
568
  return createdOffer;
533
569
  }
534
570
  createSctpTransport() {
535
- const sctp = this.sctpManager.createSctpTransport(this.config.maxMessageSize);
571
+ const sctp = this.sctpManager.createSctpTransport(this.config.maxMessageSize, this.config.sctp);
536
572
  const dtlsTransport = this.findOrCreateTransport();
537
573
  sctp.setDtlsTransport(dtlsTransport);
538
574
  return sctp;
@@ -553,8 +589,39 @@ class RTCPeerConnection extends helper_1.EventTarget {
553
589
  if (this.isClosed) {
554
590
  throw (0, errors_1.createWebRtcDomException)("InvalidStateError", "peer closed");
555
591
  }
556
- this.transceiverManager.removeTrack(sender);
557
- this.needNegotiation();
592
+ if (this.transceiverManager.removeTrack(sender)) {
593
+ this.needNegotiation();
594
+ }
595
+ }
596
+ /**未交渉の変更が残っていれば negotiationneeded を予約する (stable 復帰時の再判定にも使う) */
597
+ scheduleNegotiationneeded() {
598
+ this.invalidateLastCreatedDescriptions();
599
+ this.shouldNegotiationneeded = true;
600
+ if (this.negotiationneeded ||
601
+ this.negotiationneededScheduled ||
602
+ this.signalingState !== "stable") {
603
+ return;
604
+ }
605
+ this.shouldNegotiationneeded = false;
606
+ this.negotiationneededScheduled = true;
607
+ setImmediate(() => {
608
+ this.negotiationneededScheduled = false;
609
+ if (this.negotiatedChangeSeq >= this.negotiationChangeSeq) {
610
+ // 発火前に適用された local offer が変更をすべて含んでいる
611
+ return;
612
+ }
613
+ if (this.signalingState !== "stable") {
614
+ // stable に戻った時点で改めて判定する
615
+ this.shouldNegotiationneeded = true;
616
+ return;
617
+ }
618
+ this.negotiationneeded = true;
619
+ this.onNegotiationneeded.execute();
620
+ if (this.onnegotiationneeded) {
621
+ this.onnegotiationneeded(new globalThis.Event("negotiationneeded"));
622
+ }
623
+ this.emit("negotiationneeded");
624
+ });
558
625
  }
559
626
  invalidateLastCreatedDescriptions() {
560
627
  this.lastCreatedAnswer = undefined;
@@ -569,7 +636,6 @@ class RTCPeerConnection extends helper_1.EventTarget {
569
636
  }
570
637
  findOrCreateTransport() {
571
638
  const existingDtlsTransport = this.dtlsTransports.find((transport) => transport.state !== "closed");
572
- const existing = existingDtlsTransport?.iceTransport;
573
639
  // Gather ICE candidates for only one track. If the remote endpoint is not bundle-aware, negotiate only one media track.
574
640
  // https://w3c.github.io/webrtc-pc/#rtcbundlepolicy-enum
575
641
  if (this.sdpManager.bundlePolicy === "max-bundle" ||
@@ -578,7 +644,16 @@ class RTCPeerConnection extends helper_1.EventTarget {
578
644
  return existingDtlsTransport;
579
645
  }
580
646
  }
581
- const dtlsTransport = this.secureManager.createTransport();
647
+ return this.createDtlsTransport();
648
+ }
649
+ /**
650
+ * @param inheritCredentials false の場合は BUNDLE 外の独立 transport として
651
+ * 既存 transport と異なる ICE credentials を使う
652
+ */
653
+ createDtlsTransport({ inheritCredentials = true } = {}) {
654
+ const dtlsTransport = this.secureManager.createTransport({
655
+ inheritCredentials,
656
+ });
582
657
  dtlsTransport.onRtp.subscribe((rtp) => {
583
658
  this.router.routeRtp(rtp);
584
659
  });
@@ -607,21 +682,87 @@ class RTCPeerConnection extends helper_1.EventTarget {
607
682
  log("localDescription not found when ice candidate was gathered");
608
683
  return;
609
684
  }
685
+ const owner = this.resolveLocalCandidateOwner(iceTransport);
686
+ if (!owner) {
687
+ // 停止 / 拒否した m-line だけが使う transport の候補は通知しない
688
+ log("no accepted m-line owns the ice transport", iceTransport.id);
689
+ return;
690
+ }
610
691
  this.secureManager.handleNewIceCandidate({
611
692
  candidate,
612
- bundlePolicy: this.sdpManager.bundlePolicy,
613
- remoteIsBundled: !!this.sdpManager.remoteIsBundled,
614
- media: this._localDescription.media[0],
615
- transceiver: this.transceiverManager
616
- .getTransceivers()
617
- .find((t) => t?.dtlsTransport?.iceTransport.id === iceTransport.id),
618
- sctpTransport: this.sctpTransport?.dtlsTransport.iceTransport.id === iceTransport.id
619
- ? this.sctpTransport
620
- : undefined,
693
+ sdpMid: owner.mid,
694
+ sdpMLineIndex: owner.index,
621
695
  });
622
696
  });
623
697
  return dtlsTransport;
624
698
  }
699
+ /**
700
+ * local candidate の sdpMid / sdpMLineIndex を、その ICE transport を実際に
701
+ * 所有する受け入れ済み m-line (BUNDLE なら tag) に合わせる。
702
+ */
703
+ resolveLocalCandidateOwner(iceTransport) {
704
+ const description = this._localDescription;
705
+ if (!description) {
706
+ return;
707
+ }
708
+ const owners = description.media
709
+ .map((media, index) => ({ media, index }))
710
+ .filter(({ media }) => {
711
+ if (media.port === 0) {
712
+ return false;
713
+ }
714
+ const mid = media.rtp.muxId;
715
+ if (media.kind === "application") {
716
+ return (!!this.sctpTransport &&
717
+ this.sctpTransport.mid === mid &&
718
+ this.sctpTransport.dtlsTransport?.iceTransport.id ===
719
+ iceTransport.id);
720
+ }
721
+ const transceiver = this.transceiverManager
722
+ .getTransceivers()
723
+ .find((t) => t.mid != undefined && t.mid === mid);
724
+ return (!!transceiver &&
725
+ !transceiver.stopped &&
726
+ transceiver.dtlsTransport?.iceTransport.id === iceTransport.id);
727
+ });
728
+ const tags = description.group
729
+ .filter((group) => group.semantic === "BUNDLE")
730
+ .map((group) => group.items[0]);
731
+ const owner = owners.find(({ media }) => tags.includes(media.rtp.muxId)) ?? owners[0];
732
+ if (!owner) {
733
+ return;
734
+ }
735
+ return { mid: owner.media.rtp.muxId, index: owner.index };
736
+ }
737
+ /**
738
+ * answer 確定後の stopping transceiver を整理する。
739
+ * @returns port 0 を自分の offer で交渉すべき transceiver が残っているか
740
+ */
741
+ settleStoppingTransceivers() {
742
+ const negotiatedMids = new Set((this.sdpManager.currentLocalDescription?.media ?? [])
743
+ .filter((media) => media.port !== 0 && media.rtp.muxId)
744
+ .map((media) => media.rtp.muxId));
745
+ const pending = this.transceiverManager.settleStoppingTransceivers(negotiatedMids);
746
+ this.closeIdleTransports();
747
+ return pending;
748
+ }
749
+ /**停止した transceiver の transport のうち、live な利用者がいないものを閉じる */
750
+ closeIdleTransports(candidates = this.transceiverManager
751
+ .getTransceivers()
752
+ .filter((t) => t.stopped)
753
+ .map((t) => t.dtlsTransport)) {
754
+ const live = new Set(this.dtlsTransports.map((t) => t.id));
755
+ for (const transport of new Set(candidates)) {
756
+ if (!transport ||
757
+ transport.state === "closed" ||
758
+ live.has(transport.id)) {
759
+ continue;
760
+ }
761
+ transport.stop().catch((error) => {
762
+ log("failed to close idle transport", error);
763
+ });
764
+ }
765
+ }
625
766
  async setLocalDescription(sessionDescription) {
626
767
  // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/setLocalDescription#type
627
768
  const implicitOfferState = [
@@ -632,9 +773,13 @@ class RTCPeerConnection extends helper_1.EventTarget {
632
773
  await this.waitForPendingDescriptionTask();
633
774
  if (sessionDescription?.type === "rollback") {
634
775
  this.sdpManager.rollbackLocalDescription(this.signalingState);
776
+ for (const transceiver of this.transceiverManager.getTransceivers()) {
777
+ transceiver.pendingLocalOfferCodecs = undefined;
778
+ }
779
+ this.pendingOfferChangeSeq = undefined;
635
780
  this.setSignalingState("stable");
636
781
  if (this.shouldNegotiationneeded) {
637
- this.needNegotiation();
782
+ this.scheduleNegotiationneeded();
638
783
  }
639
784
  this.invalidateLastCreatedDescriptions();
640
785
  return;
@@ -674,6 +819,11 @@ class RTCPeerConnection extends helper_1.EventTarget {
674
819
  });
675
820
  // # update signaling state
676
821
  if (description.type === "offer") {
822
+ // この offer は作成時点までの変更を含む。answer の適用で交渉済みにする
823
+ this.pendingOfferChangeSeq =
824
+ this.lastCreatedOffer?.sdp === sessionDescription.sdp
825
+ ? this.lastCreatedOfferChangeSeq
826
+ : undefined;
677
827
  this.setSignalingState("have-local-offer");
678
828
  }
679
829
  else if (description.type === "answer") {
@@ -682,14 +832,39 @@ class RTCPeerConnection extends helper_1.EventTarget {
682
832
  else if (description.type === "pranswer") {
683
833
  this.setSignalingState("have-local-pranswer");
684
834
  }
835
+ if (description.type === "answer") {
836
+ // answer の確定で拒否予定の m-line を停止し、RTP pipeline / 遊休 transport を解放する
837
+ this.transceiverManager.commitRemoteOffer();
838
+ // port 0 で答えた m-line (aggressive の inactive など) は offerer 側で拒否として停止が確定する。
839
+ // answerer 側も同じ位置を停止確定にし、offerer が新 MID で再利用できるようにする
840
+ for (const media of description.media) {
841
+ if (!["audio", "video"].includes(media.kind) || media.port !== 0) {
842
+ continue;
843
+ }
844
+ const transceiver = this.transceiverManager
845
+ .getTransceivers()
846
+ .find((t) => t.mid === media.rtp.muxId && !t.stopped);
847
+ transceiver?.commitStopped({ rejected: true });
848
+ }
849
+ this.closeIdleTransports();
850
+ }
685
851
  // # assign MID
686
852
  for (const [i, media] of (0, helper_1.enumerate)(description.media)) {
687
853
  const mid = media.rtp.muxId;
688
854
  this.sdpManager.registerMid(mid);
689
- if (["audio", "video"].includes(media.kind)) {
690
- const transceiver = this.transceiverManager.getTransceiverByMLineIndex(i);
855
+ if (["audio", "video"].includes(media.kind) && media.port !== 0) {
856
+ // 停止済み、別 kind、別 MID の transceiver には割り当てない (MID の一意性を保つ)
857
+ const transceiver = this.transceiverManager
858
+ .getTransceivers()
859
+ .find((t) => t.mLineIndex === i &&
860
+ !t.stopped &&
861
+ t.kind === media.kind &&
862
+ (t.mid == null || t.mid === mid));
691
863
  if (transceiver) {
692
864
  transceiver.mid = mid;
865
+ if (description.type === "offer") {
866
+ transceiver.pendingLocalOfferCodecs = [...media.rtp.codecs];
867
+ }
693
868
  }
694
869
  }
695
870
  if (media.kind === "application" && this.sctpTransport) {
@@ -699,14 +874,43 @@ class RTCPeerConnection extends helper_1.EventTarget {
699
874
  // setup ice,dtls role
700
875
  const role = description.media.find((media) => media.dtlsParams)?.dtlsParams
701
876
  ?.role;
877
+ // transport ごとに、その transport を使う受け入れ済み m-line の a=setup を適用する
878
+ // (BUNDLE group 外の独立 transport は先頭 m-line と異なる role を持ちうる)
879
+ const roleOfTransport = new Map();
880
+ for (const media of description.media) {
881
+ const mid = media.rtp.muxId;
882
+ const mediaRole = media.dtlsParams?.role;
883
+ if (media.port === 0 || mid == undefined || !mediaRole) {
884
+ continue;
885
+ }
886
+ const owner = media.kind === "application"
887
+ ? this.sctpTransport?.mid === mid
888
+ ? this.sctpTransport
889
+ : undefined
890
+ : this.transceiverManager
891
+ .getTransceivers()
892
+ .find((t) => t.mid === mid && !t.stopped);
893
+ if (owner?.dtlsTransport && !roleOfTransport.has(owner.dtlsTransport)) {
894
+ roleOfTransport.set(owner.dtlsTransport, mediaRole);
895
+ }
896
+ }
702
897
  this.secureManager.setLocalRole({
703
898
  type: description.type === "offer" ? "offer" : "answer",
704
899
  role,
900
+ roleOfTransport,
705
901
  });
706
902
  // # configure direction
707
903
  if (["answer", "pranswer"].includes(description.type)) {
708
- for (const t of this.transceiverManager.getTransceivers()) {
709
- const direction = (0, utils_1.andDirection)(t.direction, t.offerDirection);
904
+ for (const media of description.media) {
905
+ const t = this.transceiverManager
906
+ .getTransceivers()
907
+ .find((t) => t.mid != undefined && t.mid === media.rtp.muxId);
908
+ if (!t || t.stopped || t.pendingRejection) {
909
+ continue;
910
+ }
911
+ const direction = t.stopping
912
+ ? "inactive"
913
+ : (0, utils_1.andDirection)(t.direction, t.offerDirection);
710
914
  t.setCurrentDirection(direction);
711
915
  }
712
916
  }
@@ -723,14 +927,16 @@ class RTCPeerConnection extends helper_1.EventTarget {
723
927
  });
724
928
  }
725
929
  this.sdpManager.setLocal(description, this.transceiverManager.getTransceivers(), this.sctpTransport);
726
- if (this.shouldNegotiationneeded) {
727
- this.needNegotiation();
930
+ // answerer の stop() は次の自分の offer で交渉する
931
+ const hasUnnegotiatedStop = description.type === "answer" && this.settleStoppingTransceivers();
932
+ if (this.shouldNegotiationneeded || hasUnnegotiatedStop) {
933
+ this.scheduleNegotiationneeded();
728
934
  }
729
935
  this.invalidateLastCreatedDescriptions();
730
936
  return description;
731
937
  }
732
938
  async gatherCandidates() {
733
- await this.secureManager.gatherCandidates(!!this.sdpManager.remoteIsBundled);
939
+ await this.secureManager.gatherCandidates();
734
940
  }
735
941
  async addIceCandidate(candidateMessage = {}) {
736
942
  if (this.isClosed) {
@@ -821,61 +1027,81 @@ class RTCPeerConnection extends helper_1.EventTarget {
821
1027
  sessionDescription = sessionDescription.toSdp();
822
1028
  }
823
1029
  await this.waitForPendingDescriptionTask();
1030
+ let codecPlan = new Map();
1031
+ if (sessionDescription.type &&
1032
+ sessionDescription.type !== "rollback" &&
1033
+ sessionDescription.sdp) {
1034
+ const preview = this.sdpManager.parseSdp({
1035
+ sdp: sessionDescription.sdp,
1036
+ isLocal: false,
1037
+ signalingState: this.signalingState,
1038
+ type: sessionDescription.type,
1039
+ });
1040
+ // 本適用と同じ対応付けで全 m-line の codec を副作用なしに解決する
1041
+ const planned = new Set();
1042
+ codecPlan = this.transceiverManager.planRemoteRtpCodecs(preview, (remoteMedia, index) => {
1043
+ const transceiver = this.findTransceiverForRemoteMedia(remoteMedia, index, planned);
1044
+ if (transceiver)
1045
+ planned.add(transceiver);
1046
+ return transceiver;
1047
+ });
1048
+ }
824
1049
  const needsImplicitLocalRollback = sessionDescription.type === "offer" &&
825
1050
  ["have-local-offer", "have-local-pranswer"].includes(this.signalingState);
826
1051
  if (needsImplicitLocalRollback) {
827
1052
  this.sdpManager.rollbackLocalDescription(this.signalingState);
1053
+ for (const transceiver of this.transceiverManager.getTransceivers()) {
1054
+ transceiver.pendingLocalOfferCodecs = undefined;
1055
+ }
1056
+ this.pendingOfferChangeSeq = undefined;
828
1057
  this.shouldNegotiationneeded = true;
829
1058
  this.setSignalingState("stable");
830
1059
  await Promise.resolve();
831
1060
  }
832
1061
  // # parse and validate description
1062
+ this.assertAnswerKeepsSharedTransports(sessionDescription);
833
1063
  const remoteSdp = this.sdpManager.setRemoteDescription(sessionDescription, this.signalingState);
834
1064
  if (!remoteSdp) {
1065
+ // 非対応 re-offer などの rollback では元の transceiver 対応を復元する
1066
+ const transportsBeforeRollback = this.transceiverManager
1067
+ .getTransceivers()
1068
+ .map((t) => t.dtlsTransport);
1069
+ this.transceiverManager.rollbackRemoteOffer();
1070
+ this.closeIdleTransports(transportsBeforeRollback);
835
1071
  this.setSignalingState("stable");
836
1072
  if (this.shouldNegotiationneeded) {
837
- this.needNegotiation();
1073
+ this.scheduleNegotiationneeded();
838
1074
  }
839
1075
  this.invalidateLastCreatedDescriptions();
840
1076
  return;
841
1077
  }
842
- let bundleTransport;
1078
+ if (remoteSdp.type === "offer") {
1079
+ this.transceiverManager.beginRemoteOffer();
1080
+ }
843
1081
  // # apply description
844
- const matchTransceiverWithMedia = (transceiver, media) => transceiver.kind === media.kind &&
845
- [null, media.rtp.muxId].includes(transceiver.mid);
846
- let transports = remoteSdp.media.map((remoteMedia, i) => {
847
- let dtlsTransport;
1082
+ const bundleGroups = this.config.bundlePolicy === "disable"
1083
+ ? []
1084
+ : remoteSdp.group.filter((group) => group.semantic === "BUNDLE");
1085
+ const groupOfMid = (mid) => mid == undefined
1086
+ ? undefined
1087
+ : bundleGroups.find((group) => group.items.includes(mid));
1088
+ // ## associate m-lines with transceivers / sctp
1089
+ const associated = new Set();
1090
+ const entries = remoteSdp.media.map((remoteMedia, index) => {
848
1091
  if (["audio", "video"].includes(remoteMedia.kind)) {
849
- let transceiver = this.transceiverManager
850
- .getTransceivers()
851
- .find((t) => matchTransceiverWithMedia(t, remoteMedia));
1092
+ let transceiver = this.findTransceiverForRemoteMedia(remoteMedia, index, associated);
852
1093
  if (!transceiver) {
853
- // create remote transceiver
854
- transceiver = this.addTransceiver(remoteMedia.kind, {
855
- direction: "recvonly",
856
- });
857
- transceiver.mid = remoteMedia.rtp.muxId ?? null;
858
- this.onRemoteTransceiverAdded.execute(transceiver);
859
- }
860
- else {
861
- if (transceiver.direction === "inactive" && transceiver.stopping) {
862
- transceiver.stopped = true;
863
- if (sessionDescription.type === "answer") {
864
- transceiver.setCurrentDirection("inactive");
865
- }
866
- return;
1094
+ if (remoteMedia.port === 0) {
1095
+ // 未知の MID の拒否済み m-line には transceiver を関連付けない
1096
+ return { remoteMedia, index };
867
1097
  }
1098
+ transceiver = this.addRemoteTransceiver(remoteMedia, index);
868
1099
  }
869
- if (this.sdpManager.remoteIsBundled) {
870
- if (!bundleTransport) {
871
- bundleTransport = transceiver.dtlsTransport;
872
- }
873
- else {
874
- transceiver.setDtlsTransport(bundleTransport);
875
- }
1100
+ else if (transceiver.mid == null) {
1101
+ this.transceiverManager.associateMLine(transceiver, index);
876
1102
  }
877
- dtlsTransport = transceiver.dtlsTransport;
878
- this.transceiverManager.setRemoteRTP(transceiver, remoteMedia, remoteSdp.type, i);
1103
+ associated.add(transceiver);
1104
+ return { remoteMedia, index, transceiver };
879
1105
  }
880
1106
  else if (remoteMedia.kind === "application") {
881
1107
  let sctpTransport = this.sctpTransport;
@@ -883,32 +1109,126 @@ class RTCPeerConnection extends helper_1.EventTarget {
883
1109
  sctpTransport = this.createSctpTransport();
884
1110
  sctpTransport.mid = remoteMedia.rtp.muxId;
885
1111
  }
886
- if (this.sdpManager.remoteIsBundled) {
887
- if (!bundleTransport) {
888
- bundleTransport = sctpTransport.dtlsTransport;
889
- }
890
- else {
891
- sctpTransport.setDtlsTransport(bundleTransport);
892
- }
893
- }
894
- dtlsTransport = sctpTransport.dtlsTransport;
895
- this.sctpManager.setRemoteSCTP(remoteMedia, i);
1112
+ return { remoteMedia, index, sctpTransport };
896
1113
  }
897
1114
  else {
898
1115
  throw new Error("invalid media kind");
899
1116
  }
900
- const iceTransport = dtlsTransport.iceTransport;
901
- if (remoteMedia.iceParams) {
902
- const renomination = !!this.sdpManager.inactiveRemoteMedia;
903
- iceTransport.setRemoteParams(remoteMedia.iceParams, renomination);
904
- // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
905
- // RFC 8445 S6.1.1
906
- if (remoteMedia.iceParams.iceLite && !iceTransport.connection.iceLite) {
907
- iceTransport.connection.iceControlling = true;
1117
+ });
1118
+ if (remoteSdp.type === "offer") {
1119
+ this.transceiverManager.releaseUnassociatedReservations(associated, remoteSdp.media.length);
1120
+ }
1121
+ const ownerOf = (entry) => entry.transceiver ?? entry.sctpTransport;
1122
+ // ## transport ownership
1123
+ // remote offer の所有関係は bundlePolicy によらず offer の BUNDLE group だけで決まる
1124
+ // (max-bundle は自分の offer にだけ効く)。group ごとに 1 つの transport を共有し、
1125
+ // 異なる group や group 外の m-line とは共有しない。
1126
+ // remote answer では offer で渡した ICE credentials を保つため transport を作り直さない
1127
+ // (共有 transport を分割する answer は assertAnswerKeepsSharedTransports で事前に拒否済み)
1128
+ const isRemoteOffer = remoteSdp.type === "offer";
1129
+ const liveTransport = (transport) => !!transport && transport.state !== "closed";
1130
+ const groupTransports = new Map();
1131
+ const claimed = new Set();
1132
+ for (const group of bundleGroups) {
1133
+ const members = entries.filter((entry) => !!ownerOf(entry) &&
1134
+ group.items.includes(entry.remoteMedia.rtp.muxId));
1135
+ if (members.length === 0) {
1136
+ continue;
1137
+ }
1138
+ // 既存 member の transport を優先し、他の group が使っていれば新しい transport を作る
1139
+ let shared = members
1140
+ .map((entry) => ownerOf(entry).dtlsTransport)
1141
+ .find((transport) => liveTransport(transport) &&
1142
+ (!isRemoteOffer || !claimed.has(transport)));
1143
+ if (!shared) {
1144
+ shared = this.createDtlsTransport({ inheritCredentials: false });
1145
+ }
1146
+ groupTransports.set(group, shared);
1147
+ claimed.add(shared);
1148
+ for (const entry of members) {
1149
+ const owner = ownerOf(entry);
1150
+ if (owner.dtlsTransport !== shared) {
1151
+ owner.setDtlsTransport(shared);
1152
+ }
1153
+ }
1154
+ }
1155
+ if (isRemoteOffer) {
1156
+ // group 外 (group のない offer を含む) で受け入れた m-line は独立した transport / ICE credentials を持つ
1157
+ for (const entry of entries) {
1158
+ const owner = ownerOf(entry);
1159
+ if (!owner ||
1160
+ entry.remoteMedia.port === 0 ||
1161
+ groupOfMid(entry.remoteMedia.rtp.muxId)) {
1162
+ continue;
1163
+ }
1164
+ if (!liveTransport(owner.dtlsTransport) ||
1165
+ claimed.has(owner.dtlsTransport)) {
1166
+ owner.setDtlsTransport(this.createDtlsTransport({ inheritCredentials: false }));
1167
+ }
1168
+ claimed.add(owner.dtlsTransport);
1169
+ }
1170
+ }
1171
+ // ## apply RTP / SCTP (受け入れ判定)
1172
+ const acceptedEntries = new Set();
1173
+ for (const entry of entries) {
1174
+ const { remoteMedia, index } = entry;
1175
+ if (entry.transceiver) {
1176
+ if (this.transceiverManager.setRemoteRTP(entry.transceiver, remoteMedia, remoteSdp.type, index, codecPlan.get(index) ?? [])) {
1177
+ acceptedEntries.add(entry);
908
1178
  }
909
1179
  }
910
- if (remoteMedia.dtlsParams) {
911
- dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
1180
+ else if (entry.sctpTransport) {
1181
+ this.sctpManager.setRemoteSCTP(remoteMedia, index);
1182
+ acceptedEntries.add(entry);
1183
+ }
1184
+ }
1185
+ // ICE / DTLS パラメータは group で最初に受け入れた member (tag) から適用する
1186
+ // (codec 不一致で拒否した member のパラメータは使わない)
1187
+ const groupParamSources = new Map();
1188
+ for (const group of bundleGroups) {
1189
+ for (const mid of group.items) {
1190
+ const entry = entries.find((e) => e.remoteMedia.rtp.muxId === mid &&
1191
+ e.remoteMedia.port !== 0 &&
1192
+ acceptedEntries.has(e));
1193
+ if (entry) {
1194
+ groupParamSources.set(group, entry);
1195
+ break;
1196
+ }
1197
+ }
1198
+ }
1199
+ // ## apply transport parameters
1200
+ for (const entry of entries) {
1201
+ const { remoteMedia } = entry;
1202
+ const owner = ownerOf(entry);
1203
+ if (!owner) {
1204
+ continue;
1205
+ }
1206
+ const accepted = acceptedEntries.has(entry);
1207
+ const dtlsTransport = owner.dtlsTransport;
1208
+ const group = groupOfMid(remoteMedia.rtp.muxId);
1209
+ if (remoteMedia.port === 0 ||
1210
+ !dtlsTransport ||
1211
+ dtlsTransport.state === "closed" ||
1212
+ // group 外の拒否 section の transport は使わない
1213
+ (!accepted && !group)) {
1214
+ continue;
1215
+ }
1216
+ const iceTransport = dtlsTransport.iceTransport;
1217
+ const ownsTransportParams = !group || groupParamSources.get(group) === entry;
1218
+ if (ownsTransportParams) {
1219
+ if (remoteMedia.iceParams) {
1220
+ const renomination = !!this.sdpManager.inactiveRemoteMedia;
1221
+ iceTransport.setRemoteParams(remoteMedia.iceParams, renomination);
1222
+ // One agent full, one lite: The full agent MUST take the controlling role, and the lite agent MUST take the controlled role
1223
+ // RFC 8445 S6.1.1
1224
+ if (remoteMedia.iceParams.iceLite &&
1225
+ !iceTransport.connection.iceLite) {
1226
+ iceTransport.connection.iceControlling = true;
1227
+ }
1228
+ }
1229
+ if (remoteMedia.dtlsParams) {
1230
+ dtlsTransport.setRemoteParams(remoteMedia.dtlsParams);
1231
+ }
912
1232
  }
913
1233
  // # add ICE candidates
914
1234
  remoteMedia.iceCandidates.forEach(iceTransport.addRemoteCandidate);
@@ -920,21 +1240,17 @@ class RTCPeerConnection extends helper_1.EventTarget {
920
1240
  dtlsTransport.role =
921
1241
  remoteMedia.dtlsParams.role === "client" ? "server" : "client";
922
1242
  }
923
- return iceTransport;
924
- });
925
- // filter out inactive transports
926
- transports = transports.filter((iceTransport) => !!iceTransport);
927
- const removedTransceivers = this.transceiverManager
928
- .getTransceivers()
929
- .filter((t) => remoteSdp.media.find((m) => matchTransceiverWithMedia(t, m)) ==
930
- undefined);
931
- if (sessionDescription.type === "answer") {
932
- for (const transceiver of removedTransceivers) {
933
- // todo: handle answer side transceiver removal work.
934
- // event should trigger to notify media source to stop.
935
- transceiver.stop();
936
- transceiver.stopped = true;
1243
+ }
1244
+ if (remoteSdp.type === "answer") {
1245
+ // answer に含まれない既存 m-line は remote 起因の停止として確定する
1246
+ for (const transceiver of this.transceiverManager.getTransceivers()) {
1247
+ if (!associated.has(transceiver) &&
1248
+ transceiver.mid != undefined &&
1249
+ !transceiver.stopped) {
1250
+ transceiver.commitStopped({ rejected: true });
1251
+ }
937
1252
  }
1253
+ this.closeIdleTransports();
938
1254
  }
939
1255
  if (remoteSdp.type === "offer") {
940
1256
  this.setSignalingState("have-remote-offer");
@@ -955,11 +1271,96 @@ class RTCPeerConnection extends helper_1.EventTarget {
955
1271
  });
956
1272
  }
957
1273
  this.negotiationneeded = false;
958
- if (this.shouldNegotiationneeded) {
959
- this.needNegotiation();
1274
+ if (remoteSdp.type === "answer" &&
1275
+ this.pendingOfferChangeSeq != undefined) {
1276
+ this.negotiatedChangeSeq = Math.max(this.negotiatedChangeSeq, this.pendingOfferChangeSeq);
1277
+ this.pendingOfferChangeSeq = undefined;
1278
+ }
1279
+ const hasUnnegotiatedStop = remoteSdp.type === "answer" && this.settleStoppingTransceivers();
1280
+ if (this.shouldNegotiationneeded || hasUnnegotiatedStop) {
1281
+ this.scheduleNegotiationneeded();
960
1282
  }
961
1283
  this.invalidateLastCreatedDescriptions();
962
1284
  }
1285
+ /**
1286
+ * remote answer / pranswer の検証 (offerer 側、状態を変更する前に実行する)。
1287
+ * local offer で 1 つの transport を共有した m-line を、answer が同じ BUNDLE group に置かずに
1288
+ * 異なる ICE credentials で受け入れることはできない (RFC 8843 7.3.2)。共有 transport は分割できず、
1289
+ * 新しい transport を作ると offer で渡した ICE credentials とも一致しなくなる。
1290
+ * group の記述が MID と一致しなくても remote の ICE credentials が同じなら、同じ transport のまま扱う。
1291
+ */
1292
+ assertAnswerKeepsSharedTransports(sessionDescription) {
1293
+ if (!["answer", "pranswer"].includes(sessionDescription.type ?? "") ||
1294
+ !["have-local-offer", "have-local-pranswer"].includes(this.signalingState) ||
1295
+ !sessionDescription.sdp ||
1296
+ this.config.bundlePolicy === "disable") {
1297
+ return;
1298
+ }
1299
+ const answer = sdp_1.SessionDescription.parse(sessionDescription.sdp);
1300
+ const answerGroups = answer.group.filter((g) => g.semantic === "BUNDLE");
1301
+ const byTransport = new Map();
1302
+ for (const media of answer.media) {
1303
+ const mid = media.rtp.muxId;
1304
+ if (media.port === 0 || mid == undefined) {
1305
+ continue;
1306
+ }
1307
+ const owner = media.kind === "application"
1308
+ ? this.sctpTransport?.mid === mid
1309
+ ? this.sctpTransport
1310
+ : undefined
1311
+ : this.transceiverManager
1312
+ .getTransceivers()
1313
+ .find((t) => t.mid === mid && !t.stopped);
1314
+ const transport = owner?.dtlsTransport;
1315
+ if (!transport) {
1316
+ continue;
1317
+ }
1318
+ const current = {
1319
+ group: answerGroups.find((g) => g.items.includes(mid)),
1320
+ credentials: `${media.iceParams?.usernameFragment}:${media.iceParams?.password}`,
1321
+ };
1322
+ const first = byTransport.get(transport);
1323
+ if (!first) {
1324
+ byTransport.set(transport, current);
1325
+ continue;
1326
+ }
1327
+ const sameGroup = !!current.group && current.group === first.group;
1328
+ if (!sameGroup && current.credentials !== first.credentials) {
1329
+ throw (0, errors_1.createWebRtcDomException)("InvalidAccessError", `mid=${mid} shares a transport in the local offer but the remote ${sessionDescription.type} moves it out of that BUNDLE group`);
1330
+ }
1331
+ }
1332
+ }
1333
+ /**
1334
+ * remote m-line に対応する transceiver を探す。MID の一致を優先し、
1335
+ * 未関連付け transceiver は同じ offer 内で異なる m-line に割り当てる。
1336
+ */
1337
+ findTransceiverForRemoteMedia(remoteMedia, index, associated) {
1338
+ const candidates = this.transceiverManager
1339
+ .getTransceivers()
1340
+ .filter((t) => !associated.has(t) && t.kind === remoteMedia.kind);
1341
+ const mid = remoteMedia.rtp.muxId;
1342
+ const byMid = mid != undefined ? candidates.find((t) => t.mid === mid) : undefined;
1343
+ if (byMid) {
1344
+ return byMid;
1345
+ }
1346
+ if (remoteMedia.port === 0) {
1347
+ return;
1348
+ }
1349
+ const unassociated = candidates.filter((t) => t.mid == null && !t.stopping && !t.stopped);
1350
+ return unassociated.find((t) => t.mLineIndex === index) ?? unassociated[0];
1351
+ }
1352
+ /**
1353
+ * remote offer / answer の m-line 用に transceiver を作る。
1354
+ * 同じ位置に停止済みの旧 transceiver があれば、それを復活させず新しい receiver/router で置き換える。
1355
+ */
1356
+ addRemoteTransceiver(remoteMedia, index) {
1357
+ const dtlsTransport = this.findOrCreateTransport();
1358
+ const transceiver = this.transceiverManager.addTransceiver(remoteMedia.kind, dtlsTransport, { direction: "recvonly" }, { remoteMLineIndex: index });
1359
+ transceiver.mid = remoteMedia.rtp.muxId ?? null;
1360
+ this.secureManager.updateIceConnectionState();
1361
+ this.onRemoteTransceiverAdded.execute(transceiver);
1362
+ return transceiver;
1363
+ }
963
1364
  addTransceiver(trackOrKind, options = {}) {
964
1365
  const dtlsTransport = this.findOrCreateTransport();
965
1366
  const transceiver = this.transceiverManager.addTransceiver(trackOrKind, dtlsTransport, options);
@@ -983,6 +1384,38 @@ class RTCPeerConnection extends helper_1.EventTarget {
983
1384
  async createAnswer() {
984
1385
  this.assertNotClosed();
985
1386
  await this.secureManager.ensureCerts();
1387
+ // setCodecPreferences() で無効化された transceiver を再解決する。
1388
+ // offer 側と異なり answer は remote offer との交渉結果を使い、
1389
+ // sender/receiver の codec 状態も新しい negotiated codec に同期する。
1390
+ for (const transceiver of this.transceiverManager.getTransceivers()) {
1391
+ if (transceiver.codecs.length !== 0 &&
1392
+ !transceiver.codecPreferencesNeedResolution) {
1393
+ continue;
1394
+ }
1395
+ // 拒否予定 / 停止済みの m-line は port 0 で answer するため codec を解決しない
1396
+ if (transceiver.pendingRejection ||
1397
+ transceiver.stopping ||
1398
+ transceiver.stopped) {
1399
+ continue;
1400
+ }
1401
+ const remoteDescription = this.sdpManager._remoteDescription;
1402
+ const remoteMedia = remoteDescription?.media.find((media) => media.rtp.muxId != undefined &&
1403
+ media.rtp.muxId === transceiver.mid &&
1404
+ media.kind === transceiver.kind) ??
1405
+ (transceiver.mLineIndex != undefined
1406
+ ? remoteDescription?.media[transceiver.mLineIndex]
1407
+ : undefined);
1408
+ if (remoteMedia && ["audio", "video"].includes(remoteMedia.kind)) {
1409
+ this.transceiverManager.resyncAnswerCodecs(transceiver, remoteMedia);
1410
+ }
1411
+ else {
1412
+ this.transceiverManager.assignTransceiverCodecs(transceiver);
1413
+ }
1414
+ if (transceiver.headerExtensions.length === 0) {
1415
+ transceiver.headerExtensions =
1416
+ this.config.headerExtensions[transceiver.kind] ?? [];
1417
+ }
1418
+ }
986
1419
  const description = this.sdpManager.buildAnswerSdp({
987
1420
  transceivers: this.transceiverManager.getTransceivers(),
988
1421
  sctpTransport: this.sctpTransport,
@@ -1045,8 +1478,9 @@ class RTCPeerConnection extends helper_1.EventTarget {
1045
1478
  this.pendingRemoteCandidates.length = 0;
1046
1479
  this.setSignalingState("closed");
1047
1480
  this.transceiverManager.close();
1048
- await this.secureManager.close();
1481
+ // SCTP ABORT は DTLS/ICE が生きている間に送る(close は abrupt であり SHUTDOWN ではない)
1049
1482
  await this.sctpManager.close();
1483
+ await this.secureManager.close();
1050
1484
  // 公開 Event を完了させ、購読者・クロージャが PeerConnection を保持し続けないようにする
1051
1485
  this.completePeerEvents();
1052
1486
  log("peerConnection closed");
@@ -1071,16 +1505,41 @@ class RTCPeerConnection extends helper_1.EventTarget {
1071
1505
  }
1072
1506
  }
1073
1507
  exports.RTCPeerConnection = RTCPeerConnection;
1508
+ const MLineReuseModes = ["compatible", "aggressive"];
1074
1509
  const findCodecByMimeType = (codecs, target) => codecs.find((localCodec) => localCodec.mimeType.toLowerCase() === target.mimeType.toLowerCase())
1075
1510
  ? target
1076
1511
  : undefined;
1077
1512
  exports.findCodecByMimeType = findCodecByMimeType;
1513
+ function assignDynamicPayloadTypes(config) {
1514
+ for (const [i, codecParams] of (0, helper_1.enumerate)([
1515
+ ...(config.codecs.audio || []),
1516
+ ...(config.codecs.video || []),
1517
+ ])) {
1518
+ if (codecParams.payloadType != undefined) {
1519
+ continue;
1520
+ }
1521
+ codecParams.payloadType = 96 + i;
1522
+ switch (codecParams.name.toLowerCase()) {
1523
+ case "rtx":
1524
+ {
1525
+ codecParams.parameters = `apt=${codecParams.payloadType - 1}`;
1526
+ }
1527
+ break;
1528
+ case "red":
1529
+ {
1530
+ if (codecParams.contentType === "audio") {
1531
+ const redundant = codecParams.payloadType + 1;
1532
+ codecParams.parameters = `${redundant}/${redundant}`;
1533
+ codecParams.payloadType = 63;
1534
+ }
1535
+ }
1536
+ break;
1537
+ }
1538
+ }
1539
+ }
1078
1540
  function generateDefaultPeerConfig() {
1079
1541
  return {
1080
- codecs: {
1081
- audio: [(0, media_1.useOPUS)(), (0, media_1.usePCMU)()],
1082
- video: [(0, media_1.useVP8)()],
1083
- },
1542
+ codecs: (0, media_1.defaultCodecs)(),
1084
1543
  headerExtensions: {
1085
1544
  audio: [],
1086
1545
  video: [],
@@ -1094,8 +1553,12 @@ function generateDefaultPeerConfig() {
1094
1553
  iceUseIpv4: true,
1095
1554
  iceUseIpv6: true,
1096
1555
  iceUseTcp: false,
1556
+ iceTcpPassive: true,
1557
+ iceStunGatherTimeout: undefined,
1558
+ iceTurnConnectTimeout: undefined,
1097
1559
  turnTransport: undefined,
1098
1560
  turnTlsOptions: undefined,
1561
+ turnUdpFamily: undefined,
1099
1562
  iceFilterStunResponse: undefined,
1100
1563
  iceFilterCandidatePair: undefined,
1101
1564
  icePasswordPrefix: undefined,
@@ -1109,12 +1572,18 @@ function generateDefaultPeerConfig() {
1109
1572
  midSuffix: false,
1110
1573
  forceTurnTCP: false,
1111
1574
  maxMessageSize: sctp_1.DEFAULT_MAX_MESSAGE_SIZE,
1575
+ sctp: { mtu: src_1.DEFAULT_SCTP_MTU },
1576
+ pendingRtp: false,
1577
+ mLineReuse: "compatible",
1112
1578
  };
1113
1579
  }
1114
1580
  exports.defaultPeerConfig = generateDefaultPeerConfig();
1115
1581
  function normalizePeerConfiguration(config) {
1116
1582
  const input = Object(config ?? {});
1117
1583
  const normalizedConfig = { ...input };
1584
+ if (input.sctp !== undefined) {
1585
+ normalizedConfig.sctp = { ...input.sctp };
1586
+ }
1118
1587
  if (input.bundlePolicy === "balanced") {
1119
1588
  normalizedConfig.bundlePolicy = "max-compat";
1120
1589
  }
@@ -1177,6 +1646,10 @@ function clonePeerConfiguration(config) {
1177
1646
  dtls: { ...config.dtls },
1178
1647
  certificates: [...config.certificates],
1179
1648
  debug: { ...config.debug },
1649
+ sctp: { ...config.sctp },
1650
+ pendingRtp: typeof config.pendingRtp === "object" && config.pendingRtp != undefined
1651
+ ? { ...config.pendingRtp }
1652
+ : config.pendingRtp,
1180
1653
  };
1181
1654
  }
1182
1655
  class RTCTrackEvent {