livekit-client 2.20.1 → 2.21.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 (156) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +37 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2279 -651
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +37 -7
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts.map +1 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/logger.d.ts +3 -1
  17. package/dist/src/logger.d.ts.map +1 -1
  18. package/dist/src/room/PCTransport.d.ts +2 -0
  19. package/dist/src/room/PCTransport.d.ts.map +1 -1
  20. package/dist/src/room/PCTransportManager.d.ts +1 -0
  21. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  22. package/dist/src/room/RTCEngine.d.ts +31 -26
  23. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  24. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  25. package/dist/src/room/Room.d.ts +4 -0
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  28. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  29. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  30. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  31. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  32. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  33. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  34. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  35. package/dist/src/room/data-channel/types.d.ts +12 -0
  36. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  37. package/dist/src/room/data-stream/compression.d.ts +49 -0
  38. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  39. package/dist/src/room/data-stream/constants.d.ts +18 -0
  40. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  41. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  44. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  45. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  46. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  47. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  48. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  49. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  50. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  51. package/dist/src/room/errors.d.ts +3 -1
  52. package/dist/src/room/errors.d.ts.map +1 -1
  53. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  54. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  55. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  56. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  57. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  58. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  59. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  60. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  61. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  62. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  63. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  64. package/dist/src/room/track/Track.d.ts.map +1 -1
  65. package/dist/src/room/track/options.d.ts +11 -1
  66. package/dist/src/room/track/options.d.ts.map +1 -1
  67. package/dist/src/room/types.d.ts +17 -0
  68. package/dist/src/room/types.d.ts.map +1 -1
  69. package/dist/src/room/utils.d.ts +13 -0
  70. package/dist/src/room/utils.d.ts.map +1 -1
  71. package/dist/src/test/signalToken.d.ts +26 -0
  72. package/dist/src/test/signalToken.d.ts.map +1 -0
  73. package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
  74. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  75. package/dist/src/version.d.ts +5 -1
  76. package/dist/src/version.d.ts.map +1 -1
  77. package/dist/ts4.2/index.d.ts +1 -1
  78. package/dist/ts4.2/logger.d.ts +3 -1
  79. package/dist/ts4.2/room/PCTransport.d.ts +2 -0
  80. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  81. package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
  82. package/dist/ts4.2/room/Room.d.ts +4 -0
  83. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  84. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  85. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  86. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  87. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  88. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  89. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  90. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  91. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  92. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  93. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  94. package/dist/ts4.2/room/errors.d.ts +3 -1
  95. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  96. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  97. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  98. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  99. package/dist/ts4.2/room/track/options.d.ts +11 -1
  100. package/dist/ts4.2/room/types.d.ts +17 -0
  101. package/dist/ts4.2/room/utils.d.ts +13 -0
  102. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  103. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
  104. package/dist/ts4.2/version.d.ts +5 -1
  105. package/package.json +8 -4
  106. package/src/api/SignalClient.e2e.test.ts +264 -0
  107. package/src/api/SignalClient.ts +34 -2
  108. package/src/index.ts +2 -0
  109. package/src/logger.ts +2 -0
  110. package/src/room/PCTransport.ts +66 -31
  111. package/src/room/PCTransportManager.ts +10 -6
  112. package/src/room/RTCEngine.test.ts +238 -6
  113. package/src/room/RTCEngine.ts +101 -338
  114. package/src/room/RegionUrlProvider.ts +4 -2
  115. package/src/room/Room.test.ts +4 -1
  116. package/src/room/Room.ts +47 -9
  117. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  118. package/src/room/data-channel/DataChannelManager.ts +237 -0
  119. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  120. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  121. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  122. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  123. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  124. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  125. package/src/room/data-channel/types.ts +30 -0
  126. package/src/room/data-stream/compression.ts +117 -0
  127. package/src/room/data-stream/constants.ts +18 -0
  128. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  129. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  130. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  131. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  132. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  133. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  134. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
  135. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
  136. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
  137. package/src/room/errors.ts +10 -4
  138. package/src/room/participant/LocalParticipant.ts +29 -6
  139. package/src/room/participant/Participant.ts +5 -0
  140. package/src/room/participant/RemoteParticipant.ts +19 -5
  141. package/src/room/participant/publishUtils.ts +14 -11
  142. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  143. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  144. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  145. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  146. package/src/room/track/LocalAudioTrack.ts +16 -0
  147. package/src/room/track/Track.ts +10 -6
  148. package/src/room/track/options.ts +11 -1
  149. package/src/room/types.ts +18 -0
  150. package/src/room/utils.ts +67 -0
  151. package/src/test/signalServerSetup.ts +151 -0
  152. package/src/test/signalToken.ts +59 -0
  153. package/src/test/vitest-context.d.ts +10 -0
  154. package/src/utils/dataPacketBuffer.test.ts +100 -0
  155. package/src/utils/dataPacketBuffer.ts +46 -1
  156. package/src/version.ts +5 -1
@@ -3983,6 +3983,9 @@ const PacketTrailerFeature = /* @__PURE__ */proto3.makeEnum("livekit.PacketTrail
3983
3983
  }, {
3984
3984
  no: 1,
3985
3985
  name: "PTF_FRAME_ID"
3986
+ }, {
3987
+ no: 2,
3988
+ name: "PTF_USER_DATA"
3986
3989
  }]);
3987
3990
  const Room$1 = /* @__PURE__ */proto3.makeMessageType("livekit.Room", () => [{
3988
3991
  no: 1,
@@ -4250,6 +4253,12 @@ const ParticipantInfo = /* @__PURE__ */proto3.makeMessageType("livekit.Participa
4250
4253
  kind: "scalar",
4251
4254
  T: 5
4252
4255
  /* ScalarType.INT32 */
4256
+ }, {
4257
+ no: 21,
4258
+ name: "capabilities",
4259
+ kind: "enum",
4260
+ T: proto3.getEnumType(ClientInfo_Capability),
4261
+ repeated: true
4253
4262
  }]);
4254
4263
  const ParticipantInfo_State = /* @__PURE__ */proto3.makeEnum("livekit.ParticipantInfo.State", [{
4255
4264
  no: 0,
@@ -4301,6 +4310,9 @@ const ParticipantInfo_KindDetail = /* @__PURE__ */proto3.makeEnum("livekit.Parti
4301
4310
  }, {
4302
4311
  no: 4,
4303
4312
  name: "BRIDGE_RTSP"
4313
+ }, {
4314
+ no: 5,
4315
+ name: "SIMULATION"
4304
4316
  }]);
4305
4317
  const Encryption_Type = /* @__PURE__ */proto3.makeEnum("livekit.Encryption.Type", [{
4306
4318
  no: 0,
@@ -4493,6 +4505,122 @@ const DataTrackInfo$1 = /* @__PURE__ */proto3.makeMessageType("livekit.DataTrack
4493
4505
  name: "encryption",
4494
4506
  kind: "enum",
4495
4507
  T: proto3.getEnumType(Encryption_Type)
4508
+ }, {
4509
+ no: 5,
4510
+ name: "frame_encoding",
4511
+ kind: "message",
4512
+ T: DataTrackFrameEncoding,
4513
+ opt: true
4514
+ }, {
4515
+ no: 6,
4516
+ name: "schema",
4517
+ kind: "message",
4518
+ T: DataTrackSchemaId,
4519
+ opt: true
4520
+ }]);
4521
+ const DataTrackFrameEncoding = /* @__PURE__ */proto3.makeMessageType("livekit.DataTrackFrameEncoding", () => [{
4522
+ no: 1,
4523
+ name: "well_known",
4524
+ kind: "enum",
4525
+ T: proto3.getEnumType(DataTrackFrameEncoding_WellKnownFrameEncoding),
4526
+ oneof: "value"
4527
+ }, {
4528
+ no: 2,
4529
+ name: "custom",
4530
+ kind: "scalar",
4531
+ T: 9,
4532
+ oneof: "value"
4533
+ }]);
4534
+ const DataTrackFrameEncoding_WellKnownFrameEncoding = /* @__PURE__ */proto3.makeEnum("livekit.DataTrackFrameEncoding.WellKnownFrameEncoding", [{
4535
+ no: 0,
4536
+ name: "WELL_KNOWN_FRAME_ENCODING_UNSPECIFIED",
4537
+ localName: "UNSPECIFIED"
4538
+ }, {
4539
+ no: 1,
4540
+ name: "WELL_KNOWN_FRAME_ENCODING_ROS1",
4541
+ localName: "ROS1"
4542
+ }, {
4543
+ no: 2,
4544
+ name: "WELL_KNOWN_FRAME_ENCODING_CDR",
4545
+ localName: "CDR"
4546
+ }, {
4547
+ no: 3,
4548
+ name: "WELL_KNOWN_FRAME_ENCODING_PROTOBUF",
4549
+ localName: "PROTOBUF"
4550
+ }, {
4551
+ no: 4,
4552
+ name: "WELL_KNOWN_FRAME_ENCODING_FLATBUFFER",
4553
+ localName: "FLATBUFFER"
4554
+ }, {
4555
+ no: 5,
4556
+ name: "WELL_KNOWN_FRAME_ENCODING_CBOR",
4557
+ localName: "CBOR"
4558
+ }, {
4559
+ no: 6,
4560
+ name: "WELL_KNOWN_FRAME_ENCODING_MSGPACK",
4561
+ localName: "MSGPACK"
4562
+ }, {
4563
+ no: 7,
4564
+ name: "WELL_KNOWN_FRAME_ENCODING_JSON",
4565
+ localName: "JSON"
4566
+ }]);
4567
+ const DataTrackSchemaEncoding = /* @__PURE__ */proto3.makeMessageType("livekit.DataTrackSchemaEncoding", () => [{
4568
+ no: 1,
4569
+ name: "well_known",
4570
+ kind: "enum",
4571
+ T: proto3.getEnumType(DataTrackSchemaEncoding_WellKnownSchemaEncoding),
4572
+ oneof: "value"
4573
+ }, {
4574
+ no: 2,
4575
+ name: "custom",
4576
+ kind: "scalar",
4577
+ T: 9,
4578
+ oneof: "value"
4579
+ }]);
4580
+ const DataTrackSchemaEncoding_WellKnownSchemaEncoding = /* @__PURE__ */proto3.makeEnum("livekit.DataTrackSchemaEncoding.WellKnownSchemaEncoding", [{
4581
+ no: 0,
4582
+ name: "WELL_KNOWN_SCHEMA_ENCODING_UNSPECIFIED",
4583
+ localName: "UNSPECIFIED"
4584
+ }, {
4585
+ no: 1,
4586
+ name: "WELL_KNOWN_SCHEMA_ENCODING_PROTOBUF",
4587
+ localName: "PROTOBUF"
4588
+ }, {
4589
+ no: 2,
4590
+ name: "WELL_KNOWN_SCHEMA_ENCODING_FLATBUFFER",
4591
+ localName: "FLATBUFFER"
4592
+ }, {
4593
+ no: 3,
4594
+ name: "WELL_KNOWN_SCHEMA_ENCODING_ROS1_MSG",
4595
+ localName: "ROS1_MSG"
4596
+ }, {
4597
+ no: 4,
4598
+ name: "WELL_KNOWN_SCHEMA_ENCODING_ROS2_MSG",
4599
+ localName: "ROS2_MSG"
4600
+ }, {
4601
+ no: 5,
4602
+ name: "WELL_KNOWN_SCHEMA_ENCODING_ROS2_IDL",
4603
+ localName: "ROS2_IDL"
4604
+ }, {
4605
+ no: 6,
4606
+ name: "WELL_KNOWN_SCHEMA_ENCODING_OMG_IDL",
4607
+ localName: "OMG_IDL"
4608
+ }, {
4609
+ no: 7,
4610
+ name: "WELL_KNOWN_SCHEMA_ENCODING_JSON_SCHEMA",
4611
+ localName: "JSON_SCHEMA"
4612
+ }]);
4613
+ const DataTrackSchemaId = /* @__PURE__ */proto3.makeMessageType("livekit.DataTrackSchemaId", () => [{
4614
+ no: 1,
4615
+ name: "name",
4616
+ kind: "scalar",
4617
+ T: 9
4618
+ /* ScalarType.STRING */
4619
+ }, {
4620
+ no: 2,
4621
+ name: "encoding",
4622
+ kind: "message",
4623
+ T: DataTrackSchemaEncoding
4496
4624
  }]);
4497
4625
  const DataTrackSubscriptionOptions = /* @__PURE__ */proto3.makeMessageType("livekit.DataTrackSubscriptionOptions", () => [{
4498
4626
  no: 1,
@@ -4501,6 +4629,31 @@ const DataTrackSubscriptionOptions = /* @__PURE__ */proto3.makeMessageType("live
4501
4629
  T: 13,
4502
4630
  opt: true
4503
4631
  }]);
4632
+ const DataBlobKey = /* @__PURE__ */proto3.makeMessageType("livekit.DataBlobKey", () => [{
4633
+ no: 1,
4634
+ name: "generic",
4635
+ kind: "scalar",
4636
+ T: 9,
4637
+ oneof: "key"
4638
+ }, {
4639
+ no: 2,
4640
+ name: "schema_id",
4641
+ kind: "message",
4642
+ T: DataTrackSchemaId,
4643
+ oneof: "key"
4644
+ }]);
4645
+ const DataBlob = /* @__PURE__ */proto3.makeMessageType("livekit.DataBlob", () => [{
4646
+ no: 1,
4647
+ name: "key",
4648
+ kind: "message",
4649
+ T: DataBlobKey
4650
+ }, {
4651
+ no: 2,
4652
+ name: "contents",
4653
+ kind: "scalar",
4654
+ T: 12
4655
+ /* ScalarType.BYTES */
4656
+ }]);
4504
4657
  const VideoLayer = /* @__PURE__ */proto3.makeMessageType("livekit.VideoLayer", () => [{
4505
4658
  no: 1,
4506
4659
  name: "quality",
@@ -5223,6 +5376,9 @@ const ClientInfo_Capability = /* @__PURE__ */proto3.makeEnum("livekit.ClientInfo
5223
5376
  }, {
5224
5377
  no: 1,
5225
5378
  name: "CAP_PACKET_TRAILER"
5379
+ }, {
5380
+ no: 2,
5381
+ name: "CAP_COMPRESSION_DEFLATE_RAW"
5226
5382
  }]);
5227
5383
  const ClientConfiguration = /* @__PURE__ */proto3.makeMessageType("livekit.ClientConfiguration", () => [{
5228
5384
  no: 1,
@@ -5295,6 +5451,13 @@ const DataStream_OperationType = /* @__PURE__ */proto3.makeEnum("livekit.DataStr
5295
5451
  no: 3,
5296
5452
  name: "REACTION"
5297
5453
  }]);
5454
+ const DataStream_CompressionType = /* @__PURE__ */proto3.makeEnum("livekit.DataStream.CompressionType", [{
5455
+ no: 0,
5456
+ name: "NONE"
5457
+ }, {
5458
+ no: 1,
5459
+ name: "DEFLATE_RAW"
5460
+ }]);
5298
5461
  const DataStream_TextHeader = /* @__PURE__ */proto3.makeMessageType("livekit.DataStream.TextHeader", () => [{
5299
5462
  no: 1,
5300
5463
  name: "operation_type",
@@ -5393,6 +5556,17 @@ const DataStream_Header = /* @__PURE__ */proto3.makeMessageType("livekit.DataStr
5393
5556
  kind: "message",
5394
5557
  T: DataStream_ByteHeader,
5395
5558
  oneof: "content_header"
5559
+ }, {
5560
+ no: 11,
5561
+ name: "inline_content",
5562
+ kind: "scalar",
5563
+ T: 12,
5564
+ opt: true
5565
+ }, {
5566
+ no: 12,
5567
+ name: "compression",
5568
+ kind: "enum",
5569
+ T: proto3.getEnumType(DataStream_CompressionType)
5396
5570
  }], {
5397
5571
  localName: "DataStream_Header"
5398
5572
  });
@@ -5528,6 +5702,16 @@ const RoomAgentDispatch = /* @__PURE__ */proto3.makeMessageType("livekit.RoomAge
5528
5702
  kind: "scalar",
5529
5703
  T: 9
5530
5704
  /* ScalarType.STRING */
5705
+ }, {
5706
+ no: 5,
5707
+ name: "attributes",
5708
+ kind: "map",
5709
+ K: 9,
5710
+ V: {
5711
+ kind: "scalar",
5712
+ T: 9
5713
+ /* ScalarType.STRING */
5714
+ }
5531
5715
  }]);
5532
5716
  const EncodingOptionsPreset = /* @__PURE__ */proto3.makeEnum("livekit.EncodingOptionsPreset", [{
5533
5717
  no: 0,
@@ -6426,6 +6610,18 @@ const SignalRequest = /* @__PURE__ */proto3.makeMessageType("livekit.SignalReque
6426
6610
  kind: "message",
6427
6611
  T: UpdateDataSubscription,
6428
6612
  oneof: "message"
6613
+ }, {
6614
+ no: 22,
6615
+ name: "store_data_blob_request",
6616
+ kind: "message",
6617
+ T: StoreDataBlobRequest,
6618
+ oneof: "message"
6619
+ }, {
6620
+ no: 23,
6621
+ name: "get_data_blob_request",
6622
+ kind: "message",
6623
+ T: GetDataBlobRequest,
6624
+ oneof: "message"
6429
6625
  }]);
6430
6626
  const SignalResponse = /* @__PURE__ */proto3.makeMessageType("livekit.SignalResponse", () => [{
6431
6627
  no: 1,
@@ -6595,6 +6791,18 @@ const SignalResponse = /* @__PURE__ */proto3.makeMessageType("livekit.SignalResp
6595
6791
  kind: "message",
6596
6792
  T: DataTrackSubscriberHandles,
6597
6793
  oneof: "message"
6794
+ }, {
6795
+ no: 30,
6796
+ name: "store_data_blob_response",
6797
+ kind: "message",
6798
+ T: StoreDataBlobResponse,
6799
+ oneof: "message"
6800
+ }, {
6801
+ no: 31,
6802
+ name: "get_data_blob_response",
6803
+ kind: "message",
6804
+ T: GetDataBlobResponse,
6805
+ oneof: "message"
6598
6806
  }]);
6599
6807
  const SimulcastCodec = /* @__PURE__ */proto3.makeMessageType("livekit.SimulcastCodec", () => [{
6600
6808
  no: 1,
@@ -6742,6 +6950,18 @@ const PublishDataTrackRequest = /* @__PURE__ */proto3.makeMessageType("livekit.P
6742
6950
  name: "encryption",
6743
6951
  kind: "enum",
6744
6952
  T: proto3.getEnumType(Encryption_Type)
6953
+ }, {
6954
+ no: 4,
6955
+ name: "frame_encoding",
6956
+ kind: "message",
6957
+ T: DataTrackFrameEncoding,
6958
+ opt: true
6959
+ }, {
6960
+ no: 5,
6961
+ name: "schema",
6962
+ kind: "message",
6963
+ T: DataTrackSchemaId,
6964
+ opt: true
6745
6965
  }]);
6746
6966
  const PublishDataTrackResponse = /* @__PURE__ */proto3.makeMessageType("livekit.PublishDataTrackResponse", () => [{
6747
6967
  no: 1,
@@ -7035,6 +7255,60 @@ const UpdateDataSubscription_Update = /* @__PURE__ */proto3.makeMessageType("liv
7035
7255
  }], {
7036
7256
  localName: "UpdateDataSubscription_Update"
7037
7257
  });
7258
+ const StoreDataBlobRequest = /* @__PURE__ */proto3.makeMessageType("livekit.StoreDataBlobRequest", () => [{
7259
+ no: 1,
7260
+ name: "request_id",
7261
+ kind: "scalar",
7262
+ T: 13
7263
+ /* ScalarType.UINT32 */
7264
+ }, {
7265
+ no: 2,
7266
+ name: "blob",
7267
+ kind: "message",
7268
+ T: DataBlob
7269
+ }]);
7270
+ const StoreDataBlobResponse = /* @__PURE__ */proto3.makeMessageType("livekit.StoreDataBlobResponse", () => [{
7271
+ no: 1,
7272
+ name: "request_id",
7273
+ kind: "scalar",
7274
+ T: 13
7275
+ /* ScalarType.UINT32 */
7276
+ }, {
7277
+ no: 2,
7278
+ name: "key",
7279
+ kind: "message",
7280
+ T: DataBlobKey
7281
+ }]);
7282
+ const GetDataBlobRequest = /* @__PURE__ */proto3.makeMessageType("livekit.GetDataBlobRequest", () => [{
7283
+ no: 1,
7284
+ name: "request_id",
7285
+ kind: "scalar",
7286
+ T: 13
7287
+ /* ScalarType.UINT32 */
7288
+ }, {
7289
+ no: 2,
7290
+ name: "participant_identity",
7291
+ kind: "scalar",
7292
+ T: 9
7293
+ /* ScalarType.STRING */
7294
+ }, {
7295
+ no: 3,
7296
+ name: "key",
7297
+ kind: "message",
7298
+ T: DataBlobKey
7299
+ }]);
7300
+ const GetDataBlobResponse = /* @__PURE__ */proto3.makeMessageType("livekit.GetDataBlobResponse", () => [{
7301
+ no: 1,
7302
+ name: "request_id",
7303
+ kind: "scalar",
7304
+ T: 13
7305
+ /* ScalarType.UINT32 */
7306
+ }, {
7307
+ no: 2,
7308
+ name: "blob",
7309
+ kind: "message",
7310
+ T: DataBlob
7311
+ }]);
7038
7312
  const UpdateTrackSettings = /* @__PURE__ */proto3.makeMessageType("livekit.UpdateTrackSettings", () => [{
7039
7313
  no: 1,
7040
7314
  name: "track_sids",
@@ -7697,6 +7971,9 @@ const RequestResponse_Reason = /* @__PURE__ */proto3.makeEnum("livekit.RequestRe
7697
7971
  }, {
7698
7972
  no: 10,
7699
7973
  name: "DUPLICATE_NAME"
7974
+ }, {
7975
+ no: 11,
7976
+ name: "INVALID_REQUEST"
7700
7977
  }]);
7701
7978
  const TrackSubscribed = /* @__PURE__ */proto3.makeMessageType("livekit.TrackSubscribed", () => [{
7702
7979
  no: 1,
@@ -8226,6 +8503,8 @@ var LoggerNames;
8226
8503
  LoggerNames["PCTransport"] = "livekit-pc-transport";
8227
8504
  LoggerNames["E2EE"] = "lk-e2ee";
8228
8505
  LoggerNames["DataTracks"] = "livekit-data-tracks";
8506
+ LoggerNames["Region"] = "livekit-region";
8507
+ LoggerNames["ICE"] = "livekit-ice";
8229
8508
  })(LoggerNames || (LoggerNames = {}));
8230
8509
  let livekitLogger = loglevelExports.getLogger(LoggerNames.Default);
8231
8510
  const livekitLoggers = Object.values(LoggerNames).map(name => loglevelExports.getLogger(name));
@@ -8810,7 +9089,7 @@ function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
8810
9089
  }
8811
9090
  const addEventListener = Object.getOwnPropertyDescriptor(EventTarget.prototype, 'addEventListener');
8812
9091
  if (!addEventListener.writable) {
8813
- log$4('Unable to polyfill events');
9092
+ log$5('Unable to polyfill events');
8814
9093
  return;
8815
9094
  }
8816
9095
  const proto = window.RTCPeerConnection.prototype;
@@ -8890,7 +9169,7 @@ function disableWarnings(bool) {
8890
9169
  deprecationWarnings_ = !bool;
8891
9170
  return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
8892
9171
  }
8893
- function log$4() {
9172
+ function log$5() {
8894
9173
  if (typeof window === 'object') {
8895
9174
  if (logDisabled_) {
8896
9175
  return;
@@ -9052,7 +9331,7 @@ function filterStats(result, track, outbound) {
9052
9331
  * tree.
9053
9332
  */
9054
9333
  /* eslint-env node */
9055
- const logging = log$4;
9334
+ const logging = log$5;
9056
9335
  function shimGetUserMedia$2(window, browserDetails) {
9057
9336
  if (browserDetails.version >= 64) {
9058
9337
  return;
@@ -11679,7 +11958,7 @@ function adapterFactory() {
11679
11958
  shimSafari: true
11680
11959
  };
11681
11960
  // Utils.
11682
- const logging = log$4;
11961
+ const logging = log$5;
11683
11962
  const browserDetails = detectBrowser(window);
11684
11963
  const adapter = {
11685
11964
  browserDetails,
@@ -11881,16 +12160,20 @@ function getMatch(exp, ua) {
11881
12160
  }
11882
12161
  function getOSVersion(ua) {
11883
12162
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
11884
- }var version$1 = "2.20.1";const version = version$1;
12163
+ }var version$1 = "2.21.0";const version = version$1;
11885
12164
  const protocolVersion = 17;
11886
12165
  /** Initial client protocol. */
11887
12166
  const CLIENT_PROTOCOL_DEFAULT = 0;
11888
12167
  /** Replaces RPC v1 protocol with a v2 data streams based one to support unlimited request /
11889
12168
  * response payload length. */
11890
12169
  const CLIENT_PROTOCOL_DATA_STREAM_RPC = 1;
12170
+ /** "Data streams v2": the client knows how to receive a single-packet data stream (a stream whose
12171
+ * entire payload is smuggled into the header packet, with no chunk/trailer packets). Senders only
12172
+ * use the single-packet optimization when the recipient advertises at least this protocol. */
12173
+ const CLIENT_PROTOCOL_DATA_STREAM_V2 = 2;
11891
12174
  /** The client protocol version indicates what level of support that the client has for
11892
12175
  * client <-> client api interactions. */
11893
- const clientProtocol = CLIENT_PROTOCOL_DATA_STREAM_RPC;/** Base error that all LiveKit specific custom errors inherit from. */
12176
+ const clientProtocol = CLIENT_PROTOCOL_DATA_STREAM_V2;/** Base error that all LiveKit specific custom errors inherit from. */
11894
12177
  class LivekitError extends Error {
11895
12178
  constructor(code, message, options) {
11896
12179
  super(message || 'an error has occurred');
@@ -11972,25 +12255,25 @@ class TrackInvalidError extends LivekitError {
11972
12255
  }
11973
12256
  class UnsupportedServer extends LivekitError {
11974
12257
  constructor(message) {
11975
- super(10, message !== null && message !== void 0 ? message : 'unsupported server');
12258
+ super(10, message || 'unsupported server');
11976
12259
  this.name = 'UnsupportedServer';
11977
12260
  }
11978
12261
  }
11979
12262
  class UnexpectedConnectionState extends LivekitError {
11980
12263
  constructor(message) {
11981
- super(12, message !== null && message !== void 0 ? message : 'unexpected connection state');
12264
+ super(12, message || 'unexpected connection state');
11982
12265
  this.name = 'UnexpectedConnectionState';
11983
12266
  }
11984
12267
  }
11985
12268
  class NegotiationError extends LivekitError {
11986
12269
  constructor(message) {
11987
- super(13, message !== null && message !== void 0 ? message : 'unable to negotiate');
12270
+ super(13, message || 'unable to negotiate');
11988
12271
  this.name = 'NegotiationError';
11989
12272
  }
11990
12273
  }
11991
12274
  class PublishDataError extends LivekitError {
11992
12275
  constructor(message) {
11993
- super(14, message !== null && message !== void 0 ? message : 'unable to publish data');
12276
+ super(14, message || 'unable to publish data');
11994
12277
  this.name = 'PublishDataError';
11995
12278
  }
11996
12279
  }
@@ -12026,6 +12309,10 @@ var DataStreamErrorReason;
12026
12309
  DataStreamErrorReason[DataStreamErrorReason["HandlerAlreadyRegistered"] = 7] = "HandlerAlreadyRegistered";
12027
12310
  // Encryption type mismatch.
12028
12311
  DataStreamErrorReason[DataStreamErrorReason["EncryptionTypeMismatch"] = 8] = "EncryptionTypeMismatch";
12312
+ // The serialized stream header packet (driven mainly by attributes) exceeds the MTU budget.
12313
+ DataStreamErrorReason[DataStreamErrorReason["HeaderTooLarge"] = 9] = "HeaderTooLarge";
12314
+ // The stream's (decompressed) payload exceeds the maximum allowed size.
12315
+ DataStreamErrorReason[DataStreamErrorReason["PayloadTooLarge"] = 10] = "PayloadTooLarge";
12029
12316
  })(DataStreamErrorReason || (DataStreamErrorReason = {}));
12030
12317
  class DataStreamError extends LivekitReasonedError {
12031
12318
  constructor(message, reason) {
@@ -13195,6 +13482,9 @@ class Track extends eventsExports.EventEmitter {
13195
13482
  }
13196
13483
  /** @internal */
13197
13484
  setStreamState(value) {
13485
+ if (this._streamState !== value) {
13486
+ this.log.debug("stream state changed: ".concat(this._streamState, " -> ").concat(value));
13487
+ }
13198
13488
  this._streamState = value;
13199
13489
  }
13200
13490
  constructor(mediaTrack, kind) {
@@ -13219,8 +13509,8 @@ class Track extends eventsExports.EventEmitter {
13219
13509
  this.handleAppVisibilityChanged();
13220
13510
  }
13221
13511
  };
13222
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track);
13223
13512
  this.loggerContextCb = loggerOptions.loggerContextCb;
13513
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.Track, () => this.logContext);
13224
13514
  this.setMaxListeners(100);
13225
13515
  this.kind = kind;
13226
13516
  this._mediaStreamTrack = mediaTrack;
@@ -13288,9 +13578,11 @@ class Track extends eventsExports.EventEmitter {
13288
13578
  this.emit(hasAudio ? TrackEvent.AudioPlaybackFailed : TrackEvent.VideoPlaybackFailed, e);
13289
13579
  } else if (e.name === 'AbortError') {
13290
13580
  // commonly triggered by another `play` request, only log for debugging purposes
13291
- livekitLogger.debug("".concat(hasAudio ? 'audio' : 'video', " playback aborted, likely due to new play request"));
13581
+ this.log.debug("".concat(hasAudio ? 'audio' : 'video', " playback aborted, likely due to new play request"));
13292
13582
  } else {
13293
- livekitLogger.warn("could not playback ".concat(hasAudio ? 'audio' : 'video'), e);
13583
+ this.log.warn("could not playback ".concat(hasAudio ? 'audio' : 'video'), {
13584
+ error: e
13585
+ });
13294
13586
  }
13295
13587
  // If audio playback isn't allowed make sure we still play back the video
13296
13588
  if (hasAudio && element && allMediaStreamTracks.some(tr => tr.kind === 'video') && e.name === 'NotAllowedError') {
@@ -13333,6 +13625,7 @@ class Track extends eventsExports.EventEmitter {
13333
13625
  }
13334
13626
  }
13335
13627
  stop() {
13628
+ this.log.debug('stopping track');
13336
13629
  this.stopMonitor();
13337
13630
  this._mediaStreamTrack.stop();
13338
13631
  }
@@ -13353,12 +13646,12 @@ class Track extends eventsExports.EventEmitter {
13353
13646
  }
13354
13647
  /** @internal */
13355
13648
  updateLoggerOptions(loggerOptions) {
13356
- if (loggerOptions.loggerName) {
13357
- this.log = getLogger(loggerOptions.loggerName);
13358
- }
13359
13649
  if (loggerOptions.loggerContextCb) {
13360
13650
  this.loggerContextCb = loggerOptions.loggerContextCb;
13361
13651
  }
13652
+ if (loggerOptions.loggerName) {
13653
+ this.log = getLogger(loggerOptions.loggerName, () => this.logContext);
13654
+ }
13362
13655
  }
13363
13656
  recycleElement(element) {
13364
13657
  if (element instanceof HTMLAudioElement) {
@@ -14113,6 +14406,53 @@ function splitUtf8(s, n) {
14113
14406
  }
14114
14407
  return result;
14115
14408
  }
14409
+ /** Wraps a byte array in a `ReadableStream` that yields it as a single chunk and then closes. */
14410
+ function readableFromBytes(bytes) {
14411
+ return new ReadableStream({
14412
+ start(controller) {
14413
+ controller.enqueue(bytes);
14414
+ controller.close();
14415
+ }
14416
+ });
14417
+ }
14418
+ /**
14419
+ * Re-chunks a byte stream into pieces of exactly `chunkSize` bytes (the final piece may be
14420
+ * smaller), coalescing or splitting the source's pieces as needed. Memory use is bounded to roughly
14421
+ * `chunkSize` plus one source read, so it never buffers the whole stream — used to pack
14422
+ * `CompressionStream`/`file.stream()` output into MTU-sized data-stream chunks.
14423
+ */
14424
+ function readBytesInChunks(source, chunkSize) {
14425
+ return __asyncGenerator(this, arguments, function* readBytesInChunks_1() {
14426
+ const reader = source.getReader();
14427
+ let buffer = new Uint8Array(0);
14428
+ try {
14429
+ while (true) {
14430
+ const _yield$__await = yield __await(reader.read()),
14431
+ done = _yield$__await.done,
14432
+ value = _yield$__await.value;
14433
+ if (done) {
14434
+ break;
14435
+ }
14436
+ if (value.byteLength === 0) {
14437
+ continue;
14438
+ }
14439
+ const merged = new Uint8Array(buffer.byteLength + value.byteLength);
14440
+ merged.set(buffer);
14441
+ merged.set(value, buffer.byteLength);
14442
+ buffer = merged;
14443
+ while (buffer.byteLength >= chunkSize) {
14444
+ yield yield __await(buffer.slice(0, chunkSize));
14445
+ buffer = buffer.slice(chunkSize);
14446
+ }
14447
+ }
14448
+ if (buffer.byteLength > 0) {
14449
+ yield yield __await(buffer);
14450
+ }
14451
+ } finally {
14452
+ reader.releaseLock();
14453
+ }
14454
+ });
14455
+ }
14116
14456
  function extractMaxAgeFromRequestHeaders(headers) {
14117
14457
  var _a;
14118
14458
  const cacheControl = headers.get('Cache-Control');
@@ -16317,6 +16657,10 @@ var SignalConnectionState;
16317
16657
  })(SignalConnectionState || (SignalConnectionState = {}));
16318
16658
  /** specifies how much time (in ms) we allow for the ws to close its connection gracefully before continuing */
16319
16659
  const MAX_WS_CLOSE_TIME = 250;
16660
+ /**
16661
+ * How long (in ms) to wait for the first message after the WebSocket upgrade.
16662
+ */
16663
+ const JOIN_RESPONSE_TIMEOUT = 5000;
16320
16664
  /** @internal */
16321
16665
  class SignalClient {
16322
16666
  get currentState() {
@@ -16465,7 +16809,6 @@ class SignalClient {
16465
16809
  this.ws = new WebSocketStream(rtcUrl);
16466
16810
  try {
16467
16811
  this.ws.closed.then(closeInfo => {
16468
- var _a;
16469
16812
  if (this.isEstablishingConnection) {
16470
16813
  reject(ConnectionError.internal("Websocket got closed during a (re)connection attempt: ".concat(closeInfo.reason)));
16471
16814
  }
@@ -16477,7 +16820,7 @@ class SignalClient {
16477
16820
  state: this.state
16478
16821
  });
16479
16822
  if (this.state === SignalConnectionState.CONNECTED) {
16480
- this.handleOnClose((_a = closeInfo.reason) !== null && _a !== void 0 ? _a : 'Unexpected WS error');
16823
+ this.handleOnClose(closeInfo.reason || 'Unexpected WS error');
16481
16824
  }
16482
16825
  }
16483
16826
  return;
@@ -16505,7 +16848,26 @@ class SignalClient {
16505
16848
  }
16506
16849
  const signalReader = connection.readable.getReader();
16507
16850
  this.streamWriter = connection.writable.getWriter();
16508
- const firstMessage = yield signalReader.read();
16851
+ // wsTimeout only guarded the upgrade; guard the first-message read with
16852
+ // its own timeout so a silent server can't hang join() forever.
16853
+ let firstMessage;
16854
+ let firstMessageTimeout;
16855
+ try {
16856
+ firstMessage = yield Promise.race([signalReader.read(), new Promise((_, rejectRead) => {
16857
+ firstMessageTimeout = setTimeout(() => {
16858
+ rejectRead(ConnectionError.timeout('signal connection timed out while waiting for the first message'));
16859
+ }, JOIN_RESPONSE_TIMEOUT);
16860
+ })]);
16861
+ } catch (e) {
16862
+ // No first message in time: release the reader and tear down the ws
16863
+ // so we surface the timeout instead of leaking an open connection.
16864
+ signalReader.releaseLock();
16865
+ reject(e);
16866
+ this.close();
16867
+ return;
16868
+ } finally {
16869
+ clearTimeout(firstMessageTimeout);
16870
+ }
16509
16871
  signalReader.releaseLock();
16510
16872
  if (!firstMessage.value) {
16511
16873
  throw ConnectionError.internal('no message received as first message');
@@ -17239,67 +17601,26 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17239
17601
  params.set('join_request', bytesToBase64(wrappedBytes).replace(/\+/g, '-').replace(/\//g, '_'));
17240
17602
  return params;
17241
17603
  });
17242
- }class DataPacketBuffer {
17243
- constructor() {
17244
- this.buffer = [];
17245
- this._totalSize = 0;
17246
- }
17247
- push(item) {
17248
- this.buffer.push(item);
17249
- this._totalSize += item.data.byteLength;
17604
+ }class TTLMap {
17605
+ /**
17606
+ * @param ttl ttl of the key (ms)
17607
+ */
17608
+ constructor(ttl) {
17609
+ this._map = new Map();
17610
+ this._lastCleanup = 0;
17611
+ this.ttl = ttl;
17250
17612
  }
17251
- pop() {
17252
- const item = this.buffer.shift();
17253
- if (item) {
17254
- this._totalSize -= item.data.byteLength;
17613
+ set(key, value) {
17614
+ const now = Date.now();
17615
+ if (now - this._lastCleanup > this.ttl / 2) {
17616
+ this.cleanup();
17255
17617
  }
17256
- return item;
17257
- }
17258
- getAll() {
17259
- return this.buffer.slice();
17260
- }
17261
- popToSequence(sequence) {
17262
- while (this.buffer.length > 0) {
17263
- const first = this.buffer[0];
17264
- if (first.sequence <= sequence) {
17265
- this.pop();
17266
- } else {
17267
- break;
17268
- }
17269
- }
17270
- }
17271
- alignBufferedAmount(bufferedAmount) {
17272
- while (this.buffer.length > 0) {
17273
- const first = this.buffer[0];
17274
- if (this._totalSize - first.data.byteLength <= bufferedAmount) {
17275
- break;
17276
- }
17277
- this.pop();
17278
- }
17279
- }
17280
- get length() {
17281
- return this.buffer.length;
17282
- }
17283
- }class TTLMap {
17284
- /**
17285
- * @param ttl ttl of the key (ms)
17286
- */
17287
- constructor(ttl) {
17288
- this._map = new Map();
17289
- this._lastCleanup = 0;
17290
- this.ttl = ttl;
17291
- }
17292
- set(key, value) {
17293
- const now = Date.now();
17294
- if (now - this._lastCleanup > this.ttl / 2) {
17295
- this.cleanup();
17296
- }
17297
- const expiresAt = now + this.ttl;
17298
- this._map.set(key, {
17299
- value,
17300
- expiresAt
17301
- });
17302
- return this;
17618
+ const expiresAt = now + this.ttl;
17619
+ this._map.set(key, {
17620
+ value,
17621
+ expiresAt
17622
+ });
17623
+ return this;
17303
17624
  }
17304
17625
  get(key) {
17305
17626
  const entry = this._map.get(key);
@@ -18056,13 +18377,20 @@ function requireLib() {
18056
18377
  lib.parseImageAttributes = parser.parseImageAttributes;
18057
18378
  lib.parseSimulcastStreamList = parser.parseSimulcastStreamList;
18058
18379
  return lib;
18059
- }var libExports = requireLib();/* The svc codec (av1/vp9) would use a very low bitrate at the begining and
18060
- increase slowly by the bandwidth estimator until it reach the target bitrate. The
18061
- process commonly cost more than 10 seconds cause subscriber will get blur video at
18062
- the first few seconds. So we use a 70% of target bitrate here as the start bitrate to
18063
- eliminate this issue.
18064
- */
18065
- const startBitrateForSVC = 0.7;
18380
+ }var libExports = requireLib();/*
18381
+ * Video codecs use a very low bitrate at the beginning and increase slowly by
18382
+ * the bandwidth estimator until they reach the target bitrate. The process commonly
18383
+ * costs more than 10 seconds causing subscribers to get blurry video at the first
18384
+ * few seconds. We use x-google-start-bitrate to hint the BWE to start higher.
18385
+ *
18386
+ * Why 90%: Gives ~10% headroom for bandwidth estimation while starting close to target.
18387
+ * Why same for all codecs: Target bitrate already accounts for codec efficiency
18388
+ * (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
18389
+ * Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
18390
+ */
18391
+ const startBitrateMultiplier = 0.9;
18392
+ /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
18393
+ const maxStartBitrateKbps = 1000;
18066
18394
  const debounceInterval = 20;
18067
18395
  const PCEvents = {
18068
18396
  NegotiationStarted: 'negotiationStarted',
@@ -18087,6 +18415,7 @@ class PCTransport extends eventsExports.EventEmitter {
18087
18415
  var _a;
18088
18416
  super();
18089
18417
  this.log = livekitLogger;
18418
+ this.iceLog = livekitLogger;
18090
18419
  this.ddExtID = 0;
18091
18420
  this.latestOfferId = 0;
18092
18421
  this.latestAcknowledgedOfferId = 0;
@@ -18113,6 +18442,7 @@ class PCTransport extends eventsExports.EventEmitter {
18113
18442
  if (!this._pc) {
18114
18443
  return;
18115
18444
  }
18445
+ this.log.debug('closing peer connection');
18116
18446
  this.pendingInitialOffer = undefined;
18117
18447
  this._pc.close();
18118
18448
  this._pc.onconnectionstatechange = null;
@@ -18128,8 +18458,9 @@ class PCTransport extends eventsExports.EventEmitter {
18128
18458
  this._pc.oniceconnectionstatechange = null;
18129
18459
  this._pc = null;
18130
18460
  };
18131
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport);
18132
18461
  this.loggerOptions = loggerOptions;
18462
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCTransport, () => this.logContext);
18463
+ this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
18133
18464
  this.config = config;
18134
18465
  this._pc = this.createPC();
18135
18466
  this.offerLock = new _();
@@ -18139,26 +18470,39 @@ class PCTransport extends eventsExports.EventEmitter {
18139
18470
  pc.onicecandidate = ev => {
18140
18471
  var _a;
18141
18472
  if (!ev.candidate) return;
18473
+ this.iceLog.debug('local ICE candidate gathered', {
18474
+ candidate: ev.candidate.candidate
18475
+ });
18142
18476
  (_a = this.onIceCandidate) === null || _a === void 0 ? void 0 : _a.call(this, ev.candidate);
18143
18477
  };
18144
18478
  pc.onicecandidateerror = ev => {
18145
18479
  var _a;
18480
+ this.iceLog.debug('ICE candidate error', {
18481
+ event: ev
18482
+ });
18146
18483
  (_a = this.onIceCandidateError) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18147
18484
  };
18148
18485
  pc.oniceconnectionstatechange = () => {
18149
18486
  var _a;
18487
+ this.iceLog.debug("ICE connection state: ".concat(pc.iceConnectionState));
18150
18488
  (_a = this.onIceConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.iceConnectionState);
18151
18489
  };
18152
18490
  pc.onsignalingstatechange = () => {
18153
18491
  var _a;
18492
+ this.log.debug("signaling state: ".concat(pc.signalingState));
18154
18493
  (_a = this.onSignalingStatechange) === null || _a === void 0 ? void 0 : _a.call(this, pc.signalingState);
18155
18494
  };
18156
18495
  pc.onconnectionstatechange = () => {
18157
18496
  var _a;
18497
+ this.log.debug("connection state: ".concat(pc.connectionState));
18158
18498
  (_a = this.onConnectionStateChange) === null || _a === void 0 ? void 0 : _a.call(this, pc.connectionState);
18159
18499
  };
18160
18500
  pc.ondatachannel = ev => {
18161
18501
  var _a;
18502
+ this.log.debug('data channel opened by peer', {
18503
+ label: ev.channel.label,
18504
+ id: ev.channel.id
18505
+ });
18162
18506
  (_a = this.onDataChannel) === null || _a === void 0 ? void 0 : _a.call(this, ev);
18163
18507
  };
18164
18508
  pc.ontrack = ev => {
@@ -18179,6 +18523,9 @@ class PCTransport extends eventsExports.EventEmitter {
18179
18523
  if (this.pc.remoteDescription && !this.restartingIce) {
18180
18524
  return this.pc.addIceCandidate(candidate);
18181
18525
  }
18526
+ this.iceLog.debug('queuing remote ICE candidate until remote description applied', {
18527
+ pendingCount: this.pendingCandidates.length + 1
18528
+ });
18182
18529
  this.pendingCandidates.push(candidate);
18183
18530
  });
18184
18531
  }
@@ -18186,10 +18533,10 @@ class PCTransport extends eventsExports.EventEmitter {
18186
18533
  return __awaiter(this, void 0, void 0, function* () {
18187
18534
  var _a, _b;
18188
18535
  if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
18189
- this.log.warn('ignoring answer for old offer', Object.assign(Object.assign({}, this.logContext), {
18536
+ this.log.warn('ignoring answer for old offer', {
18190
18537
  offerId,
18191
18538
  latestOfferId: this.latestOfferId
18192
- }));
18539
+ });
18193
18540
  return false;
18194
18541
  }
18195
18542
  let mungedSDP = undefined;
@@ -18207,7 +18554,7 @@ class PCTransport extends eventsExports.EventEmitter {
18207
18554
  sdpParsed.media.forEach(media => {
18208
18555
  ensureIPAddrMatchVersion(media);
18209
18556
  });
18210
- this.log.debug('setting pending initial offer before processing answer', this.logContext);
18557
+ this.log.debug('setting pending initial offer before processing answer');
18211
18558
  yield this.setMungedSDP(initialOffer, libExports.write(sdpParsed));
18212
18559
  }
18213
18560
  const sdpParsed = libExports.parse((_b = sd.sdp) !== null && _b !== void 0 ? _b : '');
@@ -18265,6 +18612,11 @@ class PCTransport extends eventsExports.EventEmitter {
18265
18612
  mungedSDP = libExports.write(sdpParsed);
18266
18613
  }
18267
18614
  yield this.setMungedSDP(sd, mungedSDP, true);
18615
+ if (this.pendingCandidates.length > 0) {
18616
+ this.iceLog.debug('flushing queued ICE candidates', {
18617
+ count: this.pendingCandidates.length
18618
+ });
18619
+ }
18268
18620
  this.pendingCandidates.forEach(candidate => {
18269
18621
  this.pc.addIceCandidate(candidate);
18270
18622
  });
@@ -18300,7 +18652,7 @@ class PCTransport extends eventsExports.EventEmitter {
18300
18652
  const unlock = yield this.offerLock.lock();
18301
18653
  try {
18302
18654
  if (this.pc.signalingState !== 'stable') {
18303
- this.log.warn('signaling state is not stable, cannot create initial offer', this.logContext);
18655
+ this.log.warn('signaling state is not stable, cannot create initial offer');
18304
18656
  return;
18305
18657
  }
18306
18658
  const offerId = this.latestOfferId + 1;
@@ -18333,7 +18685,7 @@ class PCTransport extends eventsExports.EventEmitter {
18333
18685
  return;
18334
18686
  }
18335
18687
  if (options === null || options === void 0 ? void 0 : options.iceRestart) {
18336
- this.log.debug('restarting ICE', this.logContext);
18688
+ this.iceLog.debug('restarting ICE');
18337
18689
  this.restartingIce = true;
18338
18690
  }
18339
18691
  if (this._pc && (this._pc.signalingState === 'have-local-offer' || this.pendingInitialOffer)) {
@@ -18346,22 +18698,22 @@ class PCTransport extends eventsExports.EventEmitter {
18346
18698
  yield this._pc.setRemoteDescription(currentSD);
18347
18699
  } else {
18348
18700
  this.renegotiate = true;
18349
- this.log.debug('requesting renegotiation', Object.assign({}, this.logContext));
18701
+ this.log.debug('requesting renegotiation');
18350
18702
  return;
18351
18703
  }
18352
18704
  } else if (!this._pc || this._pc.signalingState === 'closed') {
18353
- this.log.warn('could not createOffer with closed peer connection', this.logContext);
18705
+ this.log.warn('could not createOffer with closed peer connection');
18354
18706
  return;
18355
18707
  }
18356
18708
  // actually negotiate
18357
- this.log.debug('starting to negotiate', this.logContext);
18709
+ this.log.debug('starting to negotiate');
18358
18710
  // increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
18359
18711
  const offerId = this.latestOfferId + 1;
18360
18712
  this.latestOfferId = offerId;
18361
18713
  const offer = yield this.pc.createOffer(options);
18362
- this.log.debug('original offer', Object.assign({
18714
+ this.log.debug('original offer', {
18363
18715
  sdp: offer.sdp
18364
- }, this.logContext));
18716
+ });
18365
18717
  const sdpParsed = libExports.parse((_a = offer.sdp) !== null && _a !== void 0 ? _a : '');
18366
18718
  sdpParsed.media.forEach(media => {
18367
18719
  ensureIPAddrMatchVersion(media);
@@ -18387,12 +18739,15 @@ class PCTransport extends eventsExports.EventEmitter {
18387
18739
  this.ensureVideoDDExtensionForSVC(media, sdpParsed);
18388
18740
  }
18389
18741
  // mung sdp for bitrate setting that can't apply by sendEncoding
18390
- if (!isSVCCodec(trackbr.codec)) {
18391
- return true;
18392
- }
18393
- const startBitrate = Math.round(trackbr.maxbr * startBitrateForSVC);
18742
+ // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
18743
+ // Screen share is not capped since text/UI clarity requires high bitrate from the start
18744
+ // TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
18745
+ const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
18746
+ const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18747
+ let fmtpFound = false;
18394
18748
  for (const fmtp of media.fmtp) {
18395
18749
  if (fmtp.payload === codecPayload) {
18750
+ fmtpFound = true;
18396
18751
  // if another track's fmtp already is set, we cannot override the bitrate
18397
18752
  // this has the unfortunate consequence of being forced to use the
18398
18753
  // initial track's bitrate for all tracks
@@ -18402,6 +18757,13 @@ class PCTransport extends eventsExports.EventEmitter {
18402
18757
  break;
18403
18758
  }
18404
18759
  }
18760
+ // VP8 and some codecs may not have an existing fmtp line - create one
18761
+ if (!fmtpFound) {
18762
+ media.fmtp.push({
18763
+ payload: codecPayload,
18764
+ config: "x-google-start-bitrate=".concat(startBitrate)
18765
+ });
18766
+ }
18405
18767
  return true;
18406
18768
  });
18407
18769
  }
@@ -18416,10 +18778,10 @@ class PCTransport extends eventsExports.EventEmitter {
18416
18778
  conformBundledCodecFmtp(sdpParsed.media, media => placeholderMids.has(getMidString(media.mid)));
18417
18779
  }
18418
18780
  if (this.latestOfferId > offerId) {
18419
- this.log.warn('latestOfferId mismatch', Object.assign(Object.assign({}, this.logContext), {
18781
+ this.log.warn('latestOfferId mismatch', {
18420
18782
  latestOfferId: this.latestOfferId,
18421
18783
  offerId
18422
- }));
18784
+ });
18423
18785
  return;
18424
18786
  }
18425
18787
  yield this.setMungedSDP(offer, libExports.write(sdpParsed));
@@ -18567,7 +18929,7 @@ class PCTransport extends eventsExports.EventEmitter {
18567
18929
  if (munged) {
18568
18930
  sd.sdp = munged;
18569
18931
  try {
18570
- this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"), this.logContext);
18932
+ this.log.debug("setting munged ".concat(remote ? 'remote' : 'local', " description"));
18571
18933
  if (remote) {
18572
18934
  yield this.pc.setRemoteDescription(sd);
18573
18935
  } else {
@@ -18575,11 +18937,11 @@ class PCTransport extends eventsExports.EventEmitter {
18575
18937
  }
18576
18938
  return;
18577
18939
  } catch (e) {
18578
- this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"), Object.assign(Object.assign({}, this.logContext), {
18940
+ this.log.warn("not able to set ".concat(sd.type, ", falling back to unmodified sdp"), {
18579
18941
  error: e,
18580
18942
  mungedSdp: munged,
18581
18943
  originalSdp
18582
- }));
18944
+ });
18583
18945
  sd.sdp = originalSdp;
18584
18946
  }
18585
18947
  }
@@ -18606,9 +18968,9 @@ class PCTransport extends eventsExports.EventEmitter {
18606
18968
  if (!remote && this.pc.remoteDescription) {
18607
18969
  fields.remoteSdp = this.pc.remoteDescription;
18608
18970
  }
18609
- this.log.error("unable to set ".concat(sd.type), Object.assign(Object.assign({}, this.logContext), {
18971
+ this.log.error("unable to set ".concat(sd.type), {
18610
18972
  fields
18611
- }));
18973
+ });
18612
18974
  throw new NegotiationError(msg);
18613
18975
  }
18614
18976
  });
@@ -18869,6 +19231,7 @@ class PCTransportManager {
18869
19231
  var _a;
18870
19232
  this.peerConnectionTimeout = roomConnectOptionDefaults.peerConnectionTimeout;
18871
19233
  this.log = livekitLogger;
19234
+ this.iceLog = livekitLogger;
18872
19235
  this.updateState = () => {
18873
19236
  var _a, _b;
18874
19237
  const previousState = this.state;
@@ -18887,12 +19250,13 @@ class PCTransportManager {
18887
19250
  this.state = PCTransportState.NEW;
18888
19251
  }
18889
19252
  if (previousState !== this.state) {
18890
- this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state]), this.logContext);
19253
+ this.log.debug("pc state change: from ".concat(PCTransportState[previousState], " to ").concat(PCTransportState[this.state]));
18891
19254
  (_a = this.onStateChange) === null || _a === void 0 ? void 0 : _a.call(this, this.state, this.publisher.getConnectionState(), (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.getConnectionState());
18892
19255
  }
18893
19256
  };
18894
- this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager);
18895
19257
  this.loggerOptions = loggerOptions;
19258
+ this.log = getLogger((_a = loggerOptions.loggerName) !== null && _a !== void 0 ? _a : LoggerNames.PCManager, () => this.logContext);
19259
+ this.iceLog = getLogger(LoggerNames.ICE, () => this.logContext);
18896
19260
  this.isPublisherConnectionRequired = mode !== 'subscriber-primary';
18897
19261
  this.isSubscriberConnectionRequired = mode === 'subscriber-primary';
18898
19262
  this.publisher = new PCTransport(rtcConfig, loggerOptions);
@@ -18965,9 +19329,9 @@ class PCTransportManager {
18965
19329
  publisher.removeTrack(sender);
18966
19330
  }
18967
19331
  } catch (e) {
18968
- this.log.warn('could not removeTrack', Object.assign(Object.assign({}, this.logContext), {
19332
+ this.log.warn('could not removeTrack', {
18969
19333
  error: e
18970
- }));
19334
+ });
18971
19335
  }
18972
19336
  }
18973
19337
  }
@@ -18977,6 +19341,7 @@ class PCTransportManager {
18977
19341
  }
18978
19342
  triggerIceRestart() {
18979
19343
  return __awaiter(this, void 0, void 0, function* () {
19344
+ this.iceLog.warn('triggering ICE restart');
18980
19345
  if (this.subscriber) {
18981
19346
  this.subscriber.restartingIce = true;
18982
19347
  }
@@ -18991,6 +19356,10 @@ class PCTransportManager {
18991
19356
  addIceCandidate(candidate, target) {
18992
19357
  return __awaiter(this, void 0, void 0, function* () {
18993
19358
  var _a;
19359
+ this.iceLog.debug('adding remote ICE candidate', {
19360
+ target,
19361
+ candidate
19362
+ });
18994
19363
  if (target === SignalTarget.PUBLISHER) {
18995
19364
  yield this.publisher.addIceCandidate(candidate);
18996
19365
  } else {
@@ -19001,11 +19370,11 @@ class PCTransportManager {
19001
19370
  createSubscriberAnswerFromOffer(sd, offerId) {
19002
19371
  return __awaiter(this, void 0, void 0, function* () {
19003
19372
  var _a, _b, _c;
19004
- this.log.debug('received server offer', Object.assign(Object.assign({}, this.logContext), {
19373
+ this.log.debug('received server offer', {
19005
19374
  RTCSdpType: sd.type,
19006
19375
  sdp: sd.sdp,
19007
19376
  signalingState: (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.getSignallingState().toString()
19008
- }));
19377
+ });
19009
19378
  const unlock = yield this.remoteOfferLock.lock();
19010
19379
  try {
19011
19380
  const success = yield (_b = this.subscriber) === null || _b === void 0 ? void 0 : _b.setRemoteDescription(sd, offerId);
@@ -19022,6 +19391,9 @@ class PCTransportManager {
19022
19391
  }
19023
19392
  updateConfiguration(config, iceRestart) {
19024
19393
  var _a;
19394
+ this.log.debug('updating rtc configuration', {
19395
+ iceRestart
19396
+ });
19025
19397
  this.publisher.setConfiguration(config);
19026
19398
  (_a = this.subscriber) === null || _a === void 0 ? void 0 : _a.setConfiguration(config);
19027
19399
  if (iceRestart) {
@@ -19034,7 +19406,7 @@ class PCTransportManager {
19034
19406
  const unlock = yield this.connectionLock.lock();
19035
19407
  try {
19036
19408
  if (this.isPublisherConnectionRequired && this.publisher.getConnectionState() !== 'connected' && this.publisher.getConnectionState() !== 'connecting') {
19037
- this.log.debug('negotiation required, start negotiating', this.logContext);
19409
+ this.log.debug('negotiation required, start negotiating');
19038
19410
  this.publisher.negotiate();
19039
19411
  }
19040
19412
  yield Promise.all((_a = this.requiredTransports) === null || _a === void 0 ? void 0 : _a.map(transport => this.ensureTransportConnected(transport, abortController, timeout)));
@@ -19148,7 +19520,7 @@ class PCTransportManager {
19148
19520
  }
19149
19521
  return new Promise((resolve, reject) => __awaiter(_this, void 0, void 0, function* () {
19150
19522
  const abortHandler = () => {
19151
- this.log.warn('abort transport connection', this.logContext);
19523
+ this.log.warn('abort transport connection');
19152
19524
  CriticalTimers.clearTimeout(connectTimeout);
19153
19525
  reject(ConnectionError.cancelled('room connection has been cancelled'));
19154
19526
  };
@@ -19174,6 +19546,663 @@ class PCTransportManager {
19174
19546
  }();
19175
19547
  });
19176
19548
  }
19549
+ }/**
19550
+ * Two-watermark flow control for one data channel kind.
19551
+ *
19552
+ * Owns the per-kind headroom gate: senders proceed freely while the buffer is at or below the
19553
+ * high-water mark and otherwise block — serialized FIFO through a mutex — until the browser's
19554
+ * `bufferedamountlow` event (armed at the low-water mark) signals the buffer has drained. The
19555
+ * serialization prevents woken senders from all refilling at once and overflowing the SCTP send
19556
+ * buffer (see livekit/client-sdk-js#1995).
19557
+ *
19558
+ * Waiters are parked on the channel object captured at wait entry. If that object stops being
19559
+ * current — replaced or torn down — its events may never fire again, so the owner must call
19560
+ * {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
19561
+ * gets a fresh controller.
19562
+ */
19563
+ class FlowControlledDataChannel {
19564
+ constructor(opts) {
19565
+ /** Last emitted low-water status; starts true (an empty buffer is below the mark). */
19566
+ this.bufferStatusLow = true;
19567
+ this.headroomLock = new _();
19568
+ /** Cancels parked headroom waiters when the handle is replaced or torn down. */
19569
+ this.waiterAbortController = new AbortController();
19570
+ this.kind = opts.kind;
19571
+ this.lowWaterMark = opts.lowWaterMark;
19572
+ this.highWaterMark = opts.highWaterMark;
19573
+ this.isEngineClosed = opts.isEngineClosed;
19574
+ this.onBufferStatusChanged = opts.onBufferStatusChanged;
19575
+ }
19576
+ /** The currently attached RTCDataChannel handle, if any. */
19577
+ get channelHandle() {
19578
+ return this.handle;
19579
+ }
19580
+ /**
19581
+ * Attaches the channel handle this wrapper controls. Replacing an existing handle rejects
19582
+ * parked waiters — their events would never fire again on the abandoned object — and installs a
19583
+ * fresh controller, so queued senders re-check against the new channel. Wrappers outlive their
19584
+ * handles: this is the one place handle turnover happens, which is what makes stranding a
19585
+ * waiter structurally impossible.
19586
+ */
19587
+ attach(dc) {
19588
+ if (this.handle && this.handle !== dc) {
19589
+ this.invalidateWaiters('data channel replaced');
19590
+ }
19591
+ this.handle = dc;
19592
+ }
19593
+ /** Detaches the handle on teardown, rejecting parked waiters. */
19594
+ detach() {
19595
+ let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'data channel torn down';
19596
+ if (this.handle) {
19597
+ this.invalidateWaiters(reason);
19598
+ }
19599
+ this.handle = undefined;
19600
+ }
19601
+ getChannel() {
19602
+ return this.handle;
19603
+ }
19604
+ /**
19605
+ * Whether the send buffer has room to accept more data (the send gate). Senders proceed while
19606
+ * this is true and block once it goes false. Callers resolve the handle (and decide what an
19607
+ * absent one means) before asking.
19608
+ */
19609
+ isBelowHighWaterMark(dc) {
19610
+ // RTCDataChannel has no built-in high-water mark, so we compare against our static mark.
19611
+ return dc.bufferedAmount <= this.highWaterMark;
19612
+ }
19613
+ /**
19614
+ * Whether the send buffer has drained to its low-water mark. Drives the engine's public
19615
+ * DCBufferStatusChanged event.
19616
+ */
19617
+ isBelowLowWaterMark(dc) {
19618
+ // Read the channel's own threshold: it is tuned dynamically for the lossy channel.
19619
+ return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
19620
+ }
19621
+ /**
19622
+ * Acquires the headroom lock, resolving with the unlock function. Batch senders (the resume
19623
+ * replay) hold it across all of their sends so no other sender can interleave, calling
19624
+ * {@link waitForHeadroomWithoutLock} per message to respect flow control within the batch.
19625
+ */
19626
+ lockHeadroom() {
19627
+ return this.headroomLock.lock();
19628
+ }
19629
+ /**
19630
+ * Resolves once the caller may send on this channel: immediately while the send buffer is at or
19631
+ * below its high-water mark, otherwise once the buffer has drained to the low-water mark (the
19632
+ * `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
19633
+ * the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
19634
+ * sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
19635
+ * closed/buffer checks run inside the lock so queued callers proceed in FIFO order.
19636
+ */
19637
+ waitForHeadroomWithLock() {
19638
+ return __awaiter(this, void 0, void 0, function* () {
19639
+ const unlock = yield this.lockHeadroom();
19640
+ try {
19641
+ yield this.waitForHeadroomWithoutLock();
19642
+ } finally {
19643
+ unlock();
19644
+ }
19645
+ });
19646
+ }
19647
+ /** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
19648
+ waitForHeadroomWithoutLock() {
19649
+ return __awaiter(this, void 0, void 0, function* () {
19650
+ if (this.isEngineClosed()) {
19651
+ throw new UnexpectedConnectionState('engine closed');
19652
+ }
19653
+ const dc = this.getChannel();
19654
+ if (!dc) {
19655
+ throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
19656
+ }
19657
+ if (this.isBelowHighWaterMark(dc)) {
19658
+ return;
19659
+ }
19660
+ const abortSignal = this.waiterAbortController.signal;
19661
+ yield new TypedPromise((resolve, reject) => {
19662
+ const onBufferedAmountLow = () => {
19663
+ cleanup();
19664
+ resolve();
19665
+ };
19666
+ const onDCClose = () => {
19667
+ cleanup();
19668
+ reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " closed while draining the buffer")));
19669
+ };
19670
+ const onAbort = () => {
19671
+ cleanup();
19672
+ reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " was replaced or torn down while waiting for headroom")));
19673
+ };
19674
+ const cleanup = () => {
19675
+ dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
19676
+ dc.removeEventListener('close', onDCClose);
19677
+ abortSignal.removeEventListener('abort', onAbort);
19678
+ };
19679
+ if (abortSignal.aborted) {
19680
+ onAbort();
19681
+ return;
19682
+ }
19683
+ dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
19684
+ // Proxy along any error caused by the data channel closing while we wait.
19685
+ dc.addEventListener('close', onDCClose);
19686
+ // The channel object we're parked on can be abandoned without ever firing another event
19687
+ // (e.g. the engine recreating channels); the abort is our way out.
19688
+ abortSignal.addEventListener('abort', onAbort);
19689
+ });
19690
+ });
19691
+ }
19692
+ /** Rejects all parked headroom waiters; the next waiter gets a fresh controller. */
19693
+ invalidateWaiters(reason) {
19694
+ this.waiterAbortController.abort(reason);
19695
+ this.waiterAbortController = new AbortController();
19696
+ }
19697
+ /**
19698
+ * Recomputes whether the buffer has drained to the low-water mark and, if that changed since the
19699
+ * last check, notifies the status listener. Two independent triggers land here: a send (which
19700
+ * raises the buffer) and the `bufferedamountlow` drain event (which lowers it) — the latter has
19701
+ * no send to hang the work off, which is why this is a shared entry point rather than a tail of
19702
+ * `send`.
19703
+ */
19704
+ refreshBufferStatus() {
19705
+ var _a;
19706
+ const dc = this.getChannel();
19707
+ if (!dc) {
19708
+ return;
19709
+ }
19710
+ const isLow = this.isBelowLowWaterMark(dc);
19711
+ if (isLow !== this.bufferStatusLow) {
19712
+ this.bufferStatusLow = isLow;
19713
+ (_a = this.onBufferStatusChanged) === null || _a === void 0 ? void 0 : _a.call(this, isLow);
19714
+ }
19715
+ }
19716
+ }/**
19717
+ * A lossy channel: flow control plus a per-instance full-buffer policy.
19718
+ *
19719
+ * Each instance owns its own byterate stat, drop counter, and (when tuning is started) the
19720
+ * dynamic `bufferedAmountLowThreshold` adjustment that keeps the drop gate at roughly 100ms of
19721
+ * buffered latency. Keeping these per instance is what prevents one channel's traffic from
19722
+ * steering another channel's policy.
19723
+ */
19724
+ class LossyDataChannel extends FlowControlledDataChannel {
19725
+ constructor(opts) {
19726
+ super(opts);
19727
+ this.statCurrentBytes = 0;
19728
+ this.statByterate = 0;
19729
+ this.dropCount = 0;
19730
+ this.bufferFullBehavior = opts.bufferFullBehavior;
19731
+ this.shouldSkipSends = opts.shouldSkipSends;
19732
+ }
19733
+ /** Sends prepared bytes with this channel's full-buffer policy (drop or wait). */
19734
+ send(msg) {
19735
+ return __awaiter(this, void 0, void 0, function* () {
19736
+ const dc = this.getChannel();
19737
+ if (!dc) {
19738
+ return;
19739
+ }
19740
+ // Depending on the channel's policy, either drop or wait for the buffer to drain below the
19741
+ // high-water mark before continuing.
19742
+ switch (this.bufferFullBehavior) {
19743
+ case 'wait':
19744
+ if (!this.isBelowHighWaterMark(dc)) {
19745
+ yield this.waitForHeadroomWithLock();
19746
+ }
19747
+ break;
19748
+ case 'drop':
19749
+ // We check against the actual threshold on the DC here, as it is tuned dynamically.
19750
+ if (!this.isBelowLowWaterMark(dc)) {
19751
+ // Drop messages to reduce latency
19752
+ this.dropCount += 1;
19753
+ if (this.dropCount % 100 === 0) {
19754
+ livekitLogger.warn("dropping lossy data channel messages, total dropped: ".concat(this.dropCount));
19755
+ }
19756
+ return;
19757
+ }
19758
+ }
19759
+ this.statCurrentBytes += msg.byteLength;
19760
+ if (this.shouldSkipSends()) {
19761
+ return;
19762
+ }
19763
+ try {
19764
+ dc.send(msg);
19765
+ this.refreshBufferStatus();
19766
+ } catch (error) {
19767
+ // Preserve prior surface behaviour: a send that fails because the channel is closing is
19768
+ // logged, not thrown, so lossy/data-track sends don't reject during teardown windows.
19769
+ if (error instanceof TypeError) {
19770
+ livekitLogger.error(error);
19771
+ } else {
19772
+ throw error;
19773
+ }
19774
+ }
19775
+ });
19776
+ }
19777
+ /**
19778
+ * Starts the once-per-second adjustment of the channel's `bufferedAmountLowThreshold` to the
19779
+ * observed byterate, keeping the drop gate at roughly 100ms of buffered latency (clamped to
19780
+ * the watermarks). Restarts cleanly if already running.
19781
+ */
19782
+ startThresholdTuning() {
19783
+ this.stopThresholdTuning();
19784
+ this.statInterval = CriticalTimers.setInterval(() => {
19785
+ this.statByterate = this.statCurrentBytes;
19786
+ this.statCurrentBytes = 0;
19787
+ const dc = this.getChannel();
19788
+ if (dc) {
19789
+ // control buffered latency to ~100ms
19790
+ const threshold = this.statByterate / 10;
19791
+ dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, this.lowWaterMark), this.highWaterMark);
19792
+ }
19793
+ }, 1000);
19794
+ }
19795
+ /** Stops the threshold tuning and resets the stats and drop counter. */
19796
+ stopThresholdTuning() {
19797
+ this.statByterate = 0;
19798
+ this.statCurrentBytes = 0;
19799
+ if (this.statInterval) {
19800
+ CriticalTimers.clearInterval(this.statInterval);
19801
+ this.statInterval = undefined;
19802
+ }
19803
+ this.dropCount = 0;
19804
+ }
19805
+ }class DataPacketBuffer {
19806
+ constructor() {
19807
+ this.buffer = [];
19808
+ this._totalSize = 0;
19809
+ this._sentSize = 0;
19810
+ }
19811
+ push(item) {
19812
+ this.buffer.push(item);
19813
+ this._totalSize += item.data.byteLength;
19814
+ if (item.sent) {
19815
+ this._sentSize += item.data.byteLength;
19816
+ }
19817
+ }
19818
+ pop() {
19819
+ const item = this.buffer.shift();
19820
+ if (item) {
19821
+ this._totalSize -= item.data.byteLength;
19822
+ if (item.sent) {
19823
+ this._sentSize -= item.data.byteLength;
19824
+ }
19825
+ }
19826
+ return item;
19827
+ }
19828
+ getAll() {
19829
+ return this.buffer.slice();
19830
+ }
19831
+ /** Every queued packet not yet handed to the channel, in sequence order. */
19832
+ getUnsent() {
19833
+ return this.buffer.filter(item => !item.sent);
19834
+ }
19835
+ /** Marks a single queued packet as handed to the channel. */
19836
+ markSent(item) {
19837
+ if (!item.sent) {
19838
+ item.sent = true;
19839
+ this._sentSize += item.data.byteLength;
19840
+ }
19841
+ }
19842
+ /**
19843
+ * Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
19844
+ * still buffered was sent on the previous channel (or deferred) and must be re-handed to the
19845
+ * current one, so none of it counts as sent until the replay actually transmits it.
19846
+ */
19847
+ markAllUnsent() {
19848
+ for (const item of this.buffer) {
19849
+ item.sent = false;
19850
+ }
19851
+ this._sentSize = 0;
19852
+ }
19853
+ popToSequence(sequence) {
19854
+ while (this.buffer.length > 0) {
19855
+ const first = this.buffer[0];
19856
+ if (first.sequence <= sequence) {
19857
+ this.pop();
19858
+ } else {
19859
+ break;
19860
+ }
19861
+ }
19862
+ }
19863
+ alignBufferedAmount(bufferedAmount) {
19864
+ while (this.buffer.length > 0) {
19865
+ const first = this.buffer[0];
19866
+ // Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
19867
+ // resume replay — trimming them would silently lose them.
19868
+ if (!first.sent) {
19869
+ break;
19870
+ }
19871
+ if (this._sentSize - first.data.byteLength <= bufferedAmount) {
19872
+ break;
19873
+ }
19874
+ this.pop();
19875
+ }
19876
+ }
19877
+ get length() {
19878
+ return this.buffer.length;
19879
+ }
19880
+ }/**
19881
+ * The reliable channel: flow control plus delivery-across-resume semantics.
19882
+ *
19883
+ * Every packet gets a monotonic sequence (stamped into the protobuf by the caller before
19884
+ * serialization, via {@link nextSequence}) and is retained in a replay buffer until the channel's
19885
+ * `bufferedAmount` confirms it has been handed to the transport. Sends that land in a reconnect
19886
+ * window — or whose headroom wait is torn down transiently — are queued unsent and resolve;
19887
+ * {@link replay} delivers them (plus any unacked packets) after a resume. Only an engine close
19888
+ * rejects, because no replay is coming after that.
19889
+ */
19890
+ class ReliableDataChannel extends FlowControlledDataChannel {
19891
+ constructor(opts) {
19892
+ super(opts);
19893
+ this.messageBuffer = new DataPacketBuffer();
19894
+ this.sequence = 1;
19895
+ this.isDeferringSends = opts.isDeferringSends;
19896
+ }
19897
+ /**
19898
+ * Claims the next packet sequence. The caller stamps it into the packet before serialization,
19899
+ * then passes it back to {@link send} so the replay buffer stays keyed by wire sequence.
19900
+ */
19901
+ nextSequence() {
19902
+ const sequence = this.sequence;
19903
+ this.sequence += 1;
19904
+ return sequence;
19905
+ }
19906
+ /**
19907
+ * Sends prepared bytes with reliable semantics. Resolves once the packet has either been handed
19908
+ * to the channel or queued for the resume replay; throws only when the engine is closed.
19909
+ */
19910
+ send(msg, sequence) {
19911
+ return __awaiter(this, void 0, void 0, function* () {
19912
+ if (this.isDeferringSends()) {
19913
+ // A reconnect is already underway — queue for the resume replay instead of parking on a
19914
+ // channel that is being torn down. The send resolves; delivery is deferred to the replay.
19915
+ this.messageBuffer.push({
19916
+ data: msg,
19917
+ sequence,
19918
+ sent: false
19919
+ });
19920
+ return;
19921
+ }
19922
+ const dc = this.getChannel();
19923
+ if (!dc) {
19924
+ return;
19925
+ }
19926
+ try {
19927
+ yield this.waitForHeadroomWithLock();
19928
+ } catch (error) {
19929
+ if (this.isEngineClosed()) {
19930
+ // No replay is coming after an engine close — surface the failure.
19931
+ throw error;
19932
+ }
19933
+ // Transient teardown (the channel closed or was replaced while we waited): the reliable
19934
+ // channel promises delivery across resume, so queue the packet for the replay instead of
19935
+ // rejecting a send the app can't meaningfully retry.
19936
+ this.messageBuffer.push({
19937
+ data: msg,
19938
+ sequence,
19939
+ sent: false
19940
+ });
19941
+ return;
19942
+ }
19943
+ if (this.isDeferringSends()) {
19944
+ // A reconnect began while we waited for headroom — same deal as above.
19945
+ this.messageBuffer.push({
19946
+ data: msg,
19947
+ sequence,
19948
+ sent: false
19949
+ });
19950
+ return;
19951
+ }
19952
+ this.messageBuffer.push({
19953
+ data: msg,
19954
+ sequence,
19955
+ sent: true
19956
+ });
19957
+ dc.send(msg);
19958
+ this.refreshBufferStatus();
19959
+ });
19960
+ }
19961
+ /**
19962
+ * Replays the buffered backlog after a resume: drops everything the server acked
19963
+ * (`lastMessageSeq`), then re-sends the rest in order. The headroom lock is held across the
19964
+ * whole replay — releasing it between messages would let a concurrent send (whose newer
19965
+ * sequence was already assigned before it queued on the lock) hit the wire mid-replay, and
19966
+ * receivers would then discard the remaining lower-sequence resent messages as duplicates.
19967
+ */
19968
+ replay(lastMessageSeq) {
19969
+ return __awaiter(this, void 0, void 0, function* () {
19970
+ const dc = this.getChannel();
19971
+ if (!dc) {
19972
+ return;
19973
+ }
19974
+ this.messageBuffer.popToSequence(lastMessageSeq);
19975
+ const unlock = yield this.lockHeadroom();
19976
+ try {
19977
+ // Everything left after the ack cutoff must be re-handed to the current channel.
19978
+ this.messageBuffer.markAllUnsent();
19979
+ // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
19980
+ // sent:false) during our own awaits appends after this pass started, so we pick it up on
19981
+ // the next one. Mark each packet only once we've actually handed it to the channel — a
19982
+ // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
19983
+ // and a later alignBufferedAmount would then drop it for good. If the loop throws
19984
+ // mid-drain, unsent entries keep their flag and the next replay picks them up.
19985
+ for (let batch = this.messageBuffer.getUnsent(); batch.length > 0; batch = this.messageBuffer.getUnsent()) {
19986
+ for (const item of batch) {
19987
+ // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
19988
+ yield this.waitForHeadroomWithoutLock();
19989
+ dc.send(item.data);
19990
+ this.messageBuffer.markSent(item);
19991
+ }
19992
+ }
19993
+ } finally {
19994
+ unlock();
19995
+ }
19996
+ this.refreshBufferStatus();
19997
+ });
19998
+ }
19999
+ /**
20000
+ * Before recomputing status, trim packets the transport has now delivered — a send or a drain
20001
+ * may have acked buffered packets, and the replay buffer is keyed off the channel's buffered
20002
+ * bytes.
20003
+ */
20004
+ refreshBufferStatus() {
20005
+ const dc = this.channelHandle;
20006
+ if (dc) {
20007
+ this.messageBuffer.alignBufferedAmount(dc.bufferedAmount);
20008
+ }
20009
+ super.refreshBufferStatus();
20010
+ }
20011
+ /**
20012
+ * Drops all replay state and restarts sequencing. Only valid on a full reconnect, where the
20013
+ * session (and the receivers' sequence tracking) starts over.
20014
+ */
20015
+ reset() {
20016
+ this.messageBuffer = new DataPacketBuffer();
20017
+ this.sequence = 1;
20018
+ }
20019
+ }var DataChannelKind;
20020
+ (function (DataChannelKind) {
20021
+ DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
20022
+ DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
20023
+ DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
20024
+ })(DataChannelKind || (DataChannelKind = {}));
20025
+ // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
20026
+ // freely up to the high-water mark; once it's exceeded they block until the browser's
20027
+ // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
20028
+ // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
20029
+ // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
20030
+ // the channel (see livekit/client-sdk-js#1995).
20031
+ const reliableDataChannelWaterMarkLow = 64 * 1024;
20032
+ const reliableDataChannelWaterMarkHigh = 1024 * 1024;
20033
+ const lossyDataChannelWaterMarkLow = 8 * 1024;
20034
+ const lossyDataChannelWaterMarkHigh = 256 * 1024;
20035
+ function dataChannelLowWaterMark(kind) {
20036
+ return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkLow : lossyDataChannelWaterMarkLow;
20037
+ }
20038
+ function dataChannelHighWaterMark(kind) {
20039
+ return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
20040
+ }const lossyDataChannelLabel = '_lossy';
20041
+ const reliableDataChannelLabel = '_reliable';
20042
+ const dataTrackDataChannelLabel = '_data_track';
20043
+ /**
20044
+ * Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
20045
+ * live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
20046
+ * connections come and go) plus the subscriber-side receive handles adopted by label.
20047
+ *
20048
+ * Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which reject
20049
+ * parked headroom waiters as a built-in — there is no separate invalidation step to forget.
20050
+ */
20051
+ class DataChannelManager {
20052
+ constructor(opts) {
20053
+ this.opts = opts;
20054
+ const flowControlOptions = kind => ({
20055
+ kind,
20056
+ lowWaterMark: dataChannelLowWaterMark(kind),
20057
+ highWaterMark: dataChannelHighWaterMark(kind),
20058
+ isEngineClosed: opts.isEngineClosed,
20059
+ onBufferStatusChanged: isLow => opts.onBufferStatusChanged(kind, isLow)
20060
+ });
20061
+ this.reliable = new ReliableDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.RELIABLE)), {
20062
+ isDeferringSends: opts.isReconnecting
20063
+ }));
20064
+ this.lossy = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.LOSSY)), {
20065
+ // Classic lossy user data: a stale packet is worthless, so drop instead of queueing.
20066
+ bufferFullBehavior: 'drop',
20067
+ shouldSkipSends: opts.isReconnecting
20068
+ }));
20069
+ this.dataTrack = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.DATA_TRACK_LOSSY)), {
20070
+ // Data tracks backpressure the producer instead — it decides what to skip at frame
20071
+ // granularity rather than the engine dropping arbitrary chunks out of frames.
20072
+ bufferFullBehavior: 'wait',
20073
+ shouldSkipSends: opts.isReconnecting
20074
+ }));
20075
+ }
20076
+ /** The flow-control wrapper for `kind`. */
20077
+ channelFor(kind) {
20078
+ switch (kind) {
20079
+ case DataChannelKind.RELIABLE:
20080
+ return this.reliable;
20081
+ case DataChannelKind.LOSSY:
20082
+ return this.lossy;
20083
+ case DataChannelKind.DATA_TRACK_LOSSY:
20084
+ return this.dataTrack;
20085
+ }
20086
+ }
20087
+ /** The raw RTCDataChannel handle for `kind`, publisher side by default. */
20088
+ getHandle(kind) {
20089
+ let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
20090
+ if (!subscriber) {
20091
+ return this.channelFor(kind).channelHandle;
20092
+ }
20093
+ switch (kind) {
20094
+ case DataChannelKind.RELIABLE:
20095
+ return this.reliableSub;
20096
+ case DataChannelKind.LOSSY:
20097
+ return this.lossySub;
20098
+ case DataChannelKind.DATA_TRACK_LOSSY:
20099
+ return this.dataTrackSub;
20100
+ }
20101
+ }
20102
+ get hasPublisherChannels() {
20103
+ return Boolean(this.reliable.channelHandle || this.lossy.channelHandle || this.dataTrack.channelHandle);
20104
+ }
20105
+ /**
20106
+ * Creates the three publisher data channels on the given transport, wires their handlers, and
20107
+ * attaches them to the wrappers — attaching rejects any waiters still parked on replaced
20108
+ * channel objects.
20109
+ */
20110
+ createPublisherChannels(pcManager) {
20111
+ // clear old data channel callbacks if recreate
20112
+ for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
20113
+ const old = channel.channelHandle;
20114
+ if (old) {
20115
+ old.onmessage = null;
20116
+ old.onerror = null;
20117
+ old.onclose = null;
20118
+ }
20119
+ }
20120
+ const wire = (channel, dc, onMessage) => {
20121
+ // also handle messages over the pub channel, for backwards compatibility
20122
+ dc.onmessage = onMessage;
20123
+ // handle datachannel errors
20124
+ dc.onerror = this.opts.onDataError;
20125
+ // detect unexpected publisher data channel closes
20126
+ dc.onclose = () => this.opts.onChannelClose(channel.kind);
20127
+ // set up dc buffer threshold - if this is not set, it will default to 0
20128
+ dc.bufferedAmountLowThreshold = channel.lowWaterMark;
20129
+ // handle buffer amount low events
20130
+ dc.onbufferedamountlow = () => channel.refreshBufferStatus();
20131
+ channel.attach(dc);
20132
+ };
20133
+ wire(this.lossy, pcManager.createPublisherDataChannel(lossyDataChannelLabel, {
20134
+ ordered: false,
20135
+ maxRetransmits: 0
20136
+ }), this.opts.onDataMessage);
20137
+ wire(this.reliable, pcManager.createPublisherDataChannel(reliableDataChannelLabel, {
20138
+ ordered: true
20139
+ }), this.opts.onDataMessage);
20140
+ wire(this.dataTrack, pcManager.createPublisherDataChannel(dataTrackDataChannelLabel, {
20141
+ ordered: false,
20142
+ maxRetransmits: 0
20143
+ }), this.opts.onDataTrackMessage);
20144
+ this.lossy.startThresholdTuning();
20145
+ }
20146
+ /**
20147
+ * Adopts a subscriber-side data channel by label, wiring the matching receive handler.
20148
+ * Returns false for labels this manager doesn't own.
20149
+ */
20150
+ adoptSubscriberChannel(channel) {
20151
+ let handler;
20152
+ if (channel.label === reliableDataChannelLabel) {
20153
+ this.reliableSub = channel;
20154
+ handler = this.opts.onDataMessage;
20155
+ } else if (channel.label === lossyDataChannelLabel) {
20156
+ this.lossySub = channel;
20157
+ handler = this.opts.onDataMessage;
20158
+ } else if (channel.label === dataTrackDataChannelLabel) {
20159
+ this.dataTrackSub = channel;
20160
+ handler = this.opts.onDataTrackMessage;
20161
+ } else {
20162
+ return false;
20163
+ }
20164
+ channel.onmessage = handler;
20165
+ return true;
20166
+ }
20167
+ /**
20168
+ * Tears down all channels for a peer-connection cleanup: rejects parked waiters (detach — the
20169
+ * spec allows `pc.close()` to transition channels to 'closed' without firing events, so waiting
20170
+ * for browser close events is not an option), strips handlers, closes the handles, and resets
20171
+ * the reliable session state.
20172
+ */
20173
+ teardown() {
20174
+ const dcCleanup = dc => {
20175
+ if (!dc) {
20176
+ return;
20177
+ }
20178
+ // Detach the data channel handlers before closing anything. Closing a peer connection tears
20179
+ // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
20180
+ // channels; if our handlers are still attached at that point, handleDataError logs a spurious
20181
+ // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
20182
+ // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
20183
+ // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
20184
+ dc.onbufferedamountlow = null;
20185
+ dc.onclose = null;
20186
+ dc.onclosing = null;
20187
+ dc.onerror = null;
20188
+ dc.onmessage = null;
20189
+ dc.onopen = null;
20190
+ dc.close();
20191
+ };
20192
+ for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
20193
+ const dc = channel.channelHandle;
20194
+ channel.detach('peer connections cleaned up');
20195
+ dcCleanup(dc);
20196
+ }
20197
+ dcCleanup(this.lossySub);
20198
+ dcCleanup(this.reliableSub);
20199
+ dcCleanup(this.dataTrackSub);
20200
+ this.lossySub = undefined;
20201
+ this.reliableSub = undefined;
20202
+ this.dataTrackSub = undefined;
20203
+ // Full teardown starts the session over: drop replay state and restart sequencing.
20204
+ this.reliable.reset();
20205
+ }
19177
20206
  }// Check if MediaRecorder is available
19178
20207
  const isMediaRecorderAvailable = typeof MediaRecorder !== 'undefined';
19179
20208
  // Fallback class for environments without MediaRecorder
@@ -19984,6 +21013,18 @@ class LocalTrack extends Track {
19984
21013
  yield this.restart(constraints);
19985
21014
  });
19986
21015
  }
21016
+ applyConstraints(constraints) {
21017
+ return __awaiter(this, void 0, void 0, function* () {
21018
+ const unlock = yield this.trackChangeLock.lock();
21019
+ try {
21020
+ const res = yield this._mediaStreamTrack.applyConstraints(constraints);
21021
+ this._constraints = Object.assign(Object.assign({}, this._constraints), constraints);
21022
+ return res;
21023
+ } finally {
21024
+ unlock();
21025
+ }
21026
+ });
21027
+ }
19987
21028
  restart(constraints, isUnmuting) {
19988
21029
  const _super = Object.create(null, {
19989
21030
  restart: {
@@ -20420,14 +21461,21 @@ class ScalabilityMode {
20420
21461
  return "L".concat(this.spatial, "T").concat(this.temporal).concat((_a = this.suffix) !== null && _a !== void 0 ? _a : '');
20421
21462
  }
20422
21463
  }
21464
+ /**
21465
+ * Returns the appropriate degradation preference for a video track based on its source.
21466
+ *
21467
+ * - Camera: 'maintain-framerate' (smoother video for real-time communication)
21468
+ * - Screen share: 'maintain-resolution' (clarity is critical for reading text/UI)
21469
+ * - Other/unknown: 'balanced'
21470
+ */
20423
21471
  function getDefaultDegradationPreference(track) {
20424
- // a few of reasons we have different default paths:
20425
- // 1. without this, Chrome seems to aggressively resize the SVC video stating `quality-limitation: bandwidth` even when BW isn't an issue
20426
- // 2. since we are overriding contentHint to motion (to workaround L1T3 publishing), it overrides the default degradationPreference to `balanced`
20427
- if (track.source === Track.Source.ScreenShare || track.constraints.height && unwrapConstraint(track.constraints.height) >= 1080) {
20428
- return 'maintain-resolution';
20429
- } else {
20430
- return 'balanced';
21472
+ switch (track.source) {
21473
+ case Track.Source.Camera:
21474
+ return 'maintain-framerate';
21475
+ case Track.Source.ScreenShare:
21476
+ return 'maintain-resolution';
21477
+ default:
21478
+ return 'balanced';
20431
21479
  }
20432
21480
  }const refreshSubscribedCodecAfterNewCodec = 5000;
20433
21481
  class LocalVideoTrack extends LocalTrack {
@@ -21189,14 +22237,9 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
21189
22237
  ssrc: 0
21190
22238
  });
21191
22239
  });
21192
- }const lossyDataChannel = '_lossy';
21193
- const reliableDataChannel = '_reliable';
21194
- const dataTrackDataChannel = '_data_track';
21195
- const minReconnectWait = 2 * 1000;
22240
+ }const minReconnectWait = 2 * 1000;
21196
22241
  const leaveReconnect = 'leave-reconnect';
21197
22242
  const reliabeReceiveStateTTL = 30000;
21198
- const lossyDataChannelBufferThresholdMin = 8 * 1024;
21199
- const lossyDataChannelBufferThresholdMax = 256 * 1024;
21200
22243
  const initialMediaSectionsAudio = 3;
21201
22244
  const initialMediaSectionsVideo = 3;
21202
22245
  var PCState;
@@ -21207,12 +22250,6 @@ var PCState;
21207
22250
  PCState[PCState["Reconnecting"] = 3] = "Reconnecting";
21208
22251
  PCState[PCState["Closed"] = 4] = "Closed";
21209
22252
  })(PCState || (PCState = {}));
21210
- var DataChannelKind;
21211
- (function (DataChannelKind) {
21212
- DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
21213
- DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
21214
- DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
21215
- })(DataChannelKind || (DataChannelKind = {}));
21216
22253
  // Default data-channel max message size (bytes), used when the remote SDP
21217
22254
  // answer does not advertise an `a=max-message-size` attribute (RFC 8841).
21218
22255
  // `0` means "no limit".
@@ -21228,6 +22265,15 @@ class RTCEngine extends eventsExports.EventEmitter {
21228
22265
  get pendingReconnect() {
21229
22266
  return !!this.reconnectTimeout;
21230
22267
  }
22268
+ get reliableChannel() {
22269
+ return this.dataChannels.reliable;
22270
+ }
22271
+ get lossyChannel() {
22272
+ return this.dataChannels.lossy;
22273
+ }
22274
+ get dataTrackChannel() {
22275
+ return this.dataChannels.dataTrack;
22276
+ }
21231
22277
  constructor(options) {
21232
22278
  var _a;
21233
22279
  super();
@@ -21254,16 +22300,10 @@ class RTCEngine extends eventsExports.EventEmitter {
21254
22300
  this.shouldFailNext = false;
21255
22301
  this.shouldFailOnV1Path = false;
21256
22302
  this.log = livekitLogger;
21257
- this.reliableDataSequence = 1;
21258
- this.reliableMessageBuffer = new DataPacketBuffer();
21259
22303
  this.reliableReceivedState = new TTLMap(reliabeReceiveStateTTL);
21260
- this.lossyDataStatCurrentBytes = 0;
21261
- this.lossyDataStatByterate = 0;
21262
- this.lossyDataDropCount = 0;
21263
22304
  this.midToTrackId = {};
21264
22305
  /** used to indicate whether the browser is currently waiting to reconnect */
21265
22306
  this.isWaitingForNetworkReconnect = false;
21266
- this.bufferStatusLowClosingFuture = new Future();
21267
22307
  this.handleDataChannel = _a => __awaiter(this, [_a], void 0, function (_ref) {
21268
22308
  var _this = this;
21269
22309
  let channel = _ref.channel;
@@ -21271,21 +22311,9 @@ class RTCEngine extends eventsExports.EventEmitter {
21271
22311
  if (!channel) {
21272
22312
  return;
21273
22313
  }
21274
- let handler;
21275
- if (channel.label === reliableDataChannel) {
21276
- _this.reliableDCSub = channel;
21277
- handler = _this.handleDataMessage;
21278
- } else if (channel.label === lossyDataChannel) {
21279
- _this.lossyDCSub = channel;
21280
- handler = _this.handleDataMessage;
21281
- } else if (channel.label === dataTrackDataChannel) {
21282
- _this.dataTrackDCSub = channel;
21283
- handler = _this.handleDataTrackMessage;
21284
- } else {
21285
- return;
22314
+ if (_this.dataChannels.adoptSubscriberChannel(channel)) {
22315
+ _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21286
22316
  }
21287
- _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21288
- channel.onmessage = handler;
21289
22317
  }();
21290
22318
  });
21291
22319
  this.handleDataMessage = message => __awaiter(this, void 0, void 0, function* () {
@@ -21293,19 +22321,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21293
22321
  // make sure to respect incoming data message order by processing message events one after the other
21294
22322
  const unlock = yield this.dataProcessLock.lock();
21295
22323
  try {
21296
- // decode
21297
- let buffer;
21298
- if (message.data instanceof ArrayBuffer) {
21299
- buffer = message.data;
21300
- } else if (message.data instanceof Blob) {
21301
- buffer = yield message.data.arrayBuffer();
21302
- } else {
21303
- this.log.error('unsupported data type', {
21304
- data: message.data
21305
- });
22324
+ const bytes = yield this.decodeDataMessage(message);
22325
+ if (!bytes) {
21306
22326
  return;
21307
22327
  }
21308
- const dp = DataPacket.fromBinary(new Uint8Array(buffer));
22328
+ const dp = DataPacket.fromBinary(bytes);
21309
22329
  if (dp.sequence > 0 && dp.participantSid !== '') {
21310
22330
  const lastSeq = this.reliableReceivedState.get(dp.participantSid);
21311
22331
  if (lastSeq && dp.sequence <= lastSeq) {
@@ -21346,19 +22366,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21346
22366
  }
21347
22367
  });
21348
22368
  this.handleDataTrackMessage = message => __awaiter(this, void 0, void 0, function* () {
21349
- // Decode / normalize into a common format
21350
- let buffer;
21351
- if (message.data instanceof ArrayBuffer) {
21352
- buffer = message.data;
21353
- } else if (message.data instanceof Blob) {
21354
- buffer = yield message.data.arrayBuffer();
21355
- } else {
21356
- this.log.error('unsupported data type', {
21357
- data: message.data
21358
- });
22369
+ const bytes = yield this.decodeDataMessage(message);
22370
+ if (!bytes) {
21359
22371
  return;
21360
22372
  }
21361
- this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
22373
+ this.emit('dataTrackPacketReceived', bytes);
21362
22374
  });
21363
22375
  this.handleDataError = event => {
21364
22376
  // Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as
@@ -21392,9 +22404,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21392
22404
  this.log.error("publisher data channel '".concat(DataChannelKind[kind], "' closed unexpectedly"), this.logContext);
21393
22405
  }
21394
22406
  };
21395
- this.handleBufferedAmountLow = channelKind => {
21396
- this.updateAndEmitDCBufferStatus(channelKind);
21397
- };
21398
22407
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
21399
22408
  // continues to work, we can reconnect to websocket to continue the session
21400
22409
  // after a number of retries, we'll close and give up permanently
@@ -21450,25 +22459,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21450
22459
  this.once(EngineEvent.Disconnected, onDisconnected);
21451
22460
  });
21452
22461
  };
21453
- this.updateAndEmitDCBufferStatus = kind => {
21454
- if (kind === DataChannelKind.RELIABLE) {
21455
- const dc = this.dataChannelForKind(kind);
21456
- if (dc) {
21457
- this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
21458
- }
21459
- }
21460
- const status = this.isBufferStatusLow(kind);
21461
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
21462
- this.dcBufferStatus.set(kind, status);
21463
- this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
21464
- }
21465
- };
21466
- this.isBufferStatusLow = kind => {
21467
- const dc = this.dataChannelForKind(kind);
21468
- if (dc) {
21469
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
21470
- }
21471
- };
21472
22462
  this.onRtpMapAvailable = rtpTypes => {
21473
22463
  const rtpMap = new Map();
21474
22464
  rtpTypes.forEach(rtp => {
@@ -21528,7 +22518,15 @@ class RTCEngine extends eventsExports.EventEmitter {
21528
22518
  this.reconnectPolicy = this.options.reconnectPolicy;
21529
22519
  this.closingLock = new _();
21530
22520
  this.dataProcessLock = new _();
21531
- this.dcBufferStatus = new Map([[DataChannelKind.RELIABLE, true], [DataChannelKind.LOSSY, true], [DataChannelKind.DATA_TRACK_LOSSY, true]]);
22521
+ this.dataChannels = new DataChannelManager({
22522
+ isEngineClosed: () => this.isClosed,
22523
+ isReconnecting: () => this.attemptingReconnect,
22524
+ onDataMessage: message => this.handleDataMessage(message),
22525
+ onDataTrackMessage: message => this.handleDataTrackMessage(message),
22526
+ onDataError: event => this.handleDataError(event),
22527
+ onChannelClose: kind => this.handleDataChannelClose(kind)(),
22528
+ onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
22529
+ });
21532
22530
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21533
22531
  this.client.onConnectionQuality = update => this.emit(EngineEvent.ConnectionQualityUpdate, update);
21534
22532
  this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
@@ -21539,13 +22537,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21539
22537
  this.client.onRequestResponse = response => this.emit(EngineEvent.SignalRequestResponse, response);
21540
22538
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21541
22539
  this.client.onJoined = joinResponse => this.emit(EngineEvent.Joined, joinResponse);
21542
- this.on(EngineEvent.Closing, () => {
21543
- var _a, _b;
21544
- (_b = (_a = this.bufferStatusLowClosingFuture).reject) === null || _b === void 0 ? void 0 : _b.call(_a, new UnexpectedConnectionState('engine closed'));
21545
- });
21546
- // Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
21547
- // when no waitForBufferStatusLow callers are attached.
21548
- this.bufferStatusLowClosingFuture.promise.catch(() => {});
21549
22540
  }
21550
22541
  /** @internal */
21551
22542
  get logContext() {
@@ -21691,51 +22682,14 @@ class RTCEngine extends eventsExports.EventEmitter {
21691
22682
  cleanupPeerConnections() {
21692
22683
  return __awaiter(this, void 0, void 0, function* () {
21693
22684
  var _a;
21694
- const dcCleanup = dc => {
21695
- if (!dc) {
21696
- return;
21697
- }
21698
- // Detach the data channel handlers before closing anything. Closing a peer connection tears
21699
- // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
21700
- // channels; if our handlers are still attached at that point, handleDataError logs a spurious
21701
- // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
21702
- // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
21703
- // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
21704
- dc.onbufferedamountlow = null;
21705
- dc.onclose = null;
21706
- dc.onclosing = null;
21707
- dc.onerror = null;
21708
- dc.onmessage = null;
21709
- dc.onopen = null;
21710
- dc.close();
21711
- };
21712
- dcCleanup(this.lossyDC);
21713
- dcCleanup(this.lossyDCSub);
21714
- dcCleanup(this.reliableDC);
21715
- dcCleanup(this.reliableDCSub);
21716
- dcCleanup(this.dataTrackDC);
21717
- dcCleanup(this.dataTrackDCSub);
22685
+ this.dataChannels.teardown();
21718
22686
  yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
21719
22687
  this.pcManager = undefined;
21720
- this.lossyDC = undefined;
21721
- this.lossyDCSub = undefined;
21722
- this.reliableDC = undefined;
21723
- this.reliableDCSub = undefined;
21724
- this.dataTrackDC = undefined;
21725
- this.dataTrackDCSub = undefined;
21726
- this.reliableMessageBuffer = new DataPacketBuffer();
21727
- this.reliableDataSequence = 1;
21728
22688
  this.reliableReceivedState.clear();
21729
22689
  });
21730
22690
  }
21731
22691
  cleanupLossyDataStats() {
21732
- this.lossyDataStatByterate = 0;
21733
- this.lossyDataStatCurrentBytes = 0;
21734
- if (this.lossyDataStatInterval) {
21735
- clearInterval(this.lossyDataStatInterval);
21736
- this.lossyDataStatInterval = undefined;
21737
- }
21738
- this.lossyDataDropCount = 0;
22692
+ this.lossyChannel.stopThresholdTuning();
21739
22693
  }
21740
22694
  cleanupClient() {
21741
22695
  return __awaiter(this, void 0, void 0, function* () {
@@ -21802,7 +22756,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21802
22756
  }
21803
22757
  get dataSubscriberReadyState() {
21804
22758
  var _a;
21805
- return (_a = this.reliableDCSub) === null || _a === void 0 ? void 0 : _a.readyState;
22759
+ return (_a = this.dataChannelForKind(DataChannelKind.RELIABLE, true)) === null || _a === void 0 ? void 0 : _a.readyState;
21806
22760
  }
21807
22761
  getConnectedServerAddress() {
21808
22762
  return __awaiter(this, void 0, void 0, function* () {
@@ -22062,65 +23016,22 @@ class RTCEngine extends eventsExports.EventEmitter {
22062
23016
  if (!this.pcManager) {
22063
23017
  return;
22064
23018
  }
22065
- // clear old data channel callbacks if recreate
22066
- if (this.lossyDC) {
22067
- this.lossyDC.onmessage = null;
22068
- this.lossyDC.onerror = null;
22069
- this.lossyDC.onclose = null;
22070
- }
22071
- if (this.reliableDC) {
22072
- this.reliableDC.onmessage = null;
22073
- this.reliableDC.onerror = null;
22074
- this.reliableDC.onclose = null;
22075
- }
22076
- if (this.dataTrackDC) {
22077
- this.dataTrackDC.onmessage = null;
22078
- this.dataTrackDC.onerror = null;
22079
- this.dataTrackDC.onclose = null;
22080
- }
22081
- // create data channels
22082
- this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
22083
- ordered: false,
22084
- maxRetransmits: 0
22085
- });
22086
- this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
22087
- ordered: true
22088
- });
22089
- this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
22090
- ordered: false,
22091
- maxRetransmits: 0
22092
- });
22093
- // also handle messages over the pub channel, for backwards compatibility
22094
- this.lossyDC.onmessage = this.handleDataMessage;
22095
- this.reliableDC.onmessage = this.handleDataMessage;
22096
- this.dataTrackDC.onmessage = this.handleDataTrackMessage;
22097
- // handle datachannel errors
22098
- this.lossyDC.onerror = this.handleDataError;
22099
- this.reliableDC.onerror = this.handleDataError;
22100
- this.dataTrackDC.onerror = this.handleDataError;
22101
- // detect unexpected publisher data channel closes
22102
- this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
22103
- this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
22104
- this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
22105
- // set up dc buffer threshold, set to 64kB (otherwise 0 by default)
22106
- this.lossyDC.bufferedAmountLowThreshold = 65535;
22107
- this.reliableDC.bufferedAmountLowThreshold = 65535;
22108
- this.dataTrackDC.bufferedAmountLowThreshold = 65535;
22109
- // handle buffer amount low events
22110
- this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
22111
- this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
22112
- this.dataTrackDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
22113
- this.cleanupLossyDataStats();
22114
- this.lossyDataStatInterval = setInterval(() => {
22115
- this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
22116
- this.lossyDataStatCurrentBytes = 0;
22117
- const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
22118
- if (dc) {
22119
- // control buffered latency to ~100ms
22120
- const threshold = this.lossyDataStatByterate / 10;
22121
- dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelBufferThresholdMin), lossyDataChannelBufferThresholdMax);
23019
+ this.dataChannels.createPublisherChannels(this.pcManager);
23020
+ }
23021
+ /** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
23022
+ decodeDataMessage(message) {
23023
+ return __awaiter(this, void 0, void 0, function* () {
23024
+ if (message.data instanceof ArrayBuffer) {
23025
+ return new Uint8Array(message.data);
22122
23026
  }
22123
- }, 1000);
23027
+ if (message.data instanceof Blob) {
23028
+ return new Uint8Array(yield message.data.arrayBuffer());
23029
+ }
23030
+ this.log.error('unsupported data type', {
23031
+ data: message.data
23032
+ });
23033
+ return undefined;
23034
+ });
22124
23035
  }
22125
23036
  createSender(track, opts, encodings) {
22126
23037
  return __awaiter(this, void 0, void 0, function* () {
@@ -22372,7 +23283,6 @@ class RTCEngine extends eventsExports.EventEmitter {
22372
23283
  }
22373
23284
  resumeConnection(reason) {
22374
23285
  return __awaiter(this, void 0, void 0, function* () {
22375
- var _a;
22376
23286
  if (!this.url || !this.token) {
22377
23287
  // permanent failure, don't attempt reconnection
22378
23288
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -22426,11 +23336,16 @@ class RTCEngine extends eventsExports.EventEmitter {
22426
23336
  this.client.setReconnected();
22427
23337
  // recreate publish datachannel if it's id is null
22428
23338
  // (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
22429
- if (((_a = this.reliableDC) === null || _a === void 0 ? void 0 : _a.readyState) === 'open' && this.reliableDC.id === null) {
23339
+ const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
23340
+ if ((reliableDC === null || reliableDC === void 0 ? void 0 : reliableDC.readyState) === 'open' && reliableDC.id === null) {
22430
23341
  this.createDataChannels();
22431
23342
  }
22432
23343
  if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
22433
- this.resendReliableMessagesForResume(res.lastMessageSeq);
23344
+ this.resendReliableMessagesForResume(res.lastMessageSeq).catch(error => {
23345
+ this.log.warn('failed to resend reliable messages after resume', Object.assign(Object.assign({}, this.logContext), {
23346
+ error
23347
+ }));
23348
+ });
22434
23349
  }
22435
23350
  // resume success
22436
23351
  this.emit(EngineEvent.Resumed);
@@ -22479,7 +23394,8 @@ class RTCEngine extends eventsExports.EventEmitter {
22479
23394
  });
22480
23395
  }
22481
23396
  /* @internal */
22482
- sendDataPacket(packet, kind) {
23397
+ sendDataPacket(packet, /** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
23398
+ kind) {
22483
23399
  return __awaiter(this, void 0, void 0, function* () {
22484
23400
  var _a, _b;
22485
23401
  // make sure we do have a data connection
@@ -22499,8 +23415,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22499
23415
  }
22500
23416
  }
22501
23417
  if (kind === DataChannelKind.RELIABLE) {
22502
- packet.sequence = this.reliableDataSequence;
22503
- this.reliableDataSequence += 1;
23418
+ packet.sequence = this.reliableChannel.nextSequence();
22504
23419
  }
22505
23420
  const msg = packet.toBinary();
22506
23421
  // Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
@@ -22510,98 +23425,48 @@ class RTCEngine extends eventsExports.EventEmitter {
22510
23425
  if (typeof maxPublisherMessageSizeBytes !== 'undefined' && maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ && msg.byteLength > maxPublisherMessageSizeBytes) {
22511
23426
  throw new PublishDataError("cannot publish data packet larger than ".concat(maxPublisherMessageSizeBytes, " bytes (got ").concat(msg.byteLength, ")"));
22512
23427
  }
22513
- switch (kind) {
22514
- case DataChannelKind.LOSSY:
22515
- case DataChannelKind.DATA_TRACK_LOSSY:
22516
- return this.sendLossyBytes(msg, kind);
22517
- case DataChannelKind.RELIABLE:
22518
- const dc = this.dataChannelForKind(kind);
22519
- if (dc) {
22520
- yield this.waitForBufferStatusLow(kind);
22521
- this.reliableMessageBuffer.push({
22522
- data: msg,
22523
- sequence: packet.sequence
22524
- });
22525
- if (this.attemptingReconnect) {
22526
- return;
22527
- }
22528
- dc.send(msg);
22529
- }
22530
- this.updateAndEmitDCBufferStatus(kind);
22531
- break;
23428
+ // The full-buffer policy (drop for lossy, wait/replay for reliable) is the channel's own, as
23429
+ // is emitting the buffer-status change once the send settles.
23430
+ if (kind === DataChannelKind.RELIABLE) {
23431
+ yield this.reliableChannel.send(msg, packet.sequence);
23432
+ } else {
23433
+ yield this.lossyChannel.send(msg);
22532
23434
  }
22533
23435
  });
22534
23436
  }
22535
- /* @internal */
22536
- sendLossyBytes(bytes_1, kind_1) {
22537
- return __awaiter(this, arguments, void 0, function (bytes, kind) {
22538
- var _this3 = this;
22539
- let bufferStatusLowBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22540
- return function* () {
22541
- // make sure we do have a data connection
22542
- yield _this3.ensurePublisherConnected(kind);
22543
- const dc = _this3.dataChannelForKind(kind);
22544
- if (dc) {
22545
- if (!_this3.isBufferStatusLow(kind)) {
22546
- // Depending on the exact circumstance that data is being sent, either drop or wait for the
22547
- // buffer status to not be low before continuing.
22548
- switch (bufferStatusLowBehavior) {
22549
- case 'wait':
22550
- yield _this3.waitForBufferStatusLow(kind);
22551
- break;
22552
- case 'drop':
22553
- // this.log.warn(`dropping lossy data channel message`, this.logContext);
22554
- // Drop messages to reduce latency
22555
- _this3.lossyDataDropCount += 1;
22556
- if (_this3.lossyDataDropCount % 100 === 0) {
22557
- _this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
22558
- }
22559
- return;
22560
- }
22561
- }
22562
- _this3.lossyDataStatCurrentBytes += bytes.byteLength;
22563
- if (_this3.attemptingReconnect) {
22564
- return;
22565
- }
22566
- dc.send(bytes);
22567
- }
22568
- _this3.updateAndEmitDCBufferStatus(kind);
22569
- }();
23437
+ /**
23438
+ * Sends pre-serialized bytes on the data-track channel. This is the one send path that doesn't
23439
+ * go through {@link sendDataPacket} Room's `packetAvailable` handler calls it directly with
23440
+ * bytes the data-track pipeline already serialized.
23441
+ *
23442
+ * @internal
23443
+ */
23444
+ sendDataTrackFrame(bytes) {
23445
+ return __awaiter(this, void 0, void 0, function* () {
23446
+ // Make sure we do have a data connection: on lazily negotiated publisher connections this is
23447
+ // what kicks negotiation off, and it waits for the channel to open. Memoized, so the
23448
+ // steady-state cost is one await on an already-resolved promise.
23449
+ yield this.ensurePublisherConnected(DataChannelKind.DATA_TRACK_LOSSY);
23450
+ yield this.dataTrackChannel.send(bytes);
22570
23451
  });
22571
23452
  }
22572
23453
  resendReliableMessagesForResume(lastMessageSeq) {
22573
23454
  return __awaiter(this, void 0, void 0, function* () {
22574
23455
  yield this.ensurePublisherConnected(DataChannelKind.RELIABLE);
22575
- const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
22576
- if (dc) {
22577
- this.reliableMessageBuffer.popToSequence(lastMessageSeq);
22578
- this.reliableMessageBuffer.getAll().forEach(msg => {
22579
- dc.send(msg.data);
22580
- });
22581
- }
22582
- this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
23456
+ yield this.reliableChannel.replay(lastMessageSeq);
22583
23457
  });
22584
23458
  }
22585
- waitForBufferStatusLow(kind) {
23459
+ /** The flow-control gate for `kind` — see {@link FlowControlledDataChannel}. */
23460
+ flowControlFor(kind) {
23461
+ return this.dataChannels.channelFor(kind);
23462
+ }
23463
+ /**
23464
+ * Resolves once the caller may send on the `kind` channel — see
23465
+ * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
23466
+ */
23467
+ waitForBufferHeadroom(kind) {
22586
23468
  return __awaiter(this, void 0, void 0, function* () {
22587
- return new TypedPromise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
22588
- if (this.isClosed) {
22589
- reject(new UnexpectedConnectionState('engine closed'));
22590
- }
22591
- if (this.isBufferStatusLow(kind)) {
22592
- resolve();
22593
- } else {
22594
- const dc = this.dataChannelForKind(kind);
22595
- if (!dc) {
22596
- reject(new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind)));
22597
- return;
22598
- }
22599
- this.bufferStatusLowClosingFuture.promise.catch(e => reject(e));
22600
- dc.addEventListener('bufferedamountlow', () => resolve(), {
22601
- once: true
22602
- });
22603
- }
22604
- }));
23469
+ return this.flowControlFor(kind).waitForHeadroomWithLock();
22605
23470
  });
22606
23471
  }
22607
23472
  /**
@@ -22609,40 +23474,40 @@ class RTCEngine extends eventsExports.EventEmitter {
22609
23474
  */
22610
23475
  ensureDataTransportConnected(kind_1) {
22611
23476
  return __awaiter(this, arguments, void 0, function (kind) {
22612
- var _this4 = this;
23477
+ var _this3 = this;
22613
23478
  let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.subscriberPrimary;
22614
23479
  return function* () {
22615
23480
  var _a;
22616
- if (!_this4.pcManager) {
23481
+ if (!_this3.pcManager) {
22617
23482
  throw new UnexpectedConnectionState('PC manager is closed');
22618
23483
  }
22619
- const transport = subscriber ? _this4.pcManager.subscriber : _this4.pcManager.publisher;
23484
+ const transport = subscriber ? _this3.pcManager.subscriber : _this3.pcManager.publisher;
22620
23485
  const transportName = subscriber ? 'Subscriber' : 'Publisher';
22621
23486
  if (!transport) {
22622
23487
  throw ConnectionError.internal("".concat(transportName, " connection not set"));
22623
23488
  }
22624
23489
  let needNegotiation = false;
22625
- if (!subscriber && !_this4.dataChannelForKind(kind, subscriber)) {
22626
- _this4.createDataChannels();
23490
+ if (!subscriber && !_this3.dataChannelForKind(kind, subscriber)) {
23491
+ _this3.createDataChannels();
22627
23492
  needNegotiation = true;
22628
23493
  }
22629
- if (!needNegotiation && !subscriber && !_this4.pcManager.publisher.isICEConnected && _this4.pcManager.publisher.getICEConnectionState() !== 'checking') {
23494
+ if (!needNegotiation && !subscriber && !_this3.pcManager.publisher.isICEConnected && _this3.pcManager.publisher.getICEConnectionState() !== 'checking') {
22630
23495
  needNegotiation = true;
22631
23496
  }
22632
23497
  if (needNegotiation) {
22633
23498
  // start negotiation
22634
- _this4.negotiate().catch(err => {
22635
- _this4.log.error(err);
23499
+ _this3.negotiate().catch(err => {
23500
+ _this3.log.error(err);
22636
23501
  });
22637
23502
  }
22638
- const targetChannel = _this4.dataChannelForKind(kind, subscriber);
23503
+ const targetChannel = _this3.dataChannelForKind(kind, subscriber);
22639
23504
  if ((targetChannel === null || targetChannel === void 0 ? void 0 : targetChannel.readyState) === 'open') {
22640
23505
  return;
22641
23506
  }
22642
23507
  // wait until ICE connected
22643
- const endTime = new Date().getTime() + _this4.peerConnectionTimeout;
23508
+ const endTime = new Date().getTime() + _this3.peerConnectionTimeout;
22644
23509
  while (new Date().getTime() < endTime) {
22645
- if (transport.isICEConnected && ((_a = _this4.dataChannelForKind(kind, subscriber)) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
23510
+ if (transport.isICEConnected && ((_a = _this3.dataChannelForKind(kind, subscriber)) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
22646
23511
  return;
22647
23512
  }
22648
23513
  yield sleep(50);
@@ -22685,7 +23550,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22685
23550
  }
22686
23551
  this.pcManager.requirePublisher();
22687
23552
  // don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
22688
- if (this.pcManager.publisher.getTransceivers().length == 0 && !this.lossyDC && !this.reliableDC && !this.dataTrackDC) {
23553
+ if (this.pcManager.publisher.getTransceivers().length == 0 && !this.dataChannels.hasPublisherChannels) {
22689
23554
  this.createDataChannels();
22690
23555
  }
22691
23556
  const abortController = new AbortController();
@@ -22729,26 +23594,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22729
23594
  });
22730
23595
  }
22731
23596
  dataChannelForKind(kind, sub) {
22732
- switch (kind) {
22733
- case DataChannelKind.RELIABLE:
22734
- if (!sub) {
22735
- return this.reliableDC;
22736
- } else {
22737
- return this.reliableDCSub;
22738
- }
22739
- case DataChannelKind.LOSSY:
22740
- if (!sub) {
22741
- return this.lossyDC;
22742
- } else {
22743
- return this.lossyDCSub;
22744
- }
22745
- case DataChannelKind.DATA_TRACK_LOSSY:
22746
- if (!sub) {
22747
- return this.dataTrackDC;
22748
- } else {
22749
- return this.dataTrackDCSub;
22750
- }
22751
- }
23597
+ return this.dataChannels.getHandle(kind, sub);
22752
23598
  }
22753
23599
  /** @internal */
22754
23600
  sendSyncState(remoteTracks, localTracks, localDataTrackInfos) {
@@ -22883,7 +23729,8 @@ function applyUserDataCompat(newObj, oldObj) {
22883
23729
  const destinationIdentities = newObj.destinationIdentities.length !== 0 ? newObj.destinationIdentities : oldObj.destinationIdentities;
22884
23730
  newObj.destinationIdentities = destinationIdentities;
22885
23731
  oldObj.destinationIdentities = destinationIdentities;
22886
- }const DEFAULT_MAX_AGE_MS = 5000;
23732
+ }const log$4 = getLogger(LoggerNames.Region);
23733
+ const DEFAULT_MAX_AGE_MS = 5000;
22887
23734
  const STOP_REFETCH_DELAY_MS = 30000;
22888
23735
  class RegionUrlProvider {
22889
23736
  static fetchRegionSettings(serverUrl, token, signal) {
@@ -22937,10 +23784,10 @@ class RegionUrlProvider {
22937
23784
  RegionUrlProvider.updateCachedRegionSettings(url, token, newSettings);
22938
23785
  } catch (error) {
22939
23786
  if (error instanceof ConnectionError && error.reason === ConnectionErrorReason.NotAllowed) {
22940
- livekitLogger.debug('token is not valid, cancelling auto region refresh');
23787
+ log$4.debug('token is not valid, cancelling auto region refresh');
22941
23788
  return;
22942
23789
  }
22943
- livekitLogger.debug('auto refetching of region settings failed', {
23790
+ log$4.debug('auto refetching of region settings failed', {
22944
23791
  error
22945
23792
  });
22946
23793
  // continue retrying with the same max age
@@ -22973,7 +23820,7 @@ class RegionUrlProvider {
22973
23820
  tracker.cleanupTimeout = setTimeout(() => {
22974
23821
  const currentTracker = RegionUrlProvider.connectionTrackers.get(hostname);
22975
23822
  if (currentTracker && currentTracker.connectionCount === 0) {
22976
- livekitLogger.debug('stopping region refetch after disconnect delay', {
23823
+ log$4.debug('stopping region refetch after disconnect delay', {
22977
23824
  hostname
22978
23825
  });
22979
23826
  RegionUrlProvider.stopRefetch(hostname);
@@ -23053,7 +23900,9 @@ class RegionUrlProvider {
23053
23900
  if (regionsLeft.length > 0) {
23054
23901
  const nextRegion = regionsLeft[0];
23055
23902
  this.attemptedRegions.push(nextRegion);
23056
- livekitLogger.debug("next region: ".concat(nextRegion.region));
23903
+ log$4.info("switching to region: ".concat(nextRegion.region), {
23904
+ region: nextRegion.region
23905
+ });
23057
23906
  return nextRegion.url;
23058
23907
  } else {
23059
23908
  return null;
@@ -23073,7 +23922,90 @@ RegionUrlProvider.connectionTrackers = new Map();
23073
23922
  RegionUrlProvider.fetchLock = new _();
23074
23923
  function getCloudConfigUrl(serverUrl) {
23075
23924
  return "".concat(serverUrl.protocol.replace('ws', 'http'), "//").concat(serverUrl.host, "/settings");
23076
- }class BaseStreamReader {
23925
+ }/**
23926
+ * A `deflate-raw` compression transform (inverse of {@link inflateRawTransform}): pipe a byte stream
23927
+ * through it to get the compressed bytes without buffering the whole payload. Used for the chunked
23928
+ * `sendText`/`sendBytes`/`sendFile` paths, where the full payload is known up front but is streamed
23929
+ * (e.g. from `file.stream()`) rather than held in memory.
23930
+ */
23931
+ function deflateRawTransform() {
23932
+ return new CompressionStream('deflate-raw');
23933
+ }
23934
+ /**
23935
+ * A `deflate-raw` decompression transform (inverse of {@link deflateRawTransform}): pipe a
23936
+ * stream of compressed bytes through it to get the decompressed bytes. Inflate emits output greedily,
23937
+ * so as long as the sender flushed at write boundaries each write's content is produced as soon as
23938
+ * its compressed bytes arrive.
23939
+ */
23940
+ function inflateRawTransform() {
23941
+ return new DecompressionStream('deflate-raw');
23942
+ }
23943
+ /**
23944
+ * Decompresses a raw-deflate byte array in full (inverse of {@link deflateRawCompress}).
23945
+ * `maxByteLength` bounds the decompressed output (decompression-bomb guard); exceeding it rejects
23946
+ * with a `PayloadTooLarge` error.
23947
+ */
23948
+ function deflateRawDecompress(data, maxByteLength) {
23949
+ return __awaiter(this, void 0, void 0, function* () {
23950
+ const ds = new DecompressionStream('deflate-raw');
23951
+ const writer = ds.writable.getWriter();
23952
+ // The writer promises are intentionally not awaited (output is consumed via `collect`), but
23953
+ // they reject when the byte cap cancels the readable mid-stream — swallow that so an enforced
23954
+ // cap doesn't surface as an unhandled rejection.
23955
+ writer.write(data).catch(() => {});
23956
+ writer.close().catch(() => {});
23957
+ return collect(ds.readable, maxByteLength);
23958
+ });
23959
+ }
23960
+ /**
23961
+ * Drains a byte stream, concatenating all of its chunks into a single array. When
23962
+ * `maxByteLength` is given, drops the stream and throws `PayloadTooLarge` as soon as the
23963
+ * accumulated output exceeds it.
23964
+ */
23965
+ function collect(stream, maxByteLength) {
23966
+ return __awaiter(this, void 0, void 0, function* () {
23967
+ const reader = stream.getReader();
23968
+ const chunks = [];
23969
+ let total = 0;
23970
+ while (true) {
23971
+ const _yield$reader$read = yield reader.read(),
23972
+ done = _yield$reader$read.done,
23973
+ value = _yield$reader$read.value;
23974
+ if (done) {
23975
+ break;
23976
+ }
23977
+ chunks.push(value);
23978
+ total += value.byteLength;
23979
+ if (typeof maxByteLength === 'number' && total > maxByteLength) {
23980
+ yield reader.cancel();
23981
+ throw new DataStreamError("Decompressed payload exceeds the maximum payload size of ".concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
23982
+ }
23983
+ }
23984
+ const result = new Uint8Array(total);
23985
+ let offset = 0;
23986
+ for (const chunk of chunks) {
23987
+ result.set(chunk, offset);
23988
+ offset += chunk.byteLength;
23989
+ }
23990
+ return result;
23991
+ });
23992
+ }/**
23993
+ * Maximum size of a single data-stream chunk in bytes, and the budget used to decide whether a
23994
+ * payload can be sent inline as a single header packet. Kept below the ~16k data-channel MTU to
23995
+ * leave headroom for protocol framing and E2EE overhead.
23996
+ *
23997
+ * @internal
23998
+ */
23999
+ const STREAM_CHUNK_SIZE_BYTES = 15000;
24000
+ /**
24001
+ * Default cap on the number of decompressed bytes a single incoming compressed data stream may
24002
+ * produce (5 GB). A tiny compressed payload can inflate to an arbitrarily large output
24003
+ * (decompression bomb), so the decompressor's output is bounded rather than trusting the wire
24004
+ * size; streams exceeding the cap error with `PayloadTooLarge`.
24005
+ *
24006
+ * @internal
24007
+ */
24008
+ const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5000000000;class BaseStreamReader {
23077
24009
  get info() {
23078
24010
  return this._info;
23079
24011
  }
@@ -23115,6 +24047,7 @@ class ByteStreamReader extends BaseStreamReader {
23115
24047
  };
23116
24048
  return {
23117
24049
  next: () => __awaiter(this, void 0, void 0, function* () {
24050
+ var _a;
23118
24051
  try {
23119
24052
  const signal = this.signal;
23120
24053
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -23135,6 +24068,9 @@ class ByteStreamReader extends BaseStreamReader {
23135
24068
  });
23136
24069
  if (result.done) {
23137
24070
  this.validateBytesReceived(true);
24071
+ if (typeof this.totalByteSize === 'number') {
24072
+ (_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
24073
+ }
23138
24074
  return {
23139
24075
  done: true,
23140
24076
  value: undefined
@@ -23252,6 +24188,7 @@ class TextStreamReader extends BaseStreamReader {
23252
24188
  };
23253
24189
  return {
23254
24190
  next: () => __awaiter(this, void 0, void 0, function* () {
24191
+ var _a;
23255
24192
  try {
23256
24193
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
23257
24194
  throw signal.reason;
@@ -23271,6 +24208,9 @@ class TextStreamReader extends BaseStreamReader {
23271
24208
  });
23272
24209
  if (result.done) {
23273
24210
  this.validateBytesReceived(true);
24211
+ if (typeof this.totalByteSize === 'number') {
24212
+ (_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
24213
+ }
23274
24214
  return {
23275
24215
  done: true,
23276
24216
  value: undefined
@@ -23349,6 +24289,7 @@ class TextStreamReader extends BaseStreamReader {
23349
24289
  }
23350
24290
  }class IncomingDataStreamManager {
23351
24291
  constructor() {
24292
+ let maxPayloadByteLength = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_MAX_PAYLOAD_BYTE_LENGTH;
23352
24293
  this.log = livekitLogger;
23353
24294
  this.byteStreamControllers = new Map();
23354
24295
  this.textStreamControllers = new Map();
@@ -23356,6 +24297,7 @@ class TextStreamReader extends BaseStreamReader {
23356
24297
  this.textStreamHandlers = new Map();
23357
24298
  this.isConnected = false;
23358
24299
  this.bufferedPackets = [];
24300
+ this.maxPayloadByteLength = maxPayloadByteLength;
23359
24301
  }
23360
24302
  setConnected(connected) {
23361
24303
  this.isConnected = connected;
@@ -23438,74 +24380,150 @@ class TextStreamReader extends BaseStreamReader {
23438
24380
  }
23439
24381
  handleStreamHeader(streamHeader, participantIdentity, encryptionType) {
23440
24382
  var _a;
23441
- if (streamHeader.contentHeader.case === 'byteHeader') {
23442
- const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
23443
- if (!streamHandlerCallback) {
23444
- this.log.debug('ignoring incoming byte stream due to no handler for topic', streamHeader.topic);
23445
- return;
23446
- }
23447
- let streamController;
23448
- const info = {
23449
- id: streamHeader.streamId,
23450
- name: (_a = streamHeader.contentHeader.value.name) !== null && _a !== void 0 ? _a : 'unknown',
23451
- mimeType: streamHeader.mimeType,
23452
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
23453
- topic: streamHeader.topic,
23454
- timestamp: bigIntToNumber(streamHeader.timestamp),
23455
- attributes: streamHeader.attributes,
23456
- encryptionType
23457
- };
23458
- const stream = new ReadableStream({
23459
- start: controller => {
23460
- streamController = controller;
23461
- if (this.textStreamControllers.has(streamHeader.streamId)) {
23462
- throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24383
+ switch (streamHeader.contentHeader.case) {
24384
+ case 'byteHeader':
24385
+ {
24386
+ const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
24387
+ if (!streamHandlerCallback) {
24388
+ this.log.debug('ignoring incoming byte stream due to no handler for topic', streamHeader.topic);
24389
+ return;
24390
+ }
24391
+ let streamController;
24392
+ const info = {
24393
+ id: streamHeader.streamId,
24394
+ name: (_a = streamHeader.contentHeader.value.name) !== null && _a !== void 0 ? _a : 'unknown',
24395
+ mimeType: streamHeader.mimeType,
24396
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
24397
+ topic: streamHeader.topic,
24398
+ timestamp: bigIntToNumber(streamHeader.timestamp),
24399
+ attributes: streamHeader.attributes,
24400
+ encryptionType
24401
+ };
24402
+ // Determine if the byte payload needs to be decompressed.
24403
+ let compressed;
24404
+ switch (streamHeader.compression) {
24405
+ case DataStream_CompressionType.DEFLATE_RAW:
24406
+ if (!isCompressionStreamSupported()) {
24407
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24408
+ // isn't properly abiding by the data streams v2 protocol.
24409
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
24410
+ return;
24411
+ }
24412
+ compressed = true;
24413
+ break;
24414
+ case DataStream_CompressionType.NONE:
24415
+ compressed = false;
24416
+ break;
24417
+ default:
24418
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24419
+ // isn't properly abiding by the data streams v2 protocol.
24420
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
24421
+ return;
23463
24422
  }
23464
- this.byteStreamControllers.set(streamHeader.streamId, {
23465
- info,
23466
- controller: streamController,
23467
- startTime: Date.now(),
23468
- sendingParticipantIdentity: participantIdentity
24423
+ // Single-packet stream: the entire payload was packaged into the header's `inlineContent`.
24424
+ // Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
24425
+ const inlineContent = streamHeader.inlineContent;
24426
+ if (typeof inlineContent !== 'undefined') {
24427
+ // Inline bytes are the raw payload, optionally deflate-raw compressed.
24428
+ streamHandlerCallback(new ByteStreamReader(info, createInlineStream(streamHeader.streamId, compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent), bigIntToNumber(streamHeader.totalLength)), {
24429
+ identity: participantIdentity
24430
+ });
24431
+ return;
24432
+ }
24433
+ const stream = new ReadableStream({
24434
+ start: controller => {
24435
+ streamController = controller;
24436
+ if (this.byteStreamControllers.has(streamHeader.streamId)) {
24437
+ throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24438
+ }
24439
+ this.byteStreamControllers.set(streamHeader.streamId, {
24440
+ info,
24441
+ controller: streamController,
24442
+ startTime: Date.now(),
24443
+ sendingParticipantIdentity: participantIdentity
24444
+ });
24445
+ }
23469
24446
  });
24447
+ streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
24448
+ // `totalLength` is the pre-compression size, and the reader counts decompressed bytes,
24449
+ // so it applies to both paths (mirrors text).
24450
+ bigIntToNumber(streamHeader.totalLength)), {
24451
+ identity: participantIdentity
24452
+ });
24453
+ return;
23470
24454
  }
23471
- });
23472
- streamHandlerCallback(new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
23473
- identity: participantIdentity
23474
- });
23475
- } else if (streamHeader.contentHeader.case === 'textHeader') {
23476
- const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
23477
- if (!streamHandlerCallback) {
23478
- this.log.debug('ignoring incoming text stream due to no handler for topic', streamHeader.topic);
23479
- return;
23480
- }
23481
- let streamController;
23482
- const info = {
23483
- id: streamHeader.streamId,
23484
- mimeType: streamHeader.mimeType,
23485
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
23486
- topic: streamHeader.topic,
23487
- timestamp: Number(streamHeader.timestamp),
23488
- attributes: streamHeader.attributes,
23489
- encryptionType,
23490
- attachedStreamIds: streamHeader.contentHeader.value.attachedStreamIds
23491
- };
23492
- const stream = new ReadableStream({
23493
- start: controller => {
23494
- streamController = controller;
23495
- if (this.textStreamControllers.has(streamHeader.streamId)) {
23496
- throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24455
+ case 'textHeader':
24456
+ {
24457
+ const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
24458
+ if (!streamHandlerCallback) {
24459
+ this.log.debug('ignoring incoming text stream due to no handler for topic', streamHeader.topic);
24460
+ return;
23497
24461
  }
23498
- this.textStreamControllers.set(streamHeader.streamId, {
23499
- info,
23500
- controller: streamController,
23501
- startTime: Date.now(),
23502
- sendingParticipantIdentity: participantIdentity
24462
+ let streamController;
24463
+ const info = {
24464
+ id: streamHeader.streamId,
24465
+ mimeType: streamHeader.mimeType,
24466
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
24467
+ topic: streamHeader.topic,
24468
+ timestamp: Number(streamHeader.timestamp),
24469
+ attributes: streamHeader.attributes,
24470
+ encryptionType,
24471
+ attachedStreamIds: streamHeader.contentHeader.value.attachedStreamIds
24472
+ };
24473
+ // Determine if the byte payload needs to be decompressed.
24474
+ let compressed;
24475
+ switch (streamHeader.compression) {
24476
+ case DataStream_CompressionType.DEFLATE_RAW:
24477
+ if (!isCompressionStreamSupported()) {
24478
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24479
+ // isn't properly abiding by the data streams v2 protocol.
24480
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
24481
+ return;
24482
+ }
24483
+ compressed = true;
24484
+ break;
24485
+ case DataStream_CompressionType.NONE:
24486
+ compressed = false;
24487
+ break;
24488
+ default:
24489
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24490
+ // isn't properly abiding by the data streams v2 protocol.
24491
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
24492
+ return;
24493
+ }
24494
+ // Single-packet stream: the entire payload was smuggled into the header's `inlineContent`.
24495
+ // Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
24496
+ const inlineContent = streamHeader.inlineContent;
24497
+ if (typeof inlineContent !== 'undefined') {
24498
+ // Inline text is the raw UTF-8 payload, optionally deflate-raw compressed.
24499
+ const content = compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent;
24500
+ streamHandlerCallback(new TextStreamReader(info, createInlineStream(streamHeader.streamId, content), bigIntToNumber(streamHeader.totalLength)), {
24501
+ identity: participantIdentity
24502
+ });
24503
+ return;
24504
+ }
24505
+ const stream = new ReadableStream({
24506
+ start: controller => {
24507
+ streamController = controller;
24508
+ if (this.textStreamControllers.has(streamHeader.streamId)) {
24509
+ throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24510
+ }
24511
+ this.textStreamControllers.set(streamHeader.streamId, {
24512
+ info,
24513
+ controller: streamController,
24514
+ startTime: Date.now(),
24515
+ sendingParticipantIdentity: participantIdentity
24516
+ });
24517
+ }
23503
24518
  });
24519
+ streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
24520
+ // `totalLength` is the pre-compression size, and the reader sees decompressed bytes, so
24521
+ // it applies to both paths.
24522
+ bigIntToNumber(streamHeader.totalLength)), {
24523
+ identity: participantIdentity
24524
+ });
24525
+ return;
23504
24526
  }
23505
- });
23506
- streamHandlerCallback(new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
23507
- identity: participantIdentity
23508
- });
23509
24527
  }
23510
24528
  }
23511
24529
  handleStreamChunk(chunk, encryptionType) {
@@ -23535,21 +24553,190 @@ class TextStreamReader extends BaseStreamReader {
23535
24553
  textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23536
24554
  } else {
23537
24555
  textBuffer.info.attributes = Object.assign(Object.assign({}, textBuffer.info.attributes), trailer.attributes);
23538
- textBuffer.controller.close();
23539
- this.textStreamControllers.delete(trailer.streamId);
24556
+ if (trailer.reason) {
24557
+ // A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
24558
+ // surface it as an error rather than pretending the stream completed.
24559
+ textBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
24560
+ } else {
24561
+ textBuffer.controller.close();
24562
+ }
24563
+ }
24564
+ this.textStreamControllers.delete(trailer.streamId);
24565
+ }
24566
+ const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
24567
+ if (fileBuffer) {
24568
+ if (fileBuffer.info.encryptionType !== encryptionType) {
24569
+ fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
24570
+ } else {
24571
+ fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
24572
+ if (trailer.reason) {
24573
+ // A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
24574
+ // surface it as an error rather than pretending the stream completed.
24575
+ fileBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
24576
+ } else {
24577
+ fileBuffer.controller.close();
24578
+ }
24579
+ }
24580
+ this.byteStreamControllers.delete(trailer.streamId);
24581
+ }
24582
+ }
24583
+ }
24584
+ /**
24585
+ * Builds a `ReadableStream` that yields the given content as a single chunk and then immediately
24586
+ * closes - used to surface an inline (single-packet) data stream as a fully-formed stream. `content`
24587
+ * may be a promise (e.g. async gzip decompression); a rejection errors the stream.
24588
+ */
24589
+ function createInlineStream(streamId, content) {
24590
+ return new ReadableStream({
24591
+ start: controller => __awaiter(this, void 0, void 0, function* () {
24592
+ try {
24593
+ const bytes = yield content;
24594
+ controller.enqueue(new DataStream_Chunk({
24595
+ streamId,
24596
+ chunkIndex: BigInt(0),
24597
+ content: bytes
24598
+ }));
24599
+ controller.close();
24600
+ } catch (err) {
24601
+ controller.error(err);
24602
+ }
24603
+ })
24604
+ });
24605
+ }
24606
+ /**
24607
+ * Validates that chunks are received in order, dropping duplicates and throwing if gaps are found.
24608
+ *
24609
+ * A stateful decompressor silently corrupts on duplicated or out-of-order input, so duplicates are
24610
+ * dropped (with a warning - in-order delivery is expected on the reliable channel, but reconnect
24611
+ * handling may replay) and a gap is a hard error. Shared by the text and byte deflate-raw decoders.
24612
+ */
24613
+ function ensureOrderedChunks(streamId) {
24614
+ let lastChunkIndex = -1;
24615
+ return new TransformStream({
24616
+ transform: (value, controller) => {
24617
+ const index = bigIntToNumber(value.chunkIndex);
24618
+ if (index <= lastChunkIndex) {
24619
+ livekitLogger.warn("ignoring duplicate chunk ".concat(index, " for compressed data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
24620
+ return;
24621
+ }
24622
+ if (index > lastChunkIndex + 1) {
24623
+ throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for compressed data stream ").concat(streamId, " - cannot continue decompressing"), DataStreamErrorReason.Incomplete);
24624
+ }
24625
+ lastChunkIndex = index;
24626
+ controller.enqueue(value);
24627
+ }
24628
+ });
24629
+ }
24630
+ /** Unwraps compressed `DataStream_Chunk`s to their compressed bytes (in `chunkIndex` order), */
24631
+ function chunksToBytes() {
24632
+ return new TransformStream({
24633
+ transform: (value, controller) => {
24634
+ controller.enqueue(value.content);
24635
+ }
24636
+ });
24637
+ }
24638
+ /** Re-wraps decompressed bytes into contiguous `DataStream_Chunk`s, skipping inflate's empty reads. */
24639
+ function bytesToChunks(streamId) {
24640
+ let outIndex = 0;
24641
+ return new TransformStream({
24642
+ transform: (value, controller) => {
24643
+ // Inflate can emit empty reads; only synthesize a chunk when there is content.
24644
+ if (value.byteLength > 0) {
24645
+ controller.enqueue(new DataStream_Chunk({
24646
+ streamId,
24647
+ chunkIndex: numberToBigInt(outIndex),
24648
+ content: value
24649
+ }));
24650
+ outIndex += 1;
24651
+ }
24652
+ }
24653
+ });
24654
+ }
24655
+ /**
24656
+ * Reframes decompressed byte chunks onto UTF-8 character boundaries via a streaming `TextDecoder`
24657
+ * (a write larger than the MTU spans several packets, which may split a codepoint), so each
24658
+ * synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
24659
+ */
24660
+ function bytesToDecodedUtf8(streamId) {
24661
+ const decoder = new TextDecoder('utf-8', {
24662
+ fatal: true
24663
+ });
24664
+ const encoder = new TextEncoder();
24665
+ let outIndex = 0;
24666
+ const decodeOrThrow = bytes => {
24667
+ try {
24668
+ return bytes ? decoder.decode(bytes, {
24669
+ stream: true
24670
+ }) : decoder.decode();
24671
+ } catch (err) {
24672
+ throw new DataStreamError("Cannot decode compressed data stream ".concat(streamId, " as text: ").concat(err), DataStreamErrorReason.DecodeFailed);
24673
+ }
24674
+ };
24675
+ return new TransformStream({
24676
+ transform: (value, controller) => {
24677
+ const text = decodeOrThrow(value);
24678
+ // Everything so far may have been a partial codepoint; only emit once we have characters.
24679
+ if (text.length > 0) {
24680
+ controller.enqueue(new DataStream_Chunk({
24681
+ streamId,
24682
+ chunkIndex: numberToBigInt(outIndex),
24683
+ content: encoder.encode(text)
24684
+ }));
24685
+ outIndex += 1;
24686
+ }
24687
+ },
24688
+ flush: controller => {
24689
+ const tail = decodeOrThrow();
24690
+ if (tail.length > 0) {
24691
+ controller.enqueue(new DataStream_Chunk({
24692
+ streamId,
24693
+ chunkIndex: numberToBigInt(outIndex),
24694
+ content: encoder.encode(tail)
24695
+ }));
24696
+ outIndex += 1;
23540
24697
  }
23541
24698
  }
23542
- const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
23543
- if (fileBuffer) {
23544
- if (fileBuffer.info.encryptionType !== encryptionType) {
23545
- fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23546
- } else {
23547
- fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
23548
- fileBuffer.controller.close();
24699
+ });
24700
+ }
24701
+ /**
24702
+ * Transforms a stream of deflate-raw-compressed byte `DataStream_Chunk`s into a stream of
24703
+ * decompressed chunks, so `ByteStreamReader` consumes it unchanged. All chunk contents are fed (in
24704
+ * `chunkIndex` order) through ONE raw-deflate decompressor for the stream's lifetime; decompressed
24705
+ * output is re-wrapped as chunks as soon as it is produced. The sender (sendFile) compresses the
24706
+ * whole payload in one shot, but the format also supports a single context-takeover stream
24707
+ * sync-flushed at write boundaries, so a future incremental streamBytes could compress with no
24708
+ * protocol change. Errors and cancellation propagate through the pipe chain.
24709
+ */
24710
+ function inflateRawByteChunkStream(raw, streamId, maxPayloadByteLength) {
24711
+ return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToChunks(streamId));
24712
+ }
24713
+ /**
24714
+ * Transforms a stream of deflate-raw-compressed text `DataStream_Chunk`s into a stream of
24715
+ * decompressed chunks, so `TextStreamReader` consumes it unchanged. Builds on
24716
+ * {@link inflateRawByteChunkStream} (single decompressor + ordering guard) and adds a streaming
24717
+ * `TextDecoder` that reframes the decompressed bytes on UTF-8 character boundaries so each
24718
+ * synthesized chunk decodes independently. Errors and cancellation propagate through the pipe chain.
24719
+ */
24720
+ function inflateRawChunkStream(raw, streamId, maxPayloadByteLength) {
24721
+ return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToDecodedUtf8(streamId));
24722
+ }
24723
+ /**
24724
+ * Caps the total decompressed byte count of a compressed stream (decompression-bomb guard): a
24725
+ * tiny compressed payload can inflate to an arbitrarily large output, so the decompressor's
24726
+ * output is bounded rather than trusting the wire size. Exceeding the cap errors the stream with
24727
+ * `PayloadTooLarge`.
24728
+ */
24729
+ function maxDecompressedLengthGuard(streamId, maxByteLength) {
24730
+ let total = 0;
24731
+ return new TransformStream({
24732
+ transform: (value, controller) => {
24733
+ total += value.byteLength;
24734
+ if (total > maxByteLength) {
24735
+ throw new DataStreamError("Data stream ".concat(streamId, " exceeds the maximum payload size of ").concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
23549
24736
  }
23550
- this.byteStreamControllers.delete(trailer.streamId);
24737
+ controller.enqueue(value);
23551
24738
  }
23552
- }
24739
+ });
23553
24740
  }class BaseStreamWriter {
23554
24741
  constructor(writableStream, info, onClose) {
23555
24742
  this.writableStream = writableStream;
@@ -23570,15 +24757,70 @@ class TextStreamReader extends BaseStreamReader {
23570
24757
  }
23571
24758
  }
23572
24759
  class TextStreamWriter extends BaseStreamWriter {}
23573
- class ByteStreamWriter extends BaseStreamWriter {}const STREAM_CHUNK_SIZE = 15000;
24760
+ class ByteStreamWriter extends BaseStreamWriter {}/** Builds the `DataStream_Header` for a text stream from its info and stream options. */
24761
+ function buildTextStreamHeader(info, options, v2) {
24762
+ var _a;
24763
+ return new DataStream_Header({
24764
+ streamId: info.id,
24765
+ mimeType: info.mimeType,
24766
+ topic: info.topic,
24767
+ timestamp: numberToBigInt(info.timestamp),
24768
+ totalLength: numberToBigInt(info.size),
24769
+ attributes: info.attributes,
24770
+ compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
24771
+ inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
24772
+ contentHeader: {
24773
+ case: 'textHeader',
24774
+ value: new DataStream_TextHeader({
24775
+ version: options === null || options === void 0 ? void 0 : options.version,
24776
+ attachedStreamIds: info.attachedStreamIds,
24777
+ replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
24778
+ operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
24779
+ })
24780
+ }
24781
+ });
24782
+ }
24783
+ /** Builds the `DataStream_Header` for a byte stream from its info. */
24784
+ function buildByteStreamHeader(info, v2) {
24785
+ var _a;
24786
+ return new DataStream_Header({
24787
+ streamId: info.id,
24788
+ mimeType: info.mimeType,
24789
+ topic: info.topic,
24790
+ timestamp: numberToBigInt(info.timestamp),
24791
+ totalLength: numberToBigInt(info.size),
24792
+ attributes: info.attributes,
24793
+ compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
24794
+ inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
24795
+ contentHeader: {
24796
+ case: 'byteHeader',
24797
+ value: new DataStream_ByteHeader({
24798
+ name: info.name
24799
+ })
24800
+ }
24801
+ });
24802
+ }
24803
+ /** Wraps a `DataStream_Header` in a `DataPacket` ready to be sent over a data channel. */
24804
+ function createStreamHeaderPacket(header, destinationIdentities) {
24805
+ return new DataPacket({
24806
+ destinationIdentities,
24807
+ value: {
24808
+ case: 'streamHeader',
24809
+ value: header
24810
+ }
24811
+ });
24812
+ }const textEncoder = new TextEncoder();
23574
24813
  /**
23575
24814
  * Manages sending custom user data via data channels.
23576
24815
  * @internal
23577
24816
  */
23578
24817
  class OutgoingDataStreamManager {
23579
- constructor(engine, log) {
24818
+ constructor(engine, log, getRemoteParticipantClientProtocol, getRemoteParticipantCapabilities, getAllRemoteParticipantIdentities) {
23580
24819
  this.engine = engine;
23581
24820
  this.log = log;
24821
+ this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
24822
+ this.getRemoteParticipantCapabilities = getRemoteParticipantCapabilities;
24823
+ this.getAllRemoteParticipantIdentities = getAllRemoteParticipantIdentities;
23582
24824
  }
23583
24825
  setupEngine(engine) {
23584
24826
  this.engine = engine;
@@ -23586,42 +24828,236 @@ class OutgoingDataStreamManager {
23586
24828
  /** {@inheritDoc LocalParticipant.sendText} */
23587
24829
  sendText(text, options) {
23588
24830
  return __awaiter(this, void 0, void 0, function* () {
23589
- var _a;
24831
+ var _a, _b, _c, _d, _e;
23590
24832
  const streamId = crypto.randomUUID();
23591
- const textInBytes = new TextEncoder().encode(text);
24833
+ const textInBytes = textEncoder.encode(text);
23592
24834
  const totalTextLength = textInBytes.byteLength;
23593
- const fileIds = (_a = options === null || options === void 0 ? void 0 : options.attachments) === null || _a === void 0 ? void 0 : _a.map(() => crypto.randomUUID());
23594
- const progresses = new Array(fileIds ? fileIds.length + 1 : 1).fill(0);
24835
+ const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
24836
+ let info = {
24837
+ id: streamId,
24838
+ mimeType: 'text/plain',
24839
+ timestamp: Date.now(),
24840
+ topic: (_b = options === null || options === void 0 ? void 0 : options.topic) !== null && _b !== void 0 ? _b : '',
24841
+ size: totalTextLength,
24842
+ // NOTE: size is always the pre-compression byte length
24843
+ attributes: options === null || options === void 0 ? void 0 : options.attributes,
24844
+ encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
24845
+ };
24846
+ const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities) && this.allRecipientsSupportCompression(options === null || options === void 0 ? void 0 : options.destinationIdentities);
24847
+ let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(textInBytes).pipeThrough(deflateRawTransform())) : null;
24848
+ // Phase 1: Try to send as a single packet data stream
24849
+ const noAttachments = !(options === null || options === void 0 ? void 0 : options.attachments) || options.attachments.length === 0;
24850
+ if (noAttachments && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities)) {
24851
+ // The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
24852
+ // if it actually shrinks the payload (deflate framing makes tiny strings larger). The
24853
+ // compression flag is carried in the header's `compression` field; user attributes are left
24854
+ // untouched.
24855
+ let inlineContent = textInBytes;
24856
+ let compression = DataStream_CompressionType.NONE;
24857
+ if (compressedStream) {
24858
+ const collectedBytes = yield compressedStream.collect();
24859
+ if (collectedBytes.byteLength < textInBytes.byteLength) {
24860
+ inlineContent = collectedBytes;
24861
+ compression = DataStream_CompressionType.DEFLATE_RAW;
24862
+ }
24863
+ }
24864
+ const header = buildTextStreamHeader(info, undefined, {
24865
+ compression,
24866
+ inlineContent
24867
+ });
24868
+ const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
24869
+ if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
24870
+ yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
24871
+ (_d = options === null || options === void 0 ? void 0 : options.onProgress) === null || _d === void 0 ? void 0 : _d.call(options, 1);
24872
+ return info;
24873
+ }
24874
+ }
24875
+ const fileIds = (_e = options === null || options === void 0 ? void 0 : options.attachments) === null || _e === void 0 ? void 0 : _e.map(() => crypto.randomUUID());
24876
+ // Progress is split evenly across the text part (slot 0) and one slot per attachment, then
24877
+ // normalized to a [0,1] fraction. Each slot climbs monotonically to 1, so the aggregate ends at
24878
+ // exactly 1 once every part has completed.
24879
+ const parts = fileIds ? fileIds.length + 1 : 1;
24880
+ const progresses = new Array(parts).fill(0);
23595
24881
  const handleProgress = (progress, idx) => {
23596
24882
  var _a;
23597
24883
  progresses[idx] = progress;
23598
- const totalProgress = progresses.reduce((acc, val) => acc + val, 0);
23599
- (_a = options === null || options === void 0 ? void 0 : options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, totalProgress);
24884
+ (_a = options === null || options === void 0 ? void 0 : options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, progresses.reduce((acc, val) => acc + val, 0) / parts);
23600
24885
  };
23601
- const writer = yield this.streamText({
23602
- streamId,
23603
- totalSize: totalTextLength,
23604
- destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities,
23605
- topic: options === null || options === void 0 ? void 0 : options.topic,
23606
- attachedStreamIds: fileIds,
23607
- attributes: options === null || options === void 0 ? void 0 : options.attributes
23608
- });
23609
- yield writer.write(text);
23610
- // set text part of progress to 1
23611
- handleProgress(1, 0);
23612
- yield writer.close();
24886
+ // Phase 2: Try to send a multi packet data stream with compressed bytes
24887
+ if (compressedStream) {
24888
+ info.attachedStreamIds = fileIds;
24889
+ const header = buildTextStreamHeader(info, undefined, {
24890
+ compression: DataStream_CompressionType.DEFLATE_RAW
24891
+ });
24892
+ const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
24893
+ yield this.sendChunkedByteStream(packet, streamId, options === null || options === void 0 ? void 0 : options.destinationIdentities, compressedStream.stream().pipeThrough(progressReportingStream(textInBytes.length, progress => handleProgress(progress, 0))));
24894
+ // Ensure there's always a 100% progress event fired, even if the string is zero bytes long
24895
+ if (textInBytes.length === 0) {
24896
+ handleProgress(1, 0);
24897
+ }
24898
+ } else {
24899
+ // Phase 3 / fallback: header + plain uncompressed chunk packets + trailer.
24900
+ const writer = yield this.streamText({
24901
+ streamId,
24902
+ totalSize: totalTextLength,
24903
+ destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities,
24904
+ topic: options === null || options === void 0 ? void 0 : options.topic,
24905
+ attachedStreamIds: fileIds,
24906
+ attributes: options === null || options === void 0 ? void 0 : options.attributes
24907
+ });
24908
+ yield writer.write(text);
24909
+ // set text part of progress to 1
24910
+ handleProgress(1, 0);
24911
+ yield writer.close();
24912
+ info = writer.info;
24913
+ }
23613
24914
  if ((options === null || options === void 0 ? void 0 : options.attachments) && fileIds) {
23614
24915
  yield Promise.all(options.attachments.map((file, idx) => __awaiter(this, void 0, void 0, function* () {
23615
24916
  return this._sendFile(fileIds[idx], file, {
23616
24917
  topic: options.topic,
23617
24918
  mimeType: file.type,
24919
+ destinationIdentities: options.destinationIdentities,
24920
+ compress: options.compress,
23618
24921
  onProgress: progress => {
23619
24922
  handleProgress(progress, idx + 1);
23620
24923
  }
23621
24924
  });
23622
24925
  })));
23623
24926
  }
23624
- return writer.info;
24927
+ return info;
24928
+ });
24929
+ }
24930
+ /**
24931
+ * Sends a complete in-memory byte payload. Mirrors {@link sendText}'s semantics: when every
24932
+ * recipient supports data streams v2 the payload rides inline in a single header packet
24933
+ * (optionally deflate-raw compressed), otherwise it is sent as a (optionally compressed)
24934
+ * chunked byte stream. Unlike {@link sendFile}, the whole payload is already in memory, so the
24935
+ * inline single-packet fast path applies.
24936
+ */
24937
+ sendBytes(bytes, options) {
24938
+ return __awaiter(this, void 0, void 0, function* () {
24939
+ var _a, _b, _c, _d, _e, _f, _g;
24940
+ const streamId = crypto.randomUUID();
24941
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
24942
+ const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
24943
+ const info = {
24944
+ id: streamId,
24945
+ name: (_b = options === null || options === void 0 ? void 0 : options.name) !== null && _b !== void 0 ? _b : 'unknown',
24946
+ mimeType: (_c = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _c !== void 0 ? _c : 'application/octet-stream',
24947
+ timestamp: Date.now(),
24948
+ topic: (_d = options === null || options === void 0 ? void 0 : options.topic) !== null && _d !== void 0 ? _d : '',
24949
+ size: bytes.byteLength,
24950
+ // NOTE: size is always the pre-compression byte length
24951
+ attributes: options === null || options === void 0 ? void 0 : options.attributes,
24952
+ encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
24953
+ };
24954
+ const progressMonitorTap = progressReportingStream(bytes.length, options === null || options === void 0 ? void 0 : options.onProgress);
24955
+ const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
24956
+ let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(bytes).pipeThrough(progressMonitorTap).pipeThrough(deflateRawTransform())) : null;
24957
+ // Phase 1: Try to send as a single packet data stream
24958
+ if (this.allRecipientsSupportV2(destinationIdentities)) {
24959
+ // The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
24960
+ // if it actually shrinks the payload (deflate framing makes tiny payloads larger). The
24961
+ // compression flag is carried in the header's `compression` field; user attributes are left
24962
+ // untouched.
24963
+ let inlineContent = bytes;
24964
+ let compression = DataStream_CompressionType.NONE;
24965
+ if (compressedStream) {
24966
+ const collectedBytes = yield compressedStream.collect();
24967
+ if (collectedBytes.byteLength < bytes.byteLength) {
24968
+ inlineContent = collectedBytes;
24969
+ compression = DataStream_CompressionType.DEFLATE_RAW;
24970
+ }
24971
+ }
24972
+ const header = buildByteStreamHeader(info, {
24973
+ compression,
24974
+ inlineContent
24975
+ });
24976
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
24977
+ if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
24978
+ yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
24979
+ (_f = options === null || options === void 0 ? void 0 : options.onProgress) === null || _f === void 0 ? void 0 : _f.call(options, 1);
24980
+ return info;
24981
+ }
24982
+ }
24983
+ // Phase 2/3: header + (optionally compressed) chunk packets + trailer.
24984
+ const header = buildByteStreamHeader(info, {
24985
+ compression: compressedStream ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
24986
+ });
24987
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
24988
+ const source = compressedStream ? compressedStream.stream() : readableFromBytes(bytes).pipeThrough(progressMonitorTap);
24989
+ yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
24990
+ // Ensure there's always a 100% progress event fired, even if the buffer is zero bytes long
24991
+ if (bytes.length === 0) {
24992
+ (_g = options === null || options === void 0 ? void 0 : options.onProgress) === null || _g === void 0 ? void 0 : _g.call(options, 1);
24993
+ }
24994
+ return info;
24995
+ });
24996
+ }
24997
+ /**
24998
+ * Returns true only if every recipient is known to support data streams v2 (single-packet inline
24999
+ * streams and compression). For a targeted send this checks the named destination identities; for
25000
+ * a broadcast (no explicit destinations) it checks every remote participant currently in the room.
25001
+ * An empty room (nobody to receive) is considered eligible.
25002
+ */
25003
+ allRecipientsSupportV2(destinationIdentities) {
25004
+ const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
25005
+ return identities.every(identity => this.getRemoteParticipantClientProtocol(identity) >= CLIENT_PROTOCOL_DATA_STREAM_V2);
25006
+ }
25007
+ /**
25008
+ * Returns true only if every recipient advertises the deflate-raw compression capability (so it
25009
+ * can decompress a compressed stream). Resolved the same way as {@link allRecipientsSupportV2}:
25010
+ * named destinations, or every remote participant for a broadcast; an empty room is eligible.
25011
+ */
25012
+ allRecipientsSupportCompression(destinationIdentities) {
25013
+ const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
25014
+ return identities.every(identity => this.getRemoteParticipantCapabilities(identity).includes(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW));
25015
+ }
25016
+ /**
25017
+ * Shared chunked-stream send for `sendText`/`sendFile`: sends the prebuilt header packet, then
25018
+ * forwards `source` (optionally deflate-raw compressed) as `streamChunk` packets re-chunked to
25019
+ * the MTU budget with contiguous indices, then sends the trailer. The source is consumed
25020
+ * incrementally, so a `file.stream()` is never buffered in full. The platform compressor can't
25021
+ * flush mid-stream, so compression is only used when the whole payload is available as a stream
25022
+ * up front (not for incremental writers like `streamText`/`streamBytes`).
25023
+ */
25024
+ sendChunkedByteStream(headerPacket, streamId, destinationIdentities, source) {
25025
+ return __awaiter(this, void 0, void 0, function* () {
25026
+ var _a, e_1, _b, _c;
25027
+ const engine = this.engine;
25028
+ yield sendHeaderPacket(engine, headerPacket);
25029
+ let chunkId = 0;
25030
+ try {
25031
+ for (var _d = true, _e = __asyncValues(readBytesInChunks(source, STREAM_CHUNK_SIZE_BYTES)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
25032
+ _c = _f.value;
25033
+ _d = false;
25034
+ const chunk = _c;
25035
+ const chunkPacket = new DataPacket({
25036
+ destinationIdentities,
25037
+ value: {
25038
+ case: 'streamChunk',
25039
+ value: new DataStream_Chunk({
25040
+ content: chunk,
25041
+ streamId,
25042
+ chunkIndex: numberToBigInt(chunkId)
25043
+ })
25044
+ }
25045
+ });
25046
+ yield engine.sendDataPacket(chunkPacket, DataChannelKind.RELIABLE);
25047
+ chunkId += 1;
25048
+ }
25049
+ } catch (e_1_1) {
25050
+ e_1 = {
25051
+ error: e_1_1
25052
+ };
25053
+ } finally {
25054
+ try {
25055
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
25056
+ } finally {
25057
+ if (e_1) throw e_1.error;
25058
+ }
25059
+ }
25060
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
23625
25061
  });
23626
25062
  }
23627
25063
  /**
@@ -23631,6 +25067,7 @@ class OutgoingDataStreamManager {
23631
25067
  return __awaiter(this, void 0, void 0, function* () {
23632
25068
  var _a, _b, _c;
23633
25069
  const streamId = (_a = options === null || options === void 0 ? void 0 : options.streamId) !== null && _a !== void 0 ? _a : crypto.randomUUID();
25070
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
23634
25071
  const info = {
23635
25072
  id: streamId,
23636
25073
  mimeType: 'text/plain',
@@ -23641,39 +25078,20 @@ class OutgoingDataStreamManager {
23641
25078
  encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE,
23642
25079
  attachedStreamIds: options === null || options === void 0 ? void 0 : options.attachedStreamIds
23643
25080
  };
23644
- const header = new DataStream_Header({
23645
- streamId,
23646
- mimeType: info.mimeType,
23647
- topic: info.topic,
23648
- timestamp: numberToBigInt(info.timestamp),
23649
- totalLength: numberToBigInt(info.size),
23650
- attributes: info.attributes,
23651
- contentHeader: {
23652
- case: 'textHeader',
23653
- value: new DataStream_TextHeader({
23654
- version: options === null || options === void 0 ? void 0 : options.version,
23655
- attachedStreamIds: info.attachedStreamIds,
23656
- replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
23657
- operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
23658
- })
23659
- }
23660
- });
23661
- const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
23662
- const packet = new DataPacket({
23663
- destinationIdentities,
23664
- value: {
23665
- case: 'streamHeader',
23666
- value: header
23667
- }
23668
- });
23669
- yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25081
+ const header = buildTextStreamHeader(info, options);
25082
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25083
+ yield sendHeaderPacket(this.engine, packet);
23670
25084
  let chunkId = 0;
23671
25085
  const engine = this.engine;
25086
+ // Incremental text streams are never compressed (CompressionStream does not support flushing
25087
+ // mid-stream); one-shot compression lives in sendText.
25088
+ //
25089
+ // Note that a future streamText could send a context-takeover style deflate-raw stream with
25090
+ // intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
23672
25091
  const writableStream = new WritableStream({
23673
- // Implement the sink
23674
25092
  write(text) {
23675
25093
  return __awaiter(this, void 0, void 0, function* () {
23676
- for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE)) {
25094
+ for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE_BYTES)) {
23677
25095
  const chunk = new DataStream_Chunk({
23678
25096
  content: textByteChunk,
23679
25097
  streamId,
@@ -23693,17 +25111,7 @@ class OutgoingDataStreamManager {
23693
25111
  },
23694
25112
  close() {
23695
25113
  return __awaiter(this, void 0, void 0, function* () {
23696
- const trailer = new DataStream_Trailer({
23697
- streamId
23698
- });
23699
- const trailerPacket = new DataPacket({
23700
- destinationIdentities,
23701
- value: {
23702
- case: 'streamTrailer',
23703
- value: trailer
23704
- }
23705
- });
23706
- yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25114
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
23707
25115
  });
23708
25116
  },
23709
25117
  abort(err) {
@@ -23714,6 +25122,8 @@ class OutgoingDataStreamManager {
23714
25122
  let onEngineClose = () => __awaiter(this, void 0, void 0, function* () {
23715
25123
  yield writer.close();
23716
25124
  });
25125
+ // FIXME: make this a global event to ensure "max listener" warning won't get logged for lots of
25126
+ // in flight data streams.
23717
25127
  engine.once(EngineEvent.Closing, onEngineClose);
23718
25128
  const writer = new TextStreamWriter(writableStream, info, () => this.engine.off(EngineEvent.Closing, onEngineClose));
23719
25129
  return writer;
@@ -23728,29 +25138,47 @@ class OutgoingDataStreamManager {
23728
25138
  };
23729
25139
  });
23730
25140
  }
25141
+ /**
25142
+ * Streams a file as a chunked byte stream, compressed (deflate-raw) when the runtime supports it
25143
+ * and every recipient is on data streams v2. The file is piped `file.stream()` →
25144
+ * (`CompressionStream`) → chunk packets via {@link sendChunkedByteStream}, so it is never fully
25145
+ * buffered in memory — unlike {@link sendBytes}, there is no inline single-packet fast path for
25146
+ * files (the compressed size can't be known up front without buffering the whole file).
25147
+ */
23731
25148
  _sendFile(streamId, file, options) {
23732
25149
  return __awaiter(this, void 0, void 0, function* () {
23733
- var _a;
23734
- const writer = yield this.streamBytes({
23735
- streamId,
23736
- totalSize: file.size,
25150
+ var _a, _b, _c, _d, _e;
25151
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
25152
+ const compress = ((_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true) && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
25153
+ const info = {
25154
+ id: streamId,
23737
25155
  name: file.name,
23738
- mimeType: (_a = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _a !== void 0 ? _a : file.type,
23739
- topic: options === null || options === void 0 ? void 0 : options.topic,
23740
- destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities
25156
+ mimeType: (_b = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _b !== void 0 ? _b : file.type,
25157
+ topic: (_c = options === null || options === void 0 ? void 0 : options.topic) !== null && _c !== void 0 ? _c : '',
25158
+ timestamp: Date.now(),
25159
+ size: file.size,
25160
+ encryptionType: ((_d = this.engine.e2eeManager) === null || _d === void 0 ? void 0 : _d.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
25161
+ };
25162
+ // Phase 1: Try to send as a single packet data stream
25163
+ //
25164
+ // This is not being done explictly for files, because it's challenging to determine ahead of
25165
+ // time how well the file contents will compress (and whether the total output will be under the
25166
+ // MTU). Revisit this in the future though.
25167
+ // Phase 2 (compressed) / Phase 3 (uncompressed fallback): header + chunk packets + trailer. Both
25168
+ // funnel through the shared sendChunkedByteStream primitive, differing only by whether the file
25169
+ // stream is wrapped in the deflate-raw compressor; the file is never fully buffered in memory.
25170
+ const header = buildByteStreamHeader(info, {
25171
+ compression: compress ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
23741
25172
  });
23742
- const reader = file.stream().getReader();
23743
- while (true) {
23744
- const _yield$reader$read = yield reader.read(),
23745
- done = _yield$reader$read.done,
23746
- value = _yield$reader$read.value;
23747
- if (done) {
23748
- break;
23749
- }
23750
- yield writer.write(value);
25173
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25174
+ const tapped = file.stream().pipeThrough(progressReportingStream(file.size, options === null || options === void 0 ? void 0 : options.onProgress));
25175
+ const source = compress ? tapped.pipeThrough(deflateRawTransform()) : tapped;
25176
+ yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
25177
+ // Ensure there's always a 100% progress event fired, even if the file is zero bytes long
25178
+ if (file.size === 0) {
25179
+ (_e = options === null || options === void 0 ? void 0 : options.onProgress) === null || _e === void 0 ? void 0 : _e.call(options, 1);
23751
25180
  }
23752
- yield writer.close();
23753
- return writer.info;
25181
+ return info;
23754
25182
  });
23755
25183
  }
23756
25184
  streamBytes(options) {
@@ -23768,32 +25196,18 @@ class OutgoingDataStreamManager {
23768
25196
  name: (_d = options === null || options === void 0 ? void 0 : options.name) !== null && _d !== void 0 ? _d : 'unknown',
23769
25197
  encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
23770
25198
  };
23771
- const header = new DataStream_Header({
23772
- totalLength: numberToBigInt(info.size),
23773
- mimeType: info.mimeType,
23774
- streamId,
23775
- topic: info.topic,
23776
- timestamp: numberToBigInt(Date.now()),
23777
- attributes: info.attributes,
23778
- contentHeader: {
23779
- case: 'byteHeader',
23780
- value: new DataStream_ByteHeader({
23781
- name: info.name
23782
- })
23783
- }
23784
- });
23785
- const packet = new DataPacket({
23786
- destinationIdentities,
23787
- value: {
23788
- case: 'streamHeader',
23789
- value: header
23790
- }
23791
- });
23792
- yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25199
+ const header = buildByteStreamHeader(info);
25200
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25201
+ yield sendHeaderPacket(this.engine, packet);
23793
25202
  let chunkId = 0;
23794
25203
  const writeMutex = new _();
23795
25204
  const engine = this.engine;
23796
25205
  const logLocal = this.log;
25206
+ // Incremental byte streams are never compressed (CompressionStream does not support flushing
25207
+ // mid-stream); one-shot compression lives in sendFile.
25208
+ //
25209
+ // Note that a future streamBytes could send a context-takeover style deflate-raw stream with
25210
+ // intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
23797
25211
  const writableStream = new WritableStream({
23798
25212
  write(chunk) {
23799
25213
  return __awaiter(this, void 0, void 0, function* () {
@@ -23801,7 +25215,7 @@ class OutgoingDataStreamManager {
23801
25215
  let byteOffset = 0;
23802
25216
  try {
23803
25217
  while (byteOffset < chunk.byteLength) {
23804
- const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
25218
+ const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE_BYTES);
23805
25219
  const chunkPacket = new DataPacket({
23806
25220
  destinationIdentities,
23807
25221
  value: {
@@ -23824,17 +25238,7 @@ class OutgoingDataStreamManager {
23824
25238
  },
23825
25239
  close() {
23826
25240
  return __awaiter(this, void 0, void 0, function* () {
23827
- const trailer = new DataStream_Trailer({
23828
- streamId
23829
- });
23830
- const trailerPacket = new DataPacket({
23831
- destinationIdentities,
23832
- value: {
23833
- case: 'streamTrailer',
23834
- value: trailer
23835
- }
23836
- });
23837
- yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25241
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
23838
25242
  });
23839
25243
  },
23840
25244
  abort(err) {
@@ -23845,6 +25249,109 @@ class OutgoingDataStreamManager {
23845
25249
  return byteWriter;
23846
25250
  });
23847
25251
  }
25252
+ }
25253
+ /**
25254
+ * Wraps a stream of compressed bytes and defers buffering it into memory until (and only if) the
25255
+ * full length is actually needed.
25256
+ *
25257
+ * The ideal way to send a compressed stream is incrementally: compress each chunk and send it as
25258
+ * soon as it's ready, without ever holding the whole payload in memory. But to decide whether a
25259
+ * payload is small enough to fit in a single-packet data stream, we have to know the compressed
25260
+ * length — and the only way to learn that is to compress everything and add up the bytes. That
25261
+ * forces us to buffer the entire compressed output before we can make the decision.
25262
+ *
25263
+ * This class lets us have it both ways. It holds the compressed stream unread by default and only
25264
+ * collects it into memory ({@link collect}) when a caller needs the length for the single-packet
25265
+ * size check. If that check never happens, the stream is instead {@link stream|passed straight
25266
+ * through} to the downstream consumer without ever being fully buffered. Once collected, later
25267
+ * calls reuse the buffered bytes rather than re-collecting.
25268
+ */
25269
+ class MaybeCollectedStream {
25270
+ constructor(state) {
25271
+ this.state = state;
25272
+ }
25273
+ static fromStream(stream) {
25274
+ return new MaybeCollectedStream({
25275
+ type: 'stream',
25276
+ stream
25277
+ });
25278
+ }
25279
+ /** Collect data from the stream into memory and return as a Uint8Array. */
25280
+ collect() {
25281
+ return __awaiter(this, void 0, void 0, function* () {
25282
+ switch (this.state.type) {
25283
+ case 'stream':
25284
+ const bytes = yield collect(this.state.stream);
25285
+ this.state = {
25286
+ type: 'collected',
25287
+ bytes
25288
+ };
25289
+ return bytes;
25290
+ case 'collected':
25291
+ return this.state.bytes;
25292
+ }
25293
+ });
25294
+ }
25295
+ /** Pass wrapped stream through to downstream consumer. */
25296
+ stream() {
25297
+ switch (this.state.type) {
25298
+ case 'stream':
25299
+ return this.state.stream;
25300
+ case 'collected':
25301
+ return readableFromBytes(this.state.bytes);
25302
+ }
25303
+ }
25304
+ }
25305
+ /**
25306
+ * A pass-through byte transform that reports progress as bytes flow through it, measured against
25307
+ * `totalLength`. Used as a "tap" to instrument the source of a chunked send without threading a
25308
+ * progress callback through the send primitive.
25309
+ *
25310
+ * IMPORTANT: This should be upstream of any compression step.
25311
+ *
25312
+ * Emits nothing when the total is unknown or zero.
25313
+ */
25314
+ function progressReportingStream(totalPreCompressionLength, onProgress) {
25315
+ let sent = 0;
25316
+ return new TransformStream({
25317
+ transform(chunk, controller) {
25318
+ sent += chunk.byteLength;
25319
+ if (onProgress && typeof totalPreCompressionLength === 'number' && totalPreCompressionLength > 0) {
25320
+ onProgress(Math.min(sent / totalPreCompressionLength, 1));
25321
+ }
25322
+ controller.enqueue(chunk);
25323
+ }
25324
+ });
25325
+ }
25326
+ /**
25327
+ * Sends a stream `streamHeader` packet, enforcing that it fits the MTU budget. The header carries
25328
+ * the user attributes (plus topic/framing), and a single `DataPacket` larger than the MTU can't be
25329
+ * reliably sent — so an oversized header (almost always due to large attributes) is a hard error
25330
+ * rather than a malformed packet on the wire. The inline fast path does its own size check and
25331
+ * falls back to the chunked path instead of calling this.
25332
+ */
25333
+ function sendHeaderPacket(engine, packet) {
25334
+ return __awaiter(this, void 0, void 0, function* () {
25335
+ if (packet.toBinary().byteLength > STREAM_CHUNK_SIZE_BYTES) {
25336
+ throw new DataStreamError("data stream header exceeds the ".concat(STREAM_CHUNK_SIZE_BYTES, "-byte limit; reduce attribute size"), DataStreamErrorReason.HeaderTooLarge);
25337
+ }
25338
+ yield engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25339
+ });
25340
+ }
25341
+ /** Sends a `streamTrailer` packet, marking the end of a stream. */
25342
+ function sendStreamTrailer(streamId, destinationIdentities, engine) {
25343
+ return __awaiter(this, void 0, void 0, function* () {
25344
+ const trailerPacket = new DataPacket({
25345
+ destinationIdentities,
25346
+ value: {
25347
+ case: 'streamTrailer',
25348
+ value: new DataStream_Trailer({
25349
+ streamId
25350
+ })
25351
+ }
25352
+ });
25353
+ yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25354
+ });
23848
25355
  }/**
23849
25356
  * Implementation of AbortSignal.any
23850
25357
  * Creates a signal that will be aborted when any of the given signals is aborted.
@@ -24829,6 +26336,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24829
26336
  let bufferSize = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : READABLE_STREAM_DEFAULT_BUFFER_SIZE;
24830
26337
  let streamController = null;
24831
26338
  const sfuSubscriptionComplete = new Future();
26339
+ // Hold the descriptor by reference: SID reassignment re-keys the map, so lookups
26340
+ // with the SID captured at subscribe time would fail.
26341
+ const descriptor = this.descriptors.get(sid);
24832
26342
  const detachSignal = () => {
24833
26343
  signal === null || signal === void 0 ? void 0 : signal.removeEventListener('abort', onAbort);
24834
26344
  };
@@ -24838,8 +26348,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24838
26348
  log$1.warn("ReadableStream subscribed to ".concat(sid, " was not started."));
24839
26349
  return;
24840
26350
  }
24841
- const descriptor = this.descriptors.get(sid);
24842
- if (!descriptor) {
26351
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24843
26352
  log$1.warn("Unknown track ".concat(sid, ", skipping cancel..."));
24844
26353
  return;
24845
26354
  }
@@ -24858,9 +26367,8 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24858
26367
  if (!streamController) {
24859
26368
  return;
24860
26369
  }
24861
- const currentDescriptor = this.descriptors.get(sid);
24862
- if ((currentDescriptor === null || currentDescriptor === void 0 ? void 0 : currentDescriptor.subscription.type) === 'active') {
24863
- currentDescriptor.subscription.streamControllers.delete(streamController);
26370
+ if ((descriptor === null || descriptor === void 0 ? void 0 : descriptor.subscription.type) === 'active') {
26371
+ descriptor.subscription.streamControllers.delete(streamController);
24864
26372
  }
24865
26373
  streamController.error(DataTrackSubscribeError.cancelled());
24866
26374
  (_a = sfuSubscriptionComplete.reject) === null || _a === void 0 ? void 0 : _a.call(sfuSubscriptionComplete, DataTrackSubscribeError.cancelled());
@@ -24871,8 +26379,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24871
26379
  streamController = controller;
24872
26380
  this.subscribeRequest(sid, signal).then(() => __awaiter(this, void 0, void 0, function* () {
24873
26381
  var _a, _b, _c;
24874
- const descriptor = this.descriptors.get(sid);
24875
- if (!descriptor) {
26382
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
24876
26383
  log$1.error("Unknown track ".concat(sid));
24877
26384
  const err = DataTrackSubscribeError.disconnected();
24878
26385
  controller.error(err);
@@ -24987,7 +26494,7 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
24987
26494
  };
24988
26495
  // Let the SFU know that the subscribe has been cancelled
24989
26496
  this.emit('sfuUpdateSubscription', {
24990
- sid,
26497
+ sid: descriptor.info.sid,
24991
26498
  subscribe: false
24992
26499
  });
24993
26500
  if (previousDescriptorSubscription.type === 'pending') {
@@ -25101,6 +26608,9 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25101
26608
  if (this.descriptors.has(info.sid)) {
25102
26609
  continue;
25103
26610
  }
26611
+ if (this.handleSidReassigned(publisherIdentity, info)) {
26612
+ continue;
26613
+ }
25104
26614
  yield this.handleTrackPublished(publisherIdentity, info);
25105
26615
  }
25106
26616
  publisherParticipantToSidsInUpdate.set(publisherIdentity, sidsInUpdate);
@@ -25158,6 +26668,62 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25158
26668
  });
25159
26669
  });
25160
26670
  }
26671
+ /**
26672
+ * Detects and handles SID reassignment, which occurs when the publisher
26673
+ * republishes its tracks after a full reconnect.
26674
+ *
26675
+ * Returns `true` if an SID reassignment occurred, `false` otherwise.
26676
+ */
26677
+ handleSidReassigned(publisherIdentity, info) {
26678
+ // Publisher identity and pub handle are stable across republications.
26679
+ const existingEntry = Array.from(this.descriptors.entries()).find(_ref1 => {
26680
+ let _ref10 = _slicedToArray(_ref1, 2);
26681
+ _ref10[0];
26682
+ let descriptor = _ref10[1];
26683
+ return descriptor.publisherIdentity === publisherIdentity && descriptor.info.pubHandle === info.pubHandle;
26684
+ });
26685
+ if (!existingEntry) {
26686
+ return false;
26687
+ }
26688
+ const _existingEntry = _slicedToArray(existingEntry, 2),
26689
+ oldSid = _existingEntry[0],
26690
+ descriptor = _existingEntry[1];
26691
+ // Invariant: other than SID, info should not have changed.
26692
+ // TODO: consider refactoring to move SID out of info to allow for direct comparison.
26693
+ const _descriptor$info = descriptor.info,
26694
+ name = _descriptor$info.name,
26695
+ usesE2ee = _descriptor$info.usesE2ee;
26696
+ if (name !== info.name || usesE2ee !== info.usesE2ee) {
26697
+ log$1.warn("Info mismatch for ".concat(oldSid, ", treating as new publication"));
26698
+ return false;
26699
+ }
26700
+ const newSid = info.sid;
26701
+ log$1.debug("SID reassigned: ".concat(oldSid, " -> ").concat(newSid));
26702
+ if (!this.descriptors.delete(oldSid)) {
26703
+ return false;
26704
+ }
26705
+ descriptor.info.sid = newSid;
26706
+ switch (descriptor.subscription.type) {
26707
+ case 'none':
26708
+ break;
26709
+ case 'pending':
26710
+ case 'active':
26711
+ // The SFU does not carry subscriptions across a publisher's full
26712
+ // reconnect; re-request the subscription under the new SID.
26713
+ this.emit('sfuUpdateSubscription', {
26714
+ sid: newSid,
26715
+ subscribe: true
26716
+ });
26717
+ break;
26718
+ }
26719
+ if (descriptor.subscription.type === 'active') {
26720
+ // Keep the routing index consistent until the SFU assigns a new handle
26721
+ // (see `registerSubscriberHandle`).
26722
+ this.subscriptionHandles.set(descriptor.subscription.subcriptionHandle, newSid);
26723
+ }
26724
+ this.descriptors.set(newSid, descriptor);
26725
+ return true;
26726
+ }
25161
26727
  handleTrackUnpublished(sid) {
25162
26728
  const descriptor = this.descriptors.get(sid);
25163
26729
  if (!descriptor) {
@@ -25179,10 +26745,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25179
26745
  /** Mapping between track handles attached to incoming packets to the
25180
26746
  * track SIDs they belong to. */
25181
26747
  mapping) {
25182
- for (const _ref1 of mapping.entries()) {
25183
- var _ref10 = _slicedToArray(_ref1, 2);
25184
- const handle = _ref10[0];
25185
- const sid = _ref10[1];
26748
+ for (const _ref11 of mapping.entries()) {
26749
+ var _ref12 = _slicedToArray(_ref11, 2);
26750
+ const handle = _ref12[0];
26751
+ const sid = _ref12[1];
25186
26752
  this.registerSubscriberHandle(handle, sid);
25187
26753
  }
25188
26754
  }
@@ -25203,12 +26769,17 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25203
26769
  case 'active':
25204
26770
  {
25205
26771
  // Update handle for an active subscription. This can occur following a full reconnect.
26772
+ this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
25206
26773
  descriptor.subscription.subcriptionHandle = assignedHandle;
25207
26774
  this.subscriptionHandles.set(assignedHandle, sid);
25208
26775
  return;
25209
26776
  }
25210
26777
  case 'pending':
25211
26778
  {
26779
+ log$1.debug("data track subscription activated", {
26780
+ sid,
26781
+ handle: assignedHandle
26782
+ });
25212
26783
  const pipeline = new IncomingDataTrackPipeline({
25213
26784
  info: descriptor.info,
25214
26785
  publisherIdentity: descriptor.publisherIdentity,
@@ -25275,10 +26846,10 @@ class IncomingDataTrackManager extends eventsExports.EventEmitter {
25275
26846
  * tracks are subscribed to locally.
25276
26847
  */
25277
26848
  resendSubscriptionUpdates() {
25278
- for (const _ref11 of this.descriptors) {
25279
- var _ref12 = _slicedToArray(_ref11, 2);
25280
- const sid = _ref12[0];
25281
- const descriptor = _ref12[1];
26849
+ for (const _ref13 of this.descriptors) {
26850
+ var _ref14 = _slicedToArray(_ref13, 2);
26851
+ const sid = _ref14[0];
26852
+ const descriptor = _ref14[1];
25282
26853
  if (descriptor.subscription.type === 'none') {
25283
26854
  continue;
25284
26855
  }
@@ -25944,10 +27515,16 @@ class OutgoingDataTrackManager extends eventsExports.EventEmitter {
25944
27515
  {
25945
27516
  if (result.type === 'ok') {
25946
27517
  const info = result.data;
27518
+ log.debug("SFU accepted publish request for handle ".concat(handle), {
27519
+ sid: info.sid
27520
+ });
25947
27521
  const e2eeManager = info.usesE2ee ? this.e2eeManager : null;
25948
27522
  this.descriptors.set(info.pubHandle, Descriptor.active(info, e2eeManager));
25949
27523
  (_b = (_a = descriptor.completionFuture).resolve) === null || _b === void 0 ? void 0 : _b.call(_a);
25950
27524
  } else {
27525
+ log.debug("SFU rejected publish request for handle ".concat(handle), {
27526
+ error: result.error
27527
+ });
25951
27528
  (_d = (_c = descriptor.completionFuture).reject) === null || _d === void 0 ? void 0 : _d.call(_c, result.error);
25952
27529
  }
25953
27530
  return;
@@ -26265,7 +27842,7 @@ class RpcClientManager extends eventsExports.EventEmitter {
26265
27842
  return __awaiter(this, void 0, void 0, function* () {
26266
27843
  if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26267
27844
  // Send payload as a data stream - a "version 2" rpc request.
26268
- const writer = yield this.outgoingDataStreamManager.streamText({
27845
+ yield this.outgoingDataStreamManager.sendText(payload, {
26269
27846
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
26270
27847
  destinationIdentities: [destinationIdentity],
26271
27848
  attributes: {
@@ -26275,8 +27852,6 @@ class RpcClientManager extends eventsExports.EventEmitter {
26275
27852
  [RpcRequestAttrs.RPC_REQUEST_VERSION]: "".concat(RPC_VERSION_V2)
26276
27853
  }
26277
27854
  });
26278
- yield writer.write(payload);
26279
- yield writer.close();
26280
27855
  return;
26281
27856
  }
26282
27857
  // Fallback to sending a literal RpcRequest - a "version 1" rpc request.
@@ -26550,15 +28125,13 @@ class RpcServerManager extends eventsExports.EventEmitter {
26550
28125
  const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
26551
28126
  if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26552
28127
  // Send response as a data stream
26553
- const writer = yield this.outgoingDataStreamManager.streamText({
28128
+ yield this.outgoingDataStreamManager.sendText(payload, {
26554
28129
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
26555
28130
  destinationIdentities: [destinationIdentity],
26556
28131
  attributes: {
26557
28132
  [RpcRequestAttrs.RPC_REQUEST_ID]: requestId
26558
28133
  }
26559
28134
  });
26560
- yield writer.write(payload);
26561
- yield writer.close();
26562
28135
  return;
26563
28136
  }
26564
28137
  // Legacy client: enforce size limit and send uncompressed payload inline
@@ -27403,6 +28976,11 @@ class Participant extends eventsExports.EventEmitter {
27403
28976
  this.audioTrackPublications.forEach(track => isAudioTrack(track.track) && track.track.setAudioContext(ctx));
27404
28977
  }
27405
28978
  addTrackPublication(publication) {
28979
+ this.log.debug("adding track publication", {
28980
+ trackSid: publication.trackSid,
28981
+ source: publication.source,
28982
+ kind: publication.kind
28983
+ });
27406
28984
  // forward publication driven events
27407
28985
  publication.on(TrackEvent.Muted, () => {
27408
28986
  this.emit(ParticipantEvent.TrackMuted, publication);
@@ -28447,7 +30025,7 @@ class Participant extends eventsExports.EventEmitter {
28447
30025
  throw new UnexpectedConnectionState('cannot publish track when not connected');
28448
30026
  }
28449
30027
  const negotiate = () => __awaiter(this, void 0, void 0, function* () {
28450
- var _a, _b, _c;
30028
+ var _a, _b, _c, _d;
28451
30029
  if (!this.engine.pcManager) {
28452
30030
  throw new UnexpectedConnectionState('pcManager is not ready');
28453
30031
  }
@@ -28483,12 +30061,22 @@ class Participant extends eventsExports.EventEmitter {
28483
30061
  maxbr: ((_b = encodings[0]) === null || _b === void 0 ? void 0 : _b.maxBitrate) ? encodings[0].maxBitrate / 1000 : 0
28484
30062
  });
28485
30063
  }
28486
- } else if (track.codec && isSVCCodec(track.codec) && ((_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate)) {
28487
- this.engine.pcManager.publisher.setTrackCodecBitrate({
28488
- cid: req.cid,
28489
- codec: track.codec,
28490
- maxbr: encodings[0].maxBitrate / 1000
28491
- });
30064
+ } else if (track.codec && isVideoCodec(track.codec)) {
30065
+ // Apply start bitrate for all video codecs to prevent initial blurriness.
30066
+ // - SVC codecs: use first encoding's bitrate (single stream with built-in layers)
30067
+ // - Simulcast: sum all encoding bitrates (independent streams, BWE needs total)
30068
+ const targetBitrate = isSVCCodec(track.codec) ? (_d = (_c = encodings[0]) === null || _c === void 0 ? void 0 : _c.maxBitrate) !== null && _d !== void 0 ? _d : 0 : encodings.reduce((sum, enc) => {
30069
+ var _a;
30070
+ return sum + ((_a = enc.maxBitrate) !== null && _a !== void 0 ? _a : 0);
30071
+ }, 0);
30072
+ if (targetBitrate > 0) {
30073
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
30074
+ cid: req.cid,
30075
+ codec: track.codec,
30076
+ maxbr: targetBitrate / 1000,
30077
+ isScreenShare: track.source === Track.Source.ScreenShare
30078
+ });
30079
+ }
28492
30080
  }
28493
30081
  }
28494
30082
  yield this.engine.negotiate();
@@ -29019,6 +30607,18 @@ class Participant extends eventsExports.EventEmitter {
29019
30607
  return this.roomOutgoingDataStreamManager.sendFile(file, options);
29020
30608
  });
29021
30609
  }
30610
+ /**
30611
+ * Sends the given bytes to participants in the room via the data channel.
30612
+ * For files, consider using {@link sendFile}; for longer/incremental payloads, {@link streamBytes}.
30613
+ *
30614
+ * @param bytes The byte payload
30615
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
30616
+ */
30617
+ sendBytes(bytes, options) {
30618
+ return __awaiter(this, void 0, void 0, function* () {
30619
+ return this.roomOutgoingDataStreamManager.sendBytes(bytes, options);
30620
+ });
30621
+ }
29022
30622
  /**
29023
30623
  * Stream bytes incrementally to participants in the room via the data channel.
29024
30624
  * For sending files, consider using {@link sendFile} instead.
@@ -29548,7 +31148,7 @@ class DeferrableMap extends Map {
29548
31148
  return new RemoteDataTrack(info, manager, {
29549
31149
  publisherIdentity: pi.identity
29550
31150
  });
29551
- }), pi.clientProtocol);
31151
+ }), pi.clientProtocol, pi.capabilities);
29552
31152
  }
29553
31153
  get logContext() {
29554
31154
  return Object.assign(Object.assign({}, super.logContext), {
@@ -29561,6 +31161,7 @@ class DeferrableMap extends Map {
29561
31161
  let kind = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : ParticipantInfo_Kind.STANDARD;
29562
31162
  let remoteDataTracks = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : [];
29563
31163
  let clientProtocol = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : CLIENT_PROTOCOL_DEFAULT;
31164
+ let capabilities = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : [];
29564
31165
  super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
29565
31166
  this.signalClient = signalClient;
29566
31167
  this.trackPublications = new Map();
@@ -29571,6 +31172,7 @@ class DeferrableMap extends Map {
29571
31172
  }));
29572
31173
  this.volumeMap = new Map();
29573
31174
  this.clientProtocol = clientProtocol;
31175
+ this.capabilities = capabilities;
29574
31176
  }
29575
31177
  addTrackPublication(publication) {
29576
31178
  super.addTrackPublication(publication);
@@ -29982,11 +31584,10 @@ class Room extends eventsExports.EventEmitter {
29982
31584
  return this.connectFuture.promise;
29983
31585
  });
29984
31586
  this.connectSignal = (url, token, engine, connectOptions, roomOptions, abortController) => __awaiter(this, void 0, void 0, function* () {
29985
- var _a;
29986
31587
  const _yield$engine$join = yield engine.join(url, token, {
29987
31588
  autoSubscribe: connectOptions.autoSubscribe,
29988
31589
  adaptiveStream: typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
29989
- clientInfoCapabilities: isFrameMetadataSupported((_a = roomOptions.frameMetadata) !== null && _a !== void 0 ? _a : roomOptions.packetTrailer) || !!this.e2eeManager ? [ClientInfo_Capability.CAP_PACKET_TRAILER] : undefined,
31590
+ clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
29990
31591
  maxRetries: connectOptions.maxRetries,
29991
31592
  e2eeEnabled: !!this.e2eeManager,
29992
31593
  websocketTimeout: connectOptions.websocketTimeout
@@ -30206,7 +31807,13 @@ class Room extends eventsExports.EventEmitter {
30206
31807
  });
30207
31808
  try {
30208
31809
  yield Promise.all([this.acquireAudioContext(), ...elements.map(e => {
30209
- e.muted = false;
31810
+ // when webAudioMix is enabled, attached elements are deliberately kept muted by
31811
+ // RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
31812
+ // unmuting them here would cause double playback on platforms where element.volume
31813
+ // has no effect (e.g. iOS Safari)
31814
+ if (!this.options.webAudioMix) {
31815
+ e.muted = false;
31816
+ }
30210
31817
  return e.play();
30211
31818
  })]);
30212
31819
  this.handleAudioPlaybackStarted();
@@ -30540,6 +32147,13 @@ class Room extends eventsExports.EventEmitter {
30540
32147
  var _a, _b;
30541
32148
  return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.clientProtocol) !== null && _b !== void 0 ? _b : CLIENT_PROTOCOL_DEFAULT;
30542
32149
  };
32150
+ this.getRemoteParticipantCapabilities = identity => {
32151
+ var _a, _b;
32152
+ return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.capabilities) !== null && _b !== void 0 ? _b : [];
32153
+ };
32154
+ this.getAllRemoteParticipantIdentities = () => {
32155
+ return Array.from(this.remoteParticipants.keys());
32156
+ };
30543
32157
  this.onLocalParticipantMetadataChanged = metadata => {
30544
32158
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
30545
32159
  };
@@ -30616,7 +32230,7 @@ class Room extends eventsExports.EventEmitter {
30616
32230
  this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
30617
32231
  this.maybeCreateEngine();
30618
32232
  this.incomingDataStreamManager = new IncomingDataStreamManager();
30619
- this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
32233
+ this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
30620
32234
  this.incomingDataTrackManager = new IncomingDataTrackManager({
30621
32235
  e2eeManager: this.e2eeManager
30622
32236
  });
@@ -30653,7 +32267,7 @@ class Room extends eventsExports.EventEmitter {
30653
32267
  }).on('packetAvailable', _ref => {
30654
32268
  let handle = _ref.handle,
30655
32269
  bytes = _ref.bytes;
30656
- this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait').finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
32270
+ this.engine.sendDataTrackFrame(bytes).finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
30657
32271
  });
30658
32272
  this.registerRpcDataStreamHandler();
30659
32273
  this.rpcClientManager = new RpcClientManager(this.log, this.outgoingDataStreamManager, this.getRemoteParticipantClientProtocol, () => {
@@ -30902,10 +32516,10 @@ class Room extends eventsExports.EventEmitter {
30902
32516
  this.isResuming = false;
30903
32517
  this.log.debug('Resumed signal connection');
30904
32518
  this.updateSubscriptions();
30905
- this.emitBufferedEvents();
30906
32519
  if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
30907
32520
  this.emit(RoomEvent.Reconnected);
30908
32521
  }
32522
+ this.emitBufferedEvents();
30909
32523
  }).on(EngineEvent.SignalResumed, () => {
30910
32524
  this.bufferedEvents = [];
30911
32525
  if (this.state === ConnectionState.Reconnecting || this.isResuming) {
@@ -31736,6 +33350,20 @@ class Room extends eventsExports.EventEmitter {
31736
33350
  return this.remoteParticipants.get(identity);
31737
33351
  }
31738
33352
  }
33353
+ /** The client capabilities this SDK advertises to other participants in its `ClientInfo`. */
33354
+ getClientInfoCapabilities(roomOptions) {
33355
+ var _a;
33356
+ const capabilities = [];
33357
+ if (isFrameMetadataSupported((_a = roomOptions.frameMetadata) !== null && _a !== void 0 ? _a : roomOptions.packetTrailer) || !!this.e2eeManager) {
33358
+ capabilities.push(ClientInfo_Capability.CAP_PACKET_TRAILER);
33359
+ }
33360
+ // Advertise deflate-raw decompression support so peers know they can send us compressed data
33361
+ // streams (gated separately from clientProtocol — see the data streams v2 spec).
33362
+ if (isCompressionStreamSupported()) {
33363
+ capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
33364
+ }
33365
+ return capabilities;
33366
+ }
31739
33367
  registerRpcDataStreamHandler() {
31740
33368
  this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
31741
33369
  var _this4 = this;
@@ -33355,4 +34983,4 @@ const serializers = {
33355
34983
  json,
33356
34984
  raw,
33357
34985
  custom
33358
- };export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
34986
+ };export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map