livekit-client 2.20.2 → 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 (105) 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 +4 -0
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +1993 -791
  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 +4 -0
  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/index.d.ts +1 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/room/RTCEngine.d.ts +28 -64
  16. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  17. package/dist/src/room/Room.d.ts +4 -0
  18. package/dist/src/room/Room.d.ts.map +1 -1
  19. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  20. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  21. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  22. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  23. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  24. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  25. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  26. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  27. package/dist/src/room/data-channel/types.d.ts +12 -0
  28. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  29. package/dist/src/room/data-stream/compression.d.ts +49 -0
  30. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  31. package/dist/src/room/data-stream/constants.d.ts +18 -0
  32. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  33. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  34. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  35. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  36. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  37. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  39. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  40. package/dist/src/room/errors.d.ts +3 -1
  41. package/dist/src/room/errors.d.ts.map +1 -1
  42. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  43. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  44. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  45. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  46. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  47. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  48. package/dist/src/room/types.d.ts +17 -0
  49. package/dist/src/room/types.d.ts.map +1 -1
  50. package/dist/src/room/utils.d.ts +13 -0
  51. package/dist/src/room/utils.d.ts.map +1 -1
  52. package/dist/src/version.d.ts +5 -1
  53. package/dist/src/version.d.ts.map +1 -1
  54. package/dist/ts4.2/index.d.ts +1 -1
  55. package/dist/ts4.2/room/RTCEngine.d.ts +28 -64
  56. package/dist/ts4.2/room/Room.d.ts +4 -0
  57. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  58. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  59. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  60. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  61. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  62. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  63. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  64. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  65. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  66. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  67. package/dist/ts4.2/room/errors.d.ts +3 -1
  68. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  69. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  70. package/dist/ts4.2/room/types.d.ts +17 -0
  71. package/dist/ts4.2/room/utils.d.ts +13 -0
  72. package/dist/ts4.2/version.d.ts +5 -1
  73. package/package.json +2 -2
  74. package/src/index.ts +2 -0
  75. package/src/room/RTCEngine.test.ts +58 -99
  76. package/src/room/RTCEngine.ts +92 -534
  77. package/src/room/Room.test.ts +4 -1
  78. package/src/room/Room.ts +40 -8
  79. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  80. package/src/room/data-channel/DataChannelManager.ts +237 -0
  81. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  82. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  83. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  84. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  85. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  86. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  87. package/src/room/data-channel/types.ts +30 -0
  88. package/src/room/data-stream/compression.ts +117 -0
  89. package/src/room/data-stream/constants.ts +18 -0
  90. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  91. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  92. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  93. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  94. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  95. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  96. package/src/room/errors.ts +6 -0
  97. package/src/room/participant/LocalParticipant.ts +13 -0
  98. package/src/room/participant/RemoteParticipant.ts +19 -5
  99. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  100. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  101. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  102. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  103. package/src/room/types.ts +18 -0
  104. package/src/room/utils.ts +67 -0
  105. 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,
@@ -11883,16 +12160,20 @@ function getMatch(exp, ua) {
11883
12160
  }
11884
12161
  function getOSVersion(ua) {
11885
12162
  return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
11886
- }var version$1 = "2.20.2";const version = version$1;
12163
+ }var version$1 = "2.21.0";const version = version$1;
11887
12164
  const protocolVersion = 17;
11888
12165
  /** Initial client protocol. */
11889
12166
  const CLIENT_PROTOCOL_DEFAULT = 0;
11890
12167
  /** Replaces RPC v1 protocol with a v2 data streams based one to support unlimited request /
11891
12168
  * response payload length. */
11892
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;
11893
12174
  /** The client protocol version indicates what level of support that the client has for
11894
12175
  * client <-> client api interactions. */
11895
- 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. */
11896
12177
  class LivekitError extends Error {
11897
12178
  constructor(code, message, options) {
11898
12179
  super(message || 'an error has occurred');
@@ -12028,6 +12309,10 @@ var DataStreamErrorReason;
12028
12309
  DataStreamErrorReason[DataStreamErrorReason["HandlerAlreadyRegistered"] = 7] = "HandlerAlreadyRegistered";
12029
12310
  // Encryption type mismatch.
12030
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";
12031
12316
  })(DataStreamErrorReason || (DataStreamErrorReason = {}));
12032
12317
  class DataStreamError extends LivekitReasonedError {
12033
12318
  constructor(message, reason) {
@@ -14121,6 +14406,53 @@ function splitUtf8(s, n) {
14121
14406
  }
14122
14407
  return result;
14123
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
+ }
14124
14456
  function extractMaxAgeFromRequestHeaders(headers) {
14125
14457
  var _a;
14126
14458
  const cacheControl = headers.get('Cache-Control');
@@ -17269,110 +17601,35 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17269
17601
  params.set('join_request', bytesToBase64(wrappedBytes).replace(/\+/g, '-').replace(/\//g, '_'));
17270
17602
  return params;
17271
17603
  });
17272
- }class DataPacketBuffer {
17273
- constructor() {
17274
- this.buffer = [];
17275
- this._totalSize = 0;
17276
- this._sentSize = 0;
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;
17277
17612
  }
17278
- push(item) {
17279
- this.buffer.push(item);
17280
- this._totalSize += item.data.byteLength;
17281
- if (item.sent) {
17282
- this._sentSize += item.data.byteLength;
17613
+ set(key, value) {
17614
+ const now = Date.now();
17615
+ if (now - this._lastCleanup > this.ttl / 2) {
17616
+ this.cleanup();
17283
17617
  }
17618
+ const expiresAt = now + this.ttl;
17619
+ this._map.set(key, {
17620
+ value,
17621
+ expiresAt
17622
+ });
17623
+ return this;
17284
17624
  }
17285
- pop() {
17286
- const item = this.buffer.shift();
17287
- if (item) {
17288
- this._totalSize -= item.data.byteLength;
17289
- if (item.sent) {
17290
- this._sentSize -= item.data.byteLength;
17291
- }
17625
+ get(key) {
17626
+ const entry = this._map.get(key);
17627
+ if (!entry) return undefined;
17628
+ if (entry.expiresAt < Date.now()) {
17629
+ this._map.delete(key);
17630
+ return undefined;
17292
17631
  }
17293
- return item;
17294
- }
17295
- getAll() {
17296
- return this.buffer.slice();
17297
- }
17298
- /** Every queued packet not yet handed to the channel, in sequence order. */
17299
- getUnsent() {
17300
- return this.buffer.filter(item => !item.sent);
17301
- }
17302
- /** Marks a single queued packet as handed to the channel. */
17303
- markSent(item) {
17304
- if (!item.sent) {
17305
- item.sent = true;
17306
- this._sentSize += item.data.byteLength;
17307
- }
17308
- }
17309
- /**
17310
- * Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
17311
- * still buffered was sent on the previous channel (or deferred) and must be re-handed to the
17312
- * current one, so none of it counts as sent until the replay actually transmits it.
17313
- */
17314
- markAllUnsent() {
17315
- for (const item of this.buffer) {
17316
- item.sent = false;
17317
- }
17318
- this._sentSize = 0;
17319
- }
17320
- popToSequence(sequence) {
17321
- while (this.buffer.length > 0) {
17322
- const first = this.buffer[0];
17323
- if (first.sequence <= sequence) {
17324
- this.pop();
17325
- } else {
17326
- break;
17327
- }
17328
- }
17329
- }
17330
- alignBufferedAmount(bufferedAmount) {
17331
- while (this.buffer.length > 0) {
17332
- const first = this.buffer[0];
17333
- // Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
17334
- // resume replay — trimming them would silently lose them.
17335
- if (!first.sent) {
17336
- break;
17337
- }
17338
- if (this._sentSize - first.data.byteLength <= bufferedAmount) {
17339
- break;
17340
- }
17341
- this.pop();
17342
- }
17343
- }
17344
- get length() {
17345
- return this.buffer.length;
17346
- }
17347
- }class TTLMap {
17348
- /**
17349
- * @param ttl ttl of the key (ms)
17350
- */
17351
- constructor(ttl) {
17352
- this._map = new Map();
17353
- this._lastCleanup = 0;
17354
- this.ttl = ttl;
17355
- }
17356
- set(key, value) {
17357
- const now = Date.now();
17358
- if (now - this._lastCleanup > this.ttl / 2) {
17359
- this.cleanup();
17360
- }
17361
- const expiresAt = now + this.ttl;
17362
- this._map.set(key, {
17363
- value,
17364
- expiresAt
17365
- });
17366
- return this;
17367
- }
17368
- get(key) {
17369
- const entry = this._map.get(key);
17370
- if (!entry) return undefined;
17371
- if (entry.expiresAt < Date.now()) {
17372
- this._map.delete(key);
17373
- return undefined;
17374
- }
17375
- return entry.value;
17632
+ return entry.value;
17376
17633
  }
17377
17634
  has(key) {
17378
17635
  const entry = this._map.get(key);
@@ -19289,6 +19546,663 @@ class PCTransportManager {
19289
19546
  }();
19290
19547
  });
19291
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
+ }
19292
20206
  }// Check if MediaRecorder is available
19293
20207
  const isMediaRecorderAvailable = typeof MediaRecorder !== 'undefined';
19294
20208
  // Fallback class for environments without MediaRecorder
@@ -21318,27 +22232,14 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
21318
22232
  return new VideoLayer({
21319
22233
  quality,
21320
22234
  width: Math.ceil(width / scale),
21321
- height: Math.ceil(height / scale),
21322
- bitrate: (_c = encoding.maxBitrate) !== null && _c !== void 0 ? _c : 0,
21323
- ssrc: 0
21324
- });
21325
- });
21326
- }const lossyDataChannel = '_lossy';
21327
- const reliableDataChannel = '_reliable';
21328
- const dataTrackDataChannel = '_data_track';
21329
- const minReconnectWait = 2 * 1000;
21330
- const leaveReconnect = 'leave-reconnect';
21331
- const reliabeReceiveStateTTL = 30000;
21332
- // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
21333
- // freely up to the high-water mark; once it's exceeded they block until the browser's
21334
- // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
21335
- // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
21336
- // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
21337
- // the channel (see livekit/client-sdk-js#1995).
21338
- const reliableDataChannelWaterMarkLow = 64 * 1024;
21339
- const reliableDataChannelWaterMarkHigh = 1024 * 1024;
21340
- const lossyDataChannelWaterMarkLow = 8 * 1024;
21341
- const lossyDataChannelWaterMarkHigh = 256 * 1024;
22235
+ height: Math.ceil(height / scale),
22236
+ bitrate: (_c = encoding.maxBitrate) !== null && _c !== void 0 ? _c : 0,
22237
+ ssrc: 0
22238
+ });
22239
+ });
22240
+ }const minReconnectWait = 2 * 1000;
22241
+ const leaveReconnect = 'leave-reconnect';
22242
+ const reliabeReceiveStateTTL = 30000;
21342
22243
  const initialMediaSectionsAudio = 3;
21343
22244
  const initialMediaSectionsVideo = 3;
21344
22245
  var PCState;
@@ -21349,12 +22250,6 @@ var PCState;
21349
22250
  PCState[PCState["Reconnecting"] = 3] = "Reconnecting";
21350
22251
  PCState[PCState["Closed"] = 4] = "Closed";
21351
22252
  })(PCState || (PCState = {}));
21352
- var DataChannelKind;
21353
- (function (DataChannelKind) {
21354
- DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
21355
- DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
21356
- DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
21357
- })(DataChannelKind || (DataChannelKind = {}));
21358
22253
  // Default data-channel max message size (bytes), used when the remote SDP
21359
22254
  // answer does not advertise an `a=max-message-size` attribute (RFC 8841).
21360
22255
  // `0` means "no limit".
@@ -21370,6 +22265,15 @@ class RTCEngine extends eventsExports.EventEmitter {
21370
22265
  get pendingReconnect() {
21371
22266
  return !!this.reconnectTimeout;
21372
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
+ }
21373
22277
  constructor(options) {
21374
22278
  var _a;
21375
22279
  super();
@@ -21396,12 +22300,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21396
22300
  this.shouldFailNext = false;
21397
22301
  this.shouldFailOnV1Path = false;
21398
22302
  this.log = livekitLogger;
21399
- this.reliableDataSequence = 1;
21400
- this.reliableMessageBuffer = new DataPacketBuffer();
21401
22303
  this.reliableReceivedState = new TTLMap(reliabeReceiveStateTTL);
21402
- this.lossyDataStatCurrentBytes = 0;
21403
- this.lossyDataStatByterate = 0;
21404
- this.lossyDataDropCount = 0;
21405
22304
  this.midToTrackId = {};
21406
22305
  /** used to indicate whether the browser is currently waiting to reconnect */
21407
22306
  this.isWaitingForNetworkReconnect = false;
@@ -21412,21 +22311,9 @@ class RTCEngine extends eventsExports.EventEmitter {
21412
22311
  if (!channel) {
21413
22312
  return;
21414
22313
  }
21415
- let handler;
21416
- if (channel.label === reliableDataChannel) {
21417
- _this.reliableDCSub = channel;
21418
- handler = _this.handleDataMessage;
21419
- } else if (channel.label === lossyDataChannel) {
21420
- _this.lossyDCSub = channel;
21421
- handler = _this.handleDataMessage;
21422
- } else if (channel.label === dataTrackDataChannel) {
21423
- _this.dataTrackDCSub = channel;
21424
- handler = _this.handleDataTrackMessage;
21425
- } else {
21426
- return;
22314
+ if (_this.dataChannels.adoptSubscriberChannel(channel)) {
22315
+ _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21427
22316
  }
21428
- _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21429
- channel.onmessage = handler;
21430
22317
  }();
21431
22318
  });
21432
22319
  this.handleDataMessage = message => __awaiter(this, void 0, void 0, function* () {
@@ -21434,19 +22321,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21434
22321
  // make sure to respect incoming data message order by processing message events one after the other
21435
22322
  const unlock = yield this.dataProcessLock.lock();
21436
22323
  try {
21437
- // decode
21438
- let buffer;
21439
- if (message.data instanceof ArrayBuffer) {
21440
- buffer = message.data;
21441
- } else if (message.data instanceof Blob) {
21442
- buffer = yield message.data.arrayBuffer();
21443
- } else {
21444
- this.log.error('unsupported data type', {
21445
- data: message.data
21446
- });
22324
+ const bytes = yield this.decodeDataMessage(message);
22325
+ if (!bytes) {
21447
22326
  return;
21448
22327
  }
21449
- const dp = DataPacket.fromBinary(new Uint8Array(buffer));
22328
+ const dp = DataPacket.fromBinary(bytes);
21450
22329
  if (dp.sequence > 0 && dp.participantSid !== '') {
21451
22330
  const lastSeq = this.reliableReceivedState.get(dp.participantSid);
21452
22331
  if (lastSeq && dp.sequence <= lastSeq) {
@@ -21487,19 +22366,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21487
22366
  }
21488
22367
  });
21489
22368
  this.handleDataTrackMessage = message => __awaiter(this, void 0, void 0, function* () {
21490
- // Decode / normalize into a common format
21491
- let buffer;
21492
- if (message.data instanceof ArrayBuffer) {
21493
- buffer = message.data;
21494
- } else if (message.data instanceof Blob) {
21495
- buffer = yield message.data.arrayBuffer();
21496
- } else {
21497
- this.log.error('unsupported data type', {
21498
- data: message.data
21499
- });
22369
+ const bytes = yield this.decodeDataMessage(message);
22370
+ if (!bytes) {
21500
22371
  return;
21501
22372
  }
21502
- this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
22373
+ this.emit('dataTrackPacketReceived', bytes);
21503
22374
  });
21504
22375
  this.handleDataError = event => {
21505
22376
  // Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as
@@ -21533,9 +22404,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21533
22404
  this.log.error("publisher data channel '".concat(DataChannelKind[kind], "' closed unexpectedly"), this.logContext);
21534
22405
  }
21535
22406
  };
21536
- this.handleBufferedAmountLow = channelKind => {
21537
- this.updateAndEmitDCBufferStatus(channelKind);
21538
- };
21539
22407
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
21540
22408
  // continues to work, we can reconnect to websocket to continue the session
21541
22409
  // after a number of retries, we'll close and give up permanently
@@ -21591,60 +22459,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21591
22459
  this.once(EngineEvent.Disconnected, onDisconnected);
21592
22460
  });
21593
22461
  };
21594
- this.updateAndEmitDCBufferStatus = kind => {
21595
- if (kind === DataChannelKind.RELIABLE) {
21596
- const dc = this.dataChannelForKind(kind);
21597
- if (dc) {
21598
- this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
21599
- }
21600
- }
21601
- try {
21602
- const status = this.isBelowLowWaterMark(kind);
21603
- if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
21604
- this.dcBufferStatus.set(kind, status);
21605
- this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
21606
- }
21607
- } catch (e) {
21608
- this.log.warn('could not update buffer status', {
21609
- error: e
21610
- });
21611
- }
21612
- };
21613
- /**
21614
- * Whether the send buffer has room to accept more data (the send gate). Senders proceed while
21615
- * this is true and block once it goes false.
21616
- */
21617
- this.isBelowHighWaterMark = kind => {
21618
- const dc = this.dataChannelForKind(kind);
21619
- if (!dc) {
21620
- throw new TypeError("Could not get data channel for kind ".concat(kind));
21621
- }
21622
- // because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
21623
- const highMark = kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
21624
- return dc.bufferedAmount <= highMark;
21625
- };
21626
- /**
21627
- * Whether the send buffer has drained to its low-water mark. Drives the public
21628
- * {@link EngineEvent.DCBufferStatusChanged} event.
21629
- */
21630
- this.isBelowLowWaterMark = kind => {
21631
- const dc = this.dataChannelForKind(kind);
21632
- if (!dc) {
21633
- throw new TypeError("Could not get data channel for kind ".concat(kind));
21634
- }
21635
- // because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
21636
- return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
21637
- };
21638
- /** Per-kind lock serializing senders that have to wait for the buffer to drain. */
21639
- this.waitForBufferHeadroomLocks = new Map();
21640
- /**
21641
- * Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
21642
- * stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
21643
- * waiter is parked on the channel object it captured at wait entry; if that object is abandoned
21644
- * its events may never fire again, so the abort is what rejects the waiter and releases the
21645
- * headroom lock instead of stranding every future sender behind it.
21646
- */
21647
- this.waiterAbortControllers = new Map();
21648
22462
  this.onRtpMapAvailable = rtpTypes => {
21649
22463
  const rtpMap = new Map();
21650
22464
  rtpTypes.forEach(rtp => {
@@ -21704,7 +22518,15 @@ class RTCEngine extends eventsExports.EventEmitter {
21704
22518
  this.reconnectPolicy = this.options.reconnectPolicy;
21705
22519
  this.closingLock = new _();
21706
22520
  this.dataProcessLock = new _();
21707
- 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
+ });
21708
22530
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21709
22531
  this.client.onConnectionQuality = update => this.emit(EngineEvent.ConnectionQualityUpdate, update);
21710
22532
  this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
@@ -21860,55 +22682,14 @@ class RTCEngine extends eventsExports.EventEmitter {
21860
22682
  cleanupPeerConnections() {
21861
22683
  return __awaiter(this, void 0, void 0, function* () {
21862
22684
  var _a;
21863
- // Reject parked headroom waiters up front: closing the peer connection is allowed (per spec)
21864
- // to transition data channels to 'closed' without firing events, which would otherwise strand
21865
- // a waiter holding the headroom lock.
21866
- this.invalidateDataChannelWaiters('peer connections cleaned up');
21867
- const dcCleanup = dc => {
21868
- if (!dc) {
21869
- return;
21870
- }
21871
- // Detach the data channel handlers before closing anything. Closing a peer connection tears
21872
- // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
21873
- // channels; if our handlers are still attached at that point, handleDataError logs a spurious
21874
- // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
21875
- // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
21876
- // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
21877
- dc.onbufferedamountlow = null;
21878
- dc.onclose = null;
21879
- dc.onclosing = null;
21880
- dc.onerror = null;
21881
- dc.onmessage = null;
21882
- dc.onopen = null;
21883
- dc.close();
21884
- };
21885
- dcCleanup(this.lossyDC);
21886
- dcCleanup(this.lossyDCSub);
21887
- dcCleanup(this.reliableDC);
21888
- dcCleanup(this.reliableDCSub);
21889
- dcCleanup(this.dataTrackDC);
21890
- dcCleanup(this.dataTrackDCSub);
22685
+ this.dataChannels.teardown();
21891
22686
  yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
21892
22687
  this.pcManager = undefined;
21893
- this.lossyDC = undefined;
21894
- this.lossyDCSub = undefined;
21895
- this.reliableDC = undefined;
21896
- this.reliableDCSub = undefined;
21897
- this.dataTrackDC = undefined;
21898
- this.dataTrackDCSub = undefined;
21899
- this.reliableMessageBuffer = new DataPacketBuffer();
21900
- this.reliableDataSequence = 1;
21901
22688
  this.reliableReceivedState.clear();
21902
22689
  });
21903
22690
  }
21904
22691
  cleanupLossyDataStats() {
21905
- this.lossyDataStatByterate = 0;
21906
- this.lossyDataStatCurrentBytes = 0;
21907
- if (this.lossyDataStatInterval) {
21908
- clearInterval(this.lossyDataStatInterval);
21909
- this.lossyDataStatInterval = undefined;
21910
- }
21911
- this.lossyDataDropCount = 0;
22692
+ this.lossyChannel.stopThresholdTuning();
21912
22693
  }
21913
22694
  cleanupClient() {
21914
22695
  return __awaiter(this, void 0, void 0, function* () {
@@ -21975,7 +22756,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21975
22756
  }
21976
22757
  get dataSubscriberReadyState() {
21977
22758
  var _a;
21978
- 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;
21979
22760
  }
21980
22761
  getConnectedServerAddress() {
21981
22762
  return __awaiter(this, void 0, void 0, function* () {
@@ -22235,69 +23016,22 @@ class RTCEngine extends eventsExports.EventEmitter {
22235
23016
  if (!this.pcManager) {
22236
23017
  return;
22237
23018
  }
22238
- // Waiters parked on the old channel objects would never see another event from them once they
22239
- // are replaced below — reject them so the headroom lock is released and queued senders
22240
- // re-check against the new channels.
22241
- this.invalidateDataChannelWaiters('data channels recreated');
22242
- // clear old data channel callbacks if recreate
22243
- if (this.lossyDC) {
22244
- this.lossyDC.onmessage = null;
22245
- this.lossyDC.onerror = null;
22246
- this.lossyDC.onclose = null;
22247
- }
22248
- if (this.reliableDC) {
22249
- this.reliableDC.onmessage = null;
22250
- this.reliableDC.onerror = null;
22251
- this.reliableDC.onclose = null;
22252
- }
22253
- if (this.dataTrackDC) {
22254
- this.dataTrackDC.onmessage = null;
22255
- this.dataTrackDC.onerror = null;
22256
- this.dataTrackDC.onclose = null;
22257
- }
22258
- // create data channels
22259
- this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
22260
- ordered: false,
22261
- maxRetransmits: 0
22262
- });
22263
- this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
22264
- ordered: true
22265
- });
22266
- this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
22267
- ordered: false,
22268
- maxRetransmits: 0
22269
- });
22270
- // also handle messages over the pub channel, for backwards compatibility
22271
- this.lossyDC.onmessage = this.handleDataMessage;
22272
- this.reliableDC.onmessage = this.handleDataMessage;
22273
- this.dataTrackDC.onmessage = this.handleDataTrackMessage;
22274
- // handle datachannel errors
22275
- this.lossyDC.onerror = this.handleDataError;
22276
- this.reliableDC.onerror = this.handleDataError;
22277
- this.dataTrackDC.onerror = this.handleDataError;
22278
- // detect unexpected publisher data channel closes
22279
- this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
22280
- this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
22281
- this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
22282
- // set up dc buffer threshold - if this is not set, it will default to 0
22283
- this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
22284
- this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
22285
- this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
22286
- // handle buffer amount low events
22287
- this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
22288
- this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
22289
- this.dataTrackDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
22290
- this.cleanupLossyDataStats();
22291
- this.lossyDataStatInterval = setInterval(() => {
22292
- this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
22293
- this.lossyDataStatCurrentBytes = 0;
22294
- const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
22295
- if (dc) {
22296
- // control buffered latency to ~100ms
22297
- const threshold = this.lossyDataStatByterate / 10;
22298
- dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelWaterMarkLow), lossyDataChannelWaterMarkHigh);
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);
22299
23026
  }
22300
- }, 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
+ });
22301
23035
  }
22302
23036
  createSender(track, opts, encodings) {
22303
23037
  return __awaiter(this, void 0, void 0, function* () {
@@ -22549,7 +23283,6 @@ class RTCEngine extends eventsExports.EventEmitter {
22549
23283
  }
22550
23284
  resumeConnection(reason) {
22551
23285
  return __awaiter(this, void 0, void 0, function* () {
22552
- var _a;
22553
23286
  if (!this.url || !this.token) {
22554
23287
  // permanent failure, don't attempt reconnection
22555
23288
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -22603,7 +23336,8 @@ class RTCEngine extends eventsExports.EventEmitter {
22603
23336
  this.client.setReconnected();
22604
23337
  // recreate publish datachannel if it's id is null
22605
23338
  // (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
22606
- 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) {
22607
23341
  this.createDataChannels();
22608
23342
  }
22609
23343
  if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
@@ -22660,7 +23394,8 @@ class RTCEngine extends eventsExports.EventEmitter {
22660
23394
  });
22661
23395
  }
22662
23396
  /* @internal */
22663
- sendDataPacket(packet, kind) {
23397
+ sendDataPacket(packet, /** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
23398
+ kind) {
22664
23399
  return __awaiter(this, void 0, void 0, function* () {
22665
23400
  var _a, _b;
22666
23401
  // make sure we do have a data connection
@@ -22680,8 +23415,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22680
23415
  }
22681
23416
  }
22682
23417
  if (kind === DataChannelKind.RELIABLE) {
22683
- packet.sequence = this.reliableDataSequence;
22684
- this.reliableDataSequence += 1;
23418
+ packet.sequence = this.reliableChannel.nextSequence();
22685
23419
  }
22686
23420
  const msg = packet.toBinary();
22687
23421
  // Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
@@ -22691,249 +23425,48 @@ class RTCEngine extends eventsExports.EventEmitter {
22691
23425
  if (typeof maxPublisherMessageSizeBytes !== 'undefined' && maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ && msg.byteLength > maxPublisherMessageSizeBytes) {
22692
23426
  throw new PublishDataError("cannot publish data packet larger than ".concat(maxPublisherMessageSizeBytes, " bytes (got ").concat(msg.byteLength, ")"));
22693
23427
  }
22694
- switch (kind) {
22695
- case DataChannelKind.LOSSY:
22696
- case DataChannelKind.DATA_TRACK_LOSSY:
22697
- return this.sendLossyBytes(msg, kind);
22698
- case DataChannelKind.RELIABLE:
22699
- {
22700
- if (this.attemptingReconnect) {
22701
- // A reconnect is already underway — queue for the resume replay instead of parking on a
22702
- // channel that is being torn down. The send resolves; delivery is deferred to the replay.
22703
- this.reliableMessageBuffer.push({
22704
- data: msg,
22705
- sequence: packet.sequence,
22706
- sent: false
22707
- });
22708
- return;
22709
- }
22710
- const dc = this.dataChannelForKind(kind);
22711
- if (dc) {
22712
- try {
22713
- yield this.waitForBufferHeadroomWithLock(kind);
22714
- } catch (error) {
22715
- if (this.isClosed) {
22716
- // No replay is coming after an engine close — surface the failure.
22717
- throw error;
22718
- }
22719
- // Transient teardown (the channel closed or was replaced while we waited): the
22720
- // reliable channel promises delivery across resume, so queue the packet for the
22721
- // replay instead of rejecting a send the app can't meaningfully retry.
22722
- this.reliableMessageBuffer.push({
22723
- data: msg,
22724
- sequence: packet.sequence,
22725
- sent: false
22726
- });
22727
- return;
22728
- }
22729
- if (this.attemptingReconnect) {
22730
- // A reconnect began while we waited for headroom — same deal as above.
22731
- this.reliableMessageBuffer.push({
22732
- data: msg,
22733
- sequence: packet.sequence,
22734
- sent: false
22735
- });
22736
- return;
22737
- }
22738
- this.reliableMessageBuffer.push({
22739
- data: msg,
22740
- sequence: packet.sequence,
22741
- sent: true
22742
- });
22743
- dc.send(msg);
22744
- }
22745
- this.updateAndEmitDCBufferStatus(kind);
22746
- break;
22747
- }
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);
22748
23434
  }
22749
23435
  });
22750
23436
  }
22751
- /* @internal */
22752
- sendLossyBytes(bytes_1, kind_1) {
22753
- return __awaiter(this, arguments, void 0, function (bytes, kind) {
22754
- var _this3 = this;
22755
- let bufferStatusFullBehavior = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'drop';
22756
- return function* () {
22757
- // Make sure we do have a data connection. This matters most for the direct data-track path
22758
- // (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
22759
- // the channel to be open and on lazily negotiated publisher connections — is what kicks the
22760
- // negotiation off in the first place. The call is memoized, so the steady-state cost is one
22761
- // await on an already-resolved promise.
22762
- yield _this3.ensurePublisherConnected(kind);
22763
- const dc = _this3.dataChannelForKind(kind);
22764
- try {
22765
- if (dc) {
22766
- // Depending on the exact circumstance that data is being sent, either drop or wait for the
22767
- // buffer to drain below the high-water mark before continuing.
22768
- switch (bufferStatusFullBehavior) {
22769
- case 'wait':
22770
- if (!_this3.isBelowHighWaterMark(kind)) {
22771
- yield _this3.waitForBufferHeadroomWithLock(kind);
22772
- }
22773
- break;
22774
- case 'drop':
22775
- // we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
22776
- if (!_this3.isBelowLowWaterMark(kind)) {
22777
- // Drop messages to reduce latency
22778
- _this3.lossyDataDropCount += 1;
22779
- if (_this3.lossyDataDropCount % 100 === 0) {
22780
- _this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
22781
- }
22782
- return;
22783
- }
22784
- }
22785
- if (kind === DataChannelKind.LOSSY) {
22786
- // The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
22787
- // bytes here would inflate it and let the lossy channel buffer far more latency than the
22788
- // ~100ms the tuning targets.
22789
- _this3.lossyDataStatCurrentBytes += bytes.byteLength;
22790
- }
22791
- if (_this3.attemptingReconnect) {
22792
- return;
22793
- }
22794
- dc.send(bytes);
22795
- }
22796
- _this3.updateAndEmitDCBufferStatus(kind);
22797
- } catch (error) {
22798
- // ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
22799
- if (error instanceof TypeError) {
22800
- _this3.log.error(error);
22801
- } else {
22802
- throw error;
22803
- }
22804
- }
22805
- }();
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);
22806
23451
  });
22807
23452
  }
22808
23453
  resendReliableMessagesForResume(lastMessageSeq) {
22809
23454
  return __awaiter(this, void 0, void 0, function* () {
22810
23455
  yield this.ensurePublisherConnected(DataChannelKind.RELIABLE);
22811
- const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
22812
- if (dc) {
22813
- this.reliableMessageBuffer.popToSequence(lastMessageSeq);
22814
- // Hold the headroom lock across the whole replay. Releasing it between messages would let a
22815
- // concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
22816
- // queued on the lock — hit the wire mid-replay, and receivers would then discard the
22817
- // remaining lower-sequence resent messages as duplicates.
22818
- const unlock = yield this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
22819
- try {
22820
- // Everything left after the ack cutoff must be re-handed to the current channel.
22821
- this.reliableMessageBuffer.markAllUnsent();
22822
- // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
22823
- // sent:false) during our own awaits appends after this pass started, so we pick it up on
22824
- // the next one. We mark each packet only once we've actually handed it to the channel — a
22825
- // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
22826
- // and a later alignBufferedAmount would then drop it for good. If the loop throws
22827
- // mid-drain, unsent entries keep their flag and the next replay picks them up.
22828
- for (let batch = this.reliableMessageBuffer.getUnsent(); batch.length > 0; batch = this.reliableMessageBuffer.getUnsent()) {
22829
- for (const item of batch) {
22830
- // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
22831
- // We already hold the lock across the whole replay, so use the lock-free wait.
22832
- yield this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
22833
- dc.send(item.data);
22834
- this.reliableMessageBuffer.markSent(item);
22835
- }
22836
- }
22837
- } finally {
22838
- unlock();
22839
- }
22840
- }
22841
- this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
23456
+ yield this.reliableChannel.replay(lastMessageSeq);
22842
23457
  });
22843
23458
  }
22844
- waiterAbortSignal(kind) {
22845
- let controller = this.waiterAbortControllers.get(kind);
22846
- if (!controller) {
22847
- controller = new AbortController();
22848
- this.waiterAbortControllers.set(kind, controller);
22849
- }
22850
- return controller.signal;
22851
- }
22852
- /** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
22853
- invalidateDataChannelWaiters(reason) {
22854
- for (const controller of this.waiterAbortControllers.values()) {
22855
- controller.abort(reason);
22856
- }
22857
- this.waiterAbortControllers.clear();
22858
- }
22859
- /** Acquires the per-kind headroom lock, resolving with the unlock function. */
22860
- getBufferHeadroomLock(kind) {
22861
- let lock = this.waitForBufferHeadroomLocks.get(kind);
22862
- if (!lock) {
22863
- lock = new _();
22864
- this.waitForBufferHeadroomLocks.set(kind, lock);
22865
- }
22866
- return lock;
22867
- }
22868
- /**
22869
- * Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
22870
- * common single-send entry point. Callers that need to hold the lock across several sends (the
22871
- * resume replay) acquire it via {@link getBufferHeadroomLock} and call
22872
- * {@link waitForBufferHeadroomWithoutLock} per message instead.
22873
- */
22874
- waitForBufferHeadroomWithLock(kind) {
22875
- return __awaiter(this, void 0, void 0, function* () {
22876
- const unlock = yield this.getBufferHeadroomLock(kind).lock();
22877
- try {
22878
- yield this.waitForBufferHeadroomWithoutLock(kind);
22879
- } finally {
22880
- unlock();
22881
- }
22882
- });
23459
+ /** The flow-control gate for `kind` — see {@link FlowControlledDataChannel}. */
23460
+ flowControlFor(kind) {
23461
+ return this.dataChannels.channelFor(kind);
22883
23462
  }
22884
23463
  /**
22885
- * Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
22886
- * needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
22887
- * already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
22888
- * holds the lock across its whole batch and calls this per message so no other sender can
22889
- * interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
22890
- *
22891
- * Serializing through the per-kind lock means that, once the buffer drains, queued callers
22892
- * refill it one at a time (up to the high-water mark) rather than all at once and overflowing
22893
- * the SCTP send buffer (see livekit/client-sdk-js#1995).
23464
+ * Resolves once the caller may send on the `kind` channel see
23465
+ * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
22894
23466
  */
22895
- waitForBufferHeadroomWithoutLock(kind) {
23467
+ waitForBufferHeadroom(kind) {
22896
23468
  return __awaiter(this, void 0, void 0, function* () {
22897
- if (this.isClosed) {
22898
- throw new UnexpectedConnectionState('engine closed');
22899
- }
22900
- if (this.isBelowHighWaterMark(kind)) {
22901
- return;
22902
- }
22903
- const dc = this.dataChannelForKind(kind);
22904
- if (!dc) {
22905
- throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
22906
- }
22907
- const abortSignal = this.waiterAbortSignal(kind);
22908
- yield new TypedPromise((resolve, reject) => {
22909
- const onBufferedAmountLow = () => {
22910
- cleanup();
22911
- resolve();
22912
- };
22913
- const onDCClose = () => {
22914
- cleanup();
22915
- reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " closed while draining the buffer")));
22916
- };
22917
- const onAbort = () => {
22918
- cleanup();
22919
- reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " was replaced or torn down while waiting for headroom")));
22920
- };
22921
- const cleanup = () => {
22922
- dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
22923
- dc.removeEventListener('close', onDCClose);
22924
- abortSignal.removeEventListener('abort', onAbort);
22925
- };
22926
- if (abortSignal.aborted) {
22927
- onAbort();
22928
- return;
22929
- }
22930
- dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
22931
- // Proxy along any error caused by the data channel closing while we wait.
22932
- dc.addEventListener('close', onDCClose);
22933
- // The channel object we're parked on can be abandoned without ever firing another event
22934
- // (e.g. createDataChannels replacing it); the abort is our way out.
22935
- abortSignal.addEventListener('abort', onAbort);
22936
- });
23469
+ return this.flowControlFor(kind).waitForHeadroomWithLock();
22937
23470
  });
22938
23471
  }
22939
23472
  /**
@@ -22941,40 +23474,40 @@ class RTCEngine extends eventsExports.EventEmitter {
22941
23474
  */
22942
23475
  ensureDataTransportConnected(kind_1) {
22943
23476
  return __awaiter(this, arguments, void 0, function (kind) {
22944
- var _this4 = this;
23477
+ var _this3 = this;
22945
23478
  let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.subscriberPrimary;
22946
23479
  return function* () {
22947
23480
  var _a;
22948
- if (!_this4.pcManager) {
23481
+ if (!_this3.pcManager) {
22949
23482
  throw new UnexpectedConnectionState('PC manager is closed');
22950
23483
  }
22951
- const transport = subscriber ? _this4.pcManager.subscriber : _this4.pcManager.publisher;
23484
+ const transport = subscriber ? _this3.pcManager.subscriber : _this3.pcManager.publisher;
22952
23485
  const transportName = subscriber ? 'Subscriber' : 'Publisher';
22953
23486
  if (!transport) {
22954
23487
  throw ConnectionError.internal("".concat(transportName, " connection not set"));
22955
23488
  }
22956
23489
  let needNegotiation = false;
22957
- if (!subscriber && !_this4.dataChannelForKind(kind, subscriber)) {
22958
- _this4.createDataChannels();
23490
+ if (!subscriber && !_this3.dataChannelForKind(kind, subscriber)) {
23491
+ _this3.createDataChannels();
22959
23492
  needNegotiation = true;
22960
23493
  }
22961
- 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') {
22962
23495
  needNegotiation = true;
22963
23496
  }
22964
23497
  if (needNegotiation) {
22965
23498
  // start negotiation
22966
- _this4.negotiate().catch(err => {
22967
- _this4.log.error(err);
23499
+ _this3.negotiate().catch(err => {
23500
+ _this3.log.error(err);
22968
23501
  });
22969
23502
  }
22970
- const targetChannel = _this4.dataChannelForKind(kind, subscriber);
23503
+ const targetChannel = _this3.dataChannelForKind(kind, subscriber);
22971
23504
  if ((targetChannel === null || targetChannel === void 0 ? void 0 : targetChannel.readyState) === 'open') {
22972
23505
  return;
22973
23506
  }
22974
23507
  // wait until ICE connected
22975
- const endTime = new Date().getTime() + _this4.peerConnectionTimeout;
23508
+ const endTime = new Date().getTime() + _this3.peerConnectionTimeout;
22976
23509
  while (new Date().getTime() < endTime) {
22977
- 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') {
22978
23511
  return;
22979
23512
  }
22980
23513
  yield sleep(50);
@@ -23017,7 +23550,7 @@ class RTCEngine extends eventsExports.EventEmitter {
23017
23550
  }
23018
23551
  this.pcManager.requirePublisher();
23019
23552
  // don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
23020
- if (this.pcManager.publisher.getTransceivers().length == 0 && !this.lossyDC && !this.reliableDC && !this.dataTrackDC) {
23553
+ if (this.pcManager.publisher.getTransceivers().length == 0 && !this.dataChannels.hasPublisherChannels) {
23021
23554
  this.createDataChannels();
23022
23555
  }
23023
23556
  const abortController = new AbortController();
@@ -23061,26 +23594,7 @@ class RTCEngine extends eventsExports.EventEmitter {
23061
23594
  });
23062
23595
  }
23063
23596
  dataChannelForKind(kind, sub) {
23064
- switch (kind) {
23065
- case DataChannelKind.RELIABLE:
23066
- if (!sub) {
23067
- return this.reliableDC;
23068
- } else {
23069
- return this.reliableDCSub;
23070
- }
23071
- case DataChannelKind.LOSSY:
23072
- if (!sub) {
23073
- return this.lossyDC;
23074
- } else {
23075
- return this.lossyDCSub;
23076
- }
23077
- case DataChannelKind.DATA_TRACK_LOSSY:
23078
- if (!sub) {
23079
- return this.dataTrackDC;
23080
- } else {
23081
- return this.dataTrackDCSub;
23082
- }
23083
- }
23597
+ return this.dataChannels.getHandle(kind, sub);
23084
23598
  }
23085
23599
  /** @internal */
23086
23600
  sendSyncState(remoteTracks, localTracks, localDataTrackInfos) {
@@ -23408,7 +23922,90 @@ RegionUrlProvider.connectionTrackers = new Map();
23408
23922
  RegionUrlProvider.fetchLock = new _();
23409
23923
  function getCloudConfigUrl(serverUrl) {
23410
23924
  return "".concat(serverUrl.protocol.replace('ws', 'http'), "//").concat(serverUrl.host, "/settings");
23411
- }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 {
23412
24009
  get info() {
23413
24010
  return this._info;
23414
24011
  }
@@ -23450,6 +24047,7 @@ class ByteStreamReader extends BaseStreamReader {
23450
24047
  };
23451
24048
  return {
23452
24049
  next: () => __awaiter(this, void 0, void 0, function* () {
24050
+ var _a;
23453
24051
  try {
23454
24052
  const signal = this.signal;
23455
24053
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -23470,6 +24068,9 @@ class ByteStreamReader extends BaseStreamReader {
23470
24068
  });
23471
24069
  if (result.done) {
23472
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
+ }
23473
24074
  return {
23474
24075
  done: true,
23475
24076
  value: undefined
@@ -23587,6 +24188,7 @@ class TextStreamReader extends BaseStreamReader {
23587
24188
  };
23588
24189
  return {
23589
24190
  next: () => __awaiter(this, void 0, void 0, function* () {
24191
+ var _a;
23590
24192
  try {
23591
24193
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
23592
24194
  throw signal.reason;
@@ -23606,6 +24208,9 @@ class TextStreamReader extends BaseStreamReader {
23606
24208
  });
23607
24209
  if (result.done) {
23608
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
+ }
23609
24214
  return {
23610
24215
  done: true,
23611
24216
  value: undefined
@@ -23684,6 +24289,7 @@ class TextStreamReader extends BaseStreamReader {
23684
24289
  }
23685
24290
  }class IncomingDataStreamManager {
23686
24291
  constructor() {
24292
+ let maxPayloadByteLength = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_MAX_PAYLOAD_BYTE_LENGTH;
23687
24293
  this.log = livekitLogger;
23688
24294
  this.byteStreamControllers = new Map();
23689
24295
  this.textStreamControllers = new Map();
@@ -23691,6 +24297,7 @@ class TextStreamReader extends BaseStreamReader {
23691
24297
  this.textStreamHandlers = new Map();
23692
24298
  this.isConnected = false;
23693
24299
  this.bufferedPackets = [];
24300
+ this.maxPayloadByteLength = maxPayloadByteLength;
23694
24301
  }
23695
24302
  setConnected(connected) {
23696
24303
  this.isConnected = connected;
@@ -23773,74 +24380,150 @@ class TextStreamReader extends BaseStreamReader {
23773
24380
  }
23774
24381
  handleStreamHeader(streamHeader, participantIdentity, encryptionType) {
23775
24382
  var _a;
23776
- if (streamHeader.contentHeader.case === 'byteHeader') {
23777
- const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
23778
- if (!streamHandlerCallback) {
23779
- this.log.debug('ignoring incoming byte stream due to no handler for topic', streamHeader.topic);
23780
- return;
23781
- }
23782
- let streamController;
23783
- const info = {
23784
- id: streamHeader.streamId,
23785
- name: (_a = streamHeader.contentHeader.value.name) !== null && _a !== void 0 ? _a : 'unknown',
23786
- mimeType: streamHeader.mimeType,
23787
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
23788
- topic: streamHeader.topic,
23789
- timestamp: bigIntToNumber(streamHeader.timestamp),
23790
- attributes: streamHeader.attributes,
23791
- encryptionType
23792
- };
23793
- const stream = new ReadableStream({
23794
- start: controller => {
23795
- streamController = controller;
23796
- if (this.textStreamControllers.has(streamHeader.streamId)) {
23797
- 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;
24422
+ }
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;
23798
24432
  }
23799
- this.byteStreamControllers.set(streamHeader.streamId, {
23800
- info,
23801
- controller: streamController,
23802
- startTime: Date.now(),
23803
- sendingParticipantIdentity: participantIdentity
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
+ }
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
23804
24452
  });
24453
+ return;
23805
24454
  }
23806
- });
23807
- streamHandlerCallback(new ByteStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
23808
- identity: participantIdentity
23809
- });
23810
- } else if (streamHeader.contentHeader.case === 'textHeader') {
23811
- const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
23812
- if (!streamHandlerCallback) {
23813
- this.log.debug('ignoring incoming text stream due to no handler for topic', streamHeader.topic);
23814
- return;
23815
- }
23816
- let streamController;
23817
- const info = {
23818
- id: streamHeader.streamId,
23819
- mimeType: streamHeader.mimeType,
23820
- size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
23821
- topic: streamHeader.topic,
23822
- timestamp: Number(streamHeader.timestamp),
23823
- attributes: streamHeader.attributes,
23824
- encryptionType,
23825
- attachedStreamIds: streamHeader.contentHeader.value.attachedStreamIds
23826
- };
23827
- const stream = new ReadableStream({
23828
- start: controller => {
23829
- streamController = controller;
23830
- if (this.textStreamControllers.has(streamHeader.streamId)) {
23831
- 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;
24461
+ }
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;
23832
24504
  }
23833
- this.textStreamControllers.set(streamHeader.streamId, {
23834
- info,
23835
- controller: streamController,
23836
- startTime: Date.now(),
23837
- sendingParticipantIdentity: participantIdentity
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
+ }
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
23838
24524
  });
24525
+ return;
23839
24526
  }
23840
- });
23841
- streamHandlerCallback(new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
23842
- identity: participantIdentity
23843
- });
23844
24527
  }
23845
24528
  }
23846
24529
  handleStreamChunk(chunk, encryptionType) {
@@ -23870,9 +24553,15 @@ class TextStreamReader extends BaseStreamReader {
23870
24553
  textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23871
24554
  } else {
23872
24555
  textBuffer.info.attributes = Object.assign(Object.assign({}, textBuffer.info.attributes), trailer.attributes);
23873
- textBuffer.controller.close();
23874
- 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
+ }
23875
24563
  }
24564
+ this.textStreamControllers.delete(trailer.streamId);
23876
24565
  }
23877
24566
  const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
23878
24567
  if (fileBuffer) {
@@ -23880,11 +24569,174 @@ class TextStreamReader extends BaseStreamReader {
23880
24569
  fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23881
24570
  } else {
23882
24571
  fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
23883
- fileBuffer.controller.close();
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
+ }
23884
24579
  }
23885
24580
  this.byteStreamControllers.delete(trailer.streamId);
23886
24581
  }
23887
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;
24697
+ }
24698
+ }
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);
24736
+ }
24737
+ controller.enqueue(value);
24738
+ }
24739
+ });
23888
24740
  }class BaseStreamWriter {
23889
24741
  constructor(writableStream, info, onClose) {
23890
24742
  this.writableStream = writableStream;
@@ -23905,15 +24757,70 @@ class TextStreamReader extends BaseStreamReader {
23905
24757
  }
23906
24758
  }
23907
24759
  class TextStreamWriter extends BaseStreamWriter {}
23908
- 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();
23909
24813
  /**
23910
24814
  * Manages sending custom user data via data channels.
23911
24815
  * @internal
23912
24816
  */
23913
24817
  class OutgoingDataStreamManager {
23914
- constructor(engine, log) {
24818
+ constructor(engine, log, getRemoteParticipantClientProtocol, getRemoteParticipantCapabilities, getAllRemoteParticipantIdentities) {
23915
24819
  this.engine = engine;
23916
24820
  this.log = log;
24821
+ this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
24822
+ this.getRemoteParticipantCapabilities = getRemoteParticipantCapabilities;
24823
+ this.getAllRemoteParticipantIdentities = getAllRemoteParticipantIdentities;
23917
24824
  }
23918
24825
  setupEngine(engine) {
23919
24826
  this.engine = engine;
@@ -23921,42 +24828,236 @@ class OutgoingDataStreamManager {
23921
24828
  /** {@inheritDoc LocalParticipant.sendText} */
23922
24829
  sendText(text, options) {
23923
24830
  return __awaiter(this, void 0, void 0, function* () {
23924
- var _a;
24831
+ var _a, _b, _c, _d, _e;
23925
24832
  const streamId = crypto.randomUUID();
23926
- const textInBytes = new TextEncoder().encode(text);
24833
+ const textInBytes = textEncoder.encode(text);
23927
24834
  const totalTextLength = textInBytes.byteLength;
23928
- const fileIds = (_a = options === null || options === void 0 ? void 0 : options.attachments) === null || _a === void 0 ? void 0 : _a.map(() => crypto.randomUUID());
23929
- 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);
23930
24881
  const handleProgress = (progress, idx) => {
23931
24882
  var _a;
23932
24883
  progresses[idx] = progress;
23933
- const totalProgress = progresses.reduce((acc, val) => acc + val, 0);
23934
- (_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);
23935
24885
  };
23936
- const writer = yield this.streamText({
23937
- streamId,
23938
- totalSize: totalTextLength,
23939
- destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities,
23940
- topic: options === null || options === void 0 ? void 0 : options.topic,
23941
- attachedStreamIds: fileIds,
23942
- attributes: options === null || options === void 0 ? void 0 : options.attributes
23943
- });
23944
- yield writer.write(text);
23945
- // set text part of progress to 1
23946
- handleProgress(1, 0);
23947
- 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
+ }
23948
24914
  if ((options === null || options === void 0 ? void 0 : options.attachments) && fileIds) {
23949
24915
  yield Promise.all(options.attachments.map((file, idx) => __awaiter(this, void 0, void 0, function* () {
23950
24916
  return this._sendFile(fileIds[idx], file, {
23951
24917
  topic: options.topic,
23952
24918
  mimeType: file.type,
24919
+ destinationIdentities: options.destinationIdentities,
24920
+ compress: options.compress,
23953
24921
  onProgress: progress => {
23954
24922
  handleProgress(progress, idx + 1);
23955
24923
  }
23956
24924
  });
23957
24925
  })));
23958
24926
  }
23959
- 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);
23960
25061
  });
23961
25062
  }
23962
25063
  /**
@@ -23966,6 +25067,7 @@ class OutgoingDataStreamManager {
23966
25067
  return __awaiter(this, void 0, void 0, function* () {
23967
25068
  var _a, _b, _c;
23968
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;
23969
25071
  const info = {
23970
25072
  id: streamId,
23971
25073
  mimeType: 'text/plain',
@@ -23976,39 +25078,20 @@ class OutgoingDataStreamManager {
23976
25078
  encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE,
23977
25079
  attachedStreamIds: options === null || options === void 0 ? void 0 : options.attachedStreamIds
23978
25080
  };
23979
- const header = new DataStream_Header({
23980
- streamId,
23981
- mimeType: info.mimeType,
23982
- topic: info.topic,
23983
- timestamp: numberToBigInt(info.timestamp),
23984
- totalLength: numberToBigInt(info.size),
23985
- attributes: info.attributes,
23986
- contentHeader: {
23987
- case: 'textHeader',
23988
- value: new DataStream_TextHeader({
23989
- version: options === null || options === void 0 ? void 0 : options.version,
23990
- attachedStreamIds: info.attachedStreamIds,
23991
- replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
23992
- operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
23993
- })
23994
- }
23995
- });
23996
- const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
23997
- const packet = new DataPacket({
23998
- destinationIdentities,
23999
- value: {
24000
- case: 'streamHeader',
24001
- value: header
24002
- }
24003
- });
24004
- 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);
24005
25084
  let chunkId = 0;
24006
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.
24007
25091
  const writableStream = new WritableStream({
24008
- // Implement the sink
24009
25092
  write(text) {
24010
25093
  return __awaiter(this, void 0, void 0, function* () {
24011
- for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE)) {
25094
+ for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE_BYTES)) {
24012
25095
  const chunk = new DataStream_Chunk({
24013
25096
  content: textByteChunk,
24014
25097
  streamId,
@@ -24028,17 +25111,7 @@ class OutgoingDataStreamManager {
24028
25111
  },
24029
25112
  close() {
24030
25113
  return __awaiter(this, void 0, void 0, function* () {
24031
- const trailer = new DataStream_Trailer({
24032
- streamId
24033
- });
24034
- const trailerPacket = new DataPacket({
24035
- destinationIdentities,
24036
- value: {
24037
- case: 'streamTrailer',
24038
- value: trailer
24039
- }
24040
- });
24041
- yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25114
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
24042
25115
  });
24043
25116
  },
24044
25117
  abort(err) {
@@ -24049,6 +25122,8 @@ class OutgoingDataStreamManager {
24049
25122
  let onEngineClose = () => __awaiter(this, void 0, void 0, function* () {
24050
25123
  yield writer.close();
24051
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.
24052
25127
  engine.once(EngineEvent.Closing, onEngineClose);
24053
25128
  const writer = new TextStreamWriter(writableStream, info, () => this.engine.off(EngineEvent.Closing, onEngineClose));
24054
25129
  return writer;
@@ -24063,29 +25138,47 @@ class OutgoingDataStreamManager {
24063
25138
  };
24064
25139
  });
24065
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
+ */
24066
25148
  _sendFile(streamId, file, options) {
24067
25149
  return __awaiter(this, void 0, void 0, function* () {
24068
- var _a;
24069
- const writer = yield this.streamBytes({
24070
- streamId,
24071
- 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,
24072
25155
  name: file.name,
24073
- mimeType: (_a = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _a !== void 0 ? _a : file.type,
24074
- topic: options === null || options === void 0 ? void 0 : options.topic,
24075
- 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
24076
25172
  });
24077
- const reader = file.stream().getReader();
24078
- while (true) {
24079
- const _yield$reader$read = yield reader.read(),
24080
- done = _yield$reader$read.done,
24081
- value = _yield$reader$read.value;
24082
- if (done) {
24083
- break;
24084
- }
24085
- 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);
24086
25180
  }
24087
- yield writer.close();
24088
- return writer.info;
25181
+ return info;
24089
25182
  });
24090
25183
  }
24091
25184
  streamBytes(options) {
@@ -24103,32 +25196,18 @@ class OutgoingDataStreamManager {
24103
25196
  name: (_d = options === null || options === void 0 ? void 0 : options.name) !== null && _d !== void 0 ? _d : 'unknown',
24104
25197
  encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
24105
25198
  };
24106
- const header = new DataStream_Header({
24107
- totalLength: numberToBigInt(info.size),
24108
- mimeType: info.mimeType,
24109
- streamId,
24110
- topic: info.topic,
24111
- timestamp: numberToBigInt(Date.now()),
24112
- attributes: info.attributes,
24113
- contentHeader: {
24114
- case: 'byteHeader',
24115
- value: new DataStream_ByteHeader({
24116
- name: info.name
24117
- })
24118
- }
24119
- });
24120
- const packet = new DataPacket({
24121
- destinationIdentities,
24122
- value: {
24123
- case: 'streamHeader',
24124
- value: header
24125
- }
24126
- });
24127
- yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25199
+ const header = buildByteStreamHeader(info);
25200
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25201
+ yield sendHeaderPacket(this.engine, packet);
24128
25202
  let chunkId = 0;
24129
25203
  const writeMutex = new _();
24130
25204
  const engine = this.engine;
24131
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.
24132
25211
  const writableStream = new WritableStream({
24133
25212
  write(chunk) {
24134
25213
  return __awaiter(this, void 0, void 0, function* () {
@@ -24136,7 +25215,7 @@ class OutgoingDataStreamManager {
24136
25215
  let byteOffset = 0;
24137
25216
  try {
24138
25217
  while (byteOffset < chunk.byteLength) {
24139
- const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
25218
+ const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE_BYTES);
24140
25219
  const chunkPacket = new DataPacket({
24141
25220
  destinationIdentities,
24142
25221
  value: {
@@ -24159,17 +25238,7 @@ class OutgoingDataStreamManager {
24159
25238
  },
24160
25239
  close() {
24161
25240
  return __awaiter(this, void 0, void 0, function* () {
24162
- const trailer = new DataStream_Trailer({
24163
- streamId
24164
- });
24165
- const trailerPacket = new DataPacket({
24166
- destinationIdentities,
24167
- value: {
24168
- case: 'streamTrailer',
24169
- value: trailer
24170
- }
24171
- });
24172
- yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25241
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
24173
25242
  });
24174
25243
  },
24175
25244
  abort(err) {
@@ -24180,6 +25249,109 @@ class OutgoingDataStreamManager {
24180
25249
  return byteWriter;
24181
25250
  });
24182
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
+ });
24183
25355
  }/**
24184
25356
  * Implementation of AbortSignal.any
24185
25357
  * Creates a signal that will be aborted when any of the given signals is aborted.
@@ -26670,7 +27842,7 @@ class RpcClientManager extends eventsExports.EventEmitter {
26670
27842
  return __awaiter(this, void 0, void 0, function* () {
26671
27843
  if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26672
27844
  // Send payload as a data stream - a "version 2" rpc request.
26673
- const writer = yield this.outgoingDataStreamManager.streamText({
27845
+ yield this.outgoingDataStreamManager.sendText(payload, {
26674
27846
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
26675
27847
  destinationIdentities: [destinationIdentity],
26676
27848
  attributes: {
@@ -26680,8 +27852,6 @@ class RpcClientManager extends eventsExports.EventEmitter {
26680
27852
  [RpcRequestAttrs.RPC_REQUEST_VERSION]: "".concat(RPC_VERSION_V2)
26681
27853
  }
26682
27854
  });
26683
- yield writer.write(payload);
26684
- yield writer.close();
26685
27855
  return;
26686
27856
  }
26687
27857
  // Fallback to sending a literal RpcRequest - a "version 1" rpc request.
@@ -26955,15 +28125,13 @@ class RpcServerManager extends eventsExports.EventEmitter {
26955
28125
  const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
26956
28126
  if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26957
28127
  // Send response as a data stream
26958
- const writer = yield this.outgoingDataStreamManager.streamText({
28128
+ yield this.outgoingDataStreamManager.sendText(payload, {
26959
28129
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
26960
28130
  destinationIdentities: [destinationIdentity],
26961
28131
  attributes: {
26962
28132
  [RpcRequestAttrs.RPC_REQUEST_ID]: requestId
26963
28133
  }
26964
28134
  });
26965
- yield writer.write(payload);
26966
- yield writer.close();
26967
28135
  return;
26968
28136
  }
26969
28137
  // Legacy client: enforce size limit and send uncompressed payload inline
@@ -29439,6 +30607,18 @@ class Participant extends eventsExports.EventEmitter {
29439
30607
  return this.roomOutgoingDataStreamManager.sendFile(file, options);
29440
30608
  });
29441
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
+ }
29442
30622
  /**
29443
30623
  * Stream bytes incrementally to participants in the room via the data channel.
29444
30624
  * For sending files, consider using {@link sendFile} instead.
@@ -29968,7 +31148,7 @@ class DeferrableMap extends Map {
29968
31148
  return new RemoteDataTrack(info, manager, {
29969
31149
  publisherIdentity: pi.identity
29970
31150
  });
29971
- }), pi.clientProtocol);
31151
+ }), pi.clientProtocol, pi.capabilities);
29972
31152
  }
29973
31153
  get logContext() {
29974
31154
  return Object.assign(Object.assign({}, super.logContext), {
@@ -29981,6 +31161,7 @@ class DeferrableMap extends Map {
29981
31161
  let kind = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : ParticipantInfo_Kind.STANDARD;
29982
31162
  let remoteDataTracks = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : [];
29983
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] : [];
29984
31165
  super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
29985
31166
  this.signalClient = signalClient;
29986
31167
  this.trackPublications = new Map();
@@ -29991,6 +31172,7 @@ class DeferrableMap extends Map {
29991
31172
  }));
29992
31173
  this.volumeMap = new Map();
29993
31174
  this.clientProtocol = clientProtocol;
31175
+ this.capabilities = capabilities;
29994
31176
  }
29995
31177
  addTrackPublication(publication) {
29996
31178
  super.addTrackPublication(publication);
@@ -30402,11 +31584,10 @@ class Room extends eventsExports.EventEmitter {
30402
31584
  return this.connectFuture.promise;
30403
31585
  });
30404
31586
  this.connectSignal = (url, token, engine, connectOptions, roomOptions, abortController) => __awaiter(this, void 0, void 0, function* () {
30405
- var _a;
30406
31587
  const _yield$engine$join = yield engine.join(url, token, {
30407
31588
  autoSubscribe: connectOptions.autoSubscribe,
30408
31589
  adaptiveStream: typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
30409
- 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),
30410
31591
  maxRetries: connectOptions.maxRetries,
30411
31592
  e2eeEnabled: !!this.e2eeManager,
30412
31593
  websocketTimeout: connectOptions.websocketTimeout
@@ -30966,6 +32147,13 @@ class Room extends eventsExports.EventEmitter {
30966
32147
  var _a, _b;
30967
32148
  return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.clientProtocol) !== null && _b !== void 0 ? _b : CLIENT_PROTOCOL_DEFAULT;
30968
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
+ };
30969
32157
  this.onLocalParticipantMetadataChanged = metadata => {
30970
32158
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
30971
32159
  };
@@ -31042,7 +32230,7 @@ class Room extends eventsExports.EventEmitter {
31042
32230
  this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
31043
32231
  this.maybeCreateEngine();
31044
32232
  this.incomingDataStreamManager = new IncomingDataStreamManager();
31045
- this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
32233
+ this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
31046
32234
  this.incomingDataTrackManager = new IncomingDataTrackManager({
31047
32235
  e2eeManager: this.e2eeManager
31048
32236
  });
@@ -31079,7 +32267,7 @@ class Room extends eventsExports.EventEmitter {
31079
32267
  }).on('packetAvailable', _ref => {
31080
32268
  let handle = _ref.handle,
31081
32269
  bytes = _ref.bytes;
31082
- this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait').finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
32270
+ this.engine.sendDataTrackFrame(bytes).finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
31083
32271
  });
31084
32272
  this.registerRpcDataStreamHandler();
31085
32273
  this.rpcClientManager = new RpcClientManager(this.log, this.outgoingDataStreamManager, this.getRemoteParticipantClientProtocol, () => {
@@ -31328,10 +32516,10 @@ class Room extends eventsExports.EventEmitter {
31328
32516
  this.isResuming = false;
31329
32517
  this.log.debug('Resumed signal connection');
31330
32518
  this.updateSubscriptions();
31331
- this.emitBufferedEvents();
31332
32519
  if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
31333
32520
  this.emit(RoomEvent.Reconnected);
31334
32521
  }
32522
+ this.emitBufferedEvents();
31335
32523
  }).on(EngineEvent.SignalResumed, () => {
31336
32524
  this.bufferedEvents = [];
31337
32525
  if (this.state === ConnectionState.Reconnecting || this.isResuming) {
@@ -32162,6 +33350,20 @@ class Room extends eventsExports.EventEmitter {
32162
33350
  return this.remoteParticipants.get(identity);
32163
33351
  }
32164
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
+ }
32165
33367
  registerRpcDataStreamHandler() {
32166
33368
  this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
32167
33369
  var _this4 = this;
@@ -33781,4 +34983,4 @@ const serializers = {
33781
34983
  json,
33782
34984
  raw,
33783
34985
  custom
33784
- };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