livekit-client 2.20.2 → 2.22.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 (142) hide show
  1. package/README.md +10 -10
  2. package/dist/livekit-client.e2ee.worker.js +1 -1
  3. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  4. package/dist/livekit-client.e2ee.worker.mjs +6 -1
  5. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  6. package/dist/livekit-client.esm.mjs +2588 -1056
  7. package/dist/livekit-client.esm.mjs.map +1 -1
  8. package/dist/livekit-client.fm.worker.js +1 -1
  9. package/dist/livekit-client.fm.worker.js.map +1 -1
  10. package/dist/livekit-client.fm.worker.mjs +6 -1
  11. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  12. package/dist/livekit-client.umd.js +1 -1
  13. package/dist/livekit-client.umd.js.map +1 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/room/PCTransport.d.ts +49 -2
  17. package/dist/src/room/PCTransport.d.ts.map +1 -1
  18. package/dist/src/room/RTCEngine.d.ts +48 -65
  19. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  20. package/dist/src/room/Room.d.ts +6 -0
  21. package/dist/src/room/Room.d.ts.map +1 -1
  22. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  23. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  24. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  25. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  26. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  27. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  28. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  29. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  30. package/dist/src/room/data-channel/types.d.ts +12 -0
  31. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  32. package/dist/src/room/data-stream/compression.d.ts +49 -0
  33. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  34. package/dist/src/room/data-stream/constants.d.ts +18 -0
  35. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  36. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  37. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  38. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  39. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  40. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  41. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  42. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  43. package/dist/src/room/errors.d.ts +3 -1
  44. package/dist/src/room/errors.d.ts.map +1 -1
  45. package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
  46. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  47. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  48. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  49. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  50. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  51. package/dist/src/room/token-source/TokenSource.d.ts +19 -10
  52. package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
  53. package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
  54. package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
  55. package/dist/src/room/track/RemoteTrack.d.ts +3 -0
  56. package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
  57. package/dist/src/room/track/Track.d.ts.map +1 -1
  58. package/dist/src/room/types.d.ts +17 -0
  59. package/dist/src/room/types.d.ts.map +1 -1
  60. package/dist/src/room/utils.d.ts +28 -0
  61. package/dist/src/room/utils.d.ts.map +1 -1
  62. package/dist/src/test/promiseState.d.ts +12 -0
  63. package/dist/src/test/promiseState.d.ts.map +1 -0
  64. package/dist/src/test/signalToken.d.ts.map +1 -1
  65. package/dist/src/utils/AsyncQueue.d.ts +3 -3
  66. package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
  67. package/dist/src/version.d.ts +5 -1
  68. package/dist/src/version.d.ts.map +1 -1
  69. package/dist/ts4.2/index.d.ts +1 -1
  70. package/dist/ts4.2/room/PCTransport.d.ts +49 -2
  71. package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
  72. package/dist/ts4.2/room/Room.d.ts +6 -0
  73. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  74. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  75. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  76. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  77. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  78. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  79. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  80. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  81. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  82. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  83. package/dist/ts4.2/room/errors.d.ts +3 -1
  84. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
  85. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  86. package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
  87. package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
  88. package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
  89. package/dist/ts4.2/room/types.d.ts +17 -0
  90. package/dist/ts4.2/room/utils.d.ts +28 -0
  91. package/dist/ts4.2/test/promiseState.d.ts +12 -0
  92. package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
  93. package/dist/ts4.2/version.d.ts +5 -1
  94. package/package.json +14 -13
  95. package/src/api/SignalClient.e2e.test.ts +16 -9
  96. package/src/e2ee/utils.ts +1 -1
  97. package/src/index.ts +2 -0
  98. package/src/room/PCTransport.test.ts +243 -1
  99. package/src/room/PCTransport.ts +181 -80
  100. package/src/room/RTCEngine.test.ts +397 -101
  101. package/src/room/RTCEngine.ts +244 -545
  102. package/src/room/Room.test.ts +138 -4
  103. package/src/room/Room.ts +92 -25
  104. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  105. package/src/room/data-channel/DataChannelManager.ts +237 -0
  106. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  107. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  108. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  109. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  110. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  111. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  112. package/src/room/data-channel/types.ts +30 -0
  113. package/src/room/data-stream/compression.ts +117 -0
  114. package/src/room/data-stream/constants.ts +18 -0
  115. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  116. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  117. package/src/room/data-stream/incoming/StreamReader.ts +9 -3
  118. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  119. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  120. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  121. package/src/room/errors.ts +7 -2
  122. package/src/room/participant/LocalParticipant.ts +13 -0
  123. package/src/room/participant/RemoteParticipant.ts +19 -5
  124. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  125. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  126. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  127. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  128. package/src/room/token-source/TokenSource.ts +25 -12
  129. package/src/room/track/LocalVideoTrack.test.ts +105 -2
  130. package/src/room/track/LocalVideoTrack.ts +36 -10
  131. package/src/room/track/RemoteTrack.test.ts +144 -0
  132. package/src/room/track/RemoteTrack.ts +39 -12
  133. package/src/room/track/Track.ts +2 -1
  134. package/src/room/types.ts +18 -0
  135. package/src/room/utils.test.ts +71 -2
  136. package/src/room/utils.ts +119 -0
  137. package/src/test/promiseState.ts +23 -0
  138. package/src/test/signalServerSetup.ts +2 -1
  139. package/src/test/signalToken.ts +17 -13
  140. package/src/type-polyfills/header-extensions.d.ts +13 -0
  141. package/src/utils/AsyncQueue.ts +3 -3
  142. 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.22.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) {
@@ -13355,8 +13640,9 @@ class Track extends eventsExports.EventEmitter {
13355
13640
  if (this.monitorInterval) {
13356
13641
  clearInterval(this.monitorInterval);
13357
13642
  }
13358
- if (this.timeSyncHandle) {
13643
+ if (this.timeSyncHandle !== undefined) {
13359
13644
  cancelAnimationFrame(this.timeSyncHandle);
13645
+ this.timeSyncHandle = undefined;
13360
13646
  }
13361
13647
  }
13362
13648
  /** @internal */
@@ -13644,6 +13930,45 @@ function supportsVP9() {
13644
13930
  function isSVCCodec(codec) {
13645
13931
  return codec === 'av1' || codec === 'vp9';
13646
13932
  }
13933
+ /**
13934
+ * Opts `transceiver` into negotiating the AV1 dependency descriptor, reporting whether it will be.
13935
+ *
13936
+ * Chrome only offers the extension on transceivers that can send, so one we create to receive on
13937
+ * never negotiates it — and Chrome 152 stopped decoding AV1 that arrives without it: frames get
13938
+ * assembled, none ever decode, and the receiver asks for a keyframe forever. Asking through the
13939
+ * transceiver rather than munging the extension into the SDP leaves the browser owning the
13940
+ * extension id, which is what keeps that id consistent across the bundle and across
13941
+ * renegotiations.
13942
+ *
13943
+ * A no-op where the browser offers no such control, or does not know the extension at all.
13944
+ * @internal
13945
+ */
13946
+ function negotiateDependencyDescriptor(transceiver) {
13947
+ var _a;
13948
+ const extensions = (_a = transceiver.getHeaderExtensionsToNegotiate) === null || _a === void 0 ? void 0 : _a.call(transceiver);
13949
+ if (!extensions || !transceiver.setHeaderExtensionsToNegotiate) {
13950
+ return false;
13951
+ }
13952
+ const dd = extensions.find(ext => ext.uri === ddExtensionURI);
13953
+ if (!dd) {
13954
+ return false;
13955
+ }
13956
+ if (dd.direction !== 'stopped') {
13957
+ return true;
13958
+ }
13959
+ // sendrecv rather than recvonly, so that the extension is written as a plain `a=extmap` line —
13960
+ // the form the server already emits where it is the one offering — rather than one carrying a
13961
+ // `/recvonly` suffix that its parser may not expect
13962
+ dd.direction = 'sendrecv';
13963
+ try {
13964
+ transceiver.setHeaderExtensionsToNegotiate(extensions);
13965
+ return true;
13966
+ } catch (e) {
13967
+ // a rejected direction throws. Negotiating without the extension is what happened before this
13968
+ // existed, so it is not worth failing the connection over
13969
+ return false;
13970
+ }
13971
+ }
13647
13972
  function supportsSetSinkId(elm) {
13648
13973
  if (!document || isSafariBased()) {
13649
13974
  return false;
@@ -14121,6 +14446,53 @@ function splitUtf8(s, n) {
14121
14446
  }
14122
14447
  return result;
14123
14448
  }
14449
+ /** Wraps a byte array in a `ReadableStream` that yields it as a single chunk and then closes. */
14450
+ function readableFromBytes(bytes) {
14451
+ return new ReadableStream({
14452
+ start(controller) {
14453
+ controller.enqueue(bytes);
14454
+ controller.close();
14455
+ }
14456
+ });
14457
+ }
14458
+ /**
14459
+ * Re-chunks a byte stream into pieces of exactly `chunkSize` bytes (the final piece may be
14460
+ * smaller), coalescing or splitting the source's pieces as needed. Memory use is bounded to roughly
14461
+ * `chunkSize` plus one source read, so it never buffers the whole stream — used to pack
14462
+ * `CompressionStream`/`file.stream()` output into MTU-sized data-stream chunks.
14463
+ */
14464
+ function readBytesInChunks(source, chunkSize) {
14465
+ return __asyncGenerator(this, arguments, function* readBytesInChunks_1() {
14466
+ const reader = source.getReader();
14467
+ let buffer = new Uint8Array(0);
14468
+ try {
14469
+ while (true) {
14470
+ const _yield$__await = yield __await(reader.read()),
14471
+ done = _yield$__await.done,
14472
+ value = _yield$__await.value;
14473
+ if (done) {
14474
+ break;
14475
+ }
14476
+ if (value.byteLength === 0) {
14477
+ continue;
14478
+ }
14479
+ const merged = new Uint8Array(buffer.byteLength + value.byteLength);
14480
+ merged.set(buffer);
14481
+ merged.set(value, buffer.byteLength);
14482
+ buffer = merged;
14483
+ while (buffer.byteLength >= chunkSize) {
14484
+ yield yield __await(buffer.slice(0, chunkSize));
14485
+ buffer = buffer.slice(chunkSize);
14486
+ }
14487
+ }
14488
+ if (buffer.byteLength > 0) {
14489
+ yield yield __await(buffer);
14490
+ }
14491
+ } finally {
14492
+ reader.releaseLock();
14493
+ }
14494
+ });
14495
+ }
14124
14496
  function extractMaxAgeFromRequestHeaders(headers) {
14125
14497
  var _a;
14126
14498
  const cacheControl = headers.get('Cache-Control');
@@ -14138,6 +14510,17 @@ function isCompressionStreamSupported() {
14138
14510
  function isPublisherOfferWithJoinSupported() {
14139
14511
  // we have connectivity issue about publisher offer with join on firefox #1919
14140
14512
  return isCompressionStreamSupported() && !isFireFox();
14513
+ }
14514
+ function extractTrackSid(mediaTrack, stream) {
14515
+ const _unpackStreamId = unpackStreamId(stream.id),
14516
+ _unpackStreamId2 = _slicedToArray(_unpackStreamId, 2),
14517
+ streamId = _unpackStreamId2[1];
14518
+ if (streamId === null || streamId === void 0 ? void 0 : streamId.startsWith('TR')) {
14519
+ return streamId;
14520
+ }
14521
+ if (mediaTrack.id.startsWith('TR')) {
14522
+ return mediaTrack.id;
14523
+ }
14141
14524
  }function createRtcUrl(url, searchParams) {
14142
14525
  let useV0Path = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
14143
14526
  const v0Url = createV0RtcUrl(url, searchParams);
@@ -14636,6 +15019,34 @@ function computeBitrate(currentStats, prevStats) {
14636
15019
  }class RemoteTrack extends Track {
14637
15020
  constructor(mediaTrack, sid, kind, receiver, loggerOptions) {
14638
15021
  super(mediaTrack, kind, loggerOptions);
15022
+ this.timeSyncLoop = () => {
15023
+ var _a;
15024
+ if (this.listenerCount(TrackEvent.TimeSyncUpdate) === 0) {
15025
+ // nobody is listening anymore, pause the loop until a new listener subscribes
15026
+ this.timeSyncHandle = undefined;
15027
+ return;
15028
+ }
15029
+ this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
15030
+ const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
15031
+ if (sources) {
15032
+ const timestamp = sources.timestamp,
15033
+ rtpTimestamp = sources.rtpTimestamp;
15034
+ if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
15035
+ this.emit(TrackEvent.TimeSyncUpdate, {
15036
+ timestamp,
15037
+ rtpTimestamp
15038
+ });
15039
+ this.rtpTimestamp = rtpTimestamp;
15040
+ }
15041
+ }
15042
+ };
15043
+ this.onTimeSyncListenerAdded = event => {
15044
+ if (event === TrackEvent.TimeSyncUpdate && this.timeSyncHandle === undefined) {
15045
+ // `newListener` fires before the listener is registered, so schedule the
15046
+ // next frame instead of entering the loop (which would see a count of 0)
15047
+ this.timeSyncHandle = requestAnimationFrame(this.timeSyncLoop);
15048
+ }
15049
+ };
14639
15050
  this.sid = sid;
14640
15051
  this.receiver = receiver;
14641
15052
  }
@@ -14733,24 +15144,19 @@ function computeBitrate(currentStats, prevStats) {
14733
15144
  this.registerTimeSyncUpdate();
14734
15145
  }
14735
15146
  }
15147
+ /* @internal */
15148
+ stopMonitor() {
15149
+ super.stopMonitor();
15150
+ this.off('newListener', this.onTimeSyncListenerAdded);
15151
+ }
14736
15152
  registerTimeSyncUpdate() {
14737
- const loop = () => {
14738
- var _a;
14739
- this.timeSyncHandle = requestAnimationFrame(() => loop());
14740
- const sources = (_a = this.receiver) === null || _a === void 0 ? void 0 : _a.getSynchronizationSources()[0];
14741
- if (sources) {
14742
- const timestamp = sources.timestamp,
14743
- rtpTimestamp = sources.rtpTimestamp;
14744
- if (rtpTimestamp && this.rtpTimestamp !== rtpTimestamp) {
14745
- this.emit(TrackEvent.TimeSyncUpdate, {
14746
- timestamp,
14747
- rtpTimestamp
14748
- });
14749
- this.rtpTimestamp = rtpTimestamp;
14750
- }
14751
- }
14752
- };
14753
- loop();
15153
+ // `newListener` isn't part of the typed event map, hence the cast
15154
+ const emitter = this;
15155
+ emitter.off('newListener', this.onTimeSyncListenerAdded);
15156
+ emitter.on('newListener', this.onTimeSyncListenerAdded);
15157
+ if (this.timeSyncHandle === undefined) {
15158
+ this.timeSyncLoop();
15159
+ }
14754
15160
  }
14755
15161
  }const REACTION_DELAY = 100;
14756
15162
  class RemoteVideoTrack extends RemoteTrack {
@@ -17269,81 +17675,6 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
17269
17675
  params.set('join_request', bytesToBase64(wrappedBytes).replace(/\+/g, '-').replace(/\//g, '_'));
17270
17676
  return params;
17271
17677
  });
17272
- }class DataPacketBuffer {
17273
- constructor() {
17274
- this.buffer = [];
17275
- this._totalSize = 0;
17276
- this._sentSize = 0;
17277
- }
17278
- push(item) {
17279
- this.buffer.push(item);
17280
- this._totalSize += item.data.byteLength;
17281
- if (item.sent) {
17282
- this._sentSize += item.data.byteLength;
17283
- }
17284
- }
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
- }
17292
- }
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
17678
  }class TTLMap {
17348
17679
  /**
17349
17680
  * @param ttl ttl of the key (ms)
@@ -18135,6 +18466,51 @@ const startBitrateMultiplier = 0.9;
18135
18466
  /** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
18136
18467
  const maxStartBitrateKbps = 1000;
18137
18468
  const debounceInterval = 20;
18469
+ /**
18470
+ * Applies the configured start bitrate when this media section belongs to `cid`.
18471
+ * This SDP munging is used for a bitrate setting that cannot be applied through
18472
+ * `RTCRtpEncodingParameters`.
18473
+ *
18474
+ * Returns `undefined` when the section does not belong to the track, `0` when
18475
+ * it does but does not offer the requested codec, and the codec payload when the
18476
+ * requested codec is present (whether the bitrate was added or already set).
18477
+ *
18478
+ * @internal
18479
+ */
18480
+ function applyVideoStartBitrate(media, cid, codec, maxbr) {
18481
+ let isScreenShare = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
18482
+ var _a, _b, _c;
18483
+ if (!((_a = media.msid) === null || _a === void 0 ? void 0 : _a.includes(cid))) {
18484
+ return undefined;
18485
+ }
18486
+ const codecPayload = (_c = (_b = media.rtp.find(rtp => rtp.codec.toUpperCase() === codec.toUpperCase())) === null || _b === void 0 ? void 0 : _b.payload) !== null && _c !== void 0 ? _c : 0;
18487
+ if (codecPayload === 0) {
18488
+ return 0;
18489
+ }
18490
+ // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE
18491
+ // from starting too aggressively. Screen share is not capped since text/UI
18492
+ // clarity requires high bitrate from the start.
18493
+ // TODO: dynamically adjust start bitrate based on network conditions (e.g., previous BWE estimate)
18494
+ const calculatedStartBitrate = Math.round(maxbr * startBitrateMultiplier);
18495
+ const startBitrate = isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18496
+ const fmtp = media.fmtp.find(entry => entry.payload === codecPayload);
18497
+ if (fmtp) {
18498
+ // If another track's fmtp already has a start bitrate, it cannot be
18499
+ // overridden here because the payload type is shared across the bundle.
18500
+ // This forces every track sharing that payload to use the initial track's
18501
+ // start bitrate.
18502
+ if (!fmtp.config.includes('x-google-start-bitrate')) {
18503
+ fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
18504
+ }
18505
+ } else {
18506
+ // VP8 and some codecs may not have an existing fmtp line.
18507
+ media.fmtp.push({
18508
+ payload: codecPayload,
18509
+ config: "x-google-start-bitrate=".concat(startBitrate)
18510
+ });
18511
+ }
18512
+ return codecPayload;
18513
+ }
18138
18514
  const PCEvents = {
18139
18515
  NegotiationStarted: 'negotiationStarted',
18140
18516
  NegotiationComplete: 'negotiationComplete',
@@ -18436,9 +18812,13 @@ class PCTransport extends eventsExports.EventEmitter {
18436
18812
  // the only exception to this is when ICE restart is needed
18437
18813
  const currentSD = this._pc.remoteDescription;
18438
18814
  if ((options === null || options === void 0 ? void 0 : options.iceRestart) && currentSD) {
18439
- // TODO: handle when ICE restart is needed but we don't have a remote description
18440
- // the best thing to do is to recreate the peerconnection
18815
+ // roll the remote description back in so createOffer produces a valid
18816
+ // ICE-restart offer on top of the already-negotiated state
18441
18817
  yield this._pc.setRemoteDescription(currentSD);
18818
+ } else if (options === null || options === void 0 ? void 0 : options.iceRestart) {
18819
+ // ICE restart with no remote description to restart on: `renegotiate` would stall
18820
+ // (the pending offer is never answered), so throw for the caller to recreate the PC.
18821
+ throw new NegotiationError('ICE restart requested without a remote description, peer connection must be recreated');
18442
18822
  } else {
18443
18823
  this.renegotiate = true;
18444
18824
  this.log.debug('requesting renegotiation');
@@ -18464,48 +18844,15 @@ class PCTransport extends eventsExports.EventEmitter {
18464
18844
  ensureAudioNackAndStereo(media, ['all'], []);
18465
18845
  } else if (media.type === 'video') {
18466
18846
  this.trackBitrates.some(trackbr => {
18467
- if (!media.msid || !trackbr.cid || !media.msid.includes(trackbr.cid)) {
18847
+ if (!trackbr.cid) {
18468
18848
  return false;
18469
18849
  }
18470
- let codecPayload = 0;
18471
- media.rtp.some(rtp => {
18472
- if (rtp.codec.toUpperCase() === trackbr.codec.toUpperCase()) {
18473
- codecPayload = rtp.payload;
18474
- return true;
18475
- }
18850
+ const codecPayload = applyVideoStartBitrate(media, trackbr.cid, trackbr.codec, trackbr.maxbr, trackbr.isScreenShare);
18851
+ if (codecPayload === undefined) {
18476
18852
  return false;
18477
- });
18478
- if (codecPayload === 0) {
18479
- return true;
18480
- }
18481
- if (isSVCCodec(trackbr.codec) && !isSafari()) {
18482
- this.ensureVideoDDExtensionForSVC(media, sdpParsed);
18483
- }
18484
- // mung sdp for bitrate setting that can't apply by sendEncoding
18485
- // Use 90% of target bitrate, capped at 1 Mbps for camera to prevent BWE from starting too aggressively
18486
- // Screen share is not capped since text/UI clarity requires high bitrate from the start
18487
- // TODO: dynamically adjust start bitrate based on network conditions (e.g., use previous BWE estimate)
18488
- const calculatedStartBitrate = Math.round(trackbr.maxbr * startBitrateMultiplier);
18489
- const startBitrate = trackbr.isScreenShare ? calculatedStartBitrate : Math.min(calculatedStartBitrate, maxStartBitrateKbps);
18490
- let fmtpFound = false;
18491
- for (const fmtp of media.fmtp) {
18492
- if (fmtp.payload === codecPayload) {
18493
- fmtpFound = true;
18494
- // if another track's fmtp already is set, we cannot override the bitrate
18495
- // this has the unfortunate consequence of being forced to use the
18496
- // initial track's bitrate for all tracks
18497
- if (!fmtp.config.includes('x-google-start-bitrate')) {
18498
- fmtp.config += ";x-google-start-bitrate=".concat(startBitrate);
18499
- }
18500
- break;
18501
- }
18502
18853
  }
18503
- // VP8 and some codecs may not have an existing fmtp line - create one
18504
- if (!fmtpFound) {
18505
- media.fmtp.push({
18506
- payload: codecPayload,
18507
- config: "x-google-start-bitrate=".concat(startBitrate)
18508
- });
18854
+ if (codecPayload > 0 && isSVCCodec(trackbr.codec) && !isSafari()) {
18855
+ this.ddExtID = ensureVideoDDExtension(media, sdpParsed, this.ddExtID);
18509
18856
  }
18510
18857
  return true;
18511
18858
  });
@@ -18718,41 +19065,106 @@ class PCTransport extends eventsExports.EventEmitter {
18718
19065
  }
18719
19066
  });
18720
19067
  }
18721
- ensureVideoDDExtensionForSVC(media, sdp) {
18722
- var _a, _b;
18723
- const ddFound = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => {
18724
- if (ext.uri === ddExtensionURI) {
18725
- return true;
18726
- }
18727
- return false;
19068
+ }
19069
+ /**
19070
+ * Adds the AV1 dependency descriptor extension to `media` unless it is already there, and
19071
+ * returns the id it is mapped to so callers can pass it back in as `ddExtID` (0 when no id has
19072
+ * been chosen yet).
19073
+ *
19074
+ * A bundle has to map one URI to one id, so an id already in use for the extension anywhere in
19075
+ * `sdp` wins over both the cached one and a fresh one: Chrome advertises the extension itself on
19076
+ * sections it can send on, and an earlier offer may have munged it into others.
19077
+ * @internal
19078
+ */
19079
+ function ensureVideoDDExtension(media, sdp, ddExtID) {
19080
+ var _a, _b;
19081
+ const id = ddExtensionIDFor(sdp, ddExtID);
19082
+ if (id === undefined) {
19083
+ return ddExtID;
19084
+ }
19085
+ if (!((_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.uri === ddExtensionURI))) {
19086
+ (_b = media.ext) !== null && _b !== void 0 ? _b : media.ext = [];
19087
+ media.ext.push({
19088
+ value: id,
19089
+ uri: ddExtensionURI
18728
19090
  });
18729
- if (!ddFound) {
18730
- if (this.ddExtID === 0) {
18731
- let maxID = 0;
18732
- sdp.media.forEach(m => {
18733
- var _a;
18734
- (_a = m.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
18735
- if (ext.value > maxID) {
18736
- maxID = ext.value;
18737
- }
18738
- });
18739
- });
18740
- this.ddExtID = maxID + 1;
18741
- }
18742
- (_b = media.ext) === null || _b === void 0 ? void 0 : _b.push({
18743
- value: this.ddExtID,
18744
- uri: ddExtensionURI
18745
- });
19091
+ }
19092
+ return id;
19093
+ }
19094
+ /**
19095
+ * The id to map the dependency descriptor to throughout `sdp`, or undefined when no id would be
19096
+ * consistent for the whole bundle and the extension therefore has to be left out.
19097
+ */
19098
+ function ddExtensionIDFor(sdp, cachedID) {
19099
+ const mapped = mappedExtensionID(sdp, ddExtensionURI);
19100
+ if (mapped !== undefined) {
19101
+ // Adopting an id that also stands for another URI is what the browser rejects the bundle
19102
+ // over, and its own half of the map is not ours to renumber, so give up on this offer.
19103
+ return usedForOtherURI(sdp, mapped, ddExtensionURI) ? undefined : mapped;
19104
+ }
19105
+ // Reusing the id from the last offer keeps the mapping stable across renegotiations, but only
19106
+ // while nothing else has taken it: the browser assigns ids to its own extensions without
19107
+ // knowing about ours, so an id that was free when we picked it can since have been claimed —
19108
+ // typically by the fuller extension set that arrives with the first section we send on.
19109
+ if (cachedID !== 0 && !usedForOtherURI(sdp, cachedID, ddExtensionURI)) {
19110
+ return cachedID;
19111
+ }
19112
+ return unusedExtensionID(sdp);
19113
+ }
19114
+ /** The id `uri` is mapped to in `sdp`, if any section maps it. */
19115
+ function mappedExtensionID(sdp, uri) {
19116
+ var _a;
19117
+ for (const media of sdp.media) {
19118
+ const ext = (_a = media.ext) === null || _a === void 0 ? void 0 : _a.find(candidate => candidate.uri === uri);
19119
+ if (ext) {
19120
+ return ext.value;
18746
19121
  }
18747
19122
  }
19123
+ return undefined;
19124
+ }
19125
+ /** Whether `id` stands for anything in `sdp` other than `uri`. */
19126
+ function usedForOtherURI(sdp, id, uri) {
19127
+ return sdp.media.some(media => {
19128
+ var _a;
19129
+ return (_a = media.ext) === null || _a === void 0 ? void 0 : _a.some(ext => ext.value === id && ext.uri !== uri);
19130
+ });
19131
+ }
19132
+ /**
19133
+ * An id no extension in `sdp` uses. Stays above every id in use rather than filling gaps, so it
19134
+ * is less likely to be an id the browser goes on to allocate to another extension, and steps
19135
+ * over 15, which RFC 8285 reserves.
19136
+ */
19137
+ function unusedExtensionID(sdp) {
19138
+ let maxID = 0;
19139
+ sdp.media.forEach(media => {
19140
+ var _a;
19141
+ (_a = media.ext) === null || _a === void 0 ? void 0 : _a.forEach(ext => {
19142
+ if (ext.value > maxID) {
19143
+ maxID = ext.value;
19144
+ }
19145
+ });
19146
+ });
19147
+ return maxID + 1 === 15 ? 16 : maxID + 1;
19148
+ }
19149
+ /**
19150
+ * Checks whether an fmtp config declares `param` as an exact, `;`-delimited
19151
+ * token. A plain substring check conflates distinct opus parameters — e.g.
19152
+ * `stereo=1` is a substring of `sprop-stereo=1` — so `param` must match a whole
19153
+ * parameter, not appear anywhere within the config string.
19154
+ * @internal
19155
+ */
19156
+ function fmtpConfigHasParam(config, param) {
19157
+ return config.split(';').some(entry => entry.trim() === param);
18748
19158
  }
19159
+ /** @internal */
18749
19160
  function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
18750
19161
  // sdp-transform types don't include number however the parser outputs mids as numbers in some cases
18751
19162
  const mid = getMidString(media.mid);
18752
19163
  // found opus codec to add nack fb
18753
19164
  let opusPayload = 0;
18754
19165
  media.rtp.some(rtp => {
18755
- if (rtp.codec === 'opus') {
19166
+ // rtpmap encoding names are case-insensitive (RFC 4855)
19167
+ if (rtp.codec.toLowerCase() === 'opus') {
18756
19168
  opusPayload = rtp.payload;
18757
19169
  return true;
18758
19170
  }
@@ -18772,7 +19184,7 @@ function ensureAudioNackAndStereo(media, stereoMids, nackMids) {
18772
19184
  if (stereoMids.includes(mid) || stereoMids.length === 1 && stereoMids[0] === 'all') {
18773
19185
  media.fmtp.some(fmtp => {
18774
19186
  if (fmtp.payload === opusPayload) {
18775
- if (!fmtp.config.includes('stereo=1')) {
19187
+ if (!fmtpConfigHasParam(fmtp.config, 'stereo=1')) {
18776
19188
  fmtp.config += ';stereo=1';
18777
19189
  }
18778
19190
  return true;
@@ -18855,6 +19267,7 @@ function conformBundledCodecFmtp(media, isPlaceholder) {
18855
19267
  }
18856
19268
  }
18857
19269
  }
19270
+ /** @internal */
18858
19271
  function extractStereoAndNackAudioFromOffer(offer) {
18859
19272
  var _a;
18860
19273
  const stereoMids = [];
@@ -18866,7 +19279,8 @@ function extractStereoAndNackAudioFromOffer(offer) {
18866
19279
  const mid = getMidString(media.mid);
18867
19280
  if (media.type === 'audio') {
18868
19281
  media.rtp.some(rtp => {
18869
- if (rtp.codec === 'opus') {
19282
+ // rtpmap encoding names are case-insensitive (RFC 4855)
19283
+ if (rtp.codec.toLowerCase() === 'opus') {
18870
19284
  opusPayload = rtp.payload;
18871
19285
  return true;
18872
19286
  }
@@ -18877,7 +19291,7 @@ function extractStereoAndNackAudioFromOffer(offer) {
18877
19291
  }
18878
19292
  media.fmtp.some(fmtp => {
18879
19293
  if (fmtp.payload === opusPayload) {
18880
- if (fmtp.config.includes('sprop-stereo=1')) {
19294
+ if (fmtpConfigHasParam(fmtp.config, 'sprop-stereo=1')) {
18881
19295
  stereoMids.push(mid);
18882
19296
  }
18883
19297
  return true;
@@ -19289,105 +19703,762 @@ class PCTransportManager {
19289
19703
  }();
19290
19704
  });
19291
19705
  }
19292
- }// Check if MediaRecorder is available
19293
- const isMediaRecorderAvailable = typeof MediaRecorder !== 'undefined';
19294
- // Fallback class for environments without MediaRecorder
19295
- class FallbackRecorder {
19296
- constructor() {
19297
- throw new Error('MediaRecorder is not available in this environment');
19706
+ }/**
19707
+ * Two-watermark flow control for one data channel kind.
19708
+ *
19709
+ * Owns the per-kind headroom gate: senders proceed freely while the buffer is at or below the
19710
+ * high-water mark and otherwise block — serialized FIFO through a mutex — until the browser's
19711
+ * `bufferedamountlow` event (armed at the low-water mark) signals the buffer has drained. The
19712
+ * serialization prevents woken senders from all refilling at once and overflowing the SCTP send
19713
+ * buffer (see livekit/client-sdk-js#1995).
19714
+ *
19715
+ * Waiters are parked on the channel object captured at wait entry. If that object stops being
19716
+ * current — replaced or torn down — its events may never fire again, so the owner must call
19717
+ * {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
19718
+ * gets a fresh controller.
19719
+ */
19720
+ class FlowControlledDataChannel {
19721
+ constructor(opts) {
19722
+ /** Last emitted low-water status; starts true (an empty buffer is below the mark). */
19723
+ this.bufferStatusLow = true;
19724
+ this.headroomLock = new _();
19725
+ /** Cancels parked headroom waiters when the handle is replaced or torn down. */
19726
+ this.waiterAbortController = new AbortController();
19727
+ this.kind = opts.kind;
19728
+ this.lowWaterMark = opts.lowWaterMark;
19729
+ this.highWaterMark = opts.highWaterMark;
19730
+ this.isEngineClosed = opts.isEngineClosed;
19731
+ this.onBufferStatusChanged = opts.onBufferStatusChanged;
19298
19732
  }
19299
- }
19300
- // Use conditional inheritance to avoid parse-time errors
19301
- const RecorderBase = isMediaRecorderAvailable ? MediaRecorder : FallbackRecorder;
19302
- class LocalTrackRecorder extends RecorderBase {
19303
- constructor(track, options) {
19304
- if (!isMediaRecorderAvailable) {
19305
- throw new Error('MediaRecorder is not available in this environment');
19733
+ /** The currently attached RTCDataChannel handle, if any. */
19734
+ get channelHandle() {
19735
+ return this.handle;
19736
+ }
19737
+ /**
19738
+ * Attaches the channel handle this wrapper controls. Replacing an existing handle rejects
19739
+ * parked waiters their events would never fire again on the abandoned object — and installs a
19740
+ * fresh controller, so queued senders re-check against the new channel. Wrappers outlive their
19741
+ * handles: this is the one place handle turnover happens, which is what makes stranding a
19742
+ * waiter structurally impossible.
19743
+ */
19744
+ attach(dc) {
19745
+ if (this.handle && this.handle !== dc) {
19746
+ this.invalidateWaiters('data channel replaced');
19306
19747
  }
19307
- super(new MediaStream([track.mediaStreamTrack]), options);
19308
- let dataListener;
19309
- let streamController;
19310
- const isClosed = () => streamController === undefined;
19311
- const onStop = () => {
19312
- this.removeEventListener('dataavailable', dataListener);
19313
- this.removeEventListener('stop', onStop);
19314
- this.removeEventListener('error', onError);
19315
- streamController === null || streamController === void 0 ? void 0 : streamController.close();
19316
- streamController = undefined;
19317
- };
19318
- const onError = event => {
19319
- streamController === null || streamController === void 0 ? void 0 : streamController.error(event);
19320
- this.removeEventListener('dataavailable', dataListener);
19321
- this.removeEventListener('stop', onStop);
19322
- this.removeEventListener('error', onError);
19323
- streamController = undefined;
19324
- };
19325
- this.byteStream = new ReadableStream({
19326
- start: controller => {
19327
- streamController = controller;
19328
- dataListener = event => __awaiter(this, void 0, void 0, function* () {
19329
- let data;
19330
- if (event.data.arrayBuffer) {
19331
- const arrayBuffer = yield event.data.arrayBuffer();
19332
- data = new Uint8Array(arrayBuffer);
19333
- // @ts-expect-error react-native passes over Uint8Arrays directly
19334
- } else if (event.data.byteArray) {
19335
- // @ts-expect-error
19336
- data = event.data.byteArray;
19337
- } else {
19338
- throw new Error('no data available!');
19339
- }
19340
- if (isClosed()) {
19341
- return;
19342
- }
19343
- controller.enqueue(data);
19344
- });
19345
- this.addEventListener('dataavailable', dataListener);
19346
- },
19347
- cancel: () => {
19348
- onStop();
19349
- }
19350
- });
19351
- this.addEventListener('stop', onStop);
19352
- this.addEventListener('error', onError);
19748
+ this.handle = dc;
19353
19749
  }
19354
- }
19355
- // Helper function to check if recording is supported
19356
- function isRecordingSupported() {
19357
- return isMediaRecorderAvailable;
19358
- }const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
19359
- const PRE_CONNECT_BUFFER_TIMEOUT = 10000;
19360
- class LocalTrack extends Track {
19361
- /** @internal */
19362
- get sender() {
19363
- return this._sender;
19750
+ /** Detaches the handle on teardown, rejecting parked waiters. */
19751
+ detach() {
19752
+ let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'data channel torn down';
19753
+ if (this.handle) {
19754
+ this.invalidateWaiters(reason);
19755
+ }
19756
+ this.handle = undefined;
19364
19757
  }
19365
- /** @internal */
19366
- set sender(sender) {
19367
- this._sender = sender;
19758
+ getChannel() {
19759
+ return this.handle;
19368
19760
  }
19369
- get constraints() {
19370
- return this._constraints;
19761
+ /**
19762
+ * Whether the send buffer has room to accept more data (the send gate). Senders proceed while
19763
+ * this is true and block once it goes false. Callers resolve the handle (and decide what an
19764
+ * absent one means) before asking.
19765
+ */
19766
+ isBelowHighWaterMark(dc) {
19767
+ // RTCDataChannel has no built-in high-water mark, so we compare against our static mark.
19768
+ return dc.bufferedAmount <= this.highWaterMark;
19371
19769
  }
19372
- get hasPreConnectBuffer() {
19373
- return !!this.localTrackRecorder;
19770
+ /**
19771
+ * Whether the send buffer has drained to its low-water mark. Drives the engine's public
19772
+ * DCBufferStatusChanged event.
19773
+ */
19774
+ isBelowLowWaterMark(dc) {
19775
+ // Read the channel's own threshold: it is tuned dynamically for the lossy channel.
19776
+ return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
19374
19777
  }
19375
19778
  /**
19376
- *
19377
- * @param mediaTrack
19378
- * @param kind
19379
- * @param constraints MediaTrackConstraints that are being used when restarting or reacquiring tracks
19380
- * @param userProvidedTrack Signals to the SDK whether or not the mediaTrack should be managed (i.e. released and reacquired) internally by the SDK
19779
+ * Acquires the headroom lock, resolving with the unlock function. Batch senders (the resume
19780
+ * replay) hold it across all of their sends so no other sender can interleave, calling
19781
+ * {@link waitForHeadroomWithoutLock} per message to respect flow control within the batch.
19381
19782
  */
19382
- constructor(mediaTrack, kind, constraints) {
19383
- let userProvidedTrack = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
19384
- let loggerOptions = arguments.length > 4 ? arguments[4] : undefined;
19385
- super(mediaTrack, kind, loggerOptions);
19386
- this.manuallyStopped = false;
19387
- this.pendingDeviceChange = false;
19388
- this._isUpstreamPaused = false;
19389
- this.handleTrackMuteEvent = () => this.debouncedTrackMuteHandler().catch(() => this.log.debug('track mute bounce got cancelled by an unmute event', this.logContext));
19390
- this.debouncedTrackMuteHandler = debounce(() => __awaiter(this, void 0, void 0, function* () {
19783
+ lockHeadroom() {
19784
+ return this.headroomLock.lock();
19785
+ }
19786
+ /**
19787
+ * Resolves once the caller may send on this channel: immediately while the send buffer is at or
19788
+ * below its high-water mark, otherwise once the buffer has drained to the low-water mark (the
19789
+ * `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
19790
+ * the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
19791
+ * sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
19792
+ * closed/buffer checks run inside the lock so queued callers proceed in FIFO order.
19793
+ */
19794
+ waitForHeadroomWithLock() {
19795
+ return __awaiter(this, void 0, void 0, function* () {
19796
+ const unlock = yield this.lockHeadroom();
19797
+ try {
19798
+ yield this.waitForHeadroomWithoutLock();
19799
+ } finally {
19800
+ unlock();
19801
+ }
19802
+ });
19803
+ }
19804
+ /** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
19805
+ waitForHeadroomWithoutLock() {
19806
+ return __awaiter(this, void 0, void 0, function* () {
19807
+ if (this.isEngineClosed()) {
19808
+ throw new UnexpectedConnectionState('engine closed');
19809
+ }
19810
+ const dc = this.getChannel();
19811
+ if (!dc) {
19812
+ throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
19813
+ }
19814
+ if (this.isBelowHighWaterMark(dc)) {
19815
+ return;
19816
+ }
19817
+ const abortSignal = this.waiterAbortController.signal;
19818
+ yield new TypedPromise((resolve, reject) => {
19819
+ const onBufferedAmountLow = () => {
19820
+ cleanup();
19821
+ resolve();
19822
+ };
19823
+ const onDCClose = () => {
19824
+ cleanup();
19825
+ reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " closed while draining the buffer")));
19826
+ };
19827
+ const onAbort = () => {
19828
+ cleanup();
19829
+ reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " was replaced or torn down while waiting for headroom")));
19830
+ };
19831
+ const cleanup = () => {
19832
+ dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
19833
+ dc.removeEventListener('close', onDCClose);
19834
+ abortSignal.removeEventListener('abort', onAbort);
19835
+ };
19836
+ if (abortSignal.aborted) {
19837
+ onAbort();
19838
+ return;
19839
+ }
19840
+ dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
19841
+ // Proxy along any error caused by the data channel closing while we wait.
19842
+ dc.addEventListener('close', onDCClose);
19843
+ // The channel object we're parked on can be abandoned without ever firing another event
19844
+ // (e.g. the engine recreating channels); the abort is our way out.
19845
+ abortSignal.addEventListener('abort', onAbort);
19846
+ });
19847
+ });
19848
+ }
19849
+ /** Rejects all parked headroom waiters; the next waiter gets a fresh controller. */
19850
+ invalidateWaiters(reason) {
19851
+ this.waiterAbortController.abort(reason);
19852
+ this.waiterAbortController = new AbortController();
19853
+ }
19854
+ /**
19855
+ * Recomputes whether the buffer has drained to the low-water mark and, if that changed since the
19856
+ * last check, notifies the status listener. Two independent triggers land here: a send (which
19857
+ * raises the buffer) and the `bufferedamountlow` drain event (which lowers it) — the latter has
19858
+ * no send to hang the work off, which is why this is a shared entry point rather than a tail of
19859
+ * `send`.
19860
+ */
19861
+ refreshBufferStatus() {
19862
+ var _a;
19863
+ const dc = this.getChannel();
19864
+ if (!dc) {
19865
+ return;
19866
+ }
19867
+ const isLow = this.isBelowLowWaterMark(dc);
19868
+ if (isLow !== this.bufferStatusLow) {
19869
+ this.bufferStatusLow = isLow;
19870
+ (_a = this.onBufferStatusChanged) === null || _a === void 0 ? void 0 : _a.call(this, isLow);
19871
+ }
19872
+ }
19873
+ }/**
19874
+ * A lossy channel: flow control plus a per-instance full-buffer policy.
19875
+ *
19876
+ * Each instance owns its own byterate stat, drop counter, and (when tuning is started) the
19877
+ * dynamic `bufferedAmountLowThreshold` adjustment that keeps the drop gate at roughly 100ms of
19878
+ * buffered latency. Keeping these per instance is what prevents one channel's traffic from
19879
+ * steering another channel's policy.
19880
+ */
19881
+ class LossyDataChannel extends FlowControlledDataChannel {
19882
+ constructor(opts) {
19883
+ super(opts);
19884
+ this.statCurrentBytes = 0;
19885
+ this.statByterate = 0;
19886
+ this.dropCount = 0;
19887
+ this.bufferFullBehavior = opts.bufferFullBehavior;
19888
+ this.shouldSkipSends = opts.shouldSkipSends;
19889
+ }
19890
+ /** Sends prepared bytes with this channel's full-buffer policy (drop or wait). */
19891
+ send(msg) {
19892
+ return __awaiter(this, void 0, void 0, function* () {
19893
+ const dc = this.getChannel();
19894
+ if (!dc) {
19895
+ return;
19896
+ }
19897
+ // Depending on the channel's policy, either drop or wait for the buffer to drain below the
19898
+ // high-water mark before continuing.
19899
+ switch (this.bufferFullBehavior) {
19900
+ case 'wait':
19901
+ if (!this.isBelowHighWaterMark(dc)) {
19902
+ yield this.waitForHeadroomWithLock();
19903
+ }
19904
+ break;
19905
+ case 'drop':
19906
+ // We check against the actual threshold on the DC here, as it is tuned dynamically.
19907
+ if (!this.isBelowLowWaterMark(dc)) {
19908
+ // Drop messages to reduce latency
19909
+ this.dropCount += 1;
19910
+ if (this.dropCount % 100 === 0) {
19911
+ livekitLogger.warn("dropping lossy data channel messages, total dropped: ".concat(this.dropCount));
19912
+ }
19913
+ return;
19914
+ }
19915
+ }
19916
+ this.statCurrentBytes += msg.byteLength;
19917
+ if (this.shouldSkipSends()) {
19918
+ return;
19919
+ }
19920
+ try {
19921
+ dc.send(msg);
19922
+ this.refreshBufferStatus();
19923
+ } catch (error) {
19924
+ // Preserve prior surface behaviour: a send that fails because the channel is closing is
19925
+ // logged, not thrown, so lossy/data-track sends don't reject during teardown windows.
19926
+ if (error instanceof TypeError) {
19927
+ livekitLogger.error(error);
19928
+ } else {
19929
+ throw error;
19930
+ }
19931
+ }
19932
+ });
19933
+ }
19934
+ /**
19935
+ * Starts the once-per-second adjustment of the channel's `bufferedAmountLowThreshold` to the
19936
+ * observed byterate, keeping the drop gate at roughly 100ms of buffered latency (clamped to
19937
+ * the watermarks). Restarts cleanly if already running.
19938
+ */
19939
+ startThresholdTuning() {
19940
+ this.stopThresholdTuning();
19941
+ this.statInterval = CriticalTimers.setInterval(() => {
19942
+ this.statByterate = this.statCurrentBytes;
19943
+ this.statCurrentBytes = 0;
19944
+ const dc = this.getChannel();
19945
+ if (dc) {
19946
+ // control buffered latency to ~100ms
19947
+ const threshold = this.statByterate / 10;
19948
+ dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, this.lowWaterMark), this.highWaterMark);
19949
+ }
19950
+ }, 1000);
19951
+ }
19952
+ /** Stops the threshold tuning and resets the stats and drop counter. */
19953
+ stopThresholdTuning() {
19954
+ this.statByterate = 0;
19955
+ this.statCurrentBytes = 0;
19956
+ if (this.statInterval) {
19957
+ CriticalTimers.clearInterval(this.statInterval);
19958
+ this.statInterval = undefined;
19959
+ }
19960
+ this.dropCount = 0;
19961
+ }
19962
+ }class DataPacketBuffer {
19963
+ constructor() {
19964
+ this.buffer = [];
19965
+ this._totalSize = 0;
19966
+ this._sentSize = 0;
19967
+ }
19968
+ push(item) {
19969
+ this.buffer.push(item);
19970
+ this._totalSize += item.data.byteLength;
19971
+ if (item.sent) {
19972
+ this._sentSize += item.data.byteLength;
19973
+ }
19974
+ }
19975
+ pop() {
19976
+ const item = this.buffer.shift();
19977
+ if (item) {
19978
+ this._totalSize -= item.data.byteLength;
19979
+ if (item.sent) {
19980
+ this._sentSize -= item.data.byteLength;
19981
+ }
19982
+ }
19983
+ return item;
19984
+ }
19985
+ getAll() {
19986
+ return this.buffer.slice();
19987
+ }
19988
+ /** Every queued packet not yet handed to the channel, in sequence order. */
19989
+ getUnsent() {
19990
+ return this.buffer.filter(item => !item.sent);
19991
+ }
19992
+ /** Marks a single queued packet as handed to the channel. */
19993
+ markSent(item) {
19994
+ if (!item.sent) {
19995
+ item.sent = true;
19996
+ this._sentSize += item.data.byteLength;
19997
+ }
19998
+ }
19999
+ /**
20000
+ * Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
20001
+ * still buffered was sent on the previous channel (or deferred) and must be re-handed to the
20002
+ * current one, so none of it counts as sent until the replay actually transmits it.
20003
+ */
20004
+ markAllUnsent() {
20005
+ for (const item of this.buffer) {
20006
+ item.sent = false;
20007
+ }
20008
+ this._sentSize = 0;
20009
+ }
20010
+ popToSequence(sequence) {
20011
+ while (this.buffer.length > 0) {
20012
+ const first = this.buffer[0];
20013
+ if (first.sequence <= sequence) {
20014
+ this.pop();
20015
+ } else {
20016
+ break;
20017
+ }
20018
+ }
20019
+ }
20020
+ alignBufferedAmount(bufferedAmount) {
20021
+ while (this.buffer.length > 0) {
20022
+ const first = this.buffer[0];
20023
+ // Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
20024
+ // resume replay — trimming them would silently lose them.
20025
+ if (!first.sent) {
20026
+ break;
20027
+ }
20028
+ if (this._sentSize - first.data.byteLength <= bufferedAmount) {
20029
+ break;
20030
+ }
20031
+ this.pop();
20032
+ }
20033
+ }
20034
+ get length() {
20035
+ return this.buffer.length;
20036
+ }
20037
+ }/**
20038
+ * The reliable channel: flow control plus delivery-across-resume semantics.
20039
+ *
20040
+ * Every packet gets a monotonic sequence (stamped into the protobuf by the caller before
20041
+ * serialization, via {@link nextSequence}) and is retained in a replay buffer until the channel's
20042
+ * `bufferedAmount` confirms it has been handed to the transport. Sends that land in a reconnect
20043
+ * window — or whose headroom wait is torn down transiently — are queued unsent and resolve;
20044
+ * {@link replay} delivers them (plus any unacked packets) after a resume. Only an engine close
20045
+ * rejects, because no replay is coming after that.
20046
+ */
20047
+ class ReliableDataChannel extends FlowControlledDataChannel {
20048
+ constructor(opts) {
20049
+ super(opts);
20050
+ this.messageBuffer = new DataPacketBuffer();
20051
+ this.sequence = 1;
20052
+ this.isDeferringSends = opts.isDeferringSends;
20053
+ }
20054
+ /**
20055
+ * Claims the next packet sequence. The caller stamps it into the packet before serialization,
20056
+ * then passes it back to {@link send} so the replay buffer stays keyed by wire sequence.
20057
+ */
20058
+ nextSequence() {
20059
+ const sequence = this.sequence;
20060
+ this.sequence += 1;
20061
+ return sequence;
20062
+ }
20063
+ /**
20064
+ * Sends prepared bytes with reliable semantics. Resolves once the packet has either been handed
20065
+ * to the channel or queued for the resume replay; throws only when the engine is closed.
20066
+ */
20067
+ send(msg, sequence) {
20068
+ return __awaiter(this, void 0, void 0, function* () {
20069
+ if (this.isDeferringSends()) {
20070
+ // A reconnect is already underway — queue for the resume replay instead of parking on a
20071
+ // channel that is being torn down. The send resolves; delivery is deferred to the replay.
20072
+ this.messageBuffer.push({
20073
+ data: msg,
20074
+ sequence,
20075
+ sent: false
20076
+ });
20077
+ return;
20078
+ }
20079
+ const dc = this.getChannel();
20080
+ if (!dc) {
20081
+ return;
20082
+ }
20083
+ try {
20084
+ yield this.waitForHeadroomWithLock();
20085
+ } catch (error) {
20086
+ if (this.isEngineClosed()) {
20087
+ // No replay is coming after an engine close — surface the failure.
20088
+ throw error;
20089
+ }
20090
+ // Transient teardown (the channel closed or was replaced while we waited): the reliable
20091
+ // channel promises delivery across resume, so queue the packet for the replay instead of
20092
+ // rejecting a send the app can't meaningfully retry.
20093
+ this.messageBuffer.push({
20094
+ data: msg,
20095
+ sequence,
20096
+ sent: false
20097
+ });
20098
+ return;
20099
+ }
20100
+ if (this.isDeferringSends()) {
20101
+ // A reconnect began while we waited for headroom — same deal as above.
20102
+ this.messageBuffer.push({
20103
+ data: msg,
20104
+ sequence,
20105
+ sent: false
20106
+ });
20107
+ return;
20108
+ }
20109
+ this.messageBuffer.push({
20110
+ data: msg,
20111
+ sequence,
20112
+ sent: true
20113
+ });
20114
+ dc.send(msg);
20115
+ this.refreshBufferStatus();
20116
+ });
20117
+ }
20118
+ /**
20119
+ * Replays the buffered backlog after a resume: drops everything the server acked
20120
+ * (`lastMessageSeq`), then re-sends the rest in order. The headroom lock is held across the
20121
+ * whole replay — releasing it between messages would let a concurrent send (whose newer
20122
+ * sequence was already assigned before it queued on the lock) hit the wire mid-replay, and
20123
+ * receivers would then discard the remaining lower-sequence resent messages as duplicates.
20124
+ */
20125
+ replay(lastMessageSeq) {
20126
+ return __awaiter(this, void 0, void 0, function* () {
20127
+ const dc = this.getChannel();
20128
+ if (!dc) {
20129
+ return;
20130
+ }
20131
+ this.messageBuffer.popToSequence(lastMessageSeq);
20132
+ const unlock = yield this.lockHeadroom();
20133
+ try {
20134
+ // Everything left after the ack cutoff must be re-handed to the current channel.
20135
+ this.messageBuffer.markAllUnsent();
20136
+ // Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
20137
+ // sent:false) during our own awaits appends after this pass started, so we pick it up on
20138
+ // the next one. Mark each packet only once we've actually handed it to the channel — a
20139
+ // blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
20140
+ // and a later alignBufferedAmount would then drop it for good. If the loop throws
20141
+ // mid-drain, unsent entries keep their flag and the next replay picks them up.
20142
+ for (let batch = this.messageBuffer.getUnsent(); batch.length > 0; batch = this.messageBuffer.getUnsent()) {
20143
+ for (const item of batch) {
20144
+ // Respect flow control on resume too, so a large resend doesn't overflow the buffer.
20145
+ yield this.waitForHeadroomWithoutLock();
20146
+ dc.send(item.data);
20147
+ this.messageBuffer.markSent(item);
20148
+ }
20149
+ }
20150
+ } finally {
20151
+ unlock();
20152
+ }
20153
+ this.refreshBufferStatus();
20154
+ });
20155
+ }
20156
+ /**
20157
+ * Before recomputing status, trim packets the transport has now delivered — a send or a drain
20158
+ * may have acked buffered packets, and the replay buffer is keyed off the channel's buffered
20159
+ * bytes.
20160
+ */
20161
+ refreshBufferStatus() {
20162
+ const dc = this.channelHandle;
20163
+ if (dc) {
20164
+ this.messageBuffer.alignBufferedAmount(dc.bufferedAmount);
20165
+ }
20166
+ super.refreshBufferStatus();
20167
+ }
20168
+ /**
20169
+ * Drops all replay state and restarts sequencing. Only valid on a full reconnect, where the
20170
+ * session (and the receivers' sequence tracking) starts over.
20171
+ */
20172
+ reset() {
20173
+ this.messageBuffer = new DataPacketBuffer();
20174
+ this.sequence = 1;
20175
+ }
20176
+ }var DataChannelKind;
20177
+ (function (DataChannelKind) {
20178
+ DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
20179
+ DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
20180
+ DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
20181
+ })(DataChannelKind || (DataChannelKind = {}));
20182
+ // Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
20183
+ // freely up to the high-water mark; once it's exceeded they block until the browser's
20184
+ // `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
20185
+ // The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
20186
+ // isn't starved, while the high-water mark bounds the buffer well below the level that would abort
20187
+ // the channel (see livekit/client-sdk-js#1995).
20188
+ const reliableDataChannelWaterMarkLow = 64 * 1024;
20189
+ const reliableDataChannelWaterMarkHigh = 1024 * 1024;
20190
+ const lossyDataChannelWaterMarkLow = 8 * 1024;
20191
+ const lossyDataChannelWaterMarkHigh = 256 * 1024;
20192
+ function dataChannelLowWaterMark(kind) {
20193
+ return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkLow : lossyDataChannelWaterMarkLow;
20194
+ }
20195
+ function dataChannelHighWaterMark(kind) {
20196
+ return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
20197
+ }const lossyDataChannelLabel = '_lossy';
20198
+ const reliableDataChannelLabel = '_reliable';
20199
+ const dataTrackDataChannelLabel = '_data_track';
20200
+ /**
20201
+ * Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
20202
+ * live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
20203
+ * connections come and go) plus the subscriber-side receive handles adopted by label.
20204
+ *
20205
+ * Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which reject
20206
+ * parked headroom waiters as a built-in — there is no separate invalidation step to forget.
20207
+ */
20208
+ class DataChannelManager {
20209
+ constructor(opts) {
20210
+ this.opts = opts;
20211
+ const flowControlOptions = kind => ({
20212
+ kind,
20213
+ lowWaterMark: dataChannelLowWaterMark(kind),
20214
+ highWaterMark: dataChannelHighWaterMark(kind),
20215
+ isEngineClosed: opts.isEngineClosed,
20216
+ onBufferStatusChanged: isLow => opts.onBufferStatusChanged(kind, isLow)
20217
+ });
20218
+ this.reliable = new ReliableDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.RELIABLE)), {
20219
+ isDeferringSends: opts.isReconnecting
20220
+ }));
20221
+ this.lossy = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.LOSSY)), {
20222
+ // Classic lossy user data: a stale packet is worthless, so drop instead of queueing.
20223
+ bufferFullBehavior: 'drop',
20224
+ shouldSkipSends: opts.isReconnecting
20225
+ }));
20226
+ this.dataTrack = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.DATA_TRACK_LOSSY)), {
20227
+ // Data tracks backpressure the producer instead — it decides what to skip at frame
20228
+ // granularity rather than the engine dropping arbitrary chunks out of frames.
20229
+ bufferFullBehavior: 'wait',
20230
+ shouldSkipSends: opts.isReconnecting
20231
+ }));
20232
+ }
20233
+ /** The flow-control wrapper for `kind`. */
20234
+ channelFor(kind) {
20235
+ switch (kind) {
20236
+ case DataChannelKind.RELIABLE:
20237
+ return this.reliable;
20238
+ case DataChannelKind.LOSSY:
20239
+ return this.lossy;
20240
+ case DataChannelKind.DATA_TRACK_LOSSY:
20241
+ return this.dataTrack;
20242
+ }
20243
+ }
20244
+ /** The raw RTCDataChannel handle for `kind`, publisher side by default. */
20245
+ getHandle(kind) {
20246
+ let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
20247
+ if (!subscriber) {
20248
+ return this.channelFor(kind).channelHandle;
20249
+ }
20250
+ switch (kind) {
20251
+ case DataChannelKind.RELIABLE:
20252
+ return this.reliableSub;
20253
+ case DataChannelKind.LOSSY:
20254
+ return this.lossySub;
20255
+ case DataChannelKind.DATA_TRACK_LOSSY:
20256
+ return this.dataTrackSub;
20257
+ }
20258
+ }
20259
+ get hasPublisherChannels() {
20260
+ return Boolean(this.reliable.channelHandle || this.lossy.channelHandle || this.dataTrack.channelHandle);
20261
+ }
20262
+ /**
20263
+ * Creates the three publisher data channels on the given transport, wires their handlers, and
20264
+ * attaches them to the wrappers — attaching rejects any waiters still parked on replaced
20265
+ * channel objects.
20266
+ */
20267
+ createPublisherChannels(pcManager) {
20268
+ // clear old data channel callbacks if recreate
20269
+ for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
20270
+ const old = channel.channelHandle;
20271
+ if (old) {
20272
+ old.onmessage = null;
20273
+ old.onerror = null;
20274
+ old.onclose = null;
20275
+ }
20276
+ }
20277
+ const wire = (channel, dc, onMessage) => {
20278
+ // also handle messages over the pub channel, for backwards compatibility
20279
+ dc.onmessage = onMessage;
20280
+ // handle datachannel errors
20281
+ dc.onerror = this.opts.onDataError;
20282
+ // detect unexpected publisher data channel closes
20283
+ dc.onclose = () => this.opts.onChannelClose(channel.kind);
20284
+ // set up dc buffer threshold - if this is not set, it will default to 0
20285
+ dc.bufferedAmountLowThreshold = channel.lowWaterMark;
20286
+ // handle buffer amount low events
20287
+ dc.onbufferedamountlow = () => channel.refreshBufferStatus();
20288
+ channel.attach(dc);
20289
+ };
20290
+ wire(this.lossy, pcManager.createPublisherDataChannel(lossyDataChannelLabel, {
20291
+ ordered: false,
20292
+ maxRetransmits: 0
20293
+ }), this.opts.onDataMessage);
20294
+ wire(this.reliable, pcManager.createPublisherDataChannel(reliableDataChannelLabel, {
20295
+ ordered: true
20296
+ }), this.opts.onDataMessage);
20297
+ wire(this.dataTrack, pcManager.createPublisherDataChannel(dataTrackDataChannelLabel, {
20298
+ ordered: false,
20299
+ maxRetransmits: 0
20300
+ }), this.opts.onDataTrackMessage);
20301
+ this.lossy.startThresholdTuning();
20302
+ }
20303
+ /**
20304
+ * Adopts a subscriber-side data channel by label, wiring the matching receive handler.
20305
+ * Returns false for labels this manager doesn't own.
20306
+ */
20307
+ adoptSubscriberChannel(channel) {
20308
+ let handler;
20309
+ if (channel.label === reliableDataChannelLabel) {
20310
+ this.reliableSub = channel;
20311
+ handler = this.opts.onDataMessage;
20312
+ } else if (channel.label === lossyDataChannelLabel) {
20313
+ this.lossySub = channel;
20314
+ handler = this.opts.onDataMessage;
20315
+ } else if (channel.label === dataTrackDataChannelLabel) {
20316
+ this.dataTrackSub = channel;
20317
+ handler = this.opts.onDataTrackMessage;
20318
+ } else {
20319
+ return false;
20320
+ }
20321
+ channel.onmessage = handler;
20322
+ return true;
20323
+ }
20324
+ /**
20325
+ * Tears down all channels for a peer-connection cleanup: rejects parked waiters (detach — the
20326
+ * spec allows `pc.close()` to transition channels to 'closed' without firing events, so waiting
20327
+ * for browser close events is not an option), strips handlers, closes the handles, and resets
20328
+ * the reliable session state.
20329
+ */
20330
+ teardown() {
20331
+ const dcCleanup = dc => {
20332
+ if (!dc) {
20333
+ return;
20334
+ }
20335
+ // Detach the data channel handlers before closing anything. Closing a peer connection tears
20336
+ // down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
20337
+ // channels; if our handlers are still attached at that point, handleDataError logs a spurious
20338
+ // "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
20339
+ // before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
20340
+ // browser dispatches those teardown events. See livekit/client-sdk-js#1953.
20341
+ dc.onbufferedamountlow = null;
20342
+ dc.onclose = null;
20343
+ dc.onclosing = null;
20344
+ dc.onerror = null;
20345
+ dc.onmessage = null;
20346
+ dc.onopen = null;
20347
+ dc.close();
20348
+ };
20349
+ for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
20350
+ const dc = channel.channelHandle;
20351
+ channel.detach('peer connections cleaned up');
20352
+ dcCleanup(dc);
20353
+ }
20354
+ dcCleanup(this.lossySub);
20355
+ dcCleanup(this.reliableSub);
20356
+ dcCleanup(this.dataTrackSub);
20357
+ this.lossySub = undefined;
20358
+ this.reliableSub = undefined;
20359
+ this.dataTrackSub = undefined;
20360
+ // Full teardown starts the session over: drop replay state and restart sequencing.
20361
+ this.reliable.reset();
20362
+ }
20363
+ }// Check if MediaRecorder is available
20364
+ const isMediaRecorderAvailable = typeof MediaRecorder !== 'undefined';
20365
+ // Fallback class for environments without MediaRecorder
20366
+ class FallbackRecorder {
20367
+ constructor() {
20368
+ throw new Error('MediaRecorder is not available in this environment');
20369
+ }
20370
+ }
20371
+ // Use conditional inheritance to avoid parse-time errors
20372
+ const RecorderBase = isMediaRecorderAvailable ? MediaRecorder : FallbackRecorder;
20373
+ class LocalTrackRecorder extends RecorderBase {
20374
+ constructor(track, options) {
20375
+ if (!isMediaRecorderAvailable) {
20376
+ throw new Error('MediaRecorder is not available in this environment');
20377
+ }
20378
+ super(new MediaStream([track.mediaStreamTrack]), options);
20379
+ let dataListener;
20380
+ let streamController;
20381
+ const isClosed = () => streamController === undefined;
20382
+ const onStop = () => {
20383
+ this.removeEventListener('dataavailable', dataListener);
20384
+ this.removeEventListener('stop', onStop);
20385
+ this.removeEventListener('error', onError);
20386
+ streamController === null || streamController === void 0 ? void 0 : streamController.close();
20387
+ streamController = undefined;
20388
+ };
20389
+ const onError = event => {
20390
+ streamController === null || streamController === void 0 ? void 0 : streamController.error(event);
20391
+ this.removeEventListener('dataavailable', dataListener);
20392
+ this.removeEventListener('stop', onStop);
20393
+ this.removeEventListener('error', onError);
20394
+ streamController = undefined;
20395
+ };
20396
+ this.byteStream = new ReadableStream({
20397
+ start: controller => {
20398
+ streamController = controller;
20399
+ dataListener = event => __awaiter(this, void 0, void 0, function* () {
20400
+ let data;
20401
+ if (event.data.arrayBuffer) {
20402
+ const arrayBuffer = yield event.data.arrayBuffer();
20403
+ data = new Uint8Array(arrayBuffer);
20404
+ // @ts-expect-error react-native passes over Uint8Arrays directly
20405
+ } else if (event.data.byteArray) {
20406
+ // @ts-expect-error
20407
+ data = event.data.byteArray;
20408
+ } else {
20409
+ throw new Error('no data available!');
20410
+ }
20411
+ if (isClosed()) {
20412
+ return;
20413
+ }
20414
+ controller.enqueue(data);
20415
+ });
20416
+ this.addEventListener('dataavailable', dataListener);
20417
+ },
20418
+ cancel: () => {
20419
+ onStop();
20420
+ }
20421
+ });
20422
+ this.addEventListener('stop', onStop);
20423
+ this.addEventListener('error', onError);
20424
+ }
20425
+ }
20426
+ // Helper function to check if recording is supported
20427
+ function isRecordingSupported() {
20428
+ return isMediaRecorderAvailable;
20429
+ }const DEFAULT_DIMENSIONS_TIMEOUT = 1000;
20430
+ const PRE_CONNECT_BUFFER_TIMEOUT = 10000;
20431
+ class LocalTrack extends Track {
20432
+ /** @internal */
20433
+ get sender() {
20434
+ return this._sender;
20435
+ }
20436
+ /** @internal */
20437
+ set sender(sender) {
20438
+ this._sender = sender;
20439
+ }
20440
+ get constraints() {
20441
+ return this._constraints;
20442
+ }
20443
+ get hasPreConnectBuffer() {
20444
+ return !!this.localTrackRecorder;
20445
+ }
20446
+ /**
20447
+ *
20448
+ * @param mediaTrack
20449
+ * @param kind
20450
+ * @param constraints MediaTrackConstraints that are being used when restarting or reacquiring tracks
20451
+ * @param userProvidedTrack Signals to the SDK whether or not the mediaTrack should be managed (i.e. released and reacquired) internally by the SDK
20452
+ */
20453
+ constructor(mediaTrack, kind, constraints) {
20454
+ let userProvidedTrack = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
20455
+ let loggerOptions = arguments.length > 4 ? arguments[4] : undefined;
20456
+ super(mediaTrack, kind, loggerOptions);
20457
+ this.manuallyStopped = false;
20458
+ this.pendingDeviceChange = false;
20459
+ this._isUpstreamPaused = false;
20460
+ this.handleTrackMuteEvent = () => this.debouncedTrackMuteHandler().catch(() => this.log.debug('track mute bounce got cancelled by an unmute event', this.logContext));
20461
+ this.debouncedTrackMuteHandler = debounce(() => __awaiter(this, void 0, void 0, function* () {
19391
20462
  yield this.pauseUpstream();
19392
20463
  }), 5000);
19393
20464
  this.handleTrackUnmuteEvent = () => __awaiter(this, void 0, void 0, function* () {
@@ -21013,17 +22084,37 @@ class LocalVideoTrack extends LocalTrack {
21013
22084
  setDegradationPreference(preference) {
21014
22085
  return __awaiter(this, void 0, void 0, function* () {
21015
22086
  this.degradationPreference = preference;
21016
- if (this.sender) {
21017
- try {
21018
- this.log.debug("setting degradationPreference to ".concat(preference), this.logContext);
21019
- const params = this.sender.getParameters();
21020
- params.degradationPreference = preference;
21021
- this.sender.setParameters(params);
21022
- } catch (e) {
21023
- this.log.warn("failed to set degradationPreference", Object.assign({
21024
- error: e
21025
- }, this.logContext));
21026
- }
22087
+ // applied one sender at a time on purpose, see applyDegradationPreference
22088
+ yield this.applyDegradationPreference(this.sender);
22089
+ for (const sc of this.simulcastCodecs.values()) {
22090
+ yield this.applyDegradationPreference(sc.sender);
22091
+ }
22092
+ });
22093
+ }
22094
+ /**
22095
+ * Degradation preference is a property of the sender, not of the track, so every sender
22096
+ * publishing this track needs it applied separately. A backup codec publishes over its
22097
+ * own sender, which would otherwise let the browser resolve a preference implicitly and
22098
+ * diverge from the primary encoder.
22099
+ *
22100
+ * Callers apply this sequentially rather than concurrently: `setParameters` is only valid
22101
+ * against the parameters most recently returned by `getParameters`, which is why this file
22102
+ * serializes other sender parameter updates through `senderLock`.
22103
+ */
22104
+ applyDegradationPreference(sender) {
22105
+ return __awaiter(this, void 0, void 0, function* () {
22106
+ if (!sender) {
22107
+ return;
22108
+ }
22109
+ try {
22110
+ this.log.debug("setting degradationPreference to ".concat(this.degradationPreference), this.logContext);
22111
+ const params = sender.getParameters();
22112
+ params.degradationPreference = this.degradationPreference;
22113
+ yield sender.setParameters(params);
22114
+ } catch (e) {
22115
+ this.log.warn("failed to set degradationPreference", Object.assign({
22116
+ error: e
22117
+ }, this.logContext));
21027
22118
  }
21028
22119
  });
21029
22120
  }
@@ -21042,18 +22133,23 @@ class LocalVideoTrack extends LocalTrack {
21042
22133
  return simulcastCodecInfo;
21043
22134
  }
21044
22135
  setSimulcastTrackSender(codec, sender) {
21045
- const simulcastCodecInfo = this.simulcastCodecs.get(codec);
21046
- if (!simulcastCodecInfo) {
21047
- return;
21048
- }
21049
- simulcastCodecInfo.sender = sender;
21050
- // browser will reenable disabled codec/layers after new codec has been published,
21051
- // so refresh subscribedCodecs after publish a new codec
21052
- setTimeout(() => {
21053
- if (this.subscribedCodecs) {
21054
- this.setPublishingCodecs(this.subscribedCodecs);
22136
+ return __awaiter(this, void 0, void 0, function* () {
22137
+ const simulcastCodecInfo = this.simulcastCodecs.get(codec);
22138
+ if (!simulcastCodecInfo) {
22139
+ return;
21055
22140
  }
21056
- }, refreshSubscribedCodecAfterNewCodec);
22141
+ simulcastCodecInfo.sender = sender;
22142
+ // the backup codec publishes over its own sender, so it needs the same degradation
22143
+ // preference the primary sender resolved to.
22144
+ yield this.applyDegradationPreference(sender);
22145
+ // browser will reenable disabled codec/layers after new codec has been published,
22146
+ // so refresh subscribedCodecs after publish a new codec
22147
+ setTimeout(() => {
22148
+ if (this.subscribedCodecs) {
22149
+ this.setPublishingCodecs(this.subscribedCodecs);
22150
+ }
22151
+ }, refreshSubscribedCodecAfterNewCodec);
22152
+ });
21057
22153
  }
21058
22154
  /**
21059
22155
  * @internal
@@ -21323,22 +22419,14 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
21323
22419
  ssrc: 0
21324
22420
  });
21325
22421
  });
21326
- }const lossyDataChannel = '_lossy';
21327
- const reliableDataChannel = '_reliable';
21328
- const dataTrackDataChannel = '_data_track';
21329
- const minReconnectWait = 2 * 1000;
22422
+ }const minReconnectWait = 2 * 1000;
21330
22423
  const leaveReconnect = 'leave-reconnect';
22424
+ /**
22425
+ * How long local connection quality must stay `LOST` while connected and publishing before we
22426
+ * force a full reconnect — `LOST` is the server's verdict that it isn't receiving our media.
22427
+ */
22428
+ const connectionQualityLostTimeout = 10 * 1000;
21331
22429
  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;
21342
22430
  const initialMediaSectionsAudio = 3;
21343
22431
  const initialMediaSectionsVideo = 3;
21344
22432
  var PCState;
@@ -21349,12 +22437,6 @@ var PCState;
21349
22437
  PCState[PCState["Reconnecting"] = 3] = "Reconnecting";
21350
22438
  PCState[PCState["Closed"] = 4] = "Closed";
21351
22439
  })(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
22440
  // Default data-channel max message size (bytes), used when the remote SDP
21359
22441
  // answer does not advertise an `a=max-message-size` attribute (RFC 8841).
21360
22442
  // `0` means "no limit".
@@ -21370,6 +22452,15 @@ class RTCEngine extends eventsExports.EventEmitter {
21370
22452
  get pendingReconnect() {
21371
22453
  return !!this.reconnectTimeout;
21372
22454
  }
22455
+ get reliableChannel() {
22456
+ return this.dataChannels.reliable;
22457
+ }
22458
+ get lossyChannel() {
22459
+ return this.dataChannels.lossy;
22460
+ }
22461
+ get dataTrackChannel() {
22462
+ return this.dataChannels.dataTrack;
22463
+ }
21373
22464
  constructor(options) {
21374
22465
  var _a;
21375
22466
  super();
@@ -21396,12 +22487,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21396
22487
  this.shouldFailNext = false;
21397
22488
  this.shouldFailOnV1Path = false;
21398
22489
  this.log = livekitLogger;
21399
- this.reliableDataSequence = 1;
21400
- this.reliableMessageBuffer = new DataPacketBuffer();
21401
22490
  this.reliableReceivedState = new TTLMap(reliabeReceiveStateTTL);
21402
- this.lossyDataStatCurrentBytes = 0;
21403
- this.lossyDataStatByterate = 0;
21404
- this.lossyDataDropCount = 0;
21405
22491
  this.midToTrackId = {};
21406
22492
  /** used to indicate whether the browser is currently waiting to reconnect */
21407
22493
  this.isWaitingForNetworkReconnect = false;
@@ -21412,21 +22498,9 @@ class RTCEngine extends eventsExports.EventEmitter {
21412
22498
  if (!channel) {
21413
22499
  return;
21414
22500
  }
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;
22501
+ if (_this.dataChannels.adoptSubscriberChannel(channel)) {
22502
+ _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21427
22503
  }
21428
- _this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
21429
- channel.onmessage = handler;
21430
22504
  }();
21431
22505
  });
21432
22506
  this.handleDataMessage = message => __awaiter(this, void 0, void 0, function* () {
@@ -21434,19 +22508,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21434
22508
  // make sure to respect incoming data message order by processing message events one after the other
21435
22509
  const unlock = yield this.dataProcessLock.lock();
21436
22510
  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
- });
22511
+ const bytes = yield this.decodeDataMessage(message);
22512
+ if (!bytes) {
21447
22513
  return;
21448
22514
  }
21449
- const dp = DataPacket.fromBinary(new Uint8Array(buffer));
22515
+ const dp = DataPacket.fromBinary(bytes);
21450
22516
  if (dp.sequence > 0 && dp.participantSid !== '') {
21451
22517
  const lastSeq = this.reliableReceivedState.get(dp.participantSid);
21452
22518
  if (lastSeq && dp.sequence <= lastSeq) {
@@ -21487,19 +22553,11 @@ class RTCEngine extends eventsExports.EventEmitter {
21487
22553
  }
21488
22554
  });
21489
22555
  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
- });
22556
+ const bytes = yield this.decodeDataMessage(message);
22557
+ if (!bytes) {
21500
22558
  return;
21501
22559
  }
21502
- this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
22560
+ this.emit('dataTrackPacketReceived', bytes);
21503
22561
  });
21504
22562
  this.handleDataError = event => {
21505
22563
  // Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as
@@ -21533,9 +22591,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21533
22591
  this.log.error("publisher data channel '".concat(DataChannelKind[kind], "' closed unexpectedly"), this.logContext);
21534
22592
  }
21535
22593
  };
21536
- this.handleBufferedAmountLow = channelKind => {
21537
- this.updateAndEmitDCBufferStatus(channelKind);
21538
- };
21539
22594
  // websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
21540
22595
  // continues to work, we can reconnect to websocket to continue the session
21541
22596
  // after a number of retries, we'll close and give up permanently
@@ -21591,60 +22646,6 @@ class RTCEngine extends eventsExports.EventEmitter {
21591
22646
  this.once(EngineEvent.Disconnected, onDisconnected);
21592
22647
  });
21593
22648
  };
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
22649
  this.onRtpMapAvailable = rtpTypes => {
21649
22650
  const rtpMap = new Map();
21650
22651
  rtpTypes.forEach(rtp => {
@@ -21704,9 +22705,20 @@ class RTCEngine extends eventsExports.EventEmitter {
21704
22705
  this.reconnectPolicy = this.options.reconnectPolicy;
21705
22706
  this.closingLock = new _();
21706
22707
  this.dataProcessLock = new _();
21707
- this.dcBufferStatus = new Map([[DataChannelKind.RELIABLE, true], [DataChannelKind.LOSSY, true], [DataChannelKind.DATA_TRACK_LOSSY, true]]);
22708
+ this.dataChannels = new DataChannelManager({
22709
+ isEngineClosed: () => this.isClosed,
22710
+ isReconnecting: () => this.attemptingReconnect,
22711
+ onDataMessage: message => this.handleDataMessage(message),
22712
+ onDataTrackMessage: message => this.handleDataTrackMessage(message),
22713
+ onDataError: event => this.handleDataError(event),
22714
+ onChannelClose: kind => this.handleDataChannelClose(kind)(),
22715
+ onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
22716
+ });
21708
22717
  this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
21709
- this.client.onConnectionQuality = update => this.emit(EngineEvent.ConnectionQualityUpdate, update);
22718
+ this.client.onConnectionQuality = update => {
22719
+ this.handleLocalConnectionQuality(update);
22720
+ this.emit(EngineEvent.ConnectionQualityUpdate, update);
22721
+ };
21710
22722
  this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
21711
22723
  this.client.onSubscriptionError = resp => this.emit(EngineEvent.SubscriptionError, resp);
21712
22724
  this.client.onSubscriptionPermissionUpdate = update => this.emit(EngineEvent.SubscriptionPermissionUpdate, update);
@@ -21849,6 +22861,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21849
22861
  this.removeAllListeners();
21850
22862
  this.deregisterOnLineListener();
21851
22863
  this.clearPendingReconnect();
22864
+ this.clearLostQualityTimeout();
21852
22865
  this.cleanupLossyDataStats();
21853
22866
  yield this.cleanupPeerConnections();
21854
22867
  yield this.cleanupClient();
@@ -21860,55 +22873,16 @@ class RTCEngine extends eventsExports.EventEmitter {
21860
22873
  cleanupPeerConnections() {
21861
22874
  return __awaiter(this, void 0, void 0, function* () {
21862
22875
  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);
22876
+ this.dataChannels.teardown();
21891
22877
  yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
21892
22878
  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;
22879
+ // the connecting timestamp belongs to the transports we just tore down
22880
+ this.transportConnectingSince = undefined;
21901
22881
  this.reliableReceivedState.clear();
21902
22882
  });
21903
22883
  }
21904
22884
  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;
22885
+ this.lossyChannel.stopThresholdTuning();
21912
22886
  }
21913
22887
  cleanupClient() {
21914
22888
  return __awaiter(this, void 0, void 0, function* () {
@@ -21929,17 +22903,17 @@ class RTCEngine extends eventsExports.EventEmitter {
21929
22903
  throw new TrackInvalidError('a track with the same ID has already been published');
21930
22904
  }
21931
22905
  return new Promise((resolve, reject) => {
21932
- const publicationTimeout = setTimeout(() => {
22906
+ const publicationTimeout = CriticalTimers.setTimeout(() => {
21933
22907
  delete this.pendingTrackResolvers[req.cid];
21934
22908
  reject(ConnectionError.timeout('publication of local track timed out, no response from server'));
21935
22909
  }, 10000);
21936
22910
  this.pendingTrackResolvers[req.cid] = {
21937
22911
  resolve: info => {
21938
- clearTimeout(publicationTimeout);
22912
+ CriticalTimers.clearTimeout(publicationTimeout);
21939
22913
  resolve(info);
21940
22914
  },
21941
22915
  reject: () => {
21942
- clearTimeout(publicationTimeout);
22916
+ CriticalTimers.clearTimeout(publicationTimeout);
21943
22917
  reject(new Error('Cancelled publication by calling unpublish'));
21944
22918
  }
21945
22919
  };
@@ -21975,7 +22949,7 @@ class RTCEngine extends eventsExports.EventEmitter {
21975
22949
  }
21976
22950
  get dataSubscriberReadyState() {
21977
22951
  var _a;
21978
- return (_a = this.reliableDCSub) === null || _a === void 0 ? void 0 : _a.readyState;
22952
+ return (_a = this.dataChannelForKind(DataChannelKind.RELIABLE, true)) === null || _a === void 0 ? void 0 : _a.readyState;
21979
22953
  }
21980
22954
  getConnectedServerAddress() {
21981
22955
  return __awaiter(this, void 0, void 0, function* () {
@@ -22012,6 +22986,15 @@ class RTCEngine extends eventsExports.EventEmitter {
22012
22986
  this.pcManager.onDataChannel = this.handleDataChannel;
22013
22987
  this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
22014
22988
  this.log.debug("primary PC state changed ".concat(connectionState));
22989
+ // Record when the primary transport actually entered CONNECTING so
22990
+ // verifyTransport() can bound how long we tolerate it. Deriving it from the
22991
+ // real transition (this handler only fires on state changes) rather than from
22992
+ // observation time keeps it from going stale across peer-connection rebuilds.
22993
+ if (connectionState === PCTransportState.CONNECTING) {
22994
+ this.transportConnectingSince = Date.now();
22995
+ } else {
22996
+ this.transportConnectingSince = undefined;
22997
+ }
22015
22998
  if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
22016
22999
  // reset publisher connection promise
22017
23000
  this.publisherConnectionPromise = undefined;
@@ -22220,84 +23203,45 @@ class RTCEngine extends eventsExports.EventEmitter {
22220
23203
  }
22221
23204
  }
22222
23205
  addMediaSections(numAudios, numVideos) {
22223
- var _a, _b;
23206
+ var _a, _b, _c;
22224
23207
  const transceiverInit = {
22225
23208
  direction: 'recvonly'
22226
23209
  };
22227
23210
  for (let i = 0; i < numAudios; i++) {
22228
23211
  (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.addPublisherTransceiverOfKind('audio', transceiverInit);
22229
23212
  }
23213
+ // media only arrives on these sections when there is no subscriber connection to arrive on
23214
+ const receivesMedia = ((_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.mode) === 'publisher-only';
22230
23215
  for (let i = 0; i < numVideos; i++) {
22231
- (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.addPublisherTransceiverOfKind('video', transceiverInit);
23216
+ const transceiver = (_c = this.pcManager) === null || _c === void 0 ? void 0 : _c.addPublisherTransceiverOfKind('video', transceiverInit);
23217
+ if (receivesMedia && transceiver) {
23218
+ const negotiated = negotiateDependencyDescriptor(transceiver);
23219
+ this.log.debug('dependency descriptor negotiated for received video', {
23220
+ negotiated
23221
+ });
23222
+ }
22232
23223
  }
22233
23224
  }
22234
23225
  createDataChannels() {
22235
23226
  if (!this.pcManager) {
22236
23227
  return;
22237
23228
  }
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);
23229
+ this.dataChannels.createPublisherChannels(this.pcManager);
23230
+ }
23231
+ /** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
23232
+ decodeDataMessage(message) {
23233
+ return __awaiter(this, void 0, void 0, function* () {
23234
+ if (message.data instanceof ArrayBuffer) {
23235
+ return new Uint8Array(message.data);
22299
23236
  }
22300
- }, 1000);
23237
+ if (message.data instanceof Blob) {
23238
+ return new Uint8Array(yield message.data.arrayBuffer());
23239
+ }
23240
+ this.log.error('unsupported data type', {
23241
+ data: message.data
23242
+ });
23243
+ return undefined;
23244
+ });
22301
23245
  }
22302
23246
  createSender(track, opts, encodings) {
22303
23247
  return __awaiter(this, void 0, void 0, function* () {
@@ -22417,7 +23361,7 @@ class RTCEngine extends eventsExports.EventEmitter {
22417
23361
  if (!opts.videoCodec) {
22418
23362
  return;
22419
23363
  }
22420
- track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
23364
+ yield track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
22421
23365
  return transceiver.sender;
22422
23366
  });
22423
23367
  }
@@ -22429,6 +23373,63 @@ class RTCEngine extends eventsExports.EventEmitter {
22429
23373
  return this.pcManager.addPublisherTrack(track);
22430
23374
  });
22431
23375
  }
23376
+ /**
23377
+ * A sustained local `LOST` while connected and publishing means the server isn't receiving
23378
+ * our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
23379
+ */
23380
+ handleLocalConnectionQuality(update) {
23381
+ if (!this.participantSid) {
23382
+ return;
23383
+ }
23384
+ const localUpdate = update.updates.find(u => u.participantSid === this.participantSid);
23385
+ if (!localUpdate) {
23386
+ return;
23387
+ }
23388
+ if (localUpdate.quality === ConnectionQuality$1.LOST) {
23389
+ this.scheduleLostQualityReconnect();
23390
+ } else {
23391
+ this.clearLostQualityTimeout();
23392
+ }
23393
+ }
23394
+ scheduleLostQualityReconnect() {
23395
+ if (this.lostQualityTimeout) {
23396
+ // already counting down towards a reconnect
23397
+ return;
23398
+ }
23399
+ this.lostQualityTimeout = CriticalTimers.setTimeout(() => {
23400
+ this.lostQualityTimeout = undefined;
23401
+ if (this._isClosed || this.pcState !== PCState.Connected || this.attemptingReconnect) {
23402
+ return;
23403
+ }
23404
+ if (!this.hasActivePublisherSenders()) {
23405
+ return;
23406
+ }
23407
+ this.log.warn('local connection quality lost while publishing, triggering full reconnect', this.logContext);
23408
+ this.fullReconnectOnNext = true;
23409
+ this.handleDisconnect('connection quality lost', ReconnectReason.RR_PUBLISHER_FAILED);
23410
+ }, connectionQualityLostTimeout);
23411
+ }
23412
+ clearLostQualityTimeout() {
23413
+ if (this.lostQualityTimeout) {
23414
+ CriticalTimers.clearTimeout(this.lostQualityTimeout);
23415
+ this.lostQualityTimeout = undefined;
23416
+ }
23417
+ }
23418
+ /** Whether the publisher currently has any sender with a live track. */
23419
+ hasActivePublisherSenders() {
23420
+ var _a, _b;
23421
+ return (_b = (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.publisher.getSenders().some(sender => !!sender.track && sender.track.readyState === 'live')) !== null && _b !== void 0 ? _b : false;
23422
+ }
23423
+ /**
23424
+ * Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
23425
+ * Room's connection-reconcile safety net when the transport silently died but we looked connected.
23426
+ * @internal
23427
+ */
23428
+ reconnect() {
23429
+ let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ReconnectReason.RR_UNKNOWN;
23430
+ this.fullReconnectOnNext = true;
23431
+ this.handleDisconnect('reconcile', reason);
23432
+ }
22432
23433
  attemptReconnect(reason) {
22433
23434
  return __awaiter(this, void 0, void 0, function* () {
22434
23435
  var _a, _b, _c;
@@ -22440,21 +23441,32 @@ class RTCEngine extends eventsExports.EventEmitter {
22440
23441
  this.log.warn('already attempting reconnect, returning early');
22441
23442
  return;
22442
23443
  }
23444
+ // A pending Lost-quality countdown belongs to the session we're now leaving; cancel it so
23445
+ // it can't fire against the reconnected session before the server has evaluated it. (A resume
23446
+ // keeps the peer connections, so cleanupPeerConnections wouldn't cover this path.)
23447
+ this.clearLostQualityTimeout();
22443
23448
  if (((_a = this.clientConfiguration) === null || _a === void 0 ? void 0 : _a.resumeConnection) === ClientConfigSetting.DISABLED ||
22444
23449
  // signaling state could change to closed due to hardware sleep
22445
23450
  // those connections cannot be resumed
22446
23451
  ((_c = (_b = this.pcManager) === null || _b === void 0 ? void 0 : _b.currentState) !== null && _c !== void 0 ? _c : PCTransportState.NEW) === PCTransportState.NEW) {
22447
23452
  this.fullReconnectOnNext = true;
22448
23453
  }
23454
+ // Consume the flag up front: capture whether this attempt is a full reconnect, then reset
23455
+ // it. From here on a `true` value unambiguously represents a *new* full-reconnect request
23456
+ // that arrived while this attempt was running (e.g. a server RECONNECT leave), which the
23457
+ // finally block dispatches — for both the resume and full-reconnect paths.
23458
+ const fullReconnect = this.fullReconnectOnNext;
23459
+ this.fullReconnectOnNext = false;
23460
+ let succeeded = false;
22449
23461
  try {
22450
23462
  this.attemptingReconnect = true;
22451
- if (this.fullReconnectOnNext) {
23463
+ if (fullReconnect) {
22452
23464
  yield this.restartConnection();
22453
23465
  } else {
22454
23466
  yield this.resumeConnection(reason);
22455
23467
  }
22456
23468
  this.clearPendingReconnect();
22457
- this.fullReconnectOnNext = false;
23469
+ succeeded = true;
22458
23470
  } catch (e) {
22459
23471
  this.reconnectAttempts += 1;
22460
23472
  let recoverable = true;
@@ -22464,8 +23476,9 @@ class RTCEngine extends eventsExports.EventEmitter {
22464
23476
  });
22465
23477
  // unrecoverable
22466
23478
  recoverable = false;
22467
- } else if (!(e instanceof SignalReconnectError)) {
22468
- // cannot resume
23479
+ } else if (fullReconnect || !(e instanceof SignalReconnectError)) {
23480
+ // a failed full reconnect stays a full reconnect; a failed resume can only be
23481
+ // resumed again for a signal-level error, otherwise it escalates
22469
23482
  this.fullReconnectOnNext = true;
22470
23483
  }
22471
23484
  if (recoverable) {
@@ -22477,6 +23490,13 @@ class RTCEngine extends eventsExports.EventEmitter {
22477
23490
  }
22478
23491
  } finally {
22479
23492
  this.attemptingReconnect = false;
23493
+ // A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
23494
+ // during a resume or a restart) that a successful attempt didn't act on; dispatch it now
23495
+ // (the failure path already retries).
23496
+ if (succeeded && this.fullReconnectOnNext && !this._isClosed) {
23497
+ this.log.debug('full reconnect requested during in-progress attempt, dispatching');
23498
+ this.handleDisconnect('reconnect');
23499
+ }
22480
23500
  }
22481
23501
  });
22482
23502
  }
@@ -22549,7 +23569,6 @@ class RTCEngine extends eventsExports.EventEmitter {
22549
23569
  }
22550
23570
  resumeConnection(reason) {
22551
23571
  return __awaiter(this, void 0, void 0, function* () {
22552
- var _a;
22553
23572
  if (!this.url || !this.token) {
22554
23573
  // permanent failure, don't attempt reconnection
22555
23574
  throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
@@ -22603,7 +23622,8 @@ class RTCEngine extends eventsExports.EventEmitter {
22603
23622
  this.client.setReconnected();
22604
23623
  // recreate publish datachannel if it's id is null
22605
23624
  // (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) {
23625
+ const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
23626
+ if ((reliableDC === null || reliableDC === void 0 ? void 0 : reliableDC.readyState) === 'open' && reliableDC.id === null) {
22607
23627
  this.createDataChannels();
22608
23628
  }
22609
23629
  if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
@@ -22649,291 +23669,90 @@ class RTCEngine extends eventsExports.EventEmitter {
22649
23669
  const packet = new DataPacket({
22650
23670
  destinationIdentities: [destinationIdentity],
22651
23671
  kind: DataPacket_Kind.RELIABLE,
22652
- value: {
22653
- case: 'rpcAck',
22654
- value: new RpcAck({
22655
- requestId
22656
- })
22657
- }
22658
- });
22659
- yield this.sendDataPacket(packet, DataChannelKind.RELIABLE);
22660
- });
22661
- }
22662
- /* @internal */
22663
- sendDataPacket(packet, kind) {
22664
- return __awaiter(this, void 0, void 0, function* () {
22665
- var _a, _b;
22666
- // make sure we do have a data connection
22667
- yield this.ensurePublisherConnected(kind);
22668
- if (this.e2eeManager && this.e2eeManager.isDataChannelEncryptionEnabled) {
22669
- const encryptablePacket = asEncryptablePacket(packet);
22670
- if (encryptablePacket) {
22671
- const encryptedData = yield this.e2eeManager.encryptData(encryptablePacket.toBinary());
22672
- packet.value = {
22673
- case: 'encryptedPacket',
22674
- value: new EncryptedPacket({
22675
- encryptedValue: encryptedData.payload,
22676
- iv: encryptedData.iv,
22677
- keyIndex: encryptedData.keyIndex
22678
- })
22679
- };
22680
- }
22681
- }
22682
- if (kind === DataChannelKind.RELIABLE) {
22683
- packet.sequence = this.reliableDataSequence;
22684
- this.reliableDataSequence += 1;
22685
- }
22686
- const msg = packet.toBinary();
22687
- // Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
22688
- // than LiveKit/pion can deliver end-to-end (~64 KiB), so we trust
22689
- // the answer up untilthe built in ceiling.
22690
- const maxPublisherMessageSizeBytes = Math.min((_b = (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.getMaxPublisherMessageSize()) !== null && _b !== void 0 ? _b : DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE);
22691
- if (typeof maxPublisherMessageSizeBytes !== 'undefined' && maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ && msg.byteLength > maxPublisherMessageSizeBytes) {
22692
- throw new PublishDataError("cannot publish data packet larger than ".concat(maxPublisherMessageSizeBytes, " bytes (got ").concat(msg.byteLength, ")"));
22693
- }
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
- }
22748
- }
22749
- });
22750
- }
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
- }
23672
+ value: {
23673
+ case: 'rpcAck',
23674
+ value: new RpcAck({
23675
+ requestId
23676
+ })
22804
23677
  }
22805
- }();
23678
+ });
23679
+ yield this.sendDataPacket(packet, DataChannelKind.RELIABLE);
22806
23680
  });
22807
23681
  }
22808
- resendReliableMessagesForResume(lastMessageSeq) {
23682
+ /* @internal */
23683
+ sendDataPacket(packet, /** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
23684
+ kind) {
22809
23685
  return __awaiter(this, void 0, void 0, function* () {
22810
- 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();
23686
+ var _a, _b;
23687
+ // make sure we do have a data connection
23688
+ yield this.ensurePublisherConnected(kind);
23689
+ if (this.e2eeManager && this.e2eeManager.isDataChannelEncryptionEnabled) {
23690
+ const encryptablePacket = asEncryptablePacket(packet);
23691
+ if (encryptablePacket) {
23692
+ const encryptedData = yield this.e2eeManager.encryptData(encryptablePacket.toBinary());
23693
+ packet.value = {
23694
+ case: 'encryptedPacket',
23695
+ value: new EncryptedPacket({
23696
+ encryptedValue: encryptedData.payload,
23697
+ iv: encryptedData.iv,
23698
+ keyIndex: encryptedData.keyIndex
23699
+ })
23700
+ };
22839
23701
  }
22840
23702
  }
22841
- this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
23703
+ if (kind === DataChannelKind.RELIABLE) {
23704
+ packet.sequence = this.reliableChannel.nextSequence();
23705
+ }
23706
+ const msg = packet.toBinary();
23707
+ // Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
23708
+ // than LiveKit/pion can deliver end-to-end (~64 KiB), so we trust
23709
+ // the answer up untilthe built in ceiling.
23710
+ const maxPublisherMessageSizeBytes = Math.min((_b = (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.getMaxPublisherMessageSize()) !== null && _b !== void 0 ? _b : DEFAULT_MAX_MESSAGE_SIZE, DEFAULT_MAX_MESSAGE_SIZE);
23711
+ if (typeof maxPublisherMessageSizeBytes !== 'undefined' && maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ && msg.byteLength > maxPublisherMessageSizeBytes) {
23712
+ throw new PublishDataError("cannot publish data packet larger than ".concat(maxPublisherMessageSizeBytes, " bytes (got ").concat(msg.byteLength, ")"));
23713
+ }
23714
+ // The full-buffer policy (drop for lossy, wait/replay for reliable) is the channel's own, as
23715
+ // is emitting the buffer-status change once the send settles.
23716
+ if (kind === DataChannelKind.RELIABLE) {
23717
+ yield this.reliableChannel.send(msg, packet.sequence);
23718
+ } else {
23719
+ yield this.lossyChannel.send(msg);
23720
+ }
22842
23721
  });
22843
23722
  }
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
23723
  /**
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.
23724
+ * Sends pre-serialized bytes on the data-track channel. This is the one send path that doesn't
23725
+ * go through {@link sendDataPacket} Room's `packetAvailable` handler calls it directly with
23726
+ * bytes the data-track pipeline already serialized.
23727
+ *
23728
+ * @internal
22873
23729
  */
22874
- waitForBufferHeadroomWithLock(kind) {
23730
+ sendDataTrackFrame(bytes) {
22875
23731
  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
- }
23732
+ // Make sure we do have a data connection: on lazily negotiated publisher connections this is
23733
+ // what kicks negotiation off, and it waits for the channel to open. Memoized, so the
23734
+ // steady-state cost is one await on an already-resolved promise.
23735
+ yield this.ensurePublisherConnected(DataChannelKind.DATA_TRACK_LOSSY);
23736
+ yield this.dataTrackChannel.send(bytes);
23737
+ });
23738
+ }
23739
+ resendReliableMessagesForResume(lastMessageSeq) {
23740
+ return __awaiter(this, void 0, void 0, function* () {
23741
+ yield this.ensurePublisherConnected(DataChannelKind.RELIABLE);
23742
+ yield this.reliableChannel.replay(lastMessageSeq);
22882
23743
  });
22883
23744
  }
23745
+ /** The flow-control gate for `kind` — see {@link FlowControlledDataChannel}. */
23746
+ flowControlFor(kind) {
23747
+ return this.dataChannels.channelFor(kind);
23748
+ }
22884
23749
  /**
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).
23750
+ * Resolves once the caller may send on the `kind` channel see
23751
+ * {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
22894
23752
  */
22895
- waitForBufferHeadroomWithoutLock(kind) {
23753
+ waitForBufferHeadroom(kind) {
22896
23754
  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
- });
23755
+ return this.flowControlFor(kind).waitForHeadroomWithLock();
22937
23756
  });
22938
23757
  }
22939
23758
  /**
@@ -22941,40 +23760,40 @@ class RTCEngine extends eventsExports.EventEmitter {
22941
23760
  */
22942
23761
  ensureDataTransportConnected(kind_1) {
22943
23762
  return __awaiter(this, arguments, void 0, function (kind) {
22944
- var _this4 = this;
23763
+ var _this3 = this;
22945
23764
  let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.subscriberPrimary;
22946
23765
  return function* () {
22947
23766
  var _a;
22948
- if (!_this4.pcManager) {
23767
+ if (!_this3.pcManager) {
22949
23768
  throw new UnexpectedConnectionState('PC manager is closed');
22950
23769
  }
22951
- const transport = subscriber ? _this4.pcManager.subscriber : _this4.pcManager.publisher;
23770
+ const transport = subscriber ? _this3.pcManager.subscriber : _this3.pcManager.publisher;
22952
23771
  const transportName = subscriber ? 'Subscriber' : 'Publisher';
22953
23772
  if (!transport) {
22954
23773
  throw ConnectionError.internal("".concat(transportName, " connection not set"));
22955
23774
  }
22956
23775
  let needNegotiation = false;
22957
- if (!subscriber && !_this4.dataChannelForKind(kind, subscriber)) {
22958
- _this4.createDataChannels();
23776
+ if (!subscriber && !_this3.dataChannelForKind(kind, subscriber)) {
23777
+ _this3.createDataChannels();
22959
23778
  needNegotiation = true;
22960
23779
  }
22961
- if (!needNegotiation && !subscriber && !_this4.pcManager.publisher.isICEConnected && _this4.pcManager.publisher.getICEConnectionState() !== 'checking') {
23780
+ if (!needNegotiation && !subscriber && !_this3.pcManager.publisher.isICEConnected && _this3.pcManager.publisher.getICEConnectionState() !== 'checking') {
22962
23781
  needNegotiation = true;
22963
23782
  }
22964
23783
  if (needNegotiation) {
22965
23784
  // start negotiation
22966
- _this4.negotiate().catch(err => {
22967
- _this4.log.error(err);
23785
+ _this3.negotiate().catch(err => {
23786
+ _this3.log.error(err);
22968
23787
  });
22969
23788
  }
22970
- const targetChannel = _this4.dataChannelForKind(kind, subscriber);
23789
+ const targetChannel = _this3.dataChannelForKind(kind, subscriber);
22971
23790
  if ((targetChannel === null || targetChannel === void 0 ? void 0 : targetChannel.readyState) === 'open') {
22972
23791
  return;
22973
23792
  }
22974
23793
  // wait until ICE connected
22975
- const endTime = new Date().getTime() + _this4.peerConnectionTimeout;
23794
+ const endTime = new Date().getTime() + _this3.peerConnectionTimeout;
22976
23795
  while (new Date().getTime() < endTime) {
22977
- if (transport.isICEConnected && ((_a = _this4.dataChannelForKind(kind, subscriber)) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
23796
+ if (transport.isICEConnected && ((_a = _this3.dataChannelForKind(kind, subscriber)) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
22978
23797
  return;
22979
23798
  }
22980
23799
  yield sleep(50);
@@ -22996,14 +23815,23 @@ class RTCEngine extends eventsExports.EventEmitter {
22996
23815
  if (!this.pcManager) {
22997
23816
  return false;
22998
23817
  }
23818
+ const state = this.pcManager.currentState;
22999
23819
  const allowedConnectionStates = [PCTransportState.CONNECTING, PCTransportState.CONNECTED];
23000
- if (!allowedConnectionStates.includes(this.pcManager.currentState)) {
23820
+ if (!allowedConnectionStates.includes(state)) {
23001
23821
  return false;
23002
23822
  }
23003
23823
  // ensure signal is connected
23004
23824
  if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
23005
23825
  return false;
23006
23826
  }
23827
+ // A transport stuck in CONNECTING never reaches CONNECTED nor reports FAILED, so it would
23828
+ // otherwise look healthy forever; bound how long we tolerate it. The entry time is recorded
23829
+ // in the pcManager state-change handler (see configure()), so this is a pure read — an
23830
+ // unrecorded CONNECTING fails open rather than measuring against a stale timestamp.
23831
+ if (state === PCTransportState.CONNECTING && this.transportConnectingSince !== undefined && Date.now() - this.transportConnectingSince > this.peerConnectionTimeout) {
23832
+ this.log.warn('transport stuck in connecting state', this.logContext);
23833
+ return false;
23834
+ }
23007
23835
  return true;
23008
23836
  }
23009
23837
  /** @internal */
@@ -23017,7 +23845,7 @@ class RTCEngine extends eventsExports.EventEmitter {
23017
23845
  }
23018
23846
  this.pcManager.requirePublisher();
23019
23847
  // 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) {
23848
+ if (this.pcManager.publisher.getTransceivers().length == 0 && !this.dataChannels.hasPublisherChannels) {
23021
23849
  this.createDataChannels();
23022
23850
  }
23023
23851
  const abortController = new AbortController();
@@ -23061,26 +23889,7 @@ class RTCEngine extends eventsExports.EventEmitter {
23061
23889
  });
23062
23890
  }
23063
23891
  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
- }
23892
+ return this.dataChannels.getHandle(kind, sub);
23084
23893
  }
23085
23894
  /** @internal */
23086
23895
  sendSyncState(remoteTracks, localTracks, localDataTrackInfos) {
@@ -23408,7 +24217,90 @@ RegionUrlProvider.connectionTrackers = new Map();
23408
24217
  RegionUrlProvider.fetchLock = new _();
23409
24218
  function getCloudConfigUrl(serverUrl) {
23410
24219
  return "".concat(serverUrl.protocol.replace('ws', 'http'), "//").concat(serverUrl.host, "/settings");
23411
- }class BaseStreamReader {
24220
+ }/**
24221
+ * A `deflate-raw` compression transform (inverse of {@link inflateRawTransform}): pipe a byte stream
24222
+ * through it to get the compressed bytes without buffering the whole payload. Used for the chunked
24223
+ * `sendText`/`sendBytes`/`sendFile` paths, where the full payload is known up front but is streamed
24224
+ * (e.g. from `file.stream()`) rather than held in memory.
24225
+ */
24226
+ function deflateRawTransform() {
24227
+ return new CompressionStream('deflate-raw');
24228
+ }
24229
+ /**
24230
+ * A `deflate-raw` decompression transform (inverse of {@link deflateRawTransform}): pipe a
24231
+ * stream of compressed bytes through it to get the decompressed bytes. Inflate emits output greedily,
24232
+ * so as long as the sender flushed at write boundaries each write's content is produced as soon as
24233
+ * its compressed bytes arrive.
24234
+ */
24235
+ function inflateRawTransform() {
24236
+ return new DecompressionStream('deflate-raw');
24237
+ }
24238
+ /**
24239
+ * Decompresses a raw-deflate byte array in full (inverse of {@link deflateRawCompress}).
24240
+ * `maxByteLength` bounds the decompressed output (decompression-bomb guard); exceeding it rejects
24241
+ * with a `PayloadTooLarge` error.
24242
+ */
24243
+ function deflateRawDecompress(data, maxByteLength) {
24244
+ return __awaiter(this, void 0, void 0, function* () {
24245
+ const ds = new DecompressionStream('deflate-raw');
24246
+ const writer = ds.writable.getWriter();
24247
+ // The writer promises are intentionally not awaited (output is consumed via `collect`), but
24248
+ // they reject when the byte cap cancels the readable mid-stream — swallow that so an enforced
24249
+ // cap doesn't surface as an unhandled rejection.
24250
+ writer.write(data).catch(() => {});
24251
+ writer.close().catch(() => {});
24252
+ return collect(ds.readable, maxByteLength);
24253
+ });
24254
+ }
24255
+ /**
24256
+ * Drains a byte stream, concatenating all of its chunks into a single array. When
24257
+ * `maxByteLength` is given, drops the stream and throws `PayloadTooLarge` as soon as the
24258
+ * accumulated output exceeds it.
24259
+ */
24260
+ function collect(stream, maxByteLength) {
24261
+ return __awaiter(this, void 0, void 0, function* () {
24262
+ const reader = stream.getReader();
24263
+ const chunks = [];
24264
+ let total = 0;
24265
+ while (true) {
24266
+ const _yield$reader$read = yield reader.read(),
24267
+ done = _yield$reader$read.done,
24268
+ value = _yield$reader$read.value;
24269
+ if (done) {
24270
+ break;
24271
+ }
24272
+ chunks.push(value);
24273
+ total += value.byteLength;
24274
+ if (typeof maxByteLength === 'number' && total > maxByteLength) {
24275
+ yield reader.cancel();
24276
+ throw new DataStreamError("Decompressed payload exceeds the maximum payload size of ".concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
24277
+ }
24278
+ }
24279
+ const result = new Uint8Array(total);
24280
+ let offset = 0;
24281
+ for (const chunk of chunks) {
24282
+ result.set(chunk, offset);
24283
+ offset += chunk.byteLength;
24284
+ }
24285
+ return result;
24286
+ });
24287
+ }/**
24288
+ * Maximum size of a single data-stream chunk in bytes, and the budget used to decide whether a
24289
+ * payload can be sent inline as a single header packet. Kept below the ~16k data-channel MTU to
24290
+ * leave headroom for protocol framing and E2EE overhead.
24291
+ *
24292
+ * @internal
24293
+ */
24294
+ const STREAM_CHUNK_SIZE_BYTES = 15000;
24295
+ /**
24296
+ * Default cap on the number of decompressed bytes a single incoming compressed data stream may
24297
+ * produce (5 GB). A tiny compressed payload can inflate to an arbitrarily large output
24298
+ * (decompression bomb), so the decompressor's output is bounded rather than trusting the wire
24299
+ * size; streams exceeding the cap error with `PayloadTooLarge`.
24300
+ *
24301
+ * @internal
24302
+ */
24303
+ const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5000000000;class BaseStreamReader {
23412
24304
  get info() {
23413
24305
  return this._info;
23414
24306
  }
@@ -23450,6 +24342,7 @@ class ByteStreamReader extends BaseStreamReader {
23450
24342
  };
23451
24343
  return {
23452
24344
  next: () => __awaiter(this, void 0, void 0, function* () {
24345
+ var _a;
23453
24346
  try {
23454
24347
  const signal = this.signal;
23455
24348
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
@@ -23470,6 +24363,9 @@ class ByteStreamReader extends BaseStreamReader {
23470
24363
  });
23471
24364
  if (result.done) {
23472
24365
  this.validateBytesReceived(true);
24366
+ if (typeof this.totalByteSize === 'number') {
24367
+ (_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
24368
+ }
23473
24369
  return {
23474
24370
  done: true,
23475
24371
  value: undefined
@@ -23577,9 +24473,7 @@ class TextStreamReader extends BaseStreamReader {
23577
24473
  // Suppress unhandled rejection on reader.closed — errors are
23578
24474
  // already propagated through reader.read() to the consumer.
23579
24475
  reader.closed.catch(() => {});
23580
- const decoder = new TextDecoder('utf-8', {
23581
- fatal: true
23582
- });
24476
+ const decoder = new TextDecoder('utf-8');
23583
24477
  const signal = this.signal;
23584
24478
  const cleanup = () => {
23585
24479
  reader.releaseLock();
@@ -23587,6 +24481,7 @@ class TextStreamReader extends BaseStreamReader {
23587
24481
  };
23588
24482
  return {
23589
24483
  next: () => __awaiter(this, void 0, void 0, function* () {
24484
+ var _a;
23590
24485
  try {
23591
24486
  if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
23592
24487
  throw signal.reason;
@@ -23606,6 +24501,9 @@ class TextStreamReader extends BaseStreamReader {
23606
24501
  });
23607
24502
  if (result.done) {
23608
24503
  this.validateBytesReceived(true);
24504
+ if (typeof this.totalByteSize === 'number') {
24505
+ (_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
24506
+ }
23609
24507
  return {
23610
24508
  done: true,
23611
24509
  value: undefined
@@ -23684,6 +24582,7 @@ class TextStreamReader extends BaseStreamReader {
23684
24582
  }
23685
24583
  }class IncomingDataStreamManager {
23686
24584
  constructor() {
24585
+ let maxPayloadByteLength = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_MAX_PAYLOAD_BYTE_LENGTH;
23687
24586
  this.log = livekitLogger;
23688
24587
  this.byteStreamControllers = new Map();
23689
24588
  this.textStreamControllers = new Map();
@@ -23691,6 +24590,7 @@ class TextStreamReader extends BaseStreamReader {
23691
24590
  this.textStreamHandlers = new Map();
23692
24591
  this.isConnected = false;
23693
24592
  this.bufferedPackets = [];
24593
+ this.maxPayloadByteLength = maxPayloadByteLength;
23694
24594
  }
23695
24595
  setConnected(connected) {
23696
24596
  this.isConnected = connected;
@@ -23773,74 +24673,150 @@ class TextStreamReader extends BaseStreamReader {
23773
24673
  }
23774
24674
  handleStreamHeader(streamHeader, participantIdentity, encryptionType) {
23775
24675
  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);
24676
+ switch (streamHeader.contentHeader.case) {
24677
+ case 'byteHeader':
24678
+ {
24679
+ const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
24680
+ if (!streamHandlerCallback) {
24681
+ this.log.debug('ignoring incoming byte stream due to no handler for topic', streamHeader.topic);
24682
+ return;
24683
+ }
24684
+ let streamController;
24685
+ const info = {
24686
+ id: streamHeader.streamId,
24687
+ name: (_a = streamHeader.contentHeader.value.name) !== null && _a !== void 0 ? _a : 'unknown',
24688
+ mimeType: streamHeader.mimeType,
24689
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
24690
+ topic: streamHeader.topic,
24691
+ timestamp: bigIntToNumber(streamHeader.timestamp),
24692
+ attributes: streamHeader.attributes,
24693
+ encryptionType
24694
+ };
24695
+ // Determine if the byte payload needs to be decompressed.
24696
+ let compressed;
24697
+ switch (streamHeader.compression) {
24698
+ case DataStream_CompressionType.DEFLATE_RAW:
24699
+ if (!isCompressionStreamSupported()) {
24700
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24701
+ // isn't properly abiding by the data streams v2 protocol.
24702
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
24703
+ return;
24704
+ }
24705
+ compressed = true;
24706
+ break;
24707
+ case DataStream_CompressionType.NONE:
24708
+ compressed = false;
24709
+ break;
24710
+ default:
24711
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24712
+ // isn't properly abiding by the data streams v2 protocol.
24713
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
24714
+ return;
23798
24715
  }
23799
- this.byteStreamControllers.set(streamHeader.streamId, {
23800
- info,
23801
- controller: streamController,
23802
- startTime: Date.now(),
23803
- sendingParticipantIdentity: participantIdentity
24716
+ // Single-packet stream: the entire payload was packaged into the header's `inlineContent`.
24717
+ // Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
24718
+ const inlineContent = streamHeader.inlineContent;
24719
+ if (typeof inlineContent !== 'undefined') {
24720
+ // Inline bytes are the raw payload, optionally deflate-raw compressed.
24721
+ streamHandlerCallback(new ByteStreamReader(info, createInlineStream(streamHeader.streamId, compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent), bigIntToNumber(streamHeader.totalLength)), {
24722
+ identity: participantIdentity
24723
+ });
24724
+ return;
24725
+ }
24726
+ const stream = new ReadableStream({
24727
+ start: controller => {
24728
+ streamController = controller;
24729
+ if (this.byteStreamControllers.has(streamHeader.streamId)) {
24730
+ throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24731
+ }
24732
+ this.byteStreamControllers.set(streamHeader.streamId, {
24733
+ info,
24734
+ controller: streamController,
24735
+ startTime: Date.now(),
24736
+ sendingParticipantIdentity: participantIdentity
24737
+ });
24738
+ }
23804
24739
  });
24740
+ streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
24741
+ // `totalLength` is the pre-compression size, and the reader counts decompressed bytes,
24742
+ // so it applies to both paths (mirrors text).
24743
+ bigIntToNumber(streamHeader.totalLength)), {
24744
+ identity: participantIdentity
24745
+ });
24746
+ return;
23805
24747
  }
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);
24748
+ case 'textHeader':
24749
+ {
24750
+ const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
24751
+ if (!streamHandlerCallback) {
24752
+ this.log.debug('ignoring incoming text stream due to no handler for topic', streamHeader.topic);
24753
+ return;
24754
+ }
24755
+ let streamController;
24756
+ const info = {
24757
+ id: streamHeader.streamId,
24758
+ mimeType: streamHeader.mimeType,
24759
+ size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
24760
+ topic: streamHeader.topic,
24761
+ timestamp: Number(streamHeader.timestamp),
24762
+ attributes: streamHeader.attributes,
24763
+ encryptionType,
24764
+ attachedStreamIds: streamHeader.contentHeader.value.attachedStreamIds
24765
+ };
24766
+ // Determine if the byte payload needs to be decompressed.
24767
+ let compressed;
24768
+ switch (streamHeader.compression) {
24769
+ case DataStream_CompressionType.DEFLATE_RAW:
24770
+ if (!isCompressionStreamSupported()) {
24771
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24772
+ // isn't properly abiding by the data streams v2 protocol.
24773
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
24774
+ return;
24775
+ }
24776
+ compressed = true;
24777
+ break;
24778
+ case DataStream_CompressionType.NONE:
24779
+ compressed = false;
24780
+ break;
24781
+ default:
24782
+ // NOTE: this shouldn't really ever happen, if this warning is logged then the sender
24783
+ // isn't properly abiding by the data streams v2 protocol.
24784
+ livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
24785
+ return;
23832
24786
  }
23833
- this.textStreamControllers.set(streamHeader.streamId, {
23834
- info,
23835
- controller: streamController,
23836
- startTime: Date.now(),
23837
- sendingParticipantIdentity: participantIdentity
24787
+ // Single-packet stream: the entire payload was smuggled into the header's `inlineContent`.
24788
+ // Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
24789
+ const inlineContent = streamHeader.inlineContent;
24790
+ if (typeof inlineContent !== 'undefined') {
24791
+ // Inline text is the raw UTF-8 payload, optionally deflate-raw compressed.
24792
+ const content = compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent;
24793
+ streamHandlerCallback(new TextStreamReader(info, createInlineStream(streamHeader.streamId, content), bigIntToNumber(streamHeader.totalLength)), {
24794
+ identity: participantIdentity
24795
+ });
24796
+ return;
24797
+ }
24798
+ const stream = new ReadableStream({
24799
+ start: controller => {
24800
+ streamController = controller;
24801
+ if (this.textStreamControllers.has(streamHeader.streamId)) {
24802
+ throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
24803
+ }
24804
+ this.textStreamControllers.set(streamHeader.streamId, {
24805
+ info,
24806
+ controller: streamController,
24807
+ startTime: Date.now(),
24808
+ sendingParticipantIdentity: participantIdentity
24809
+ });
24810
+ }
23838
24811
  });
24812
+ streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
24813
+ // `totalLength` is the pre-compression size, and the reader sees decompressed bytes, so
24814
+ // it applies to both paths.
24815
+ bigIntToNumber(streamHeader.totalLength)), {
24816
+ identity: participantIdentity
24817
+ });
24818
+ return;
23839
24819
  }
23840
- });
23841
- streamHandlerCallback(new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
23842
- identity: participantIdentity
23843
- });
23844
24820
  }
23845
24821
  }
23846
24822
  handleStreamChunk(chunk, encryptionType) {
@@ -23853,38 +24829,205 @@ class TextStreamReader extends BaseStreamReader {
23853
24829
  fileBuffer.controller.enqueue(chunk);
23854
24830
  }
23855
24831
  }
23856
- const textBuffer = this.textStreamControllers.get(chunk.streamId);
23857
- if (textBuffer) {
23858
- if (textBuffer.info.encryptionType !== encryptionType) {
23859
- textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23860
- this.textStreamControllers.delete(chunk.streamId);
23861
- } else if (chunk.content.length > 0) {
23862
- textBuffer.controller.enqueue(chunk);
23863
- }
24832
+ const textBuffer = this.textStreamControllers.get(chunk.streamId);
24833
+ if (textBuffer) {
24834
+ if (textBuffer.info.encryptionType !== encryptionType) {
24835
+ textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(chunk.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
24836
+ this.textStreamControllers.delete(chunk.streamId);
24837
+ } else if (chunk.content.length > 0) {
24838
+ textBuffer.controller.enqueue(chunk);
24839
+ }
24840
+ }
24841
+ }
24842
+ handleStreamTrailer(trailer, encryptionType) {
24843
+ const textBuffer = this.textStreamControllers.get(trailer.streamId);
24844
+ if (textBuffer) {
24845
+ if (textBuffer.info.encryptionType !== encryptionType) {
24846
+ textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
24847
+ } else {
24848
+ textBuffer.info.attributes = Object.assign(Object.assign({}, textBuffer.info.attributes), trailer.attributes);
24849
+ if (trailer.reason) {
24850
+ // A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
24851
+ // surface it as an error rather than pretending the stream completed.
24852
+ textBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
24853
+ } else {
24854
+ textBuffer.controller.close();
24855
+ }
24856
+ }
24857
+ this.textStreamControllers.delete(trailer.streamId);
24858
+ }
24859
+ const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
24860
+ if (fileBuffer) {
24861
+ if (fileBuffer.info.encryptionType !== encryptionType) {
24862
+ fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
24863
+ } else {
24864
+ fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
24865
+ if (trailer.reason) {
24866
+ // A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
24867
+ // surface it as an error rather than pretending the stream completed.
24868
+ fileBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
24869
+ } else {
24870
+ fileBuffer.controller.close();
24871
+ }
24872
+ }
24873
+ this.byteStreamControllers.delete(trailer.streamId);
24874
+ }
24875
+ }
24876
+ }
24877
+ /**
24878
+ * Builds a `ReadableStream` that yields the given content as a single chunk and then immediately
24879
+ * closes - used to surface an inline (single-packet) data stream as a fully-formed stream. `content`
24880
+ * may be a promise (e.g. async gzip decompression); a rejection errors the stream.
24881
+ */
24882
+ function createInlineStream(streamId, content) {
24883
+ return new ReadableStream({
24884
+ start: controller => __awaiter(this, void 0, void 0, function* () {
24885
+ try {
24886
+ const bytes = yield content;
24887
+ controller.enqueue(new DataStream_Chunk({
24888
+ streamId,
24889
+ chunkIndex: BigInt(0),
24890
+ content: bytes
24891
+ }));
24892
+ controller.close();
24893
+ } catch (err) {
24894
+ controller.error(err);
24895
+ }
24896
+ })
24897
+ });
24898
+ }
24899
+ /**
24900
+ * Validates that chunks are received in order, dropping duplicates and throwing if gaps are found.
24901
+ *
24902
+ * A stateful decompressor silently corrupts on duplicated or out-of-order input, so duplicates are
24903
+ * dropped (with a warning - in-order delivery is expected on the reliable channel, but reconnect
24904
+ * handling may replay) and a gap is a hard error. Shared by the text and byte deflate-raw decoders.
24905
+ */
24906
+ function ensureOrderedChunks(streamId) {
24907
+ let lastChunkIndex = -1;
24908
+ return new TransformStream({
24909
+ transform: (value, controller) => {
24910
+ const index = bigIntToNumber(value.chunkIndex);
24911
+ if (index <= lastChunkIndex) {
24912
+ livekitLogger.warn("ignoring duplicate chunk ".concat(index, " for compressed data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
24913
+ return;
24914
+ }
24915
+ if (index > lastChunkIndex + 1) {
24916
+ throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for compressed data stream ").concat(streamId, " - cannot continue decompressing"), DataStreamErrorReason.Incomplete);
24917
+ }
24918
+ lastChunkIndex = index;
24919
+ controller.enqueue(value);
24920
+ }
24921
+ });
24922
+ }
24923
+ /** Unwraps compressed `DataStream_Chunk`s to their compressed bytes (in `chunkIndex` order), */
24924
+ function chunksToBytes() {
24925
+ return new TransformStream({
24926
+ transform: (value, controller) => {
24927
+ controller.enqueue(value.content);
24928
+ }
24929
+ });
24930
+ }
24931
+ /** Re-wraps decompressed bytes into contiguous `DataStream_Chunk`s, skipping inflate's empty reads. */
24932
+ function bytesToChunks(streamId) {
24933
+ let outIndex = 0;
24934
+ return new TransformStream({
24935
+ transform: (value, controller) => {
24936
+ // Inflate can emit empty reads; only synthesize a chunk when there is content.
24937
+ if (value.byteLength > 0) {
24938
+ controller.enqueue(new DataStream_Chunk({
24939
+ streamId,
24940
+ chunkIndex: numberToBigInt(outIndex),
24941
+ content: value
24942
+ }));
24943
+ outIndex += 1;
24944
+ }
24945
+ }
24946
+ });
24947
+ }
24948
+ /**
24949
+ * Reframes decompressed byte chunks onto UTF-8 character boundaries via a streaming `TextDecoder`
24950
+ * (a write larger than the MTU spans several packets, which may split a codepoint), so each
24951
+ * synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
24952
+ */
24953
+ function bytesToDecodedUtf8(streamId) {
24954
+ const decoder = new TextDecoder('utf-8');
24955
+ const encoder = new TextEncoder();
24956
+ let outIndex = 0;
24957
+ const decodeOrThrow = bytes => {
24958
+ try {
24959
+ return bytes ? decoder.decode(bytes, {
24960
+ stream: true
24961
+ }) : decoder.decode();
24962
+ } catch (err) {
24963
+ throw new DataStreamError("Cannot decode compressed data stream ".concat(streamId, " as text: ").concat(err), DataStreamErrorReason.DecodeFailed);
23864
24964
  }
23865
- }
23866
- handleStreamTrailer(trailer, encryptionType) {
23867
- const textBuffer = this.textStreamControllers.get(trailer.streamId);
23868
- if (textBuffer) {
23869
- if (textBuffer.info.encryptionType !== encryptionType) {
23870
- textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23871
- } else {
23872
- textBuffer.info.attributes = Object.assign(Object.assign({}, textBuffer.info.attributes), trailer.attributes);
23873
- textBuffer.controller.close();
23874
- this.textStreamControllers.delete(trailer.streamId);
24965
+ };
24966
+ return new TransformStream({
24967
+ transform: (value, controller) => {
24968
+ const text = decodeOrThrow(value);
24969
+ // Everything so far may have been a partial codepoint; only emit once we have characters.
24970
+ if (text.length > 0) {
24971
+ controller.enqueue(new DataStream_Chunk({
24972
+ streamId,
24973
+ chunkIndex: numberToBigInt(outIndex),
24974
+ content: encoder.encode(text)
24975
+ }));
24976
+ outIndex += 1;
24977
+ }
24978
+ },
24979
+ flush: controller => {
24980
+ const tail = decodeOrThrow();
24981
+ if (tail.length > 0) {
24982
+ controller.enqueue(new DataStream_Chunk({
24983
+ streamId,
24984
+ chunkIndex: numberToBigInt(outIndex),
24985
+ content: encoder.encode(tail)
24986
+ }));
24987
+ outIndex += 1;
23875
24988
  }
23876
24989
  }
23877
- const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
23878
- if (fileBuffer) {
23879
- if (fileBuffer.info.encryptionType !== encryptionType) {
23880
- fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
23881
- } else {
23882
- fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
23883
- fileBuffer.controller.close();
24990
+ });
24991
+ }
24992
+ /**
24993
+ * Transforms a stream of deflate-raw-compressed byte `DataStream_Chunk`s into a stream of
24994
+ * decompressed chunks, so `ByteStreamReader` consumes it unchanged. All chunk contents are fed (in
24995
+ * `chunkIndex` order) through ONE raw-deflate decompressor for the stream's lifetime; decompressed
24996
+ * output is re-wrapped as chunks as soon as it is produced. The sender (sendFile) compresses the
24997
+ * whole payload in one shot, but the format also supports a single context-takeover stream
24998
+ * sync-flushed at write boundaries, so a future incremental streamBytes could compress with no
24999
+ * protocol change. Errors and cancellation propagate through the pipe chain.
25000
+ */
25001
+ function inflateRawByteChunkStream(raw, streamId, maxPayloadByteLength) {
25002
+ return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToChunks(streamId));
25003
+ }
25004
+ /**
25005
+ * Transforms a stream of deflate-raw-compressed text `DataStream_Chunk`s into a stream of
25006
+ * decompressed chunks, so `TextStreamReader` consumes it unchanged. Builds on
25007
+ * {@link inflateRawByteChunkStream} (single decompressor + ordering guard) and adds a streaming
25008
+ * `TextDecoder` that reframes the decompressed bytes on UTF-8 character boundaries so each
25009
+ * synthesized chunk decodes independently. Errors and cancellation propagate through the pipe chain.
25010
+ */
25011
+ function inflateRawChunkStream(raw, streamId, maxPayloadByteLength) {
25012
+ return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToDecodedUtf8(streamId));
25013
+ }
25014
+ /**
25015
+ * Caps the total decompressed byte count of a compressed stream (decompression-bomb guard): a
25016
+ * tiny compressed payload can inflate to an arbitrarily large output, so the decompressor's
25017
+ * output is bounded rather than trusting the wire size. Exceeding the cap errors the stream with
25018
+ * `PayloadTooLarge`.
25019
+ */
25020
+ function maxDecompressedLengthGuard(streamId, maxByteLength) {
25021
+ let total = 0;
25022
+ return new TransformStream({
25023
+ transform: (value, controller) => {
25024
+ total += value.byteLength;
25025
+ if (total > maxByteLength) {
25026
+ throw new DataStreamError("Data stream ".concat(streamId, " exceeds the maximum payload size of ").concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
23884
25027
  }
23885
- this.byteStreamControllers.delete(trailer.streamId);
25028
+ controller.enqueue(value);
23886
25029
  }
23887
- }
25030
+ });
23888
25031
  }class BaseStreamWriter {
23889
25032
  constructor(writableStream, info, onClose) {
23890
25033
  this.writableStream = writableStream;
@@ -23905,15 +25048,70 @@ class TextStreamReader extends BaseStreamReader {
23905
25048
  }
23906
25049
  }
23907
25050
  class TextStreamWriter extends BaseStreamWriter {}
23908
- class ByteStreamWriter extends BaseStreamWriter {}const STREAM_CHUNK_SIZE = 15000;
25051
+ class ByteStreamWriter extends BaseStreamWriter {}/** Builds the `DataStream_Header` for a text stream from its info and stream options. */
25052
+ function buildTextStreamHeader(info, options, v2) {
25053
+ var _a;
25054
+ return new DataStream_Header({
25055
+ streamId: info.id,
25056
+ mimeType: info.mimeType,
25057
+ topic: info.topic,
25058
+ timestamp: numberToBigInt(info.timestamp),
25059
+ totalLength: numberToBigInt(info.size),
25060
+ attributes: info.attributes,
25061
+ compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
25062
+ inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
25063
+ contentHeader: {
25064
+ case: 'textHeader',
25065
+ value: new DataStream_TextHeader({
25066
+ version: options === null || options === void 0 ? void 0 : options.version,
25067
+ attachedStreamIds: info.attachedStreamIds,
25068
+ replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
25069
+ operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
25070
+ })
25071
+ }
25072
+ });
25073
+ }
25074
+ /** Builds the `DataStream_Header` for a byte stream from its info. */
25075
+ function buildByteStreamHeader(info, v2) {
25076
+ var _a;
25077
+ return new DataStream_Header({
25078
+ streamId: info.id,
25079
+ mimeType: info.mimeType,
25080
+ topic: info.topic,
25081
+ timestamp: numberToBigInt(info.timestamp),
25082
+ totalLength: numberToBigInt(info.size),
25083
+ attributes: info.attributes,
25084
+ compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
25085
+ inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
25086
+ contentHeader: {
25087
+ case: 'byteHeader',
25088
+ value: new DataStream_ByteHeader({
25089
+ name: info.name
25090
+ })
25091
+ }
25092
+ });
25093
+ }
25094
+ /** Wraps a `DataStream_Header` in a `DataPacket` ready to be sent over a data channel. */
25095
+ function createStreamHeaderPacket(header, destinationIdentities) {
25096
+ return new DataPacket({
25097
+ destinationIdentities,
25098
+ value: {
25099
+ case: 'streamHeader',
25100
+ value: header
25101
+ }
25102
+ });
25103
+ }const textEncoder = new TextEncoder();
23909
25104
  /**
23910
25105
  * Manages sending custom user data via data channels.
23911
25106
  * @internal
23912
25107
  */
23913
25108
  class OutgoingDataStreamManager {
23914
- constructor(engine, log) {
25109
+ constructor(engine, log, getRemoteParticipantClientProtocol, getRemoteParticipantCapabilities, getAllRemoteParticipantIdentities) {
23915
25110
  this.engine = engine;
23916
25111
  this.log = log;
25112
+ this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
25113
+ this.getRemoteParticipantCapabilities = getRemoteParticipantCapabilities;
25114
+ this.getAllRemoteParticipantIdentities = getAllRemoteParticipantIdentities;
23917
25115
  }
23918
25116
  setupEngine(engine) {
23919
25117
  this.engine = engine;
@@ -23921,42 +25119,236 @@ class OutgoingDataStreamManager {
23921
25119
  /** {@inheritDoc LocalParticipant.sendText} */
23922
25120
  sendText(text, options) {
23923
25121
  return __awaiter(this, void 0, void 0, function* () {
23924
- var _a;
25122
+ var _a, _b, _c, _d, _e;
23925
25123
  const streamId = crypto.randomUUID();
23926
- const textInBytes = new TextEncoder().encode(text);
25124
+ const textInBytes = textEncoder.encode(text);
23927
25125
  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);
25126
+ const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
25127
+ let info = {
25128
+ id: streamId,
25129
+ mimeType: 'text/plain',
25130
+ timestamp: Date.now(),
25131
+ topic: (_b = options === null || options === void 0 ? void 0 : options.topic) !== null && _b !== void 0 ? _b : '',
25132
+ size: totalTextLength,
25133
+ // NOTE: size is always the pre-compression byte length
25134
+ attributes: options === null || options === void 0 ? void 0 : options.attributes,
25135
+ encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
25136
+ };
25137
+ 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);
25138
+ let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(textInBytes).pipeThrough(deflateRawTransform())) : null;
25139
+ // Phase 1: Try to send as a single packet data stream
25140
+ const noAttachments = !(options === null || options === void 0 ? void 0 : options.attachments) || options.attachments.length === 0;
25141
+ if (noAttachments && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities)) {
25142
+ // The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
25143
+ // if it actually shrinks the payload (deflate framing makes tiny strings larger). The
25144
+ // compression flag is carried in the header's `compression` field; user attributes are left
25145
+ // untouched.
25146
+ let inlineContent = textInBytes;
25147
+ let compression = DataStream_CompressionType.NONE;
25148
+ if (compressedStream) {
25149
+ const collectedBytes = yield compressedStream.collect();
25150
+ if (collectedBytes.byteLength < textInBytes.byteLength) {
25151
+ inlineContent = collectedBytes;
25152
+ compression = DataStream_CompressionType.DEFLATE_RAW;
25153
+ }
25154
+ }
25155
+ const header = buildTextStreamHeader(info, undefined, {
25156
+ compression,
25157
+ inlineContent
25158
+ });
25159
+ const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
25160
+ if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
25161
+ yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25162
+ (_d = options === null || options === void 0 ? void 0 : options.onProgress) === null || _d === void 0 ? void 0 : _d.call(options, 1);
25163
+ return info;
25164
+ }
25165
+ }
25166
+ const fileIds = (_e = options === null || options === void 0 ? void 0 : options.attachments) === null || _e === void 0 ? void 0 : _e.map(() => crypto.randomUUID());
25167
+ // Progress is split evenly across the text part (slot 0) and one slot per attachment, then
25168
+ // normalized to a [0,1] fraction. Each slot climbs monotonically to 1, so the aggregate ends at
25169
+ // exactly 1 once every part has completed.
25170
+ const parts = fileIds ? fileIds.length + 1 : 1;
25171
+ const progresses = new Array(parts).fill(0);
23930
25172
  const handleProgress = (progress, idx) => {
23931
25173
  var _a;
23932
25174
  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);
25175
+ (_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
25176
  };
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();
25177
+ // Phase 2: Try to send a multi packet data stream with compressed bytes
25178
+ if (compressedStream) {
25179
+ info.attachedStreamIds = fileIds;
25180
+ const header = buildTextStreamHeader(info, undefined, {
25181
+ compression: DataStream_CompressionType.DEFLATE_RAW
25182
+ });
25183
+ const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
25184
+ yield this.sendChunkedByteStream(packet, streamId, options === null || options === void 0 ? void 0 : options.destinationIdentities, compressedStream.stream().pipeThrough(progressReportingStream(textInBytes.length, progress => handleProgress(progress, 0))));
25185
+ // Ensure there's always a 100% progress event fired, even if the string is zero bytes long
25186
+ if (textInBytes.length === 0) {
25187
+ handleProgress(1, 0);
25188
+ }
25189
+ } else {
25190
+ // Phase 3 / fallback: header + plain uncompressed chunk packets + trailer.
25191
+ const writer = yield this.streamText({
25192
+ streamId,
25193
+ totalSize: totalTextLength,
25194
+ destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities,
25195
+ topic: options === null || options === void 0 ? void 0 : options.topic,
25196
+ attachedStreamIds: fileIds,
25197
+ attributes: options === null || options === void 0 ? void 0 : options.attributes
25198
+ });
25199
+ yield writer.write(text);
25200
+ // set text part of progress to 1
25201
+ handleProgress(1, 0);
25202
+ yield writer.close();
25203
+ info = writer.info;
25204
+ }
23948
25205
  if ((options === null || options === void 0 ? void 0 : options.attachments) && fileIds) {
23949
25206
  yield Promise.all(options.attachments.map((file, idx) => __awaiter(this, void 0, void 0, function* () {
23950
25207
  return this._sendFile(fileIds[idx], file, {
23951
25208
  topic: options.topic,
23952
25209
  mimeType: file.type,
25210
+ destinationIdentities: options.destinationIdentities,
25211
+ compress: options.compress,
23953
25212
  onProgress: progress => {
23954
25213
  handleProgress(progress, idx + 1);
23955
25214
  }
23956
25215
  });
23957
25216
  })));
23958
25217
  }
23959
- return writer.info;
25218
+ return info;
25219
+ });
25220
+ }
25221
+ /**
25222
+ * Sends a complete in-memory byte payload. Mirrors {@link sendText}'s semantics: when every
25223
+ * recipient supports data streams v2 the payload rides inline in a single header packet
25224
+ * (optionally deflate-raw compressed), otherwise it is sent as a (optionally compressed)
25225
+ * chunked byte stream. Unlike {@link sendFile}, the whole payload is already in memory, so the
25226
+ * inline single-packet fast path applies.
25227
+ */
25228
+ sendBytes(bytes, options) {
25229
+ return __awaiter(this, void 0, void 0, function* () {
25230
+ var _a, _b, _c, _d, _e, _f, _g;
25231
+ const streamId = crypto.randomUUID();
25232
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
25233
+ const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
25234
+ const info = {
25235
+ id: streamId,
25236
+ name: (_b = options === null || options === void 0 ? void 0 : options.name) !== null && _b !== void 0 ? _b : 'unknown',
25237
+ mimeType: (_c = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _c !== void 0 ? _c : 'application/octet-stream',
25238
+ timestamp: Date.now(),
25239
+ topic: (_d = options === null || options === void 0 ? void 0 : options.topic) !== null && _d !== void 0 ? _d : '',
25240
+ size: bytes.byteLength,
25241
+ // NOTE: size is always the pre-compression byte length
25242
+ attributes: options === null || options === void 0 ? void 0 : options.attributes,
25243
+ encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
25244
+ };
25245
+ const progressMonitorTap = progressReportingStream(bytes.length, options === null || options === void 0 ? void 0 : options.onProgress);
25246
+ const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
25247
+ let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(bytes).pipeThrough(progressMonitorTap).pipeThrough(deflateRawTransform())) : null;
25248
+ // Phase 1: Try to send as a single packet data stream
25249
+ if (this.allRecipientsSupportV2(destinationIdentities)) {
25250
+ // The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
25251
+ // if it actually shrinks the payload (deflate framing makes tiny payloads larger). The
25252
+ // compression flag is carried in the header's `compression` field; user attributes are left
25253
+ // untouched.
25254
+ let inlineContent = bytes;
25255
+ let compression = DataStream_CompressionType.NONE;
25256
+ if (compressedStream) {
25257
+ const collectedBytes = yield compressedStream.collect();
25258
+ if (collectedBytes.byteLength < bytes.byteLength) {
25259
+ inlineContent = collectedBytes;
25260
+ compression = DataStream_CompressionType.DEFLATE_RAW;
25261
+ }
25262
+ }
25263
+ const header = buildByteStreamHeader(info, {
25264
+ compression,
25265
+ inlineContent
25266
+ });
25267
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25268
+ if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
25269
+ yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25270
+ (_f = options === null || options === void 0 ? void 0 : options.onProgress) === null || _f === void 0 ? void 0 : _f.call(options, 1);
25271
+ return info;
25272
+ }
25273
+ }
25274
+ // Phase 2/3: header + (optionally compressed) chunk packets + trailer.
25275
+ const header = buildByteStreamHeader(info, {
25276
+ compression: compressedStream ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
25277
+ });
25278
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25279
+ const source = compressedStream ? compressedStream.stream() : readableFromBytes(bytes).pipeThrough(progressMonitorTap);
25280
+ yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
25281
+ // Ensure there's always a 100% progress event fired, even if the buffer is zero bytes long
25282
+ if (bytes.length === 0) {
25283
+ (_g = options === null || options === void 0 ? void 0 : options.onProgress) === null || _g === void 0 ? void 0 : _g.call(options, 1);
25284
+ }
25285
+ return info;
25286
+ });
25287
+ }
25288
+ /**
25289
+ * Returns true only if every recipient is known to support data streams v2 (single-packet inline
25290
+ * streams and compression). For a targeted send this checks the named destination identities; for
25291
+ * a broadcast (no explicit destinations) it checks every remote participant currently in the room.
25292
+ * An empty room (nobody to receive) is considered eligible.
25293
+ */
25294
+ allRecipientsSupportV2(destinationIdentities) {
25295
+ const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
25296
+ return identities.every(identity => this.getRemoteParticipantClientProtocol(identity) >= CLIENT_PROTOCOL_DATA_STREAM_V2);
25297
+ }
25298
+ /**
25299
+ * Returns true only if every recipient advertises the deflate-raw compression capability (so it
25300
+ * can decompress a compressed stream). Resolved the same way as {@link allRecipientsSupportV2}:
25301
+ * named destinations, or every remote participant for a broadcast; an empty room is eligible.
25302
+ */
25303
+ allRecipientsSupportCompression(destinationIdentities) {
25304
+ const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
25305
+ return identities.every(identity => this.getRemoteParticipantCapabilities(identity).includes(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW));
25306
+ }
25307
+ /**
25308
+ * Shared chunked-stream send for `sendText`/`sendFile`: sends the prebuilt header packet, then
25309
+ * forwards `source` (optionally deflate-raw compressed) as `streamChunk` packets re-chunked to
25310
+ * the MTU budget with contiguous indices, then sends the trailer. The source is consumed
25311
+ * incrementally, so a `file.stream()` is never buffered in full. The platform compressor can't
25312
+ * flush mid-stream, so compression is only used when the whole payload is available as a stream
25313
+ * up front (not for incremental writers like `streamText`/`streamBytes`).
25314
+ */
25315
+ sendChunkedByteStream(headerPacket, streamId, destinationIdentities, source) {
25316
+ return __awaiter(this, void 0, void 0, function* () {
25317
+ var _a, e_1, _b, _c;
25318
+ const engine = this.engine;
25319
+ yield sendHeaderPacket(engine, headerPacket);
25320
+ let chunkId = 0;
25321
+ try {
25322
+ for (var _d = true, _e = __asyncValues(readBytesInChunks(source, STREAM_CHUNK_SIZE_BYTES)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
25323
+ _c = _f.value;
25324
+ _d = false;
25325
+ const chunk = _c;
25326
+ const chunkPacket = new DataPacket({
25327
+ destinationIdentities,
25328
+ value: {
25329
+ case: 'streamChunk',
25330
+ value: new DataStream_Chunk({
25331
+ content: chunk,
25332
+ streamId,
25333
+ chunkIndex: numberToBigInt(chunkId)
25334
+ })
25335
+ }
25336
+ });
25337
+ yield engine.sendDataPacket(chunkPacket, DataChannelKind.RELIABLE);
25338
+ chunkId += 1;
25339
+ }
25340
+ } catch (e_1_1) {
25341
+ e_1 = {
25342
+ error: e_1_1
25343
+ };
25344
+ } finally {
25345
+ try {
25346
+ if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
25347
+ } finally {
25348
+ if (e_1) throw e_1.error;
25349
+ }
25350
+ }
25351
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
23960
25352
  });
23961
25353
  }
23962
25354
  /**
@@ -23966,6 +25358,7 @@ class OutgoingDataStreamManager {
23966
25358
  return __awaiter(this, void 0, void 0, function* () {
23967
25359
  var _a, _b, _c;
23968
25360
  const streamId = (_a = options === null || options === void 0 ? void 0 : options.streamId) !== null && _a !== void 0 ? _a : crypto.randomUUID();
25361
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
23969
25362
  const info = {
23970
25363
  id: streamId,
23971
25364
  mimeType: 'text/plain',
@@ -23976,39 +25369,20 @@ class OutgoingDataStreamManager {
23976
25369
  encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE,
23977
25370
  attachedStreamIds: options === null || options === void 0 ? void 0 : options.attachedStreamIds
23978
25371
  };
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);
25372
+ const header = buildTextStreamHeader(info, options);
25373
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25374
+ yield sendHeaderPacket(this.engine, packet);
24005
25375
  let chunkId = 0;
24006
25376
  const engine = this.engine;
25377
+ // Incremental text streams are never compressed (CompressionStream does not support flushing
25378
+ // mid-stream); one-shot compression lives in sendText.
25379
+ //
25380
+ // Note that a future streamText could send a context-takeover style deflate-raw stream with
25381
+ // intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
24007
25382
  const writableStream = new WritableStream({
24008
- // Implement the sink
24009
25383
  write(text) {
24010
25384
  return __awaiter(this, void 0, void 0, function* () {
24011
- for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE)) {
25385
+ for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE_BYTES)) {
24012
25386
  const chunk = new DataStream_Chunk({
24013
25387
  content: textByteChunk,
24014
25388
  streamId,
@@ -24028,17 +25402,7 @@ class OutgoingDataStreamManager {
24028
25402
  },
24029
25403
  close() {
24030
25404
  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);
25405
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
24042
25406
  });
24043
25407
  },
24044
25408
  abort(err) {
@@ -24049,6 +25413,8 @@ class OutgoingDataStreamManager {
24049
25413
  let onEngineClose = () => __awaiter(this, void 0, void 0, function* () {
24050
25414
  yield writer.close();
24051
25415
  });
25416
+ // FIXME: make this a global event to ensure "max listener" warning won't get logged for lots of
25417
+ // in flight data streams.
24052
25418
  engine.once(EngineEvent.Closing, onEngineClose);
24053
25419
  const writer = new TextStreamWriter(writableStream, info, () => this.engine.off(EngineEvent.Closing, onEngineClose));
24054
25420
  return writer;
@@ -24063,29 +25429,47 @@ class OutgoingDataStreamManager {
24063
25429
  };
24064
25430
  });
24065
25431
  }
25432
+ /**
25433
+ * Streams a file as a chunked byte stream, compressed (deflate-raw) when the runtime supports it
25434
+ * and every recipient is on data streams v2. The file is piped `file.stream()` →
25435
+ * (`CompressionStream`) → chunk packets via {@link sendChunkedByteStream}, so it is never fully
25436
+ * buffered in memory — unlike {@link sendBytes}, there is no inline single-packet fast path for
25437
+ * files (the compressed size can't be known up front without buffering the whole file).
25438
+ */
24066
25439
  _sendFile(streamId, file, options) {
24067
25440
  return __awaiter(this, void 0, void 0, function* () {
24068
- var _a;
24069
- const writer = yield this.streamBytes({
24070
- streamId,
24071
- totalSize: file.size,
25441
+ var _a, _b, _c, _d, _e;
25442
+ const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
25443
+ 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);
25444
+ const info = {
25445
+ id: streamId,
24072
25446
  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
25447
+ mimeType: (_b = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _b !== void 0 ? _b : file.type,
25448
+ topic: (_c = options === null || options === void 0 ? void 0 : options.topic) !== null && _c !== void 0 ? _c : '',
25449
+ timestamp: Date.now(),
25450
+ size: file.size,
25451
+ encryptionType: ((_d = this.engine.e2eeManager) === null || _d === void 0 ? void 0 : _d.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
25452
+ };
25453
+ // Phase 1: Try to send as a single packet data stream
25454
+ //
25455
+ // This is not being done explictly for files, because it's challenging to determine ahead of
25456
+ // time how well the file contents will compress (and whether the total output will be under the
25457
+ // MTU). Revisit this in the future though.
25458
+ // Phase 2 (compressed) / Phase 3 (uncompressed fallback): header + chunk packets + trailer. Both
25459
+ // funnel through the shared sendChunkedByteStream primitive, differing only by whether the file
25460
+ // stream is wrapped in the deflate-raw compressor; the file is never fully buffered in memory.
25461
+ const header = buildByteStreamHeader(info, {
25462
+ compression: compress ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
24076
25463
  });
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);
25464
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25465
+ const tapped = file.stream().pipeThrough(progressReportingStream(file.size, options === null || options === void 0 ? void 0 : options.onProgress));
25466
+ const source = compress ? tapped.pipeThrough(deflateRawTransform()) : tapped;
25467
+ yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
25468
+ // Ensure there's always a 100% progress event fired, even if the file is zero bytes long
25469
+ if (file.size === 0) {
25470
+ (_e = options === null || options === void 0 ? void 0 : options.onProgress) === null || _e === void 0 ? void 0 : _e.call(options, 1);
24086
25471
  }
24087
- yield writer.close();
24088
- return writer.info;
25472
+ return info;
24089
25473
  });
24090
25474
  }
24091
25475
  streamBytes(options) {
@@ -24103,32 +25487,18 @@ class OutgoingDataStreamManager {
24103
25487
  name: (_d = options === null || options === void 0 ? void 0 : options.name) !== null && _d !== void 0 ? _d : 'unknown',
24104
25488
  encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
24105
25489
  };
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);
25490
+ const header = buildByteStreamHeader(info);
25491
+ const packet = createStreamHeaderPacket(header, destinationIdentities);
25492
+ yield sendHeaderPacket(this.engine, packet);
24128
25493
  let chunkId = 0;
24129
25494
  const writeMutex = new _();
24130
25495
  const engine = this.engine;
24131
25496
  const logLocal = this.log;
25497
+ // Incremental byte streams are never compressed (CompressionStream does not support flushing
25498
+ // mid-stream); one-shot compression lives in sendFile.
25499
+ //
25500
+ // Note that a future streamBytes could send a context-takeover style deflate-raw stream with
25501
+ // intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
24132
25502
  const writableStream = new WritableStream({
24133
25503
  write(chunk) {
24134
25504
  return __awaiter(this, void 0, void 0, function* () {
@@ -24136,7 +25506,7 @@ class OutgoingDataStreamManager {
24136
25506
  let byteOffset = 0;
24137
25507
  try {
24138
25508
  while (byteOffset < chunk.byteLength) {
24139
- const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE);
25509
+ const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE_BYTES);
24140
25510
  const chunkPacket = new DataPacket({
24141
25511
  destinationIdentities,
24142
25512
  value: {
@@ -24159,17 +25529,7 @@ class OutgoingDataStreamManager {
24159
25529
  },
24160
25530
  close() {
24161
25531
  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);
25532
+ yield sendStreamTrailer(streamId, destinationIdentities, engine);
24173
25533
  });
24174
25534
  },
24175
25535
  abort(err) {
@@ -24180,6 +25540,109 @@ class OutgoingDataStreamManager {
24180
25540
  return byteWriter;
24181
25541
  });
24182
25542
  }
25543
+ }
25544
+ /**
25545
+ * Wraps a stream of compressed bytes and defers buffering it into memory until (and only if) the
25546
+ * full length is actually needed.
25547
+ *
25548
+ * The ideal way to send a compressed stream is incrementally: compress each chunk and send it as
25549
+ * soon as it's ready, without ever holding the whole payload in memory. But to decide whether a
25550
+ * payload is small enough to fit in a single-packet data stream, we have to know the compressed
25551
+ * length — and the only way to learn that is to compress everything and add up the bytes. That
25552
+ * forces us to buffer the entire compressed output before we can make the decision.
25553
+ *
25554
+ * This class lets us have it both ways. It holds the compressed stream unread by default and only
25555
+ * collects it into memory ({@link collect}) when a caller needs the length for the single-packet
25556
+ * size check. If that check never happens, the stream is instead {@link stream|passed straight
25557
+ * through} to the downstream consumer without ever being fully buffered. Once collected, later
25558
+ * calls reuse the buffered bytes rather than re-collecting.
25559
+ */
25560
+ class MaybeCollectedStream {
25561
+ constructor(state) {
25562
+ this.state = state;
25563
+ }
25564
+ static fromStream(stream) {
25565
+ return new MaybeCollectedStream({
25566
+ type: 'stream',
25567
+ stream
25568
+ });
25569
+ }
25570
+ /** Collect data from the stream into memory and return as a Uint8Array. */
25571
+ collect() {
25572
+ return __awaiter(this, void 0, void 0, function* () {
25573
+ switch (this.state.type) {
25574
+ case 'stream':
25575
+ const bytes = yield collect(this.state.stream);
25576
+ this.state = {
25577
+ type: 'collected',
25578
+ bytes
25579
+ };
25580
+ return bytes;
25581
+ case 'collected':
25582
+ return this.state.bytes;
25583
+ }
25584
+ });
25585
+ }
25586
+ /** Pass wrapped stream through to downstream consumer. */
25587
+ stream() {
25588
+ switch (this.state.type) {
25589
+ case 'stream':
25590
+ return this.state.stream;
25591
+ case 'collected':
25592
+ return readableFromBytes(this.state.bytes);
25593
+ }
25594
+ }
25595
+ }
25596
+ /**
25597
+ * A pass-through byte transform that reports progress as bytes flow through it, measured against
25598
+ * `totalLength`. Used as a "tap" to instrument the source of a chunked send without threading a
25599
+ * progress callback through the send primitive.
25600
+ *
25601
+ * IMPORTANT: This should be upstream of any compression step.
25602
+ *
25603
+ * Emits nothing when the total is unknown or zero.
25604
+ */
25605
+ function progressReportingStream(totalPreCompressionLength, onProgress) {
25606
+ let sent = 0;
25607
+ return new TransformStream({
25608
+ transform(chunk, controller) {
25609
+ sent += chunk.byteLength;
25610
+ if (onProgress && typeof totalPreCompressionLength === 'number' && totalPreCompressionLength > 0) {
25611
+ onProgress(Math.min(sent / totalPreCompressionLength, 1));
25612
+ }
25613
+ controller.enqueue(chunk);
25614
+ }
25615
+ });
25616
+ }
25617
+ /**
25618
+ * Sends a stream `streamHeader` packet, enforcing that it fits the MTU budget. The header carries
25619
+ * the user attributes (plus topic/framing), and a single `DataPacket` larger than the MTU can't be
25620
+ * reliably sent — so an oversized header (almost always due to large attributes) is a hard error
25621
+ * rather than a malformed packet on the wire. The inline fast path does its own size check and
25622
+ * falls back to the chunked path instead of calling this.
25623
+ */
25624
+ function sendHeaderPacket(engine, packet) {
25625
+ return __awaiter(this, void 0, void 0, function* () {
25626
+ if (packet.toBinary().byteLength > STREAM_CHUNK_SIZE_BYTES) {
25627
+ throw new DataStreamError("data stream header exceeds the ".concat(STREAM_CHUNK_SIZE_BYTES, "-byte limit; reduce attribute size"), DataStreamErrorReason.HeaderTooLarge);
25628
+ }
25629
+ yield engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
25630
+ });
25631
+ }
25632
+ /** Sends a `streamTrailer` packet, marking the end of a stream. */
25633
+ function sendStreamTrailer(streamId, destinationIdentities, engine) {
25634
+ return __awaiter(this, void 0, void 0, function* () {
25635
+ const trailerPacket = new DataPacket({
25636
+ destinationIdentities,
25637
+ value: {
25638
+ case: 'streamTrailer',
25639
+ value: new DataStream_Trailer({
25640
+ streamId
25641
+ })
25642
+ }
25643
+ });
25644
+ yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
25645
+ });
24183
25646
  }/**
24184
25647
  * Implementation of AbortSignal.any
24185
25648
  * Creates a signal that will be aborted when any of the given signals is aborted.
@@ -26670,7 +28133,7 @@ class RpcClientManager extends eventsExports.EventEmitter {
26670
28133
  return __awaiter(this, void 0, void 0, function* () {
26671
28134
  if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26672
28135
  // Send payload as a data stream - a "version 2" rpc request.
26673
- const writer = yield this.outgoingDataStreamManager.streamText({
28136
+ yield this.outgoingDataStreamManager.sendText(payload, {
26674
28137
  topic: RPC_REQUEST_DATA_STREAM_TOPIC,
26675
28138
  destinationIdentities: [destinationIdentity],
26676
28139
  attributes: {
@@ -26680,8 +28143,6 @@ class RpcClientManager extends eventsExports.EventEmitter {
26680
28143
  [RpcRequestAttrs.RPC_REQUEST_VERSION]: "".concat(RPC_VERSION_V2)
26681
28144
  }
26682
28145
  });
26683
- yield writer.write(payload);
26684
- yield writer.close();
26685
28146
  return;
26686
28147
  }
26687
28148
  // Fallback to sending a literal RpcRequest - a "version 1" rpc request.
@@ -26955,15 +28416,13 @@ class RpcServerManager extends eventsExports.EventEmitter {
26955
28416
  const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
26956
28417
  if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
26957
28418
  // Send response as a data stream
26958
- const writer = yield this.outgoingDataStreamManager.streamText({
28419
+ yield this.outgoingDataStreamManager.sendText(payload, {
26959
28420
  topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
26960
28421
  destinationIdentities: [destinationIdentity],
26961
28422
  attributes: {
26962
28423
  [RpcRequestAttrs.RPC_REQUEST_ID]: requestId
26963
28424
  }
26964
28425
  });
26965
- yield writer.write(payload);
26966
- yield writer.close();
26967
28426
  return;
26968
28427
  }
26969
28428
  // Legacy client: enforce size limit and send uncompressed payload inline
@@ -29439,6 +30898,18 @@ class Participant extends eventsExports.EventEmitter {
29439
30898
  return this.roomOutgoingDataStreamManager.sendFile(file, options);
29440
30899
  });
29441
30900
  }
30901
+ /**
30902
+ * Sends the given bytes to participants in the room via the data channel.
30903
+ * For files, consider using {@link sendFile}; for longer/incremental payloads, {@link streamBytes}.
30904
+ *
30905
+ * @param bytes The byte payload
30906
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
30907
+ */
30908
+ sendBytes(bytes, options) {
30909
+ return __awaiter(this, void 0, void 0, function* () {
30910
+ return this.roomOutgoingDataStreamManager.sendBytes(bytes, options);
30911
+ });
30912
+ }
29442
30913
  /**
29443
30914
  * Stream bytes incrementally to participants in the room via the data channel.
29444
30915
  * For sending files, consider using {@link sendFile} instead.
@@ -29968,7 +31439,7 @@ class DeferrableMap extends Map {
29968
31439
  return new RemoteDataTrack(info, manager, {
29969
31440
  publisherIdentity: pi.identity
29970
31441
  });
29971
- }), pi.clientProtocol);
31442
+ }), pi.clientProtocol, pi.capabilities);
29972
31443
  }
29973
31444
  get logContext() {
29974
31445
  return Object.assign(Object.assign({}, super.logContext), {
@@ -29981,6 +31452,7 @@ class DeferrableMap extends Map {
29981
31452
  let kind = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : ParticipantInfo_Kind.STANDARD;
29982
31453
  let remoteDataTracks = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : [];
29983
31454
  let clientProtocol = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : CLIENT_PROTOCOL_DEFAULT;
31455
+ let capabilities = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : [];
29984
31456
  super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
29985
31457
  this.signalClient = signalClient;
29986
31458
  this.trackPublications = new Map();
@@ -29991,6 +31463,7 @@ class DeferrableMap extends Map {
29991
31463
  }));
29992
31464
  this.volumeMap = new Map();
29993
31465
  this.clientProtocol = clientProtocol;
31466
+ this.capabilities = capabilities;
29994
31467
  }
29995
31468
  addTrackPublication(publication) {
29996
31469
  super.addTrackPublication(publication);
@@ -30293,6 +31766,7 @@ class Room extends eventsExports.EventEmitter {
30293
31766
  this.log = livekitLogger;
30294
31767
  this.bufferedEvents = [];
30295
31768
  this.isResuming = false;
31769
+ this.pendingTrackAddedCallbacks = new Map();
30296
31770
  this.connect = (url, token, opts) => __awaiter(this, void 0, void 0, function* () {
30297
31771
  var _a;
30298
31772
  if (!isBrowserSupported()) {
@@ -30402,11 +31876,10 @@ class Room extends eventsExports.EventEmitter {
30402
31876
  return this.connectFuture.promise;
30403
31877
  });
30404
31878
  this.connectSignal = (url, token, engine, connectOptions, roomOptions, abortController) => __awaiter(this, void 0, void 0, function* () {
30405
- var _a;
30406
31879
  const _yield$engine$join = yield engine.join(url, token, {
30407
31880
  autoSubscribe: connectOptions.autoSubscribe,
30408
31881
  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,
31882
+ clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
30410
31883
  maxRetries: connectOptions.maxRetries,
30411
31884
  e2eeEnabled: !!this.e2eeManager,
30412
31885
  websocketTimeout: connectOptions.websocketTimeout
@@ -30809,8 +32282,9 @@ class Room extends eventsExports.EventEmitter {
30809
32282
  return;
30810
32283
  }
30811
32284
  const newStreamState = Track.streamStateFromProto(streamState.state);
32285
+ const prevStreamState = pub.track.streamState;
30812
32286
  pub.track.setStreamState(newStreamState);
30813
- if (newStreamState !== pub.track.streamState) {
32287
+ if (newStreamState !== prevStreamState) {
30814
32288
  participant.emit(ParticipantEvent.TrackStreamStateChanged, pub, pub.track.streamState);
30815
32289
  this.emitWhenConnected(RoomEvent.TrackStreamStateChanged, pub, pub.track.streamState, participant);
30816
32290
  }
@@ -30828,6 +32302,7 @@ class Room extends eventsExports.EventEmitter {
30828
32302
  pub.setAllowed(update.allowed);
30829
32303
  };
30830
32304
  this.handleSubscriptionError = update => {
32305
+ this.cancelPendingTrackAdded(update.trackSid);
30831
32306
  const participant = Array.from(this.remoteParticipants.values()).find(p => p.trackPublications.has(update.trackSid));
30832
32307
  if (!participant) {
30833
32308
  return;
@@ -30966,6 +32441,13 @@ class Room extends eventsExports.EventEmitter {
30966
32441
  var _a, _b;
30967
32442
  return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.clientProtocol) !== null && _b !== void 0 ? _b : CLIENT_PROTOCOL_DEFAULT;
30968
32443
  };
32444
+ this.getRemoteParticipantCapabilities = identity => {
32445
+ var _a, _b;
32446
+ return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.capabilities) !== null && _b !== void 0 ? _b : [];
32447
+ };
32448
+ this.getAllRemoteParticipantIdentities = () => {
32449
+ return Array.from(this.remoteParticipants.keys());
32450
+ };
30969
32451
  this.onLocalParticipantMetadataChanged = metadata => {
30970
32452
  this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
30971
32453
  };
@@ -31042,7 +32524,7 @@ class Room extends eventsExports.EventEmitter {
31042
32524
  this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
31043
32525
  this.maybeCreateEngine();
31044
32526
  this.incomingDataStreamManager = new IncomingDataStreamManager();
31045
- this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
32527
+ this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
31046
32528
  this.incomingDataTrackManager = new IncomingDataTrackManager({
31047
32529
  e2eeManager: this.e2eeManager
31048
32530
  });
@@ -31079,7 +32561,7 @@ class Room extends eventsExports.EventEmitter {
31079
32561
  }).on('packetAvailable', _ref => {
31080
32562
  let handle = _ref.handle,
31081
32563
  bytes = _ref.bytes;
31082
- this.engine.sendLossyBytes(bytes, DataChannelKind.DATA_TRACK_LOSSY, 'wait').finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
32564
+ this.engine.sendDataTrackFrame(bytes).finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
31083
32565
  });
31084
32566
  this.registerRpcDataStreamHandler();
31085
32567
  this.rpcClientManager = new RpcClientManager(this.log, this.outgoingDataStreamManager, this.getRemoteParticipantClientProtocol, () => {
@@ -31328,15 +32810,15 @@ class Room extends eventsExports.EventEmitter {
31328
32810
  this.isResuming = false;
31329
32811
  this.log.debug('Resumed signal connection');
31330
32812
  this.updateSubscriptions();
31331
- this.emitBufferedEvents();
31332
32813
  if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
31333
32814
  this.emit(RoomEvent.Reconnected);
31334
32815
  }
32816
+ this.emitBufferedEvents();
31335
32817
  }).on(EngineEvent.SignalResumed, () => {
31336
- this.bufferedEvents = [];
31337
32818
  if (this.state === ConnectionState.Reconnecting || this.isResuming) {
31338
32819
  this.sendSyncState();
31339
32820
  }
32821
+ this.emitBufferedEvents();
31340
32822
  }).on(EngineEvent.Restarting, this.handleRestarting).on(EngineEvent.Restarted, this.handleRestarted).on(EngineEvent.SignalRestarted, this.handleSignalRestarted).on(EngineEvent.Offline, () => {
31341
32823
  if (this.setAndEmitConnectionState(ConnectionState.Reconnecting)) {
31342
32824
  this.emit(RoomEvent.Reconnecting);
@@ -31753,29 +33235,43 @@ class Room extends eventsExports.EventEmitter {
31753
33235
  this.maybeCreateEngine();
31754
33236
  }
31755
33237
  onTrackAdded(mediaTrack, stream, receiver) {
33238
+ var _a, _b;
31756
33239
  // don't fire onSubscribed when connecting
31757
33240
  // WebRTC fires onTrack as soon as setRemoteDescription is called on the offer
31758
33241
  // at that time, ICE connectivity has not been established so the track is not
31759
33242
  // technically subscribed.
31760
33243
  // We'll defer these events until when the room is connected or eventually disconnected.
31761
- if (this.state === ConnectionState.Connecting || this.state === ConnectionState.Reconnecting) {
33244
+ if ([ConnectionState.Connecting, ConnectionState.Reconnecting].includes(this.state)) {
33245
+ const pendingTrackSid = extractTrackSid(mediaTrack, stream);
33246
+ this.log.debug('deferring on track for later', {
33247
+ mediaTrackId: mediaTrack.id,
33248
+ mediaStreamId: stream.id,
33249
+ tracksInStream: stream.getTracks().map(track => track.id)
33250
+ });
31762
33251
  const reconnectedHandler = () => {
31763
- this.log.debug('deferring on track for later', {
31764
- mediaTrackId: mediaTrack.id,
31765
- mediaStreamId: stream.id,
31766
- tracksInStream: stream.getTracks().map(track => track.id)
31767
- });
31768
- this.onTrackAdded(mediaTrack, stream, receiver);
31769
33252
  cleanup();
33253
+ this.onTrackAdded(mediaTrack, stream, receiver);
31770
33254
  };
31771
33255
  const cleanup = () => {
31772
33256
  this.off(RoomEvent.Reconnected, reconnectedHandler);
31773
33257
  this.off(RoomEvent.Connected, reconnectedHandler);
31774
33258
  this.off(RoomEvent.Disconnected, cleanup);
33259
+ if (pendingTrackSid) {
33260
+ const pendingCallbacks = this.pendingTrackAddedCallbacks.get(pendingTrackSid);
33261
+ pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.delete(cleanup);
33262
+ if ((pendingCallbacks === null || pendingCallbacks === void 0 ? void 0 : pendingCallbacks.size) === 0) {
33263
+ this.pendingTrackAddedCallbacks.delete(pendingTrackSid);
33264
+ }
33265
+ }
31775
33266
  };
31776
33267
  this.once(RoomEvent.Reconnected, reconnectedHandler);
31777
33268
  this.once(RoomEvent.Connected, reconnectedHandler);
31778
33269
  this.once(RoomEvent.Disconnected, cleanup);
33270
+ if (pendingTrackSid) {
33271
+ const pendingCallbacks = (_a = this.pendingTrackAddedCallbacks.get(pendingTrackSid)) !== null && _a !== void 0 ? _a : new Set();
33272
+ pendingCallbacks.add(cleanup);
33273
+ this.pendingTrackAddedCallbacks.set(pendingTrackSid, pendingCallbacks);
33274
+ }
31779
33275
  return;
31780
33276
  }
31781
33277
  if (this.state === ConnectionState.Disconnected) {
@@ -31788,11 +33284,10 @@ class Room extends eventsExports.EventEmitter {
31788
33284
  }
31789
33285
  const parts = unpackStreamId(stream.id);
31790
33286
  const participantSid = parts[0];
31791
- let streamId = parts[1];
31792
- let trackId = mediaTrack.id;
33287
+ const streamId = parts[1];
31793
33288
  // firefox will get streamId (pID|trackId) instead of (pID|streamId) as it doesn't support sync tracks by stream
31794
33289
  // and generates its own track id instead of infer from sdp track id.
31795
- if (streamId && streamId.startsWith('TR')) trackId = streamId;
33290
+ let trackId = (_b = extractTrackSid(mediaTrack, stream)) !== null && _b !== void 0 ? _b : mediaTrack.id;
31796
33291
  if (participantSid === this.localParticipant.sid) {
31797
33292
  this.log.warn('tried to create RemoteParticipant for local participant');
31798
33293
  return;
@@ -31836,6 +33331,10 @@ class Room extends eventsExports.EventEmitter {
31836
33331
  this.emit(RoomEvent.EncryptionError, new Error("Encrypted ".concat(publication.source, " track received from participant ").concat(participant.sid, ", but room does not have encryption enabled!")));
31837
33332
  }
31838
33333
  }
33334
+ cancelPendingTrackAdded(trackSid) {
33335
+ var _a;
33336
+ (_a = this.pendingTrackAddedCallbacks.get(trackSid)) === null || _a === void 0 ? void 0 : _a.forEach(cleanup => cleanup());
33337
+ }
31839
33338
  handleLocalTrackSubscribed(subscribedSid) {
31840
33339
  const findPublication = () => this.localParticipant.getTrackPublications().find(_ref6 => {
31841
33340
  let trackSid = _ref6.trackSid;
@@ -32099,6 +33598,7 @@ class Room extends eventsExports.EventEmitter {
32099
33598
  }
32100
33599
  this.emitWhenConnected(RoomEvent.TrackSubscribed, track, publication, participant);
32101
33600
  }).on(ParticipantEvent.TrackUnpublished, publication => {
33601
+ this.cancelPendingTrackAdded(publication.trackSid);
32102
33602
  this.emit(RoomEvent.TrackUnpublished, publication, participant);
32103
33603
  }).on(ParticipantEvent.TrackUnsubscribed, (track, publication) => {
32104
33604
  this.emit(RoomEvent.TrackUnsubscribed, track, publication, participant);
@@ -32162,6 +33662,20 @@ class Room extends eventsExports.EventEmitter {
32162
33662
  return this.remoteParticipants.get(identity);
32163
33663
  }
32164
33664
  }
33665
+ /** The client capabilities this SDK advertises to other participants in its `ClientInfo`. */
33666
+ getClientInfoCapabilities(roomOptions) {
33667
+ var _a;
33668
+ const capabilities = [];
33669
+ if (isFrameMetadataSupported((_a = roomOptions.frameMetadata) !== null && _a !== void 0 ? _a : roomOptions.packetTrailer) || !!this.e2eeManager) {
33670
+ capabilities.push(ClientInfo_Capability.CAP_PACKET_TRAILER);
33671
+ }
33672
+ // Advertise deflate-raw decompression support so peers know they can send us compressed data
33673
+ // streams (gated separately from clientProtocol — see the data streams v2 spec).
33674
+ if (isCompressionStreamSupported()) {
33675
+ capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
33676
+ }
33677
+ return capabilities;
33678
+ }
32165
33679
  registerRpcDataStreamHandler() {
32166
33680
  this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
32167
33681
  var _this4 = this;
@@ -32202,8 +33716,17 @@ class Room extends eventsExports.EventEmitter {
32202
33716
  } : undefined
32203
33717
  });
32204
33718
  if (consecutiveFailures >= 3) {
32205
- this.recreateEngine();
32206
- this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
33719
+ this.clearConnectionReconcile();
33720
+ if (this.engine && !this.engine.isClosed) {
33721
+ // The transport silently died while we still looked connected. Try a full reconnect
33722
+ // (keeps the room alive; the engine falls back to Disconnected if it ultimately fails).
33723
+ this.log.warn('detected connection state mismatch, attempting full reconnect');
33724
+ this.engine.reconnect();
33725
+ } else {
33726
+ // No usable engine to reconnect with; tear down.
33727
+ this.recreateEngine();
33728
+ this.handleDisconnect(this.options.stopLocalTrackOnUnpublish, DisconnectReason.STATE_MISMATCH);
33729
+ }
32207
33730
  }
32208
33731
  } else {
32209
33732
  consecutiveFailures = 0;
@@ -33600,24 +35123,26 @@ class TokenSourceEndpoint extends TokenSourceCached {
33600
35123
  const body = yield response.json();
33601
35124
  return TokenSourceResponse.fromJson(body, {
33602
35125
  // NOTE: it could be possible that the response body could contain more fields than just
33603
- // what's in TokenSourceResponse depending on the implementation (ie, SandboxTokenServer)
35126
+ // what's in TokenSourceResponse depending on the implementation (ie, DevelopmentTokenServer)
33604
35127
  ignoreUnknownFields: true
33605
35128
  });
33606
35129
  });
33607
35130
  }
33608
35131
  }
33609
- class TokenSourceSandboxTokenServer extends TokenSourceEndpoint {
33610
- constructor(sandboxId, options) {
35132
+ class TokenSourceDevelopmentTokenServer extends TokenSourceEndpoint {
35133
+ constructor(tokenServerId, options) {
33611
35134
  const _options$baseUrl = options.baseUrl,
33612
35135
  baseUrl = _options$baseUrl === void 0 ? 'https://cloud-api.livekit.io' : _options$baseUrl,
33613
35136
  rest = __rest(options, ["baseUrl"]);
33614
35137
  super("".concat(baseUrl, "/api/v2/sandbox/connection-details"), Object.assign(Object.assign({}, rest), {
33615
35138
  headers: {
33616
- 'X-Sandbox-ID': sandboxId
35139
+ 'X-Sandbox-ID': tokenServerId
33617
35140
  }
33618
35141
  }));
33619
35142
  }
33620
35143
  }
35144
+ /** @deprecated Use {@link TokenSourceDevelopmentTokenServer} instead */
35145
+ class TokenSourceSandboxTokenServer extends TokenSourceDevelopmentTokenServer {}
33621
35146
  const TokenSource = {
33622
35147
  /** TokenSource.literal contains a single, literal set of {@link TokenSourceResponseObject}
33623
35148
  * credentials, either provided directly or returned from a provided function. */
@@ -33636,24 +35161,31 @@ const TokenSource = {
33636
35161
  /**
33637
35162
  * TokenSource.endpoint creates a token source that fetches credentials from a given URL using
33638
35163
  * the standard endpoint format:
33639
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
35164
+ * @see https://docs.livekit.io/frontends/build/authentication/endpoint/
33640
35165
  */
33641
35166
  endpoint(url) {
33642
35167
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
33643
35168
  return new TokenSourceEndpoint(url, options);
33644
35169
  },
33645
35170
  /**
33646
- * TokenSource.sandboxTokenServer queries a sandbox token server for credentials,
35171
+ * @deprecated Use {@link TokenSource.developmentTokenServer} instead
35172
+ */
35173
+ sandboxTokenServer(sandboxId) {
35174
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
35175
+ return new TokenSourceSandboxTokenServer(sandboxId, options);
35176
+ },
35177
+ /**
35178
+ * TokenSource.developmentTokenServer queries a development token server for credentials,
33647
35179
  * which supports quick prototyping / getting started types of use cases.
33648
35180
  *
33649
35181
  * This token provider is INSECURE and should NOT be used in production.
33650
35182
  *
33651
35183
  * For more info:
33652
- * @see https://cloud.livekit.io/projects/p_/sandbox/templates/token-server
35184
+ * @see https://docs.livekit.io/frontends/build/authentication/sandbox-token-server/
33653
35185
  */
33654
- sandboxTokenServer(sandboxId) {
35186
+ developmentTokenServer(tokenServerId) {
33655
35187
  let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
33656
- return new TokenSourceSandboxTokenServer(sandboxId, options);
35188
+ return new TokenSourceDevelopmentTokenServer(tokenServerId, options);
33657
35189
  }
33658
35190
  };/**
33659
35191
  * Try to analyze the local track to determine the facing mode of a track.
@@ -33781,4 +35313,4 @@ const serializers = {
33781
35313
  json,
33782
35314
  raw,
33783
35315
  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
35316
+ };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