livekit-client 2.20.2 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +4 -0
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1993 -791
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +4 -0
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +28 -64
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +13 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +28 -64
- package/dist/ts4.2/room/Room.d.ts +4 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +13 -0
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/room/RTCEngine.test.ts +58 -99
- package/src/room/RTCEngine.ts +92 -534
- package/src/room/Room.test.ts +4 -1
- package/src/room/Room.ts +40 -8
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +8 -2
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/errors.ts +6 -0
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/types.ts +18 -0
- package/src/room/utils.ts +67 -0
- 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.
|
|
12163
|
+
}var version$1 = "2.21.0";const version = version$1;
|
|
11887
12164
|
const protocolVersion = 17;
|
|
11888
12165
|
/** Initial client protocol. */
|
|
11889
12166
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
11890
12167
|
/** Replaces RPC v1 protocol with a v2 data streams based one to support unlimited request /
|
|
11891
12168
|
* response payload length. */
|
|
11892
12169
|
const CLIENT_PROTOCOL_DATA_STREAM_RPC = 1;
|
|
12170
|
+
/** "Data streams v2": the client knows how to receive a single-packet data stream (a stream whose
|
|
12171
|
+
* entire payload is smuggled into the header packet, with no chunk/trailer packets). Senders only
|
|
12172
|
+
* use the single-packet optimization when the recipient advertises at least this protocol. */
|
|
12173
|
+
const CLIENT_PROTOCOL_DATA_STREAM_V2 = 2;
|
|
11893
12174
|
/** The client protocol version indicates what level of support that the client has for
|
|
11894
12175
|
* client <-> client api interactions. */
|
|
11895
|
-
const clientProtocol =
|
|
12176
|
+
const clientProtocol = CLIENT_PROTOCOL_DATA_STREAM_V2;/** Base error that all LiveKit specific custom errors inherit from. */
|
|
11896
12177
|
class LivekitError extends Error {
|
|
11897
12178
|
constructor(code, message, options) {
|
|
11898
12179
|
super(message || 'an error has occurred');
|
|
@@ -12028,6 +12309,10 @@ var DataStreamErrorReason;
|
|
|
12028
12309
|
DataStreamErrorReason[DataStreamErrorReason["HandlerAlreadyRegistered"] = 7] = "HandlerAlreadyRegistered";
|
|
12029
12310
|
// Encryption type mismatch.
|
|
12030
12311
|
DataStreamErrorReason[DataStreamErrorReason["EncryptionTypeMismatch"] = 8] = "EncryptionTypeMismatch";
|
|
12312
|
+
// The serialized stream header packet (driven mainly by attributes) exceeds the MTU budget.
|
|
12313
|
+
DataStreamErrorReason[DataStreamErrorReason["HeaderTooLarge"] = 9] = "HeaderTooLarge";
|
|
12314
|
+
// The stream's (decompressed) payload exceeds the maximum allowed size.
|
|
12315
|
+
DataStreamErrorReason[DataStreamErrorReason["PayloadTooLarge"] = 10] = "PayloadTooLarge";
|
|
12031
12316
|
})(DataStreamErrorReason || (DataStreamErrorReason = {}));
|
|
12032
12317
|
class DataStreamError extends LivekitReasonedError {
|
|
12033
12318
|
constructor(message, reason) {
|
|
@@ -14121,6 +14406,53 @@ function splitUtf8(s, n) {
|
|
|
14121
14406
|
}
|
|
14122
14407
|
return result;
|
|
14123
14408
|
}
|
|
14409
|
+
/** Wraps a byte array in a `ReadableStream` that yields it as a single chunk and then closes. */
|
|
14410
|
+
function readableFromBytes(bytes) {
|
|
14411
|
+
return new ReadableStream({
|
|
14412
|
+
start(controller) {
|
|
14413
|
+
controller.enqueue(bytes);
|
|
14414
|
+
controller.close();
|
|
14415
|
+
}
|
|
14416
|
+
});
|
|
14417
|
+
}
|
|
14418
|
+
/**
|
|
14419
|
+
* Re-chunks a byte stream into pieces of exactly `chunkSize` bytes (the final piece may be
|
|
14420
|
+
* smaller), coalescing or splitting the source's pieces as needed. Memory use is bounded to roughly
|
|
14421
|
+
* `chunkSize` plus one source read, so it never buffers the whole stream — used to pack
|
|
14422
|
+
* `CompressionStream`/`file.stream()` output into MTU-sized data-stream chunks.
|
|
14423
|
+
*/
|
|
14424
|
+
function readBytesInChunks(source, chunkSize) {
|
|
14425
|
+
return __asyncGenerator(this, arguments, function* readBytesInChunks_1() {
|
|
14426
|
+
const reader = source.getReader();
|
|
14427
|
+
let buffer = new Uint8Array(0);
|
|
14428
|
+
try {
|
|
14429
|
+
while (true) {
|
|
14430
|
+
const _yield$__await = yield __await(reader.read()),
|
|
14431
|
+
done = _yield$__await.done,
|
|
14432
|
+
value = _yield$__await.value;
|
|
14433
|
+
if (done) {
|
|
14434
|
+
break;
|
|
14435
|
+
}
|
|
14436
|
+
if (value.byteLength === 0) {
|
|
14437
|
+
continue;
|
|
14438
|
+
}
|
|
14439
|
+
const merged = new Uint8Array(buffer.byteLength + value.byteLength);
|
|
14440
|
+
merged.set(buffer);
|
|
14441
|
+
merged.set(value, buffer.byteLength);
|
|
14442
|
+
buffer = merged;
|
|
14443
|
+
while (buffer.byteLength >= chunkSize) {
|
|
14444
|
+
yield yield __await(buffer.slice(0, chunkSize));
|
|
14445
|
+
buffer = buffer.slice(chunkSize);
|
|
14446
|
+
}
|
|
14447
|
+
}
|
|
14448
|
+
if (buffer.byteLength > 0) {
|
|
14449
|
+
yield yield __await(buffer);
|
|
14450
|
+
}
|
|
14451
|
+
} finally {
|
|
14452
|
+
reader.releaseLock();
|
|
14453
|
+
}
|
|
14454
|
+
});
|
|
14455
|
+
}
|
|
14124
14456
|
function extractMaxAgeFromRequestHeaders(headers) {
|
|
14125
14457
|
var _a;
|
|
14126
14458
|
const cacheControl = headers.get('Cache-Control');
|
|
@@ -17269,110 +17601,35 @@ function createJoinRequestConnectionParams(token, info, opts, publisherOffer) {
|
|
|
17269
17601
|
params.set('join_request', bytesToBase64(wrappedBytes).replace(/\+/g, '-').replace(/\//g, '_'));
|
|
17270
17602
|
return params;
|
|
17271
17603
|
});
|
|
17272
|
-
}class
|
|
17273
|
-
|
|
17274
|
-
|
|
17275
|
-
|
|
17276
|
-
|
|
17604
|
+
}class TTLMap {
|
|
17605
|
+
/**
|
|
17606
|
+
* @param ttl ttl of the key (ms)
|
|
17607
|
+
*/
|
|
17608
|
+
constructor(ttl) {
|
|
17609
|
+
this._map = new Map();
|
|
17610
|
+
this._lastCleanup = 0;
|
|
17611
|
+
this.ttl = ttl;
|
|
17277
17612
|
}
|
|
17278
|
-
|
|
17279
|
-
|
|
17280
|
-
this.
|
|
17281
|
-
|
|
17282
|
-
this._sentSize += item.data.byteLength;
|
|
17613
|
+
set(key, value) {
|
|
17614
|
+
const now = Date.now();
|
|
17615
|
+
if (now - this._lastCleanup > this.ttl / 2) {
|
|
17616
|
+
this.cleanup();
|
|
17283
17617
|
}
|
|
17618
|
+
const expiresAt = now + this.ttl;
|
|
17619
|
+
this._map.set(key, {
|
|
17620
|
+
value,
|
|
17621
|
+
expiresAt
|
|
17622
|
+
});
|
|
17623
|
+
return this;
|
|
17284
17624
|
}
|
|
17285
|
-
|
|
17286
|
-
const
|
|
17287
|
-
if (
|
|
17288
|
-
|
|
17289
|
-
|
|
17290
|
-
|
|
17291
|
-
}
|
|
17625
|
+
get(key) {
|
|
17626
|
+
const entry = this._map.get(key);
|
|
17627
|
+
if (!entry) return undefined;
|
|
17628
|
+
if (entry.expiresAt < Date.now()) {
|
|
17629
|
+
this._map.delete(key);
|
|
17630
|
+
return undefined;
|
|
17292
17631
|
}
|
|
17293
|
-
return
|
|
17294
|
-
}
|
|
17295
|
-
getAll() {
|
|
17296
|
-
return this.buffer.slice();
|
|
17297
|
-
}
|
|
17298
|
-
/** Every queued packet not yet handed to the channel, in sequence order. */
|
|
17299
|
-
getUnsent() {
|
|
17300
|
-
return this.buffer.filter(item => !item.sent);
|
|
17301
|
-
}
|
|
17302
|
-
/** Marks a single queued packet as handed to the channel. */
|
|
17303
|
-
markSent(item) {
|
|
17304
|
-
if (!item.sent) {
|
|
17305
|
-
item.sent = true;
|
|
17306
|
-
this._sentSize += item.data.byteLength;
|
|
17307
|
-
}
|
|
17308
|
-
}
|
|
17309
|
-
/**
|
|
17310
|
-
* Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
|
|
17311
|
-
* still buffered was sent on the previous channel (or deferred) and must be re-handed to the
|
|
17312
|
-
* current one, so none of it counts as sent until the replay actually transmits it.
|
|
17313
|
-
*/
|
|
17314
|
-
markAllUnsent() {
|
|
17315
|
-
for (const item of this.buffer) {
|
|
17316
|
-
item.sent = false;
|
|
17317
|
-
}
|
|
17318
|
-
this._sentSize = 0;
|
|
17319
|
-
}
|
|
17320
|
-
popToSequence(sequence) {
|
|
17321
|
-
while (this.buffer.length > 0) {
|
|
17322
|
-
const first = this.buffer[0];
|
|
17323
|
-
if (first.sequence <= sequence) {
|
|
17324
|
-
this.pop();
|
|
17325
|
-
} else {
|
|
17326
|
-
break;
|
|
17327
|
-
}
|
|
17328
|
-
}
|
|
17329
|
-
}
|
|
17330
|
-
alignBufferedAmount(bufferedAmount) {
|
|
17331
|
-
while (this.buffer.length > 0) {
|
|
17332
|
-
const first = this.buffer[0];
|
|
17333
|
-
// Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
|
|
17334
|
-
// resume replay — trimming them would silently lose them.
|
|
17335
|
-
if (!first.sent) {
|
|
17336
|
-
break;
|
|
17337
|
-
}
|
|
17338
|
-
if (this._sentSize - first.data.byteLength <= bufferedAmount) {
|
|
17339
|
-
break;
|
|
17340
|
-
}
|
|
17341
|
-
this.pop();
|
|
17342
|
-
}
|
|
17343
|
-
}
|
|
17344
|
-
get length() {
|
|
17345
|
-
return this.buffer.length;
|
|
17346
|
-
}
|
|
17347
|
-
}class TTLMap {
|
|
17348
|
-
/**
|
|
17349
|
-
* @param ttl ttl of the key (ms)
|
|
17350
|
-
*/
|
|
17351
|
-
constructor(ttl) {
|
|
17352
|
-
this._map = new Map();
|
|
17353
|
-
this._lastCleanup = 0;
|
|
17354
|
-
this.ttl = ttl;
|
|
17355
|
-
}
|
|
17356
|
-
set(key, value) {
|
|
17357
|
-
const now = Date.now();
|
|
17358
|
-
if (now - this._lastCleanup > this.ttl / 2) {
|
|
17359
|
-
this.cleanup();
|
|
17360
|
-
}
|
|
17361
|
-
const expiresAt = now + this.ttl;
|
|
17362
|
-
this._map.set(key, {
|
|
17363
|
-
value,
|
|
17364
|
-
expiresAt
|
|
17365
|
-
});
|
|
17366
|
-
return this;
|
|
17367
|
-
}
|
|
17368
|
-
get(key) {
|
|
17369
|
-
const entry = this._map.get(key);
|
|
17370
|
-
if (!entry) return undefined;
|
|
17371
|
-
if (entry.expiresAt < Date.now()) {
|
|
17372
|
-
this._map.delete(key);
|
|
17373
|
-
return undefined;
|
|
17374
|
-
}
|
|
17375
|
-
return entry.value;
|
|
17632
|
+
return entry.value;
|
|
17376
17633
|
}
|
|
17377
17634
|
has(key) {
|
|
17378
17635
|
const entry = this._map.get(key);
|
|
@@ -19289,6 +19546,663 @@ class PCTransportManager {
|
|
|
19289
19546
|
}();
|
|
19290
19547
|
});
|
|
19291
19548
|
}
|
|
19549
|
+
}/**
|
|
19550
|
+
* Two-watermark flow control for one data channel kind.
|
|
19551
|
+
*
|
|
19552
|
+
* Owns the per-kind headroom gate: senders proceed freely while the buffer is at or below the
|
|
19553
|
+
* high-water mark and otherwise block — serialized FIFO through a mutex — until the browser's
|
|
19554
|
+
* `bufferedamountlow` event (armed at the low-water mark) signals the buffer has drained. The
|
|
19555
|
+
* serialization prevents woken senders from all refilling at once and overflowing the SCTP send
|
|
19556
|
+
* buffer (see livekit/client-sdk-js#1995).
|
|
19557
|
+
*
|
|
19558
|
+
* Waiters are parked on the channel object captured at wait entry. If that object stops being
|
|
19559
|
+
* current — replaced or torn down — its events may never fire again, so the owner must call
|
|
19560
|
+
* {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
|
|
19561
|
+
* gets a fresh controller.
|
|
19562
|
+
*/
|
|
19563
|
+
class FlowControlledDataChannel {
|
|
19564
|
+
constructor(opts) {
|
|
19565
|
+
/** Last emitted low-water status; starts true (an empty buffer is below the mark). */
|
|
19566
|
+
this.bufferStatusLow = true;
|
|
19567
|
+
this.headroomLock = new _();
|
|
19568
|
+
/** Cancels parked headroom waiters when the handle is replaced or torn down. */
|
|
19569
|
+
this.waiterAbortController = new AbortController();
|
|
19570
|
+
this.kind = opts.kind;
|
|
19571
|
+
this.lowWaterMark = opts.lowWaterMark;
|
|
19572
|
+
this.highWaterMark = opts.highWaterMark;
|
|
19573
|
+
this.isEngineClosed = opts.isEngineClosed;
|
|
19574
|
+
this.onBufferStatusChanged = opts.onBufferStatusChanged;
|
|
19575
|
+
}
|
|
19576
|
+
/** The currently attached RTCDataChannel handle, if any. */
|
|
19577
|
+
get channelHandle() {
|
|
19578
|
+
return this.handle;
|
|
19579
|
+
}
|
|
19580
|
+
/**
|
|
19581
|
+
* Attaches the channel handle this wrapper controls. Replacing an existing handle rejects
|
|
19582
|
+
* parked waiters — their events would never fire again on the abandoned object — and installs a
|
|
19583
|
+
* fresh controller, so queued senders re-check against the new channel. Wrappers outlive their
|
|
19584
|
+
* handles: this is the one place handle turnover happens, which is what makes stranding a
|
|
19585
|
+
* waiter structurally impossible.
|
|
19586
|
+
*/
|
|
19587
|
+
attach(dc) {
|
|
19588
|
+
if (this.handle && this.handle !== dc) {
|
|
19589
|
+
this.invalidateWaiters('data channel replaced');
|
|
19590
|
+
}
|
|
19591
|
+
this.handle = dc;
|
|
19592
|
+
}
|
|
19593
|
+
/** Detaches the handle on teardown, rejecting parked waiters. */
|
|
19594
|
+
detach() {
|
|
19595
|
+
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'data channel torn down';
|
|
19596
|
+
if (this.handle) {
|
|
19597
|
+
this.invalidateWaiters(reason);
|
|
19598
|
+
}
|
|
19599
|
+
this.handle = undefined;
|
|
19600
|
+
}
|
|
19601
|
+
getChannel() {
|
|
19602
|
+
return this.handle;
|
|
19603
|
+
}
|
|
19604
|
+
/**
|
|
19605
|
+
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
19606
|
+
* this is true and block once it goes false. Callers resolve the handle (and decide what an
|
|
19607
|
+
* absent one means) before asking.
|
|
19608
|
+
*/
|
|
19609
|
+
isBelowHighWaterMark(dc) {
|
|
19610
|
+
// RTCDataChannel has no built-in high-water mark, so we compare against our static mark.
|
|
19611
|
+
return dc.bufferedAmount <= this.highWaterMark;
|
|
19612
|
+
}
|
|
19613
|
+
/**
|
|
19614
|
+
* Whether the send buffer has drained to its low-water mark. Drives the engine's public
|
|
19615
|
+
* DCBufferStatusChanged event.
|
|
19616
|
+
*/
|
|
19617
|
+
isBelowLowWaterMark(dc) {
|
|
19618
|
+
// Read the channel's own threshold: it is tuned dynamically for the lossy channel.
|
|
19619
|
+
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
19620
|
+
}
|
|
19621
|
+
/**
|
|
19622
|
+
* Acquires the headroom lock, resolving with the unlock function. Batch senders (the resume
|
|
19623
|
+
* replay) hold it across all of their sends so no other sender can interleave, calling
|
|
19624
|
+
* {@link waitForHeadroomWithoutLock} per message to respect flow control within the batch.
|
|
19625
|
+
*/
|
|
19626
|
+
lockHeadroom() {
|
|
19627
|
+
return this.headroomLock.lock();
|
|
19628
|
+
}
|
|
19629
|
+
/**
|
|
19630
|
+
* Resolves once the caller may send on this channel: immediately while the send buffer is at or
|
|
19631
|
+
* below its high-water mark, otherwise once the buffer has drained to the low-water mark (the
|
|
19632
|
+
* `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
|
|
19633
|
+
* the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
|
|
19634
|
+
* sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
|
|
19635
|
+
* closed/buffer checks run inside the lock so queued callers proceed in FIFO order.
|
|
19636
|
+
*/
|
|
19637
|
+
waitForHeadroomWithLock() {
|
|
19638
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19639
|
+
const unlock = yield this.lockHeadroom();
|
|
19640
|
+
try {
|
|
19641
|
+
yield this.waitForHeadroomWithoutLock();
|
|
19642
|
+
} finally {
|
|
19643
|
+
unlock();
|
|
19644
|
+
}
|
|
19645
|
+
});
|
|
19646
|
+
}
|
|
19647
|
+
/** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
|
|
19648
|
+
waitForHeadroomWithoutLock() {
|
|
19649
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19650
|
+
if (this.isEngineClosed()) {
|
|
19651
|
+
throw new UnexpectedConnectionState('engine closed');
|
|
19652
|
+
}
|
|
19653
|
+
const dc = this.getChannel();
|
|
19654
|
+
if (!dc) {
|
|
19655
|
+
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
|
|
19656
|
+
}
|
|
19657
|
+
if (this.isBelowHighWaterMark(dc)) {
|
|
19658
|
+
return;
|
|
19659
|
+
}
|
|
19660
|
+
const abortSignal = this.waiterAbortController.signal;
|
|
19661
|
+
yield new TypedPromise((resolve, reject) => {
|
|
19662
|
+
const onBufferedAmountLow = () => {
|
|
19663
|
+
cleanup();
|
|
19664
|
+
resolve();
|
|
19665
|
+
};
|
|
19666
|
+
const onDCClose = () => {
|
|
19667
|
+
cleanup();
|
|
19668
|
+
reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " closed while draining the buffer")));
|
|
19669
|
+
};
|
|
19670
|
+
const onAbort = () => {
|
|
19671
|
+
cleanup();
|
|
19672
|
+
reject(new UnexpectedConnectionState("DataChannel ".concat(this.kind, " was replaced or torn down while waiting for headroom")));
|
|
19673
|
+
};
|
|
19674
|
+
const cleanup = () => {
|
|
19675
|
+
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
19676
|
+
dc.removeEventListener('close', onDCClose);
|
|
19677
|
+
abortSignal.removeEventListener('abort', onAbort);
|
|
19678
|
+
};
|
|
19679
|
+
if (abortSignal.aborted) {
|
|
19680
|
+
onAbort();
|
|
19681
|
+
return;
|
|
19682
|
+
}
|
|
19683
|
+
dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
19684
|
+
// Proxy along any error caused by the data channel closing while we wait.
|
|
19685
|
+
dc.addEventListener('close', onDCClose);
|
|
19686
|
+
// The channel object we're parked on can be abandoned without ever firing another event
|
|
19687
|
+
// (e.g. the engine recreating channels); the abort is our way out.
|
|
19688
|
+
abortSignal.addEventListener('abort', onAbort);
|
|
19689
|
+
});
|
|
19690
|
+
});
|
|
19691
|
+
}
|
|
19692
|
+
/** Rejects all parked headroom waiters; the next waiter gets a fresh controller. */
|
|
19693
|
+
invalidateWaiters(reason) {
|
|
19694
|
+
this.waiterAbortController.abort(reason);
|
|
19695
|
+
this.waiterAbortController = new AbortController();
|
|
19696
|
+
}
|
|
19697
|
+
/**
|
|
19698
|
+
* Recomputes whether the buffer has drained to the low-water mark and, if that changed since the
|
|
19699
|
+
* last check, notifies the status listener. Two independent triggers land here: a send (which
|
|
19700
|
+
* raises the buffer) and the `bufferedamountlow` drain event (which lowers it) — the latter has
|
|
19701
|
+
* no send to hang the work off, which is why this is a shared entry point rather than a tail of
|
|
19702
|
+
* `send`.
|
|
19703
|
+
*/
|
|
19704
|
+
refreshBufferStatus() {
|
|
19705
|
+
var _a;
|
|
19706
|
+
const dc = this.getChannel();
|
|
19707
|
+
if (!dc) {
|
|
19708
|
+
return;
|
|
19709
|
+
}
|
|
19710
|
+
const isLow = this.isBelowLowWaterMark(dc);
|
|
19711
|
+
if (isLow !== this.bufferStatusLow) {
|
|
19712
|
+
this.bufferStatusLow = isLow;
|
|
19713
|
+
(_a = this.onBufferStatusChanged) === null || _a === void 0 ? void 0 : _a.call(this, isLow);
|
|
19714
|
+
}
|
|
19715
|
+
}
|
|
19716
|
+
}/**
|
|
19717
|
+
* A lossy channel: flow control plus a per-instance full-buffer policy.
|
|
19718
|
+
*
|
|
19719
|
+
* Each instance owns its own byterate stat, drop counter, and (when tuning is started) the
|
|
19720
|
+
* dynamic `bufferedAmountLowThreshold` adjustment that keeps the drop gate at roughly 100ms of
|
|
19721
|
+
* buffered latency. Keeping these per instance is what prevents one channel's traffic from
|
|
19722
|
+
* steering another channel's policy.
|
|
19723
|
+
*/
|
|
19724
|
+
class LossyDataChannel extends FlowControlledDataChannel {
|
|
19725
|
+
constructor(opts) {
|
|
19726
|
+
super(opts);
|
|
19727
|
+
this.statCurrentBytes = 0;
|
|
19728
|
+
this.statByterate = 0;
|
|
19729
|
+
this.dropCount = 0;
|
|
19730
|
+
this.bufferFullBehavior = opts.bufferFullBehavior;
|
|
19731
|
+
this.shouldSkipSends = opts.shouldSkipSends;
|
|
19732
|
+
}
|
|
19733
|
+
/** Sends prepared bytes with this channel's full-buffer policy (drop or wait). */
|
|
19734
|
+
send(msg) {
|
|
19735
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19736
|
+
const dc = this.getChannel();
|
|
19737
|
+
if (!dc) {
|
|
19738
|
+
return;
|
|
19739
|
+
}
|
|
19740
|
+
// Depending on the channel's policy, either drop or wait for the buffer to drain below the
|
|
19741
|
+
// high-water mark before continuing.
|
|
19742
|
+
switch (this.bufferFullBehavior) {
|
|
19743
|
+
case 'wait':
|
|
19744
|
+
if (!this.isBelowHighWaterMark(dc)) {
|
|
19745
|
+
yield this.waitForHeadroomWithLock();
|
|
19746
|
+
}
|
|
19747
|
+
break;
|
|
19748
|
+
case 'drop':
|
|
19749
|
+
// We check against the actual threshold on the DC here, as it is tuned dynamically.
|
|
19750
|
+
if (!this.isBelowLowWaterMark(dc)) {
|
|
19751
|
+
// Drop messages to reduce latency
|
|
19752
|
+
this.dropCount += 1;
|
|
19753
|
+
if (this.dropCount % 100 === 0) {
|
|
19754
|
+
livekitLogger.warn("dropping lossy data channel messages, total dropped: ".concat(this.dropCount));
|
|
19755
|
+
}
|
|
19756
|
+
return;
|
|
19757
|
+
}
|
|
19758
|
+
}
|
|
19759
|
+
this.statCurrentBytes += msg.byteLength;
|
|
19760
|
+
if (this.shouldSkipSends()) {
|
|
19761
|
+
return;
|
|
19762
|
+
}
|
|
19763
|
+
try {
|
|
19764
|
+
dc.send(msg);
|
|
19765
|
+
this.refreshBufferStatus();
|
|
19766
|
+
} catch (error) {
|
|
19767
|
+
// Preserve prior surface behaviour: a send that fails because the channel is closing is
|
|
19768
|
+
// logged, not thrown, so lossy/data-track sends don't reject during teardown windows.
|
|
19769
|
+
if (error instanceof TypeError) {
|
|
19770
|
+
livekitLogger.error(error);
|
|
19771
|
+
} else {
|
|
19772
|
+
throw error;
|
|
19773
|
+
}
|
|
19774
|
+
}
|
|
19775
|
+
});
|
|
19776
|
+
}
|
|
19777
|
+
/**
|
|
19778
|
+
* Starts the once-per-second adjustment of the channel's `bufferedAmountLowThreshold` to the
|
|
19779
|
+
* observed byterate, keeping the drop gate at roughly 100ms of buffered latency (clamped to
|
|
19780
|
+
* the watermarks). Restarts cleanly if already running.
|
|
19781
|
+
*/
|
|
19782
|
+
startThresholdTuning() {
|
|
19783
|
+
this.stopThresholdTuning();
|
|
19784
|
+
this.statInterval = CriticalTimers.setInterval(() => {
|
|
19785
|
+
this.statByterate = this.statCurrentBytes;
|
|
19786
|
+
this.statCurrentBytes = 0;
|
|
19787
|
+
const dc = this.getChannel();
|
|
19788
|
+
if (dc) {
|
|
19789
|
+
// control buffered latency to ~100ms
|
|
19790
|
+
const threshold = this.statByterate / 10;
|
|
19791
|
+
dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, this.lowWaterMark), this.highWaterMark);
|
|
19792
|
+
}
|
|
19793
|
+
}, 1000);
|
|
19794
|
+
}
|
|
19795
|
+
/** Stops the threshold tuning and resets the stats and drop counter. */
|
|
19796
|
+
stopThresholdTuning() {
|
|
19797
|
+
this.statByterate = 0;
|
|
19798
|
+
this.statCurrentBytes = 0;
|
|
19799
|
+
if (this.statInterval) {
|
|
19800
|
+
CriticalTimers.clearInterval(this.statInterval);
|
|
19801
|
+
this.statInterval = undefined;
|
|
19802
|
+
}
|
|
19803
|
+
this.dropCount = 0;
|
|
19804
|
+
}
|
|
19805
|
+
}class DataPacketBuffer {
|
|
19806
|
+
constructor() {
|
|
19807
|
+
this.buffer = [];
|
|
19808
|
+
this._totalSize = 0;
|
|
19809
|
+
this._sentSize = 0;
|
|
19810
|
+
}
|
|
19811
|
+
push(item) {
|
|
19812
|
+
this.buffer.push(item);
|
|
19813
|
+
this._totalSize += item.data.byteLength;
|
|
19814
|
+
if (item.sent) {
|
|
19815
|
+
this._sentSize += item.data.byteLength;
|
|
19816
|
+
}
|
|
19817
|
+
}
|
|
19818
|
+
pop() {
|
|
19819
|
+
const item = this.buffer.shift();
|
|
19820
|
+
if (item) {
|
|
19821
|
+
this._totalSize -= item.data.byteLength;
|
|
19822
|
+
if (item.sent) {
|
|
19823
|
+
this._sentSize -= item.data.byteLength;
|
|
19824
|
+
}
|
|
19825
|
+
}
|
|
19826
|
+
return item;
|
|
19827
|
+
}
|
|
19828
|
+
getAll() {
|
|
19829
|
+
return this.buffer.slice();
|
|
19830
|
+
}
|
|
19831
|
+
/** Every queued packet not yet handed to the channel, in sequence order. */
|
|
19832
|
+
getUnsent() {
|
|
19833
|
+
return this.buffer.filter(item => !item.sent);
|
|
19834
|
+
}
|
|
19835
|
+
/** Marks a single queued packet as handed to the channel. */
|
|
19836
|
+
markSent(item) {
|
|
19837
|
+
if (!item.sent) {
|
|
19838
|
+
item.sent = true;
|
|
19839
|
+
this._sentSize += item.data.byteLength;
|
|
19840
|
+
}
|
|
19841
|
+
}
|
|
19842
|
+
/**
|
|
19843
|
+
* Marks every queued packet as not-yet-sent. Used at the start of a resume replay: whatever is
|
|
19844
|
+
* still buffered was sent on the previous channel (or deferred) and must be re-handed to the
|
|
19845
|
+
* current one, so none of it counts as sent until the replay actually transmits it.
|
|
19846
|
+
*/
|
|
19847
|
+
markAllUnsent() {
|
|
19848
|
+
for (const item of this.buffer) {
|
|
19849
|
+
item.sent = false;
|
|
19850
|
+
}
|
|
19851
|
+
this._sentSize = 0;
|
|
19852
|
+
}
|
|
19853
|
+
popToSequence(sequence) {
|
|
19854
|
+
while (this.buffer.length > 0) {
|
|
19855
|
+
const first = this.buffer[0];
|
|
19856
|
+
if (first.sequence <= sequence) {
|
|
19857
|
+
this.pop();
|
|
19858
|
+
} else {
|
|
19859
|
+
break;
|
|
19860
|
+
}
|
|
19861
|
+
}
|
|
19862
|
+
}
|
|
19863
|
+
alignBufferedAmount(bufferedAmount) {
|
|
19864
|
+
while (this.buffer.length > 0) {
|
|
19865
|
+
const first = this.buffer[0];
|
|
19866
|
+
// Unsent packets aren't part of the channel's bufferedAmount and are still awaiting the
|
|
19867
|
+
// resume replay — trimming them would silently lose them.
|
|
19868
|
+
if (!first.sent) {
|
|
19869
|
+
break;
|
|
19870
|
+
}
|
|
19871
|
+
if (this._sentSize - first.data.byteLength <= bufferedAmount) {
|
|
19872
|
+
break;
|
|
19873
|
+
}
|
|
19874
|
+
this.pop();
|
|
19875
|
+
}
|
|
19876
|
+
}
|
|
19877
|
+
get length() {
|
|
19878
|
+
return this.buffer.length;
|
|
19879
|
+
}
|
|
19880
|
+
}/**
|
|
19881
|
+
* The reliable channel: flow control plus delivery-across-resume semantics.
|
|
19882
|
+
*
|
|
19883
|
+
* Every packet gets a monotonic sequence (stamped into the protobuf by the caller before
|
|
19884
|
+
* serialization, via {@link nextSequence}) and is retained in a replay buffer until the channel's
|
|
19885
|
+
* `bufferedAmount` confirms it has been handed to the transport. Sends that land in a reconnect
|
|
19886
|
+
* window — or whose headroom wait is torn down transiently — are queued unsent and resolve;
|
|
19887
|
+
* {@link replay} delivers them (plus any unacked packets) after a resume. Only an engine close
|
|
19888
|
+
* rejects, because no replay is coming after that.
|
|
19889
|
+
*/
|
|
19890
|
+
class ReliableDataChannel extends FlowControlledDataChannel {
|
|
19891
|
+
constructor(opts) {
|
|
19892
|
+
super(opts);
|
|
19893
|
+
this.messageBuffer = new DataPacketBuffer();
|
|
19894
|
+
this.sequence = 1;
|
|
19895
|
+
this.isDeferringSends = opts.isDeferringSends;
|
|
19896
|
+
}
|
|
19897
|
+
/**
|
|
19898
|
+
* Claims the next packet sequence. The caller stamps it into the packet before serialization,
|
|
19899
|
+
* then passes it back to {@link send} so the replay buffer stays keyed by wire sequence.
|
|
19900
|
+
*/
|
|
19901
|
+
nextSequence() {
|
|
19902
|
+
const sequence = this.sequence;
|
|
19903
|
+
this.sequence += 1;
|
|
19904
|
+
return sequence;
|
|
19905
|
+
}
|
|
19906
|
+
/**
|
|
19907
|
+
* Sends prepared bytes with reliable semantics. Resolves once the packet has either been handed
|
|
19908
|
+
* to the channel or queued for the resume replay; throws only when the engine is closed.
|
|
19909
|
+
*/
|
|
19910
|
+
send(msg, sequence) {
|
|
19911
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19912
|
+
if (this.isDeferringSends()) {
|
|
19913
|
+
// A reconnect is already underway — queue for the resume replay instead of parking on a
|
|
19914
|
+
// channel that is being torn down. The send resolves; delivery is deferred to the replay.
|
|
19915
|
+
this.messageBuffer.push({
|
|
19916
|
+
data: msg,
|
|
19917
|
+
sequence,
|
|
19918
|
+
sent: false
|
|
19919
|
+
});
|
|
19920
|
+
return;
|
|
19921
|
+
}
|
|
19922
|
+
const dc = this.getChannel();
|
|
19923
|
+
if (!dc) {
|
|
19924
|
+
return;
|
|
19925
|
+
}
|
|
19926
|
+
try {
|
|
19927
|
+
yield this.waitForHeadroomWithLock();
|
|
19928
|
+
} catch (error) {
|
|
19929
|
+
if (this.isEngineClosed()) {
|
|
19930
|
+
// No replay is coming after an engine close — surface the failure.
|
|
19931
|
+
throw error;
|
|
19932
|
+
}
|
|
19933
|
+
// Transient teardown (the channel closed or was replaced while we waited): the reliable
|
|
19934
|
+
// channel promises delivery across resume, so queue the packet for the replay instead of
|
|
19935
|
+
// rejecting a send the app can't meaningfully retry.
|
|
19936
|
+
this.messageBuffer.push({
|
|
19937
|
+
data: msg,
|
|
19938
|
+
sequence,
|
|
19939
|
+
sent: false
|
|
19940
|
+
});
|
|
19941
|
+
return;
|
|
19942
|
+
}
|
|
19943
|
+
if (this.isDeferringSends()) {
|
|
19944
|
+
// A reconnect began while we waited for headroom — same deal as above.
|
|
19945
|
+
this.messageBuffer.push({
|
|
19946
|
+
data: msg,
|
|
19947
|
+
sequence,
|
|
19948
|
+
sent: false
|
|
19949
|
+
});
|
|
19950
|
+
return;
|
|
19951
|
+
}
|
|
19952
|
+
this.messageBuffer.push({
|
|
19953
|
+
data: msg,
|
|
19954
|
+
sequence,
|
|
19955
|
+
sent: true
|
|
19956
|
+
});
|
|
19957
|
+
dc.send(msg);
|
|
19958
|
+
this.refreshBufferStatus();
|
|
19959
|
+
});
|
|
19960
|
+
}
|
|
19961
|
+
/**
|
|
19962
|
+
* Replays the buffered backlog after a resume: drops everything the server acked
|
|
19963
|
+
* (`lastMessageSeq`), then re-sends the rest in order. The headroom lock is held across the
|
|
19964
|
+
* whole replay — releasing it between messages would let a concurrent send (whose newer
|
|
19965
|
+
* sequence was already assigned before it queued on the lock) hit the wire mid-replay, and
|
|
19966
|
+
* receivers would then discard the remaining lower-sequence resent messages as duplicates.
|
|
19967
|
+
*/
|
|
19968
|
+
replay(lastMessageSeq) {
|
|
19969
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
19970
|
+
const dc = this.getChannel();
|
|
19971
|
+
if (!dc) {
|
|
19972
|
+
return;
|
|
19973
|
+
}
|
|
19974
|
+
this.messageBuffer.popToSequence(lastMessageSeq);
|
|
19975
|
+
const unlock = yield this.lockHeadroom();
|
|
19976
|
+
try {
|
|
19977
|
+
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
19978
|
+
this.messageBuffer.markAllUnsent();
|
|
19979
|
+
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
19980
|
+
// sent:false) during our own awaits appends after this pass started, so we pick it up on
|
|
19981
|
+
// the next one. Mark each packet only once we've actually handed it to the channel — a
|
|
19982
|
+
// blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
|
|
19983
|
+
// and a later alignBufferedAmount would then drop it for good. If the loop throws
|
|
19984
|
+
// mid-drain, unsent entries keep their flag and the next replay picks them up.
|
|
19985
|
+
for (let batch = this.messageBuffer.getUnsent(); batch.length > 0; batch = this.messageBuffer.getUnsent()) {
|
|
19986
|
+
for (const item of batch) {
|
|
19987
|
+
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
19988
|
+
yield this.waitForHeadroomWithoutLock();
|
|
19989
|
+
dc.send(item.data);
|
|
19990
|
+
this.messageBuffer.markSent(item);
|
|
19991
|
+
}
|
|
19992
|
+
}
|
|
19993
|
+
} finally {
|
|
19994
|
+
unlock();
|
|
19995
|
+
}
|
|
19996
|
+
this.refreshBufferStatus();
|
|
19997
|
+
});
|
|
19998
|
+
}
|
|
19999
|
+
/**
|
|
20000
|
+
* Before recomputing status, trim packets the transport has now delivered — a send or a drain
|
|
20001
|
+
* may have acked buffered packets, and the replay buffer is keyed off the channel's buffered
|
|
20002
|
+
* bytes.
|
|
20003
|
+
*/
|
|
20004
|
+
refreshBufferStatus() {
|
|
20005
|
+
const dc = this.channelHandle;
|
|
20006
|
+
if (dc) {
|
|
20007
|
+
this.messageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
20008
|
+
}
|
|
20009
|
+
super.refreshBufferStatus();
|
|
20010
|
+
}
|
|
20011
|
+
/**
|
|
20012
|
+
* Drops all replay state and restarts sequencing. Only valid on a full reconnect, where the
|
|
20013
|
+
* session (and the receivers' sequence tracking) starts over.
|
|
20014
|
+
*/
|
|
20015
|
+
reset() {
|
|
20016
|
+
this.messageBuffer = new DataPacketBuffer();
|
|
20017
|
+
this.sequence = 1;
|
|
20018
|
+
}
|
|
20019
|
+
}var DataChannelKind;
|
|
20020
|
+
(function (DataChannelKind) {
|
|
20021
|
+
DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
|
|
20022
|
+
DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
|
|
20023
|
+
DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
|
|
20024
|
+
})(DataChannelKind || (DataChannelKind = {}));
|
|
20025
|
+
// Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
|
|
20026
|
+
// freely up to the high-water mark; once it's exceeded they block until the browser's
|
|
20027
|
+
// `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
|
|
20028
|
+
// The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
|
|
20029
|
+
// isn't starved, while the high-water mark bounds the buffer well below the level that would abort
|
|
20030
|
+
// the channel (see livekit/client-sdk-js#1995).
|
|
20031
|
+
const reliableDataChannelWaterMarkLow = 64 * 1024;
|
|
20032
|
+
const reliableDataChannelWaterMarkHigh = 1024 * 1024;
|
|
20033
|
+
const lossyDataChannelWaterMarkLow = 8 * 1024;
|
|
20034
|
+
const lossyDataChannelWaterMarkHigh = 256 * 1024;
|
|
20035
|
+
function dataChannelLowWaterMark(kind) {
|
|
20036
|
+
return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkLow : lossyDataChannelWaterMarkLow;
|
|
20037
|
+
}
|
|
20038
|
+
function dataChannelHighWaterMark(kind) {
|
|
20039
|
+
return kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
|
|
20040
|
+
}const lossyDataChannelLabel = '_lossy';
|
|
20041
|
+
const reliableDataChannelLabel = '_reliable';
|
|
20042
|
+
const dataTrackDataChannelLabel = '_data_track';
|
|
20043
|
+
/**
|
|
20044
|
+
* Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
|
|
20045
|
+
* live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
|
|
20046
|
+
* connections come and go) plus the subscriber-side receive handles adopted by label.
|
|
20047
|
+
*
|
|
20048
|
+
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which reject
|
|
20049
|
+
* parked headroom waiters as a built-in — there is no separate invalidation step to forget.
|
|
20050
|
+
*/
|
|
20051
|
+
class DataChannelManager {
|
|
20052
|
+
constructor(opts) {
|
|
20053
|
+
this.opts = opts;
|
|
20054
|
+
const flowControlOptions = kind => ({
|
|
20055
|
+
kind,
|
|
20056
|
+
lowWaterMark: dataChannelLowWaterMark(kind),
|
|
20057
|
+
highWaterMark: dataChannelHighWaterMark(kind),
|
|
20058
|
+
isEngineClosed: opts.isEngineClosed,
|
|
20059
|
+
onBufferStatusChanged: isLow => opts.onBufferStatusChanged(kind, isLow)
|
|
20060
|
+
});
|
|
20061
|
+
this.reliable = new ReliableDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.RELIABLE)), {
|
|
20062
|
+
isDeferringSends: opts.isReconnecting
|
|
20063
|
+
}));
|
|
20064
|
+
this.lossy = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.LOSSY)), {
|
|
20065
|
+
// Classic lossy user data: a stale packet is worthless, so drop instead of queueing.
|
|
20066
|
+
bufferFullBehavior: 'drop',
|
|
20067
|
+
shouldSkipSends: opts.isReconnecting
|
|
20068
|
+
}));
|
|
20069
|
+
this.dataTrack = new LossyDataChannel(Object.assign(Object.assign({}, flowControlOptions(DataChannelKind.DATA_TRACK_LOSSY)), {
|
|
20070
|
+
// Data tracks backpressure the producer instead — it decides what to skip at frame
|
|
20071
|
+
// granularity rather than the engine dropping arbitrary chunks out of frames.
|
|
20072
|
+
bufferFullBehavior: 'wait',
|
|
20073
|
+
shouldSkipSends: opts.isReconnecting
|
|
20074
|
+
}));
|
|
20075
|
+
}
|
|
20076
|
+
/** The flow-control wrapper for `kind`. */
|
|
20077
|
+
channelFor(kind) {
|
|
20078
|
+
switch (kind) {
|
|
20079
|
+
case DataChannelKind.RELIABLE:
|
|
20080
|
+
return this.reliable;
|
|
20081
|
+
case DataChannelKind.LOSSY:
|
|
20082
|
+
return this.lossy;
|
|
20083
|
+
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
20084
|
+
return this.dataTrack;
|
|
20085
|
+
}
|
|
20086
|
+
}
|
|
20087
|
+
/** The raw RTCDataChannel handle for `kind`, publisher side by default. */
|
|
20088
|
+
getHandle(kind) {
|
|
20089
|
+
let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
20090
|
+
if (!subscriber) {
|
|
20091
|
+
return this.channelFor(kind).channelHandle;
|
|
20092
|
+
}
|
|
20093
|
+
switch (kind) {
|
|
20094
|
+
case DataChannelKind.RELIABLE:
|
|
20095
|
+
return this.reliableSub;
|
|
20096
|
+
case DataChannelKind.LOSSY:
|
|
20097
|
+
return this.lossySub;
|
|
20098
|
+
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
20099
|
+
return this.dataTrackSub;
|
|
20100
|
+
}
|
|
20101
|
+
}
|
|
20102
|
+
get hasPublisherChannels() {
|
|
20103
|
+
return Boolean(this.reliable.channelHandle || this.lossy.channelHandle || this.dataTrack.channelHandle);
|
|
20104
|
+
}
|
|
20105
|
+
/**
|
|
20106
|
+
* Creates the three publisher data channels on the given transport, wires their handlers, and
|
|
20107
|
+
* attaches them to the wrappers — attaching rejects any waiters still parked on replaced
|
|
20108
|
+
* channel objects.
|
|
20109
|
+
*/
|
|
20110
|
+
createPublisherChannels(pcManager) {
|
|
20111
|
+
// clear old data channel callbacks if recreate
|
|
20112
|
+
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
20113
|
+
const old = channel.channelHandle;
|
|
20114
|
+
if (old) {
|
|
20115
|
+
old.onmessage = null;
|
|
20116
|
+
old.onerror = null;
|
|
20117
|
+
old.onclose = null;
|
|
20118
|
+
}
|
|
20119
|
+
}
|
|
20120
|
+
const wire = (channel, dc, onMessage) => {
|
|
20121
|
+
// also handle messages over the pub channel, for backwards compatibility
|
|
20122
|
+
dc.onmessage = onMessage;
|
|
20123
|
+
// handle datachannel errors
|
|
20124
|
+
dc.onerror = this.opts.onDataError;
|
|
20125
|
+
// detect unexpected publisher data channel closes
|
|
20126
|
+
dc.onclose = () => this.opts.onChannelClose(channel.kind);
|
|
20127
|
+
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
20128
|
+
dc.bufferedAmountLowThreshold = channel.lowWaterMark;
|
|
20129
|
+
// handle buffer amount low events
|
|
20130
|
+
dc.onbufferedamountlow = () => channel.refreshBufferStatus();
|
|
20131
|
+
channel.attach(dc);
|
|
20132
|
+
};
|
|
20133
|
+
wire(this.lossy, pcManager.createPublisherDataChannel(lossyDataChannelLabel, {
|
|
20134
|
+
ordered: false,
|
|
20135
|
+
maxRetransmits: 0
|
|
20136
|
+
}), this.opts.onDataMessage);
|
|
20137
|
+
wire(this.reliable, pcManager.createPublisherDataChannel(reliableDataChannelLabel, {
|
|
20138
|
+
ordered: true
|
|
20139
|
+
}), this.opts.onDataMessage);
|
|
20140
|
+
wire(this.dataTrack, pcManager.createPublisherDataChannel(dataTrackDataChannelLabel, {
|
|
20141
|
+
ordered: false,
|
|
20142
|
+
maxRetransmits: 0
|
|
20143
|
+
}), this.opts.onDataTrackMessage);
|
|
20144
|
+
this.lossy.startThresholdTuning();
|
|
20145
|
+
}
|
|
20146
|
+
/**
|
|
20147
|
+
* Adopts a subscriber-side data channel by label, wiring the matching receive handler.
|
|
20148
|
+
* Returns false for labels this manager doesn't own.
|
|
20149
|
+
*/
|
|
20150
|
+
adoptSubscriberChannel(channel) {
|
|
20151
|
+
let handler;
|
|
20152
|
+
if (channel.label === reliableDataChannelLabel) {
|
|
20153
|
+
this.reliableSub = channel;
|
|
20154
|
+
handler = this.opts.onDataMessage;
|
|
20155
|
+
} else if (channel.label === lossyDataChannelLabel) {
|
|
20156
|
+
this.lossySub = channel;
|
|
20157
|
+
handler = this.opts.onDataMessage;
|
|
20158
|
+
} else if (channel.label === dataTrackDataChannelLabel) {
|
|
20159
|
+
this.dataTrackSub = channel;
|
|
20160
|
+
handler = this.opts.onDataTrackMessage;
|
|
20161
|
+
} else {
|
|
20162
|
+
return false;
|
|
20163
|
+
}
|
|
20164
|
+
channel.onmessage = handler;
|
|
20165
|
+
return true;
|
|
20166
|
+
}
|
|
20167
|
+
/**
|
|
20168
|
+
* Tears down all channels for a peer-connection cleanup: rejects parked waiters (detach — the
|
|
20169
|
+
* spec allows `pc.close()` to transition channels to 'closed' without firing events, so waiting
|
|
20170
|
+
* for browser close events is not an option), strips handlers, closes the handles, and resets
|
|
20171
|
+
* the reliable session state.
|
|
20172
|
+
*/
|
|
20173
|
+
teardown() {
|
|
20174
|
+
const dcCleanup = dc => {
|
|
20175
|
+
if (!dc) {
|
|
20176
|
+
return;
|
|
20177
|
+
}
|
|
20178
|
+
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
20179
|
+
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
20180
|
+
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
20181
|
+
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
20182
|
+
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
20183
|
+
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
20184
|
+
dc.onbufferedamountlow = null;
|
|
20185
|
+
dc.onclose = null;
|
|
20186
|
+
dc.onclosing = null;
|
|
20187
|
+
dc.onerror = null;
|
|
20188
|
+
dc.onmessage = null;
|
|
20189
|
+
dc.onopen = null;
|
|
20190
|
+
dc.close();
|
|
20191
|
+
};
|
|
20192
|
+
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
20193
|
+
const dc = channel.channelHandle;
|
|
20194
|
+
channel.detach('peer connections cleaned up');
|
|
20195
|
+
dcCleanup(dc);
|
|
20196
|
+
}
|
|
20197
|
+
dcCleanup(this.lossySub);
|
|
20198
|
+
dcCleanup(this.reliableSub);
|
|
20199
|
+
dcCleanup(this.dataTrackSub);
|
|
20200
|
+
this.lossySub = undefined;
|
|
20201
|
+
this.reliableSub = undefined;
|
|
20202
|
+
this.dataTrackSub = undefined;
|
|
20203
|
+
// Full teardown starts the session over: drop replay state and restart sequencing.
|
|
20204
|
+
this.reliable.reset();
|
|
20205
|
+
}
|
|
19292
20206
|
}// Check if MediaRecorder is available
|
|
19293
20207
|
const isMediaRecorderAvailable = typeof MediaRecorder !== 'undefined';
|
|
19294
20208
|
// Fallback class for environments without MediaRecorder
|
|
@@ -21318,27 +22232,14 @@ function videoLayersFromEncodings(width, height, encodings, svc) {
|
|
|
21318
22232
|
return new VideoLayer({
|
|
21319
22233
|
quality,
|
|
21320
22234
|
width: Math.ceil(width / scale),
|
|
21321
|
-
height: Math.ceil(height / scale),
|
|
21322
|
-
bitrate: (_c = encoding.maxBitrate) !== null && _c !== void 0 ? _c : 0,
|
|
21323
|
-
ssrc: 0
|
|
21324
|
-
});
|
|
21325
|
-
});
|
|
21326
|
-
}const
|
|
21327
|
-
const
|
|
21328
|
-
const
|
|
21329
|
-
const minReconnectWait = 2 * 1000;
|
|
21330
|
-
const leaveReconnect = 'leave-reconnect';
|
|
21331
|
-
const reliabeReceiveStateTTL = 30000;
|
|
21332
|
-
// Two-watermark flow control for the reliable and data-track channels. Senders fill the buffer
|
|
21333
|
-
// freely up to the high-water mark; once it's exceeded they block until the browser's
|
|
21334
|
-
// `bufferedamountlow` event (which we arm at the low-water mark) signals the buffer has drained.
|
|
21335
|
-
// The gap between the marks keeps the SCTP send buffer saturated while we refill, so throughput
|
|
21336
|
-
// isn't starved, while the high-water mark bounds the buffer well below the level that would abort
|
|
21337
|
-
// the channel (see livekit/client-sdk-js#1995).
|
|
21338
|
-
const reliableDataChannelWaterMarkLow = 64 * 1024;
|
|
21339
|
-
const reliableDataChannelWaterMarkHigh = 1024 * 1024;
|
|
21340
|
-
const lossyDataChannelWaterMarkLow = 8 * 1024;
|
|
21341
|
-
const lossyDataChannelWaterMarkHigh = 256 * 1024;
|
|
22235
|
+
height: Math.ceil(height / scale),
|
|
22236
|
+
bitrate: (_c = encoding.maxBitrate) !== null && _c !== void 0 ? _c : 0,
|
|
22237
|
+
ssrc: 0
|
|
22238
|
+
});
|
|
22239
|
+
});
|
|
22240
|
+
}const minReconnectWait = 2 * 1000;
|
|
22241
|
+
const leaveReconnect = 'leave-reconnect';
|
|
22242
|
+
const reliabeReceiveStateTTL = 30000;
|
|
21342
22243
|
const initialMediaSectionsAudio = 3;
|
|
21343
22244
|
const initialMediaSectionsVideo = 3;
|
|
21344
22245
|
var PCState;
|
|
@@ -21349,12 +22250,6 @@ var PCState;
|
|
|
21349
22250
|
PCState[PCState["Reconnecting"] = 3] = "Reconnecting";
|
|
21350
22251
|
PCState[PCState["Closed"] = 4] = "Closed";
|
|
21351
22252
|
})(PCState || (PCState = {}));
|
|
21352
|
-
var DataChannelKind;
|
|
21353
|
-
(function (DataChannelKind) {
|
|
21354
|
-
DataChannelKind[DataChannelKind["RELIABLE"] = 0] = "RELIABLE";
|
|
21355
|
-
DataChannelKind[DataChannelKind["LOSSY"] = 1] = "LOSSY";
|
|
21356
|
-
DataChannelKind[DataChannelKind["DATA_TRACK_LOSSY"] = 2] = "DATA_TRACK_LOSSY";
|
|
21357
|
-
})(DataChannelKind || (DataChannelKind = {}));
|
|
21358
22253
|
// Default data-channel max message size (bytes), used when the remote SDP
|
|
21359
22254
|
// answer does not advertise an `a=max-message-size` attribute (RFC 8841).
|
|
21360
22255
|
// `0` means "no limit".
|
|
@@ -21370,6 +22265,15 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21370
22265
|
get pendingReconnect() {
|
|
21371
22266
|
return !!this.reconnectTimeout;
|
|
21372
22267
|
}
|
|
22268
|
+
get reliableChannel() {
|
|
22269
|
+
return this.dataChannels.reliable;
|
|
22270
|
+
}
|
|
22271
|
+
get lossyChannel() {
|
|
22272
|
+
return this.dataChannels.lossy;
|
|
22273
|
+
}
|
|
22274
|
+
get dataTrackChannel() {
|
|
22275
|
+
return this.dataChannels.dataTrack;
|
|
22276
|
+
}
|
|
21373
22277
|
constructor(options) {
|
|
21374
22278
|
var _a;
|
|
21375
22279
|
super();
|
|
@@ -21396,12 +22300,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21396
22300
|
this.shouldFailNext = false;
|
|
21397
22301
|
this.shouldFailOnV1Path = false;
|
|
21398
22302
|
this.log = livekitLogger;
|
|
21399
|
-
this.reliableDataSequence = 1;
|
|
21400
|
-
this.reliableMessageBuffer = new DataPacketBuffer();
|
|
21401
22303
|
this.reliableReceivedState = new TTLMap(reliabeReceiveStateTTL);
|
|
21402
|
-
this.lossyDataStatCurrentBytes = 0;
|
|
21403
|
-
this.lossyDataStatByterate = 0;
|
|
21404
|
-
this.lossyDataDropCount = 0;
|
|
21405
22304
|
this.midToTrackId = {};
|
|
21406
22305
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
21407
22306
|
this.isWaitingForNetworkReconnect = false;
|
|
@@ -21412,21 +22311,9 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21412
22311
|
if (!channel) {
|
|
21413
22312
|
return;
|
|
21414
22313
|
}
|
|
21415
|
-
|
|
21416
|
-
|
|
21417
|
-
_this.reliableDCSub = channel;
|
|
21418
|
-
handler = _this.handleDataMessage;
|
|
21419
|
-
} else if (channel.label === lossyDataChannel) {
|
|
21420
|
-
_this.lossyDCSub = channel;
|
|
21421
|
-
handler = _this.handleDataMessage;
|
|
21422
|
-
} else if (channel.label === dataTrackDataChannel) {
|
|
21423
|
-
_this.dataTrackDCSub = channel;
|
|
21424
|
-
handler = _this.handleDataTrackMessage;
|
|
21425
|
-
} else {
|
|
21426
|
-
return;
|
|
22314
|
+
if (_this.dataChannels.adoptSubscriberChannel(channel)) {
|
|
22315
|
+
_this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
|
|
21427
22316
|
}
|
|
21428
|
-
_this.log.debug("on data channel ".concat(channel.id, ", ").concat(channel.label));
|
|
21429
|
-
channel.onmessage = handler;
|
|
21430
22317
|
}();
|
|
21431
22318
|
});
|
|
21432
22319
|
this.handleDataMessage = message => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -21434,19 +22321,11 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21434
22321
|
// make sure to respect incoming data message order by processing message events one after the other
|
|
21435
22322
|
const unlock = yield this.dataProcessLock.lock();
|
|
21436
22323
|
try {
|
|
21437
|
-
|
|
21438
|
-
|
|
21439
|
-
if (message.data instanceof ArrayBuffer) {
|
|
21440
|
-
buffer = message.data;
|
|
21441
|
-
} else if (message.data instanceof Blob) {
|
|
21442
|
-
buffer = yield message.data.arrayBuffer();
|
|
21443
|
-
} else {
|
|
21444
|
-
this.log.error('unsupported data type', {
|
|
21445
|
-
data: message.data
|
|
21446
|
-
});
|
|
22324
|
+
const bytes = yield this.decodeDataMessage(message);
|
|
22325
|
+
if (!bytes) {
|
|
21447
22326
|
return;
|
|
21448
22327
|
}
|
|
21449
|
-
const dp = DataPacket.fromBinary(
|
|
22328
|
+
const dp = DataPacket.fromBinary(bytes);
|
|
21450
22329
|
if (dp.sequence > 0 && dp.participantSid !== '') {
|
|
21451
22330
|
const lastSeq = this.reliableReceivedState.get(dp.participantSid);
|
|
21452
22331
|
if (lastSeq && dp.sequence <= lastSeq) {
|
|
@@ -21487,19 +22366,11 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21487
22366
|
}
|
|
21488
22367
|
});
|
|
21489
22368
|
this.handleDataTrackMessage = message => __awaiter(this, void 0, void 0, function* () {
|
|
21490
|
-
|
|
21491
|
-
|
|
21492
|
-
if (message.data instanceof ArrayBuffer) {
|
|
21493
|
-
buffer = message.data;
|
|
21494
|
-
} else if (message.data instanceof Blob) {
|
|
21495
|
-
buffer = yield message.data.arrayBuffer();
|
|
21496
|
-
} else {
|
|
21497
|
-
this.log.error('unsupported data type', {
|
|
21498
|
-
data: message.data
|
|
21499
|
-
});
|
|
22369
|
+
const bytes = yield this.decodeDataMessage(message);
|
|
22370
|
+
if (!bytes) {
|
|
21500
22371
|
return;
|
|
21501
22372
|
}
|
|
21502
|
-
this.emit('dataTrackPacketReceived',
|
|
22373
|
+
this.emit('dataTrackPacketReceived', bytes);
|
|
21503
22374
|
});
|
|
21504
22375
|
this.handleDataError = event => {
|
|
21505
22376
|
// Errors fired while we're tearing the connection down (e.g. the SCTP transport aborting as
|
|
@@ -21533,9 +22404,6 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21533
22404
|
this.log.error("publisher data channel '".concat(DataChannelKind[kind], "' closed unexpectedly"), this.logContext);
|
|
21534
22405
|
}
|
|
21535
22406
|
};
|
|
21536
|
-
this.handleBufferedAmountLow = channelKind => {
|
|
21537
|
-
this.updateAndEmitDCBufferStatus(channelKind);
|
|
21538
|
-
};
|
|
21539
22407
|
// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
|
|
21540
22408
|
// continues to work, we can reconnect to websocket to continue the session
|
|
21541
22409
|
// after a number of retries, we'll close and give up permanently
|
|
@@ -21591,60 +22459,6 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21591
22459
|
this.once(EngineEvent.Disconnected, onDisconnected);
|
|
21592
22460
|
});
|
|
21593
22461
|
};
|
|
21594
|
-
this.updateAndEmitDCBufferStatus = kind => {
|
|
21595
|
-
if (kind === DataChannelKind.RELIABLE) {
|
|
21596
|
-
const dc = this.dataChannelForKind(kind);
|
|
21597
|
-
if (dc) {
|
|
21598
|
-
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
|
21599
|
-
}
|
|
21600
|
-
}
|
|
21601
|
-
try {
|
|
21602
|
-
const status = this.isBelowLowWaterMark(kind);
|
|
21603
|
-
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
|
21604
|
-
this.dcBufferStatus.set(kind, status);
|
|
21605
|
-
this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
|
|
21606
|
-
}
|
|
21607
|
-
} catch (e) {
|
|
21608
|
-
this.log.warn('could not update buffer status', {
|
|
21609
|
-
error: e
|
|
21610
|
-
});
|
|
21611
|
-
}
|
|
21612
|
-
};
|
|
21613
|
-
/**
|
|
21614
|
-
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
21615
|
-
* this is true and block once it goes false.
|
|
21616
|
-
*/
|
|
21617
|
-
this.isBelowHighWaterMark = kind => {
|
|
21618
|
-
const dc = this.dataChannelForKind(kind);
|
|
21619
|
-
if (!dc) {
|
|
21620
|
-
throw new TypeError("Could not get data channel for kind ".concat(kind));
|
|
21621
|
-
}
|
|
21622
|
-
// because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
|
|
21623
|
-
const highMark = kind === DataChannelKind.RELIABLE ? reliableDataChannelWaterMarkHigh : lossyDataChannelWaterMarkHigh;
|
|
21624
|
-
return dc.bufferedAmount <= highMark;
|
|
21625
|
-
};
|
|
21626
|
-
/**
|
|
21627
|
-
* Whether the send buffer has drained to its low-water mark. Drives the public
|
|
21628
|
-
* {@link EngineEvent.DCBufferStatusChanged} event.
|
|
21629
|
-
*/
|
|
21630
|
-
this.isBelowLowWaterMark = kind => {
|
|
21631
|
-
const dc = this.dataChannelForKind(kind);
|
|
21632
|
-
if (!dc) {
|
|
21633
|
-
throw new TypeError("Could not get data channel for kind ".concat(kind));
|
|
21634
|
-
}
|
|
21635
|
-
// because RTCDatachannel has the threshold built in we can read it dynamically to account for changing thresholds over time
|
|
21636
|
-
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
21637
|
-
};
|
|
21638
|
-
/** Per-kind lock serializing senders that have to wait for the buffer to drain. */
|
|
21639
|
-
this.waitForBufferHeadroomLocks = new Map();
|
|
21640
|
-
/**
|
|
21641
|
-
* Per-kind AbortController that cancels parked headroom waiters whenever a kind's channel object
|
|
21642
|
-
* stops being current (replaced by createDataChannels or torn down by cleanupPeerConnections). A
|
|
21643
|
-
* waiter is parked on the channel object it captured at wait entry; if that object is abandoned
|
|
21644
|
-
* its events may never fire again, so the abort is what rejects the waiter and releases the
|
|
21645
|
-
* headroom lock instead of stranding every future sender behind it.
|
|
21646
|
-
*/
|
|
21647
|
-
this.waiterAbortControllers = new Map();
|
|
21648
22462
|
this.onRtpMapAvailable = rtpTypes => {
|
|
21649
22463
|
const rtpMap = new Map();
|
|
21650
22464
|
rtpTypes.forEach(rtp => {
|
|
@@ -21704,7 +22518,15 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21704
22518
|
this.reconnectPolicy = this.options.reconnectPolicy;
|
|
21705
22519
|
this.closingLock = new _();
|
|
21706
22520
|
this.dataProcessLock = new _();
|
|
21707
|
-
this.
|
|
22521
|
+
this.dataChannels = new DataChannelManager({
|
|
22522
|
+
isEngineClosed: () => this.isClosed,
|
|
22523
|
+
isReconnecting: () => this.attemptingReconnect,
|
|
22524
|
+
onDataMessage: message => this.handleDataMessage(message),
|
|
22525
|
+
onDataTrackMessage: message => this.handleDataTrackMessage(message),
|
|
22526
|
+
onDataError: event => this.handleDataError(event),
|
|
22527
|
+
onChannelClose: kind => this.handleDataChannelClose(kind)(),
|
|
22528
|
+
onBufferStatusChanged: (kind, isLow) => this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind)
|
|
22529
|
+
});
|
|
21708
22530
|
this.client.onParticipantUpdate = updates => this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
21709
22531
|
this.client.onConnectionQuality = update => this.emit(EngineEvent.ConnectionQualityUpdate, update);
|
|
21710
22532
|
this.client.onRoomUpdate = update => this.emit(EngineEvent.RoomUpdate, update);
|
|
@@ -21860,55 +22682,14 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21860
22682
|
cleanupPeerConnections() {
|
|
21861
22683
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21862
22684
|
var _a;
|
|
21863
|
-
|
|
21864
|
-
// to transition data channels to 'closed' without firing events, which would otherwise strand
|
|
21865
|
-
// a waiter holding the headroom lock.
|
|
21866
|
-
this.invalidateDataChannelWaiters('peer connections cleaned up');
|
|
21867
|
-
const dcCleanup = dc => {
|
|
21868
|
-
if (!dc) {
|
|
21869
|
-
return;
|
|
21870
|
-
}
|
|
21871
|
-
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
21872
|
-
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
21873
|
-
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
21874
|
-
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
21875
|
-
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
21876
|
-
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
21877
|
-
dc.onbufferedamountlow = null;
|
|
21878
|
-
dc.onclose = null;
|
|
21879
|
-
dc.onclosing = null;
|
|
21880
|
-
dc.onerror = null;
|
|
21881
|
-
dc.onmessage = null;
|
|
21882
|
-
dc.onopen = null;
|
|
21883
|
-
dc.close();
|
|
21884
|
-
};
|
|
21885
|
-
dcCleanup(this.lossyDC);
|
|
21886
|
-
dcCleanup(this.lossyDCSub);
|
|
21887
|
-
dcCleanup(this.reliableDC);
|
|
21888
|
-
dcCleanup(this.reliableDCSub);
|
|
21889
|
-
dcCleanup(this.dataTrackDC);
|
|
21890
|
-
dcCleanup(this.dataTrackDCSub);
|
|
22685
|
+
this.dataChannels.teardown();
|
|
21891
22686
|
yield (_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.close();
|
|
21892
22687
|
this.pcManager = undefined;
|
|
21893
|
-
this.lossyDC = undefined;
|
|
21894
|
-
this.lossyDCSub = undefined;
|
|
21895
|
-
this.reliableDC = undefined;
|
|
21896
|
-
this.reliableDCSub = undefined;
|
|
21897
|
-
this.dataTrackDC = undefined;
|
|
21898
|
-
this.dataTrackDCSub = undefined;
|
|
21899
|
-
this.reliableMessageBuffer = new DataPacketBuffer();
|
|
21900
|
-
this.reliableDataSequence = 1;
|
|
21901
22688
|
this.reliableReceivedState.clear();
|
|
21902
22689
|
});
|
|
21903
22690
|
}
|
|
21904
22691
|
cleanupLossyDataStats() {
|
|
21905
|
-
this.
|
|
21906
|
-
this.lossyDataStatCurrentBytes = 0;
|
|
21907
|
-
if (this.lossyDataStatInterval) {
|
|
21908
|
-
clearInterval(this.lossyDataStatInterval);
|
|
21909
|
-
this.lossyDataStatInterval = undefined;
|
|
21910
|
-
}
|
|
21911
|
-
this.lossyDataDropCount = 0;
|
|
22692
|
+
this.lossyChannel.stopThresholdTuning();
|
|
21912
22693
|
}
|
|
21913
22694
|
cleanupClient() {
|
|
21914
22695
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -21975,7 +22756,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
21975
22756
|
}
|
|
21976
22757
|
get dataSubscriberReadyState() {
|
|
21977
22758
|
var _a;
|
|
21978
|
-
return (_a = this.
|
|
22759
|
+
return (_a = this.dataChannelForKind(DataChannelKind.RELIABLE, true)) === null || _a === void 0 ? void 0 : _a.readyState;
|
|
21979
22760
|
}
|
|
21980
22761
|
getConnectedServerAddress() {
|
|
21981
22762
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -22235,69 +23016,22 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22235
23016
|
if (!this.pcManager) {
|
|
22236
23017
|
return;
|
|
22237
23018
|
}
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
this.lossyDC.onerror = null;
|
|
22246
|
-
this.lossyDC.onclose = null;
|
|
22247
|
-
}
|
|
22248
|
-
if (this.reliableDC) {
|
|
22249
|
-
this.reliableDC.onmessage = null;
|
|
22250
|
-
this.reliableDC.onerror = null;
|
|
22251
|
-
this.reliableDC.onclose = null;
|
|
22252
|
-
}
|
|
22253
|
-
if (this.dataTrackDC) {
|
|
22254
|
-
this.dataTrackDC.onmessage = null;
|
|
22255
|
-
this.dataTrackDC.onerror = null;
|
|
22256
|
-
this.dataTrackDC.onclose = null;
|
|
22257
|
-
}
|
|
22258
|
-
// create data channels
|
|
22259
|
-
this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
|
|
22260
|
-
ordered: false,
|
|
22261
|
-
maxRetransmits: 0
|
|
22262
|
-
});
|
|
22263
|
-
this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
|
|
22264
|
-
ordered: true
|
|
22265
|
-
});
|
|
22266
|
-
this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
|
|
22267
|
-
ordered: false,
|
|
22268
|
-
maxRetransmits: 0
|
|
22269
|
-
});
|
|
22270
|
-
// also handle messages over the pub channel, for backwards compatibility
|
|
22271
|
-
this.lossyDC.onmessage = this.handleDataMessage;
|
|
22272
|
-
this.reliableDC.onmessage = this.handleDataMessage;
|
|
22273
|
-
this.dataTrackDC.onmessage = this.handleDataTrackMessage;
|
|
22274
|
-
// handle datachannel errors
|
|
22275
|
-
this.lossyDC.onerror = this.handleDataError;
|
|
22276
|
-
this.reliableDC.onerror = this.handleDataError;
|
|
22277
|
-
this.dataTrackDC.onerror = this.handleDataError;
|
|
22278
|
-
// detect unexpected publisher data channel closes
|
|
22279
|
-
this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
|
|
22280
|
-
this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
|
|
22281
|
-
this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
|
|
22282
|
-
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
22283
|
-
this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
22284
|
-
this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
|
|
22285
|
-
this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
22286
|
-
// handle buffer amount low events
|
|
22287
|
-
this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
|
|
22288
|
-
this.reliableDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
|
|
22289
|
-
this.dataTrackDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
|
|
22290
|
-
this.cleanupLossyDataStats();
|
|
22291
|
-
this.lossyDataStatInterval = setInterval(() => {
|
|
22292
|
-
this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
|
|
22293
|
-
this.lossyDataStatCurrentBytes = 0;
|
|
22294
|
-
const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
|
|
22295
|
-
if (dc) {
|
|
22296
|
-
// control buffered latency to ~100ms
|
|
22297
|
-
const threshold = this.lossyDataStatByterate / 10;
|
|
22298
|
-
dc.bufferedAmountLowThreshold = Math.min(Math.max(threshold, lossyDataChannelWaterMarkLow), lossyDataChannelWaterMarkHigh);
|
|
23019
|
+
this.dataChannels.createPublisherChannels(this.pcManager);
|
|
23020
|
+
}
|
|
23021
|
+
/** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
|
|
23022
|
+
decodeDataMessage(message) {
|
|
23023
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23024
|
+
if (message.data instanceof ArrayBuffer) {
|
|
23025
|
+
return new Uint8Array(message.data);
|
|
22299
23026
|
}
|
|
22300
|
-
|
|
23027
|
+
if (message.data instanceof Blob) {
|
|
23028
|
+
return new Uint8Array(yield message.data.arrayBuffer());
|
|
23029
|
+
}
|
|
23030
|
+
this.log.error('unsupported data type', {
|
|
23031
|
+
data: message.data
|
|
23032
|
+
});
|
|
23033
|
+
return undefined;
|
|
23034
|
+
});
|
|
22301
23035
|
}
|
|
22302
23036
|
createSender(track, opts, encodings) {
|
|
22303
23037
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -22549,7 +23283,6 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22549
23283
|
}
|
|
22550
23284
|
resumeConnection(reason) {
|
|
22551
23285
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22552
|
-
var _a;
|
|
22553
23286
|
if (!this.url || !this.token) {
|
|
22554
23287
|
// permanent failure, don't attempt reconnection
|
|
22555
23288
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
|
@@ -22603,7 +23336,8 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22603
23336
|
this.client.setReconnected();
|
|
22604
23337
|
// recreate publish datachannel if it's id is null
|
|
22605
23338
|
// (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
|
|
22606
|
-
|
|
23339
|
+
const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
|
|
23340
|
+
if ((reliableDC === null || reliableDC === void 0 ? void 0 : reliableDC.readyState) === 'open' && reliableDC.id === null) {
|
|
22607
23341
|
this.createDataChannels();
|
|
22608
23342
|
}
|
|
22609
23343
|
if (res === null || res === void 0 ? void 0 : res.lastMessageSeq) {
|
|
@@ -22660,7 +23394,8 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22660
23394
|
});
|
|
22661
23395
|
}
|
|
22662
23396
|
/* @internal */
|
|
22663
|
-
sendDataPacket(packet,
|
|
23397
|
+
sendDataPacket(packet, /** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
|
|
23398
|
+
kind) {
|
|
22664
23399
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22665
23400
|
var _a, _b;
|
|
22666
23401
|
// make sure we do have a data connection
|
|
@@ -22680,8 +23415,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22680
23415
|
}
|
|
22681
23416
|
}
|
|
22682
23417
|
if (kind === DataChannelKind.RELIABLE) {
|
|
22683
|
-
packet.sequence = this.
|
|
22684
|
-
this.reliableDataSequence += 1;
|
|
23418
|
+
packet.sequence = this.reliableChannel.nextSequence();
|
|
22685
23419
|
}
|
|
22686
23420
|
const msg = packet.toBinary();
|
|
22687
23421
|
// Clamp to the SDK default - libwebrtc advertises larger (~256 KiB)
|
|
@@ -22691,249 +23425,48 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22691
23425
|
if (typeof maxPublisherMessageSizeBytes !== 'undefined' && maxPublisherMessageSizeBytes !== 0 /* 0 means "no limit" */ && msg.byteLength > maxPublisherMessageSizeBytes) {
|
|
22692
23426
|
throw new PublishDataError("cannot publish data packet larger than ".concat(maxPublisherMessageSizeBytes, " bytes (got ").concat(msg.byteLength, ")"));
|
|
22693
23427
|
}
|
|
22694
|
-
|
|
22695
|
-
|
|
22696
|
-
|
|
22697
|
-
|
|
22698
|
-
|
|
22699
|
-
|
|
22700
|
-
if (this.attemptingReconnect) {
|
|
22701
|
-
// A reconnect is already underway — queue for the resume replay instead of parking on a
|
|
22702
|
-
// channel that is being torn down. The send resolves; delivery is deferred to the replay.
|
|
22703
|
-
this.reliableMessageBuffer.push({
|
|
22704
|
-
data: msg,
|
|
22705
|
-
sequence: packet.sequence,
|
|
22706
|
-
sent: false
|
|
22707
|
-
});
|
|
22708
|
-
return;
|
|
22709
|
-
}
|
|
22710
|
-
const dc = this.dataChannelForKind(kind);
|
|
22711
|
-
if (dc) {
|
|
22712
|
-
try {
|
|
22713
|
-
yield this.waitForBufferHeadroomWithLock(kind);
|
|
22714
|
-
} catch (error) {
|
|
22715
|
-
if (this.isClosed) {
|
|
22716
|
-
// No replay is coming after an engine close — surface the failure.
|
|
22717
|
-
throw error;
|
|
22718
|
-
}
|
|
22719
|
-
// Transient teardown (the channel closed or was replaced while we waited): the
|
|
22720
|
-
// reliable channel promises delivery across resume, so queue the packet for the
|
|
22721
|
-
// replay instead of rejecting a send the app can't meaningfully retry.
|
|
22722
|
-
this.reliableMessageBuffer.push({
|
|
22723
|
-
data: msg,
|
|
22724
|
-
sequence: packet.sequence,
|
|
22725
|
-
sent: false
|
|
22726
|
-
});
|
|
22727
|
-
return;
|
|
22728
|
-
}
|
|
22729
|
-
if (this.attemptingReconnect) {
|
|
22730
|
-
// A reconnect began while we waited for headroom — same deal as above.
|
|
22731
|
-
this.reliableMessageBuffer.push({
|
|
22732
|
-
data: msg,
|
|
22733
|
-
sequence: packet.sequence,
|
|
22734
|
-
sent: false
|
|
22735
|
-
});
|
|
22736
|
-
return;
|
|
22737
|
-
}
|
|
22738
|
-
this.reliableMessageBuffer.push({
|
|
22739
|
-
data: msg,
|
|
22740
|
-
sequence: packet.sequence,
|
|
22741
|
-
sent: true
|
|
22742
|
-
});
|
|
22743
|
-
dc.send(msg);
|
|
22744
|
-
}
|
|
22745
|
-
this.updateAndEmitDCBufferStatus(kind);
|
|
22746
|
-
break;
|
|
22747
|
-
}
|
|
23428
|
+
// The full-buffer policy (drop for lossy, wait/replay for reliable) is the channel's own, as
|
|
23429
|
+
// is emitting the buffer-status change once the send settles.
|
|
23430
|
+
if (kind === DataChannelKind.RELIABLE) {
|
|
23431
|
+
yield this.reliableChannel.send(msg, packet.sequence);
|
|
23432
|
+
} else {
|
|
23433
|
+
yield this.lossyChannel.send(msg);
|
|
22748
23434
|
}
|
|
22749
23435
|
});
|
|
22750
23436
|
}
|
|
22751
|
-
|
|
22752
|
-
|
|
22753
|
-
|
|
22754
|
-
|
|
22755
|
-
|
|
22756
|
-
|
|
22757
|
-
|
|
22758
|
-
|
|
22759
|
-
|
|
22760
|
-
|
|
22761
|
-
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
|
|
22765
|
-
if (dc) {
|
|
22766
|
-
// Depending on the exact circumstance that data is being sent, either drop or wait for the
|
|
22767
|
-
// buffer to drain below the high-water mark before continuing.
|
|
22768
|
-
switch (bufferStatusFullBehavior) {
|
|
22769
|
-
case 'wait':
|
|
22770
|
-
if (!_this3.isBelowHighWaterMark(kind)) {
|
|
22771
|
-
yield _this3.waitForBufferHeadroomWithLock(kind);
|
|
22772
|
-
}
|
|
22773
|
-
break;
|
|
22774
|
-
case 'drop':
|
|
22775
|
-
// we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
|
|
22776
|
-
if (!_this3.isBelowLowWaterMark(kind)) {
|
|
22777
|
-
// Drop messages to reduce latency
|
|
22778
|
-
_this3.lossyDataDropCount += 1;
|
|
22779
|
-
if (_this3.lossyDataDropCount % 100 === 0) {
|
|
22780
|
-
_this3.log.warn("dropping lossy data channel messages, total dropped: ".concat(_this3.lossyDataDropCount));
|
|
22781
|
-
}
|
|
22782
|
-
return;
|
|
22783
|
-
}
|
|
22784
|
-
}
|
|
22785
|
-
if (kind === DataChannelKind.LOSSY) {
|
|
22786
|
-
// The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
|
|
22787
|
-
// bytes here would inflate it and let the lossy channel buffer far more latency than the
|
|
22788
|
-
// ~100ms the tuning targets.
|
|
22789
|
-
_this3.lossyDataStatCurrentBytes += bytes.byteLength;
|
|
22790
|
-
}
|
|
22791
|
-
if (_this3.attemptingReconnect) {
|
|
22792
|
-
return;
|
|
22793
|
-
}
|
|
22794
|
-
dc.send(bytes);
|
|
22795
|
-
}
|
|
22796
|
-
_this3.updateAndEmitDCBufferStatus(kind);
|
|
22797
|
-
} catch (error) {
|
|
22798
|
-
// ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
|
|
22799
|
-
if (error instanceof TypeError) {
|
|
22800
|
-
_this3.log.error(error);
|
|
22801
|
-
} else {
|
|
22802
|
-
throw error;
|
|
22803
|
-
}
|
|
22804
|
-
}
|
|
22805
|
-
}();
|
|
23437
|
+
/**
|
|
23438
|
+
* Sends pre-serialized bytes on the data-track channel. This is the one send path that doesn't
|
|
23439
|
+
* go through {@link sendDataPacket} — Room's `packetAvailable` handler calls it directly with
|
|
23440
|
+
* bytes the data-track pipeline already serialized.
|
|
23441
|
+
*
|
|
23442
|
+
* @internal
|
|
23443
|
+
*/
|
|
23444
|
+
sendDataTrackFrame(bytes) {
|
|
23445
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23446
|
+
// Make sure we do have a data connection: on lazily negotiated publisher connections this is
|
|
23447
|
+
// what kicks negotiation off, and it waits for the channel to open. Memoized, so the
|
|
23448
|
+
// steady-state cost is one await on an already-resolved promise.
|
|
23449
|
+
yield this.ensurePublisherConnected(DataChannelKind.DATA_TRACK_LOSSY);
|
|
23450
|
+
yield this.dataTrackChannel.send(bytes);
|
|
22806
23451
|
});
|
|
22807
23452
|
}
|
|
22808
23453
|
resendReliableMessagesForResume(lastMessageSeq) {
|
|
22809
23454
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22810
23455
|
yield this.ensurePublisherConnected(DataChannelKind.RELIABLE);
|
|
22811
|
-
|
|
22812
|
-
if (dc) {
|
|
22813
|
-
this.reliableMessageBuffer.popToSequence(lastMessageSeq);
|
|
22814
|
-
// Hold the headroom lock across the whole replay. Releasing it between messages would let a
|
|
22815
|
-
// concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
|
|
22816
|
-
// queued on the lock — hit the wire mid-replay, and receivers would then discard the
|
|
22817
|
-
// remaining lower-sequence resent messages as duplicates.
|
|
22818
|
-
const unlock = yield this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
|
|
22819
|
-
try {
|
|
22820
|
-
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
22821
|
-
this.reliableMessageBuffer.markAllUnsent();
|
|
22822
|
-
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
22823
|
-
// sent:false) during our own awaits appends after this pass started, so we pick it up on
|
|
22824
|
-
// the next one. We mark each packet only once we've actually handed it to the channel — a
|
|
22825
|
-
// blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
|
|
22826
|
-
// and a later alignBufferedAmount would then drop it for good. If the loop throws
|
|
22827
|
-
// mid-drain, unsent entries keep their flag and the next replay picks them up.
|
|
22828
|
-
for (let batch = this.reliableMessageBuffer.getUnsent(); batch.length > 0; batch = this.reliableMessageBuffer.getUnsent()) {
|
|
22829
|
-
for (const item of batch) {
|
|
22830
|
-
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
22831
|
-
// We already hold the lock across the whole replay, so use the lock-free wait.
|
|
22832
|
-
yield this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
|
|
22833
|
-
dc.send(item.data);
|
|
22834
|
-
this.reliableMessageBuffer.markSent(item);
|
|
22835
|
-
}
|
|
22836
|
-
}
|
|
22837
|
-
} finally {
|
|
22838
|
-
unlock();
|
|
22839
|
-
}
|
|
22840
|
-
}
|
|
22841
|
-
this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
|
|
23456
|
+
yield this.reliableChannel.replay(lastMessageSeq);
|
|
22842
23457
|
});
|
|
22843
23458
|
}
|
|
22844
|
-
|
|
22845
|
-
|
|
22846
|
-
|
|
22847
|
-
controller = new AbortController();
|
|
22848
|
-
this.waiterAbortControllers.set(kind, controller);
|
|
22849
|
-
}
|
|
22850
|
-
return controller.signal;
|
|
22851
|
-
}
|
|
22852
|
-
/** Rejects all parked headroom waiters (all kinds); the next waiter gets a fresh controller. */
|
|
22853
|
-
invalidateDataChannelWaiters(reason) {
|
|
22854
|
-
for (const controller of this.waiterAbortControllers.values()) {
|
|
22855
|
-
controller.abort(reason);
|
|
22856
|
-
}
|
|
22857
|
-
this.waiterAbortControllers.clear();
|
|
22858
|
-
}
|
|
22859
|
-
/** Acquires the per-kind headroom lock, resolving with the unlock function. */
|
|
22860
|
-
getBufferHeadroomLock(kind) {
|
|
22861
|
-
let lock = this.waitForBufferHeadroomLocks.get(kind);
|
|
22862
|
-
if (!lock) {
|
|
22863
|
-
lock = new _();
|
|
22864
|
-
this.waitForBufferHeadroomLocks.set(kind, lock);
|
|
22865
|
-
}
|
|
22866
|
-
return lock;
|
|
22867
|
-
}
|
|
22868
|
-
/**
|
|
22869
|
-
* Acquires the `kind` headroom lock, waits until the send buffer has room, then releases. The
|
|
22870
|
-
* common single-send entry point. Callers that need to hold the lock across several sends (the
|
|
22871
|
-
* resume replay) acquire it via {@link getBufferHeadroomLock} and call
|
|
22872
|
-
* {@link waitForBufferHeadroomWithoutLock} per message instead.
|
|
22873
|
-
*/
|
|
22874
|
-
waitForBufferHeadroomWithLock(kind) {
|
|
22875
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
22876
|
-
const unlock = yield this.getBufferHeadroomLock(kind).lock();
|
|
22877
|
-
try {
|
|
22878
|
-
yield this.waitForBufferHeadroomWithoutLock(kind);
|
|
22879
|
-
} finally {
|
|
22880
|
-
unlock();
|
|
22881
|
-
}
|
|
22882
|
-
});
|
|
23459
|
+
/** The flow-control gate for `kind` — see {@link FlowControlledDataChannel}. */
|
|
23460
|
+
flowControlFor(kind) {
|
|
23461
|
+
return this.dataChannels.channelFor(kind);
|
|
22883
23462
|
}
|
|
22884
23463
|
/**
|
|
22885
|
-
*
|
|
22886
|
-
*
|
|
22887
|
-
* already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
|
|
22888
|
-
* holds the lock across its whole batch and calls this per message so no other sender can
|
|
22889
|
-
* interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
|
|
22890
|
-
*
|
|
22891
|
-
* Serializing through the per-kind lock means that, once the buffer drains, queued callers
|
|
22892
|
-
* refill it one at a time (up to the high-water mark) rather than all at once and overflowing
|
|
22893
|
-
* the SCTP send buffer (see livekit/client-sdk-js#1995).
|
|
23464
|
+
* Resolves once the caller may send on the `kind` channel — see
|
|
23465
|
+
* {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
|
|
22894
23466
|
*/
|
|
22895
|
-
|
|
23467
|
+
waitForBufferHeadroom(kind) {
|
|
22896
23468
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22897
|
-
|
|
22898
|
-
throw new UnexpectedConnectionState('engine closed');
|
|
22899
|
-
}
|
|
22900
|
-
if (this.isBelowHighWaterMark(kind)) {
|
|
22901
|
-
return;
|
|
22902
|
-
}
|
|
22903
|
-
const dc = this.dataChannelForKind(kind);
|
|
22904
|
-
if (!dc) {
|
|
22905
|
-
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
|
|
22906
|
-
}
|
|
22907
|
-
const abortSignal = this.waiterAbortSignal(kind);
|
|
22908
|
-
yield new TypedPromise((resolve, reject) => {
|
|
22909
|
-
const onBufferedAmountLow = () => {
|
|
22910
|
-
cleanup();
|
|
22911
|
-
resolve();
|
|
22912
|
-
};
|
|
22913
|
-
const onDCClose = () => {
|
|
22914
|
-
cleanup();
|
|
22915
|
-
reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " closed while draining the buffer")));
|
|
22916
|
-
};
|
|
22917
|
-
const onAbort = () => {
|
|
22918
|
-
cleanup();
|
|
22919
|
-
reject(new UnexpectedConnectionState("DataChannel ".concat(kind, " was replaced or torn down while waiting for headroom")));
|
|
22920
|
-
};
|
|
22921
|
-
const cleanup = () => {
|
|
22922
|
-
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
22923
|
-
dc.removeEventListener('close', onDCClose);
|
|
22924
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
22925
|
-
};
|
|
22926
|
-
if (abortSignal.aborted) {
|
|
22927
|
-
onAbort();
|
|
22928
|
-
return;
|
|
22929
|
-
}
|
|
22930
|
-
dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
22931
|
-
// Proxy along any error caused by the data channel closing while we wait.
|
|
22932
|
-
dc.addEventListener('close', onDCClose);
|
|
22933
|
-
// The channel object we're parked on can be abandoned without ever firing another event
|
|
22934
|
-
// (e.g. createDataChannels replacing it); the abort is our way out.
|
|
22935
|
-
abortSignal.addEventListener('abort', onAbort);
|
|
22936
|
-
});
|
|
23469
|
+
return this.flowControlFor(kind).waitForHeadroomWithLock();
|
|
22937
23470
|
});
|
|
22938
23471
|
}
|
|
22939
23472
|
/**
|
|
@@ -22941,40 +23474,40 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
22941
23474
|
*/
|
|
22942
23475
|
ensureDataTransportConnected(kind_1) {
|
|
22943
23476
|
return __awaiter(this, arguments, void 0, function (kind) {
|
|
22944
|
-
var
|
|
23477
|
+
var _this3 = this;
|
|
22945
23478
|
let subscriber = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.subscriberPrimary;
|
|
22946
23479
|
return function* () {
|
|
22947
23480
|
var _a;
|
|
22948
|
-
if (!
|
|
23481
|
+
if (!_this3.pcManager) {
|
|
22949
23482
|
throw new UnexpectedConnectionState('PC manager is closed');
|
|
22950
23483
|
}
|
|
22951
|
-
const transport = subscriber ?
|
|
23484
|
+
const transport = subscriber ? _this3.pcManager.subscriber : _this3.pcManager.publisher;
|
|
22952
23485
|
const transportName = subscriber ? 'Subscriber' : 'Publisher';
|
|
22953
23486
|
if (!transport) {
|
|
22954
23487
|
throw ConnectionError.internal("".concat(transportName, " connection not set"));
|
|
22955
23488
|
}
|
|
22956
23489
|
let needNegotiation = false;
|
|
22957
|
-
if (!subscriber && !
|
|
22958
|
-
|
|
23490
|
+
if (!subscriber && !_this3.dataChannelForKind(kind, subscriber)) {
|
|
23491
|
+
_this3.createDataChannels();
|
|
22959
23492
|
needNegotiation = true;
|
|
22960
23493
|
}
|
|
22961
|
-
if (!needNegotiation && !subscriber && !
|
|
23494
|
+
if (!needNegotiation && !subscriber && !_this3.pcManager.publisher.isICEConnected && _this3.pcManager.publisher.getICEConnectionState() !== 'checking') {
|
|
22962
23495
|
needNegotiation = true;
|
|
22963
23496
|
}
|
|
22964
23497
|
if (needNegotiation) {
|
|
22965
23498
|
// start negotiation
|
|
22966
|
-
|
|
22967
|
-
|
|
23499
|
+
_this3.negotiate().catch(err => {
|
|
23500
|
+
_this3.log.error(err);
|
|
22968
23501
|
});
|
|
22969
23502
|
}
|
|
22970
|
-
const targetChannel =
|
|
23503
|
+
const targetChannel = _this3.dataChannelForKind(kind, subscriber);
|
|
22971
23504
|
if ((targetChannel === null || targetChannel === void 0 ? void 0 : targetChannel.readyState) === 'open') {
|
|
22972
23505
|
return;
|
|
22973
23506
|
}
|
|
22974
23507
|
// wait until ICE connected
|
|
22975
|
-
const endTime = new Date().getTime() +
|
|
23508
|
+
const endTime = new Date().getTime() + _this3.peerConnectionTimeout;
|
|
22976
23509
|
while (new Date().getTime() < endTime) {
|
|
22977
|
-
if (transport.isICEConnected && ((_a =
|
|
23510
|
+
if (transport.isICEConnected && ((_a = _this3.dataChannelForKind(kind, subscriber)) === null || _a === void 0 ? void 0 : _a.readyState) === 'open') {
|
|
22978
23511
|
return;
|
|
22979
23512
|
}
|
|
22980
23513
|
yield sleep(50);
|
|
@@ -23017,7 +23550,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23017
23550
|
}
|
|
23018
23551
|
this.pcManager.requirePublisher();
|
|
23019
23552
|
// don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
|
|
23020
|
-
if (this.pcManager.publisher.getTransceivers().length == 0 && !this.
|
|
23553
|
+
if (this.pcManager.publisher.getTransceivers().length == 0 && !this.dataChannels.hasPublisherChannels) {
|
|
23021
23554
|
this.createDataChannels();
|
|
23022
23555
|
}
|
|
23023
23556
|
const abortController = new AbortController();
|
|
@@ -23061,26 +23594,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
23061
23594
|
});
|
|
23062
23595
|
}
|
|
23063
23596
|
dataChannelForKind(kind, sub) {
|
|
23064
|
-
|
|
23065
|
-
case DataChannelKind.RELIABLE:
|
|
23066
|
-
if (!sub) {
|
|
23067
|
-
return this.reliableDC;
|
|
23068
|
-
} else {
|
|
23069
|
-
return this.reliableDCSub;
|
|
23070
|
-
}
|
|
23071
|
-
case DataChannelKind.LOSSY:
|
|
23072
|
-
if (!sub) {
|
|
23073
|
-
return this.lossyDC;
|
|
23074
|
-
} else {
|
|
23075
|
-
return this.lossyDCSub;
|
|
23076
|
-
}
|
|
23077
|
-
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
23078
|
-
if (!sub) {
|
|
23079
|
-
return this.dataTrackDC;
|
|
23080
|
-
} else {
|
|
23081
|
-
return this.dataTrackDCSub;
|
|
23082
|
-
}
|
|
23083
|
-
}
|
|
23597
|
+
return this.dataChannels.getHandle(kind, sub);
|
|
23084
23598
|
}
|
|
23085
23599
|
/** @internal */
|
|
23086
23600
|
sendSyncState(remoteTracks, localTracks, localDataTrackInfos) {
|
|
@@ -23408,7 +23922,90 @@ RegionUrlProvider.connectionTrackers = new Map();
|
|
|
23408
23922
|
RegionUrlProvider.fetchLock = new _();
|
|
23409
23923
|
function getCloudConfigUrl(serverUrl) {
|
|
23410
23924
|
return "".concat(serverUrl.protocol.replace('ws', 'http'), "//").concat(serverUrl.host, "/settings");
|
|
23411
|
-
}
|
|
23925
|
+
}/**
|
|
23926
|
+
* A `deflate-raw` compression transform (inverse of {@link inflateRawTransform}): pipe a byte stream
|
|
23927
|
+
* through it to get the compressed bytes without buffering the whole payload. Used for the chunked
|
|
23928
|
+
* `sendText`/`sendBytes`/`sendFile` paths, where the full payload is known up front but is streamed
|
|
23929
|
+
* (e.g. from `file.stream()`) rather than held in memory.
|
|
23930
|
+
*/
|
|
23931
|
+
function deflateRawTransform() {
|
|
23932
|
+
return new CompressionStream('deflate-raw');
|
|
23933
|
+
}
|
|
23934
|
+
/**
|
|
23935
|
+
* A `deflate-raw` decompression transform (inverse of {@link deflateRawTransform}): pipe a
|
|
23936
|
+
* stream of compressed bytes through it to get the decompressed bytes. Inflate emits output greedily,
|
|
23937
|
+
* so as long as the sender flushed at write boundaries each write's content is produced as soon as
|
|
23938
|
+
* its compressed bytes arrive.
|
|
23939
|
+
*/
|
|
23940
|
+
function inflateRawTransform() {
|
|
23941
|
+
return new DecompressionStream('deflate-raw');
|
|
23942
|
+
}
|
|
23943
|
+
/**
|
|
23944
|
+
* Decompresses a raw-deflate byte array in full (inverse of {@link deflateRawCompress}).
|
|
23945
|
+
* `maxByteLength` bounds the decompressed output (decompression-bomb guard); exceeding it rejects
|
|
23946
|
+
* with a `PayloadTooLarge` error.
|
|
23947
|
+
*/
|
|
23948
|
+
function deflateRawDecompress(data, maxByteLength) {
|
|
23949
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23950
|
+
const ds = new DecompressionStream('deflate-raw');
|
|
23951
|
+
const writer = ds.writable.getWriter();
|
|
23952
|
+
// The writer promises are intentionally not awaited (output is consumed via `collect`), but
|
|
23953
|
+
// they reject when the byte cap cancels the readable mid-stream — swallow that so an enforced
|
|
23954
|
+
// cap doesn't surface as an unhandled rejection.
|
|
23955
|
+
writer.write(data).catch(() => {});
|
|
23956
|
+
writer.close().catch(() => {});
|
|
23957
|
+
return collect(ds.readable, maxByteLength);
|
|
23958
|
+
});
|
|
23959
|
+
}
|
|
23960
|
+
/**
|
|
23961
|
+
* Drains a byte stream, concatenating all of its chunks into a single array. When
|
|
23962
|
+
* `maxByteLength` is given, drops the stream and throws `PayloadTooLarge` as soon as the
|
|
23963
|
+
* accumulated output exceeds it.
|
|
23964
|
+
*/
|
|
23965
|
+
function collect(stream, maxByteLength) {
|
|
23966
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23967
|
+
const reader = stream.getReader();
|
|
23968
|
+
const chunks = [];
|
|
23969
|
+
let total = 0;
|
|
23970
|
+
while (true) {
|
|
23971
|
+
const _yield$reader$read = yield reader.read(),
|
|
23972
|
+
done = _yield$reader$read.done,
|
|
23973
|
+
value = _yield$reader$read.value;
|
|
23974
|
+
if (done) {
|
|
23975
|
+
break;
|
|
23976
|
+
}
|
|
23977
|
+
chunks.push(value);
|
|
23978
|
+
total += value.byteLength;
|
|
23979
|
+
if (typeof maxByteLength === 'number' && total > maxByteLength) {
|
|
23980
|
+
yield reader.cancel();
|
|
23981
|
+
throw new DataStreamError("Decompressed payload exceeds the maximum payload size of ".concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
|
|
23982
|
+
}
|
|
23983
|
+
}
|
|
23984
|
+
const result = new Uint8Array(total);
|
|
23985
|
+
let offset = 0;
|
|
23986
|
+
for (const chunk of chunks) {
|
|
23987
|
+
result.set(chunk, offset);
|
|
23988
|
+
offset += chunk.byteLength;
|
|
23989
|
+
}
|
|
23990
|
+
return result;
|
|
23991
|
+
});
|
|
23992
|
+
}/**
|
|
23993
|
+
* Maximum size of a single data-stream chunk in bytes, and the budget used to decide whether a
|
|
23994
|
+
* payload can be sent inline as a single header packet. Kept below the ~16k data-channel MTU to
|
|
23995
|
+
* leave headroom for protocol framing and E2EE overhead.
|
|
23996
|
+
*
|
|
23997
|
+
* @internal
|
|
23998
|
+
*/
|
|
23999
|
+
const STREAM_CHUNK_SIZE_BYTES = 15000;
|
|
24000
|
+
/**
|
|
24001
|
+
* Default cap on the number of decompressed bytes a single incoming compressed data stream may
|
|
24002
|
+
* produce (5 GB). A tiny compressed payload can inflate to an arbitrarily large output
|
|
24003
|
+
* (decompression bomb), so the decompressor's output is bounded rather than trusting the wire
|
|
24004
|
+
* size; streams exceeding the cap error with `PayloadTooLarge`.
|
|
24005
|
+
*
|
|
24006
|
+
* @internal
|
|
24007
|
+
*/
|
|
24008
|
+
const DEFAULT_MAX_PAYLOAD_BYTE_LENGTH = 5000000000;class BaseStreamReader {
|
|
23412
24009
|
get info() {
|
|
23413
24010
|
return this._info;
|
|
23414
24011
|
}
|
|
@@ -23450,6 +24047,7 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
23450
24047
|
};
|
|
23451
24048
|
return {
|
|
23452
24049
|
next: () => __awaiter(this, void 0, void 0, function* () {
|
|
24050
|
+
var _a;
|
|
23453
24051
|
try {
|
|
23454
24052
|
const signal = this.signal;
|
|
23455
24053
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
@@ -23470,6 +24068,9 @@ class ByteStreamReader extends BaseStreamReader {
|
|
|
23470
24068
|
});
|
|
23471
24069
|
if (result.done) {
|
|
23472
24070
|
this.validateBytesReceived(true);
|
|
24071
|
+
if (typeof this.totalByteSize === 'number') {
|
|
24072
|
+
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
|
|
24073
|
+
}
|
|
23473
24074
|
return {
|
|
23474
24075
|
done: true,
|
|
23475
24076
|
value: undefined
|
|
@@ -23587,6 +24188,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23587
24188
|
};
|
|
23588
24189
|
return {
|
|
23589
24190
|
next: () => __awaiter(this, void 0, void 0, function* () {
|
|
24191
|
+
var _a;
|
|
23590
24192
|
try {
|
|
23591
24193
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
23592
24194
|
throw signal.reason;
|
|
@@ -23606,6 +24208,9 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23606
24208
|
});
|
|
23607
24209
|
if (result.done) {
|
|
23608
24210
|
this.validateBytesReceived(true);
|
|
24211
|
+
if (typeof this.totalByteSize === 'number') {
|
|
24212
|
+
(_a = this.onProgress) === null || _a === void 0 ? void 0 : _a.call(this, 1);
|
|
24213
|
+
}
|
|
23609
24214
|
return {
|
|
23610
24215
|
done: true,
|
|
23611
24216
|
value: undefined
|
|
@@ -23684,6 +24289,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23684
24289
|
}
|
|
23685
24290
|
}class IncomingDataStreamManager {
|
|
23686
24291
|
constructor() {
|
|
24292
|
+
let maxPayloadByteLength = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_MAX_PAYLOAD_BYTE_LENGTH;
|
|
23687
24293
|
this.log = livekitLogger;
|
|
23688
24294
|
this.byteStreamControllers = new Map();
|
|
23689
24295
|
this.textStreamControllers = new Map();
|
|
@@ -23691,6 +24297,7 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23691
24297
|
this.textStreamHandlers = new Map();
|
|
23692
24298
|
this.isConnected = false;
|
|
23693
24299
|
this.bufferedPackets = [];
|
|
24300
|
+
this.maxPayloadByteLength = maxPayloadByteLength;
|
|
23694
24301
|
}
|
|
23695
24302
|
setConnected(connected) {
|
|
23696
24303
|
this.isConnected = connected;
|
|
@@ -23773,74 +24380,150 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23773
24380
|
}
|
|
23774
24381
|
handleStreamHeader(streamHeader, participantIdentity, encryptionType) {
|
|
23775
24382
|
var _a;
|
|
23776
|
-
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
|
|
23781
|
-
|
|
23782
|
-
|
|
23783
|
-
|
|
23784
|
-
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
23794
|
-
|
|
23795
|
-
|
|
23796
|
-
|
|
23797
|
-
|
|
24383
|
+
switch (streamHeader.contentHeader.case) {
|
|
24384
|
+
case 'byteHeader':
|
|
24385
|
+
{
|
|
24386
|
+
const streamHandlerCallback = this.byteStreamHandlers.get(streamHeader.topic);
|
|
24387
|
+
if (!streamHandlerCallback) {
|
|
24388
|
+
this.log.debug('ignoring incoming byte stream due to no handler for topic', streamHeader.topic);
|
|
24389
|
+
return;
|
|
24390
|
+
}
|
|
24391
|
+
let streamController;
|
|
24392
|
+
const info = {
|
|
24393
|
+
id: streamHeader.streamId,
|
|
24394
|
+
name: (_a = streamHeader.contentHeader.value.name) !== null && _a !== void 0 ? _a : 'unknown',
|
|
24395
|
+
mimeType: streamHeader.mimeType,
|
|
24396
|
+
size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
24397
|
+
topic: streamHeader.topic,
|
|
24398
|
+
timestamp: bigIntToNumber(streamHeader.timestamp),
|
|
24399
|
+
attributes: streamHeader.attributes,
|
|
24400
|
+
encryptionType
|
|
24401
|
+
};
|
|
24402
|
+
// Determine if the byte payload needs to be decompressed.
|
|
24403
|
+
let compressed;
|
|
24404
|
+
switch (streamHeader.compression) {
|
|
24405
|
+
case DataStream_CompressionType.DEFLATE_RAW:
|
|
24406
|
+
if (!isCompressionStreamSupported()) {
|
|
24407
|
+
// NOTE: this shouldn't really ever happen, if this warning is logged then the sender
|
|
24408
|
+
// isn't properly abiding by the data streams v2 protocol.
|
|
24409
|
+
livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
|
|
24410
|
+
return;
|
|
24411
|
+
}
|
|
24412
|
+
compressed = true;
|
|
24413
|
+
break;
|
|
24414
|
+
case DataStream_CompressionType.NONE:
|
|
24415
|
+
compressed = false;
|
|
24416
|
+
break;
|
|
24417
|
+
default:
|
|
24418
|
+
// NOTE: this shouldn't really ever happen, if this warning is logged then the sender
|
|
24419
|
+
// isn't properly abiding by the data streams v2 protocol.
|
|
24420
|
+
livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
|
|
24421
|
+
return;
|
|
24422
|
+
}
|
|
24423
|
+
// Single-packet stream: the entire payload was packaged into the header's `inlineContent`.
|
|
24424
|
+
// Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
|
|
24425
|
+
const inlineContent = streamHeader.inlineContent;
|
|
24426
|
+
if (typeof inlineContent !== 'undefined') {
|
|
24427
|
+
// Inline bytes are the raw payload, optionally deflate-raw compressed.
|
|
24428
|
+
streamHandlerCallback(new ByteStreamReader(info, createInlineStream(streamHeader.streamId, compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent), bigIntToNumber(streamHeader.totalLength)), {
|
|
24429
|
+
identity: participantIdentity
|
|
24430
|
+
});
|
|
24431
|
+
return;
|
|
23798
24432
|
}
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
|
|
23802
|
-
|
|
23803
|
-
|
|
24433
|
+
const stream = new ReadableStream({
|
|
24434
|
+
start: controller => {
|
|
24435
|
+
streamController = controller;
|
|
24436
|
+
if (this.byteStreamControllers.has(streamHeader.streamId)) {
|
|
24437
|
+
throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
|
|
24438
|
+
}
|
|
24439
|
+
this.byteStreamControllers.set(streamHeader.streamId, {
|
|
24440
|
+
info,
|
|
24441
|
+
controller: streamController,
|
|
24442
|
+
startTime: Date.now(),
|
|
24443
|
+
sendingParticipantIdentity: participantIdentity
|
|
24444
|
+
});
|
|
24445
|
+
}
|
|
24446
|
+
});
|
|
24447
|
+
streamHandlerCallback(new ByteStreamReader(info, compressed ? inflateRawByteChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
24448
|
+
// `totalLength` is the pre-compression size, and the reader counts decompressed bytes,
|
|
24449
|
+
// so it applies to both paths (mirrors text).
|
|
24450
|
+
bigIntToNumber(streamHeader.totalLength)), {
|
|
24451
|
+
identity: participantIdentity
|
|
23804
24452
|
});
|
|
24453
|
+
return;
|
|
23805
24454
|
}
|
|
23806
|
-
|
|
23807
|
-
|
|
23808
|
-
|
|
23809
|
-
|
|
23810
|
-
|
|
23811
|
-
|
|
23812
|
-
|
|
23813
|
-
|
|
23814
|
-
|
|
23815
|
-
|
|
23816
|
-
|
|
23817
|
-
|
|
23818
|
-
|
|
23819
|
-
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
24455
|
+
case 'textHeader':
|
|
24456
|
+
{
|
|
24457
|
+
const streamHandlerCallback = this.textStreamHandlers.get(streamHeader.topic);
|
|
24458
|
+
if (!streamHandlerCallback) {
|
|
24459
|
+
this.log.debug('ignoring incoming text stream due to no handler for topic', streamHeader.topic);
|
|
24460
|
+
return;
|
|
24461
|
+
}
|
|
24462
|
+
let streamController;
|
|
24463
|
+
const info = {
|
|
24464
|
+
id: streamHeader.streamId,
|
|
24465
|
+
mimeType: streamHeader.mimeType,
|
|
24466
|
+
size: streamHeader.totalLength ? Number(streamHeader.totalLength) : undefined,
|
|
24467
|
+
topic: streamHeader.topic,
|
|
24468
|
+
timestamp: Number(streamHeader.timestamp),
|
|
24469
|
+
attributes: streamHeader.attributes,
|
|
24470
|
+
encryptionType,
|
|
24471
|
+
attachedStreamIds: streamHeader.contentHeader.value.attachedStreamIds
|
|
24472
|
+
};
|
|
24473
|
+
// Determine if the byte payload needs to be decompressed.
|
|
24474
|
+
let compressed;
|
|
24475
|
+
switch (streamHeader.compression) {
|
|
24476
|
+
case DataStream_CompressionType.DEFLATE_RAW:
|
|
24477
|
+
if (!isCompressionStreamSupported()) {
|
|
24478
|
+
// NOTE: this shouldn't really ever happen, if this warning is logged then the sender
|
|
24479
|
+
// isn't properly abiding by the data streams v2 protocol.
|
|
24480
|
+
livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with deflate-raw compression, but this browser does not have support for DecompressionStream. Dropping..."));
|
|
24481
|
+
return;
|
|
24482
|
+
}
|
|
24483
|
+
compressed = true;
|
|
24484
|
+
break;
|
|
24485
|
+
case DataStream_CompressionType.NONE:
|
|
24486
|
+
compressed = false;
|
|
24487
|
+
break;
|
|
24488
|
+
default:
|
|
24489
|
+
// NOTE: this shouldn't really ever happen, if this warning is logged then the sender
|
|
24490
|
+
// isn't properly abiding by the data streams v2 protocol.
|
|
24491
|
+
livekitLogger.warn("Data stream ".concat(streamHeader.streamId, " received with unknown compression type ").concat(streamHeader.compression, ", dropping..."));
|
|
24492
|
+
return;
|
|
24493
|
+
}
|
|
24494
|
+
// Single-packet stream: the entire payload was smuggled into the header's `inlineContent`.
|
|
24495
|
+
// Synthesize an already-complete stream and skip waiting for chunk/trailer packets.
|
|
24496
|
+
const inlineContent = streamHeader.inlineContent;
|
|
24497
|
+
if (typeof inlineContent !== 'undefined') {
|
|
24498
|
+
// Inline text is the raw UTF-8 payload, optionally deflate-raw compressed.
|
|
24499
|
+
const content = compressed ? deflateRawDecompress(inlineContent, this.maxPayloadByteLength) : inlineContent;
|
|
24500
|
+
streamHandlerCallback(new TextStreamReader(info, createInlineStream(streamHeader.streamId, content), bigIntToNumber(streamHeader.totalLength)), {
|
|
24501
|
+
identity: participantIdentity
|
|
24502
|
+
});
|
|
24503
|
+
return;
|
|
23832
24504
|
}
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
24505
|
+
const stream = new ReadableStream({
|
|
24506
|
+
start: controller => {
|
|
24507
|
+
streamController = controller;
|
|
24508
|
+
if (this.textStreamControllers.has(streamHeader.streamId)) {
|
|
24509
|
+
throw new DataStreamError("A data stream read is already in progress for a stream with id ".concat(streamHeader.streamId, "."), DataStreamErrorReason.AlreadyOpened);
|
|
24510
|
+
}
|
|
24511
|
+
this.textStreamControllers.set(streamHeader.streamId, {
|
|
24512
|
+
info,
|
|
24513
|
+
controller: streamController,
|
|
24514
|
+
startTime: Date.now(),
|
|
24515
|
+
sendingParticipantIdentity: participantIdentity
|
|
24516
|
+
});
|
|
24517
|
+
}
|
|
24518
|
+
});
|
|
24519
|
+
streamHandlerCallback(new TextStreamReader(info, compressed ? inflateRawChunkStream(stream, streamHeader.streamId, this.maxPayloadByteLength) : stream,
|
|
24520
|
+
// `totalLength` is the pre-compression size, and the reader sees decompressed bytes, so
|
|
24521
|
+
// it applies to both paths.
|
|
24522
|
+
bigIntToNumber(streamHeader.totalLength)), {
|
|
24523
|
+
identity: participantIdentity
|
|
23838
24524
|
});
|
|
24525
|
+
return;
|
|
23839
24526
|
}
|
|
23840
|
-
});
|
|
23841
|
-
streamHandlerCallback(new TextStreamReader(info, stream, bigIntToNumber(streamHeader.totalLength)), {
|
|
23842
|
-
identity: participantIdentity
|
|
23843
|
-
});
|
|
23844
24527
|
}
|
|
23845
24528
|
}
|
|
23846
24529
|
handleStreamChunk(chunk, encryptionType) {
|
|
@@ -23870,9 +24553,15 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23870
24553
|
textBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(textBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
23871
24554
|
} else {
|
|
23872
24555
|
textBuffer.info.attributes = Object.assign(Object.assign({}, textBuffer.info.attributes), trailer.attributes);
|
|
23873
|
-
|
|
23874
|
-
|
|
24556
|
+
if (trailer.reason) {
|
|
24557
|
+
// A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
|
|
24558
|
+
// surface it as an error rather than pretending the stream completed.
|
|
24559
|
+
textBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
|
|
24560
|
+
} else {
|
|
24561
|
+
textBuffer.controller.close();
|
|
24562
|
+
}
|
|
23875
24563
|
}
|
|
24564
|
+
this.textStreamControllers.delete(trailer.streamId);
|
|
23876
24565
|
}
|
|
23877
24566
|
const fileBuffer = this.byteStreamControllers.get(trailer.streamId);
|
|
23878
24567
|
if (fileBuffer) {
|
|
@@ -23880,11 +24569,174 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23880
24569
|
fileBuffer.controller.error(new DataStreamError("Encryption type mismatch for stream ".concat(trailer.streamId, ". Expected ").concat(encryptionType, ", got ").concat(fileBuffer.info.encryptionType), DataStreamErrorReason.EncryptionTypeMismatch));
|
|
23881
24570
|
} else {
|
|
23882
24571
|
fileBuffer.info.attributes = Object.assign(Object.assign({}, fileBuffer.info.attributes), trailer.attributes);
|
|
23883
|
-
|
|
24572
|
+
if (trailer.reason) {
|
|
24573
|
+
// A non-empty reason marks an abnormal close by the sender (e.g. an aborted send);
|
|
24574
|
+
// surface it as an error rather than pretending the stream completed.
|
|
24575
|
+
fileBuffer.controller.error(new DataStreamError("Data stream ".concat(trailer.streamId, " closed abnormally: ").concat(trailer.reason), DataStreamErrorReason.AbnormalEnd));
|
|
24576
|
+
} else {
|
|
24577
|
+
fileBuffer.controller.close();
|
|
24578
|
+
}
|
|
23884
24579
|
}
|
|
23885
24580
|
this.byteStreamControllers.delete(trailer.streamId);
|
|
23886
24581
|
}
|
|
23887
24582
|
}
|
|
24583
|
+
}
|
|
24584
|
+
/**
|
|
24585
|
+
* Builds a `ReadableStream` that yields the given content as a single chunk and then immediately
|
|
24586
|
+
* closes - used to surface an inline (single-packet) data stream as a fully-formed stream. `content`
|
|
24587
|
+
* may be a promise (e.g. async gzip decompression); a rejection errors the stream.
|
|
24588
|
+
*/
|
|
24589
|
+
function createInlineStream(streamId, content) {
|
|
24590
|
+
return new ReadableStream({
|
|
24591
|
+
start: controller => __awaiter(this, void 0, void 0, function* () {
|
|
24592
|
+
try {
|
|
24593
|
+
const bytes = yield content;
|
|
24594
|
+
controller.enqueue(new DataStream_Chunk({
|
|
24595
|
+
streamId,
|
|
24596
|
+
chunkIndex: BigInt(0),
|
|
24597
|
+
content: bytes
|
|
24598
|
+
}));
|
|
24599
|
+
controller.close();
|
|
24600
|
+
} catch (err) {
|
|
24601
|
+
controller.error(err);
|
|
24602
|
+
}
|
|
24603
|
+
})
|
|
24604
|
+
});
|
|
24605
|
+
}
|
|
24606
|
+
/**
|
|
24607
|
+
* Validates that chunks are received in order, dropping duplicates and throwing if gaps are found.
|
|
24608
|
+
*
|
|
24609
|
+
* A stateful decompressor silently corrupts on duplicated or out-of-order input, so duplicates are
|
|
24610
|
+
* dropped (with a warning - in-order delivery is expected on the reliable channel, but reconnect
|
|
24611
|
+
* handling may replay) and a gap is a hard error. Shared by the text and byte deflate-raw decoders.
|
|
24612
|
+
*/
|
|
24613
|
+
function ensureOrderedChunks(streamId) {
|
|
24614
|
+
let lastChunkIndex = -1;
|
|
24615
|
+
return new TransformStream({
|
|
24616
|
+
transform: (value, controller) => {
|
|
24617
|
+
const index = bigIntToNumber(value.chunkIndex);
|
|
24618
|
+
if (index <= lastChunkIndex) {
|
|
24619
|
+
livekitLogger.warn("ignoring duplicate chunk ".concat(index, " for compressed data stream ").concat(streamId, " (last processed: ").concat(lastChunkIndex, ")"));
|
|
24620
|
+
return;
|
|
24621
|
+
}
|
|
24622
|
+
if (index > lastChunkIndex + 1) {
|
|
24623
|
+
throw new DataStreamError("Missing chunk(s) ".concat(lastChunkIndex + 1, "..").concat(index - 1, " for compressed data stream ").concat(streamId, " - cannot continue decompressing"), DataStreamErrorReason.Incomplete);
|
|
24624
|
+
}
|
|
24625
|
+
lastChunkIndex = index;
|
|
24626
|
+
controller.enqueue(value);
|
|
24627
|
+
}
|
|
24628
|
+
});
|
|
24629
|
+
}
|
|
24630
|
+
/** Unwraps compressed `DataStream_Chunk`s to their compressed bytes (in `chunkIndex` order), */
|
|
24631
|
+
function chunksToBytes() {
|
|
24632
|
+
return new TransformStream({
|
|
24633
|
+
transform: (value, controller) => {
|
|
24634
|
+
controller.enqueue(value.content);
|
|
24635
|
+
}
|
|
24636
|
+
});
|
|
24637
|
+
}
|
|
24638
|
+
/** Re-wraps decompressed bytes into contiguous `DataStream_Chunk`s, skipping inflate's empty reads. */
|
|
24639
|
+
function bytesToChunks(streamId) {
|
|
24640
|
+
let outIndex = 0;
|
|
24641
|
+
return new TransformStream({
|
|
24642
|
+
transform: (value, controller) => {
|
|
24643
|
+
// Inflate can emit empty reads; only synthesize a chunk when there is content.
|
|
24644
|
+
if (value.byteLength > 0) {
|
|
24645
|
+
controller.enqueue(new DataStream_Chunk({
|
|
24646
|
+
streamId,
|
|
24647
|
+
chunkIndex: numberToBigInt(outIndex),
|
|
24648
|
+
content: value
|
|
24649
|
+
}));
|
|
24650
|
+
outIndex += 1;
|
|
24651
|
+
}
|
|
24652
|
+
}
|
|
24653
|
+
});
|
|
24654
|
+
}
|
|
24655
|
+
/**
|
|
24656
|
+
* Reframes decompressed byte chunks onto UTF-8 character boundaries via a streaming `TextDecoder`
|
|
24657
|
+
* (a write larger than the MTU spans several packets, which may split a codepoint), so each
|
|
24658
|
+
* synthesized text chunk decodes independently. The `flush` emits the decoder's trailing bytes.
|
|
24659
|
+
*/
|
|
24660
|
+
function bytesToDecodedUtf8(streamId) {
|
|
24661
|
+
const decoder = new TextDecoder('utf-8', {
|
|
24662
|
+
fatal: true
|
|
24663
|
+
});
|
|
24664
|
+
const encoder = new TextEncoder();
|
|
24665
|
+
let outIndex = 0;
|
|
24666
|
+
const decodeOrThrow = bytes => {
|
|
24667
|
+
try {
|
|
24668
|
+
return bytes ? decoder.decode(bytes, {
|
|
24669
|
+
stream: true
|
|
24670
|
+
}) : decoder.decode();
|
|
24671
|
+
} catch (err) {
|
|
24672
|
+
throw new DataStreamError("Cannot decode compressed data stream ".concat(streamId, " as text: ").concat(err), DataStreamErrorReason.DecodeFailed);
|
|
24673
|
+
}
|
|
24674
|
+
};
|
|
24675
|
+
return new TransformStream({
|
|
24676
|
+
transform: (value, controller) => {
|
|
24677
|
+
const text = decodeOrThrow(value);
|
|
24678
|
+
// Everything so far may have been a partial codepoint; only emit once we have characters.
|
|
24679
|
+
if (text.length > 0) {
|
|
24680
|
+
controller.enqueue(new DataStream_Chunk({
|
|
24681
|
+
streamId,
|
|
24682
|
+
chunkIndex: numberToBigInt(outIndex),
|
|
24683
|
+
content: encoder.encode(text)
|
|
24684
|
+
}));
|
|
24685
|
+
outIndex += 1;
|
|
24686
|
+
}
|
|
24687
|
+
},
|
|
24688
|
+
flush: controller => {
|
|
24689
|
+
const tail = decodeOrThrow();
|
|
24690
|
+
if (tail.length > 0) {
|
|
24691
|
+
controller.enqueue(new DataStream_Chunk({
|
|
24692
|
+
streamId,
|
|
24693
|
+
chunkIndex: numberToBigInt(outIndex),
|
|
24694
|
+
content: encoder.encode(tail)
|
|
24695
|
+
}));
|
|
24696
|
+
outIndex += 1;
|
|
24697
|
+
}
|
|
24698
|
+
}
|
|
24699
|
+
});
|
|
24700
|
+
}
|
|
24701
|
+
/**
|
|
24702
|
+
* Transforms a stream of deflate-raw-compressed byte `DataStream_Chunk`s into a stream of
|
|
24703
|
+
* decompressed chunks, so `ByteStreamReader` consumes it unchanged. All chunk contents are fed (in
|
|
24704
|
+
* `chunkIndex` order) through ONE raw-deflate decompressor for the stream's lifetime; decompressed
|
|
24705
|
+
* output is re-wrapped as chunks as soon as it is produced. The sender (sendFile) compresses the
|
|
24706
|
+
* whole payload in one shot, but the format also supports a single context-takeover stream
|
|
24707
|
+
* sync-flushed at write boundaries, so a future incremental streamBytes could compress with no
|
|
24708
|
+
* protocol change. Errors and cancellation propagate through the pipe chain.
|
|
24709
|
+
*/
|
|
24710
|
+
function inflateRawByteChunkStream(raw, streamId, maxPayloadByteLength) {
|
|
24711
|
+
return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToChunks(streamId));
|
|
24712
|
+
}
|
|
24713
|
+
/**
|
|
24714
|
+
* Transforms a stream of deflate-raw-compressed text `DataStream_Chunk`s into a stream of
|
|
24715
|
+
* decompressed chunks, so `TextStreamReader` consumes it unchanged. Builds on
|
|
24716
|
+
* {@link inflateRawByteChunkStream} (single decompressor + ordering guard) and adds a streaming
|
|
24717
|
+
* `TextDecoder` that reframes the decompressed bytes on UTF-8 character boundaries so each
|
|
24718
|
+
* synthesized chunk decodes independently. Errors and cancellation propagate through the pipe chain.
|
|
24719
|
+
*/
|
|
24720
|
+
function inflateRawChunkStream(raw, streamId, maxPayloadByteLength) {
|
|
24721
|
+
return raw.pipeThrough(ensureOrderedChunks(streamId)).pipeThrough(chunksToBytes()).pipeThrough(inflateRawTransform()).pipeThrough(maxDecompressedLengthGuard(streamId, maxPayloadByteLength)).pipeThrough(bytesToDecodedUtf8(streamId));
|
|
24722
|
+
}
|
|
24723
|
+
/**
|
|
24724
|
+
* Caps the total decompressed byte count of a compressed stream (decompression-bomb guard): a
|
|
24725
|
+
* tiny compressed payload can inflate to an arbitrarily large output, so the decompressor's
|
|
24726
|
+
* output is bounded rather than trusting the wire size. Exceeding the cap errors the stream with
|
|
24727
|
+
* `PayloadTooLarge`.
|
|
24728
|
+
*/
|
|
24729
|
+
function maxDecompressedLengthGuard(streamId, maxByteLength) {
|
|
24730
|
+
let total = 0;
|
|
24731
|
+
return new TransformStream({
|
|
24732
|
+
transform: (value, controller) => {
|
|
24733
|
+
total += value.byteLength;
|
|
24734
|
+
if (total > maxByteLength) {
|
|
24735
|
+
throw new DataStreamError("Data stream ".concat(streamId, " exceeds the maximum payload size of ").concat(maxByteLength, " bytes"), DataStreamErrorReason.PayloadTooLarge);
|
|
24736
|
+
}
|
|
24737
|
+
controller.enqueue(value);
|
|
24738
|
+
}
|
|
24739
|
+
});
|
|
23888
24740
|
}class BaseStreamWriter {
|
|
23889
24741
|
constructor(writableStream, info, onClose) {
|
|
23890
24742
|
this.writableStream = writableStream;
|
|
@@ -23905,15 +24757,70 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
23905
24757
|
}
|
|
23906
24758
|
}
|
|
23907
24759
|
class TextStreamWriter extends BaseStreamWriter {}
|
|
23908
|
-
class ByteStreamWriter extends BaseStreamWriter {}
|
|
24760
|
+
class ByteStreamWriter extends BaseStreamWriter {}/** Builds the `DataStream_Header` for a text stream from its info and stream options. */
|
|
24761
|
+
function buildTextStreamHeader(info, options, v2) {
|
|
24762
|
+
var _a;
|
|
24763
|
+
return new DataStream_Header({
|
|
24764
|
+
streamId: info.id,
|
|
24765
|
+
mimeType: info.mimeType,
|
|
24766
|
+
topic: info.topic,
|
|
24767
|
+
timestamp: numberToBigInt(info.timestamp),
|
|
24768
|
+
totalLength: numberToBigInt(info.size),
|
|
24769
|
+
attributes: info.attributes,
|
|
24770
|
+
compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
|
|
24771
|
+
inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
|
|
24772
|
+
contentHeader: {
|
|
24773
|
+
case: 'textHeader',
|
|
24774
|
+
value: new DataStream_TextHeader({
|
|
24775
|
+
version: options === null || options === void 0 ? void 0 : options.version,
|
|
24776
|
+
attachedStreamIds: info.attachedStreamIds,
|
|
24777
|
+
replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
|
|
24778
|
+
operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
|
|
24779
|
+
})
|
|
24780
|
+
}
|
|
24781
|
+
});
|
|
24782
|
+
}
|
|
24783
|
+
/** Builds the `DataStream_Header` for a byte stream from its info. */
|
|
24784
|
+
function buildByteStreamHeader(info, v2) {
|
|
24785
|
+
var _a;
|
|
24786
|
+
return new DataStream_Header({
|
|
24787
|
+
streamId: info.id,
|
|
24788
|
+
mimeType: info.mimeType,
|
|
24789
|
+
topic: info.topic,
|
|
24790
|
+
timestamp: numberToBigInt(info.timestamp),
|
|
24791
|
+
totalLength: numberToBigInt(info.size),
|
|
24792
|
+
attributes: info.attributes,
|
|
24793
|
+
compression: (_a = v2 === null || v2 === void 0 ? void 0 : v2.compression) !== null && _a !== void 0 ? _a : DataStream_CompressionType.NONE,
|
|
24794
|
+
inlineContent: v2 === null || v2 === void 0 ? void 0 : v2.inlineContent,
|
|
24795
|
+
contentHeader: {
|
|
24796
|
+
case: 'byteHeader',
|
|
24797
|
+
value: new DataStream_ByteHeader({
|
|
24798
|
+
name: info.name
|
|
24799
|
+
})
|
|
24800
|
+
}
|
|
24801
|
+
});
|
|
24802
|
+
}
|
|
24803
|
+
/** Wraps a `DataStream_Header` in a `DataPacket` ready to be sent over a data channel. */
|
|
24804
|
+
function createStreamHeaderPacket(header, destinationIdentities) {
|
|
24805
|
+
return new DataPacket({
|
|
24806
|
+
destinationIdentities,
|
|
24807
|
+
value: {
|
|
24808
|
+
case: 'streamHeader',
|
|
24809
|
+
value: header
|
|
24810
|
+
}
|
|
24811
|
+
});
|
|
24812
|
+
}const textEncoder = new TextEncoder();
|
|
23909
24813
|
/**
|
|
23910
24814
|
* Manages sending custom user data via data channels.
|
|
23911
24815
|
* @internal
|
|
23912
24816
|
*/
|
|
23913
24817
|
class OutgoingDataStreamManager {
|
|
23914
|
-
constructor(engine, log) {
|
|
24818
|
+
constructor(engine, log, getRemoteParticipantClientProtocol, getRemoteParticipantCapabilities, getAllRemoteParticipantIdentities) {
|
|
23915
24819
|
this.engine = engine;
|
|
23916
24820
|
this.log = log;
|
|
24821
|
+
this.getRemoteParticipantClientProtocol = getRemoteParticipantClientProtocol;
|
|
24822
|
+
this.getRemoteParticipantCapabilities = getRemoteParticipantCapabilities;
|
|
24823
|
+
this.getAllRemoteParticipantIdentities = getAllRemoteParticipantIdentities;
|
|
23917
24824
|
}
|
|
23918
24825
|
setupEngine(engine) {
|
|
23919
24826
|
this.engine = engine;
|
|
@@ -23921,42 +24828,236 @@ class OutgoingDataStreamManager {
|
|
|
23921
24828
|
/** {@inheritDoc LocalParticipant.sendText} */
|
|
23922
24829
|
sendText(text, options) {
|
|
23923
24830
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23924
|
-
var _a;
|
|
24831
|
+
var _a, _b, _c, _d, _e;
|
|
23925
24832
|
const streamId = crypto.randomUUID();
|
|
23926
|
-
const textInBytes =
|
|
24833
|
+
const textInBytes = textEncoder.encode(text);
|
|
23927
24834
|
const totalTextLength = textInBytes.byteLength;
|
|
23928
|
-
const
|
|
23929
|
-
|
|
24835
|
+
const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
|
|
24836
|
+
let info = {
|
|
24837
|
+
id: streamId,
|
|
24838
|
+
mimeType: 'text/plain',
|
|
24839
|
+
timestamp: Date.now(),
|
|
24840
|
+
topic: (_b = options === null || options === void 0 ? void 0 : options.topic) !== null && _b !== void 0 ? _b : '',
|
|
24841
|
+
size: totalTextLength,
|
|
24842
|
+
// NOTE: size is always the pre-compression byte length
|
|
24843
|
+
attributes: options === null || options === void 0 ? void 0 : options.attributes,
|
|
24844
|
+
encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
|
|
24845
|
+
};
|
|
24846
|
+
const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities) && this.allRecipientsSupportCompression(options === null || options === void 0 ? void 0 : options.destinationIdentities);
|
|
24847
|
+
let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(textInBytes).pipeThrough(deflateRawTransform())) : null;
|
|
24848
|
+
// Phase 1: Try to send as a single packet data stream
|
|
24849
|
+
const noAttachments = !(options === null || options === void 0 ? void 0 : options.attachments) || options.attachments.length === 0;
|
|
24850
|
+
if (noAttachments && this.allRecipientsSupportV2(options === null || options === void 0 ? void 0 : options.destinationIdentities)) {
|
|
24851
|
+
// The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
|
|
24852
|
+
// if it actually shrinks the payload (deflate framing makes tiny strings larger). The
|
|
24853
|
+
// compression flag is carried in the header's `compression` field; user attributes are left
|
|
24854
|
+
// untouched.
|
|
24855
|
+
let inlineContent = textInBytes;
|
|
24856
|
+
let compression = DataStream_CompressionType.NONE;
|
|
24857
|
+
if (compressedStream) {
|
|
24858
|
+
const collectedBytes = yield compressedStream.collect();
|
|
24859
|
+
if (collectedBytes.byteLength < textInBytes.byteLength) {
|
|
24860
|
+
inlineContent = collectedBytes;
|
|
24861
|
+
compression = DataStream_CompressionType.DEFLATE_RAW;
|
|
24862
|
+
}
|
|
24863
|
+
}
|
|
24864
|
+
const header = buildTextStreamHeader(info, undefined, {
|
|
24865
|
+
compression,
|
|
24866
|
+
inlineContent
|
|
24867
|
+
});
|
|
24868
|
+
const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
|
|
24869
|
+
if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
|
|
24870
|
+
yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
|
|
24871
|
+
(_d = options === null || options === void 0 ? void 0 : options.onProgress) === null || _d === void 0 ? void 0 : _d.call(options, 1);
|
|
24872
|
+
return info;
|
|
24873
|
+
}
|
|
24874
|
+
}
|
|
24875
|
+
const fileIds = (_e = options === null || options === void 0 ? void 0 : options.attachments) === null || _e === void 0 ? void 0 : _e.map(() => crypto.randomUUID());
|
|
24876
|
+
// Progress is split evenly across the text part (slot 0) and one slot per attachment, then
|
|
24877
|
+
// normalized to a [0,1] fraction. Each slot climbs monotonically to 1, so the aggregate ends at
|
|
24878
|
+
// exactly 1 once every part has completed.
|
|
24879
|
+
const parts = fileIds ? fileIds.length + 1 : 1;
|
|
24880
|
+
const progresses = new Array(parts).fill(0);
|
|
23930
24881
|
const handleProgress = (progress, idx) => {
|
|
23931
24882
|
var _a;
|
|
23932
24883
|
progresses[idx] = progress;
|
|
23933
|
-
|
|
23934
|
-
(_a = options === null || options === void 0 ? void 0 : options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, totalProgress);
|
|
24884
|
+
(_a = options === null || options === void 0 ? void 0 : options.onProgress) === null || _a === void 0 ? void 0 : _a.call(options, progresses.reduce((acc, val) => acc + val, 0) / parts);
|
|
23935
24885
|
};
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
|
|
23943
|
-
|
|
23944
|
-
|
|
23945
|
-
|
|
23946
|
-
|
|
23947
|
-
|
|
24886
|
+
// Phase 2: Try to send a multi packet data stream with compressed bytes
|
|
24887
|
+
if (compressedStream) {
|
|
24888
|
+
info.attachedStreamIds = fileIds;
|
|
24889
|
+
const header = buildTextStreamHeader(info, undefined, {
|
|
24890
|
+
compression: DataStream_CompressionType.DEFLATE_RAW
|
|
24891
|
+
});
|
|
24892
|
+
const packet = createStreamHeaderPacket(header, options === null || options === void 0 ? void 0 : options.destinationIdentities);
|
|
24893
|
+
yield this.sendChunkedByteStream(packet, streamId, options === null || options === void 0 ? void 0 : options.destinationIdentities, compressedStream.stream().pipeThrough(progressReportingStream(textInBytes.length, progress => handleProgress(progress, 0))));
|
|
24894
|
+
// Ensure there's always a 100% progress event fired, even if the string is zero bytes long
|
|
24895
|
+
if (textInBytes.length === 0) {
|
|
24896
|
+
handleProgress(1, 0);
|
|
24897
|
+
}
|
|
24898
|
+
} else {
|
|
24899
|
+
// Phase 3 / fallback: header + plain uncompressed chunk packets + trailer.
|
|
24900
|
+
const writer = yield this.streamText({
|
|
24901
|
+
streamId,
|
|
24902
|
+
totalSize: totalTextLength,
|
|
24903
|
+
destinationIdentities: options === null || options === void 0 ? void 0 : options.destinationIdentities,
|
|
24904
|
+
topic: options === null || options === void 0 ? void 0 : options.topic,
|
|
24905
|
+
attachedStreamIds: fileIds,
|
|
24906
|
+
attributes: options === null || options === void 0 ? void 0 : options.attributes
|
|
24907
|
+
});
|
|
24908
|
+
yield writer.write(text);
|
|
24909
|
+
// set text part of progress to 1
|
|
24910
|
+
handleProgress(1, 0);
|
|
24911
|
+
yield writer.close();
|
|
24912
|
+
info = writer.info;
|
|
24913
|
+
}
|
|
23948
24914
|
if ((options === null || options === void 0 ? void 0 : options.attachments) && fileIds) {
|
|
23949
24915
|
yield Promise.all(options.attachments.map((file, idx) => __awaiter(this, void 0, void 0, function* () {
|
|
23950
24916
|
return this._sendFile(fileIds[idx], file, {
|
|
23951
24917
|
topic: options.topic,
|
|
23952
24918
|
mimeType: file.type,
|
|
24919
|
+
destinationIdentities: options.destinationIdentities,
|
|
24920
|
+
compress: options.compress,
|
|
23953
24921
|
onProgress: progress => {
|
|
23954
24922
|
handleProgress(progress, idx + 1);
|
|
23955
24923
|
}
|
|
23956
24924
|
});
|
|
23957
24925
|
})));
|
|
23958
24926
|
}
|
|
23959
|
-
return
|
|
24927
|
+
return info;
|
|
24928
|
+
});
|
|
24929
|
+
}
|
|
24930
|
+
/**
|
|
24931
|
+
* Sends a complete in-memory byte payload. Mirrors {@link sendText}'s semantics: when every
|
|
24932
|
+
* recipient supports data streams v2 the payload rides inline in a single header packet
|
|
24933
|
+
* (optionally deflate-raw compressed), otherwise it is sent as a (optionally compressed)
|
|
24934
|
+
* chunked byte stream. Unlike {@link sendFile}, the whole payload is already in memory, so the
|
|
24935
|
+
* inline single-packet fast path applies.
|
|
24936
|
+
*/
|
|
24937
|
+
sendBytes(bytes, options) {
|
|
24938
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24939
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
24940
|
+
const streamId = crypto.randomUUID();
|
|
24941
|
+
const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
|
|
24942
|
+
const compress = (_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true;
|
|
24943
|
+
const info = {
|
|
24944
|
+
id: streamId,
|
|
24945
|
+
name: (_b = options === null || options === void 0 ? void 0 : options.name) !== null && _b !== void 0 ? _b : 'unknown',
|
|
24946
|
+
mimeType: (_c = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _c !== void 0 ? _c : 'application/octet-stream',
|
|
24947
|
+
timestamp: Date.now(),
|
|
24948
|
+
topic: (_d = options === null || options === void 0 ? void 0 : options.topic) !== null && _d !== void 0 ? _d : '',
|
|
24949
|
+
size: bytes.byteLength,
|
|
24950
|
+
// NOTE: size is always the pre-compression byte length
|
|
24951
|
+
attributes: options === null || options === void 0 ? void 0 : options.attributes,
|
|
24952
|
+
encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
|
|
24953
|
+
};
|
|
24954
|
+
const progressMonitorTap = progressReportingStream(bytes.length, options === null || options === void 0 ? void 0 : options.onProgress);
|
|
24955
|
+
const compressEligible = compress && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
|
|
24956
|
+
let compressedStream = compressEligible ? MaybeCollectedStream.fromStream(readableFromBytes(bytes).pipeThrough(progressMonitorTap).pipeThrough(deflateRawTransform())) : null;
|
|
24957
|
+
// Phase 1: Try to send as a single packet data stream
|
|
24958
|
+
if (this.allRecipientsSupportV2(destinationIdentities)) {
|
|
24959
|
+
// The payload rides in the header's `inlineContent` (raw bytes). Keep the compressed form only
|
|
24960
|
+
// if it actually shrinks the payload (deflate framing makes tiny payloads larger). The
|
|
24961
|
+
// compression flag is carried in the header's `compression` field; user attributes are left
|
|
24962
|
+
// untouched.
|
|
24963
|
+
let inlineContent = bytes;
|
|
24964
|
+
let compression = DataStream_CompressionType.NONE;
|
|
24965
|
+
if (compressedStream) {
|
|
24966
|
+
const collectedBytes = yield compressedStream.collect();
|
|
24967
|
+
if (collectedBytes.byteLength < bytes.byteLength) {
|
|
24968
|
+
inlineContent = collectedBytes;
|
|
24969
|
+
compression = DataStream_CompressionType.DEFLATE_RAW;
|
|
24970
|
+
}
|
|
24971
|
+
}
|
|
24972
|
+
const header = buildByteStreamHeader(info, {
|
|
24973
|
+
compression,
|
|
24974
|
+
inlineContent
|
|
24975
|
+
});
|
|
24976
|
+
const packet = createStreamHeaderPacket(header, destinationIdentities);
|
|
24977
|
+
if (packet.toBinary().byteLength <= STREAM_CHUNK_SIZE_BYTES) {
|
|
24978
|
+
yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
|
|
24979
|
+
(_f = options === null || options === void 0 ? void 0 : options.onProgress) === null || _f === void 0 ? void 0 : _f.call(options, 1);
|
|
24980
|
+
return info;
|
|
24981
|
+
}
|
|
24982
|
+
}
|
|
24983
|
+
// Phase 2/3: header + (optionally compressed) chunk packets + trailer.
|
|
24984
|
+
const header = buildByteStreamHeader(info, {
|
|
24985
|
+
compression: compressedStream ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
|
|
24986
|
+
});
|
|
24987
|
+
const packet = createStreamHeaderPacket(header, destinationIdentities);
|
|
24988
|
+
const source = compressedStream ? compressedStream.stream() : readableFromBytes(bytes).pipeThrough(progressMonitorTap);
|
|
24989
|
+
yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
|
|
24990
|
+
// Ensure there's always a 100% progress event fired, even if the buffer is zero bytes long
|
|
24991
|
+
if (bytes.length === 0) {
|
|
24992
|
+
(_g = options === null || options === void 0 ? void 0 : options.onProgress) === null || _g === void 0 ? void 0 : _g.call(options, 1);
|
|
24993
|
+
}
|
|
24994
|
+
return info;
|
|
24995
|
+
});
|
|
24996
|
+
}
|
|
24997
|
+
/**
|
|
24998
|
+
* Returns true only if every recipient is known to support data streams v2 (single-packet inline
|
|
24999
|
+
* streams and compression). For a targeted send this checks the named destination identities; for
|
|
25000
|
+
* a broadcast (no explicit destinations) it checks every remote participant currently in the room.
|
|
25001
|
+
* An empty room (nobody to receive) is considered eligible.
|
|
25002
|
+
*/
|
|
25003
|
+
allRecipientsSupportV2(destinationIdentities) {
|
|
25004
|
+
const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
|
|
25005
|
+
return identities.every(identity => this.getRemoteParticipantClientProtocol(identity) >= CLIENT_PROTOCOL_DATA_STREAM_V2);
|
|
25006
|
+
}
|
|
25007
|
+
/**
|
|
25008
|
+
* Returns true only if every recipient advertises the deflate-raw compression capability (so it
|
|
25009
|
+
* can decompress a compressed stream). Resolved the same way as {@link allRecipientsSupportV2}:
|
|
25010
|
+
* named destinations, or every remote participant for a broadcast; an empty room is eligible.
|
|
25011
|
+
*/
|
|
25012
|
+
allRecipientsSupportCompression(destinationIdentities) {
|
|
25013
|
+
const identities = destinationIdentities && destinationIdentities.length > 0 ? destinationIdentities : this.getAllRemoteParticipantIdentities();
|
|
25014
|
+
return identities.every(identity => this.getRemoteParticipantCapabilities(identity).includes(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW));
|
|
25015
|
+
}
|
|
25016
|
+
/**
|
|
25017
|
+
* Shared chunked-stream send for `sendText`/`sendFile`: sends the prebuilt header packet, then
|
|
25018
|
+
* forwards `source` (optionally deflate-raw compressed) as `streamChunk` packets re-chunked to
|
|
25019
|
+
* the MTU budget with contiguous indices, then sends the trailer. The source is consumed
|
|
25020
|
+
* incrementally, so a `file.stream()` is never buffered in full. The platform compressor can't
|
|
25021
|
+
* flush mid-stream, so compression is only used when the whole payload is available as a stream
|
|
25022
|
+
* up front (not for incremental writers like `streamText`/`streamBytes`).
|
|
25023
|
+
*/
|
|
25024
|
+
sendChunkedByteStream(headerPacket, streamId, destinationIdentities, source) {
|
|
25025
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25026
|
+
var _a, e_1, _b, _c;
|
|
25027
|
+
const engine = this.engine;
|
|
25028
|
+
yield sendHeaderPacket(engine, headerPacket);
|
|
25029
|
+
let chunkId = 0;
|
|
25030
|
+
try {
|
|
25031
|
+
for (var _d = true, _e = __asyncValues(readBytesInChunks(source, STREAM_CHUNK_SIZE_BYTES)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
|
|
25032
|
+
_c = _f.value;
|
|
25033
|
+
_d = false;
|
|
25034
|
+
const chunk = _c;
|
|
25035
|
+
const chunkPacket = new DataPacket({
|
|
25036
|
+
destinationIdentities,
|
|
25037
|
+
value: {
|
|
25038
|
+
case: 'streamChunk',
|
|
25039
|
+
value: new DataStream_Chunk({
|
|
25040
|
+
content: chunk,
|
|
25041
|
+
streamId,
|
|
25042
|
+
chunkIndex: numberToBigInt(chunkId)
|
|
25043
|
+
})
|
|
25044
|
+
}
|
|
25045
|
+
});
|
|
25046
|
+
yield engine.sendDataPacket(chunkPacket, DataChannelKind.RELIABLE);
|
|
25047
|
+
chunkId += 1;
|
|
25048
|
+
}
|
|
25049
|
+
} catch (e_1_1) {
|
|
25050
|
+
e_1 = {
|
|
25051
|
+
error: e_1_1
|
|
25052
|
+
};
|
|
25053
|
+
} finally {
|
|
25054
|
+
try {
|
|
25055
|
+
if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
|
|
25056
|
+
} finally {
|
|
25057
|
+
if (e_1) throw e_1.error;
|
|
25058
|
+
}
|
|
25059
|
+
}
|
|
25060
|
+
yield sendStreamTrailer(streamId, destinationIdentities, engine);
|
|
23960
25061
|
});
|
|
23961
25062
|
}
|
|
23962
25063
|
/**
|
|
@@ -23966,6 +25067,7 @@ class OutgoingDataStreamManager {
|
|
|
23966
25067
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23967
25068
|
var _a, _b, _c;
|
|
23968
25069
|
const streamId = (_a = options === null || options === void 0 ? void 0 : options.streamId) !== null && _a !== void 0 ? _a : crypto.randomUUID();
|
|
25070
|
+
const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
|
|
23969
25071
|
const info = {
|
|
23970
25072
|
id: streamId,
|
|
23971
25073
|
mimeType: 'text/plain',
|
|
@@ -23976,39 +25078,20 @@ class OutgoingDataStreamManager {
|
|
|
23976
25078
|
encryptionType: ((_c = this.engine.e2eeManager) === null || _c === void 0 ? void 0 : _c.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE,
|
|
23977
25079
|
attachedStreamIds: options === null || options === void 0 ? void 0 : options.attachedStreamIds
|
|
23978
25080
|
};
|
|
23979
|
-
const header =
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
topic: info.topic,
|
|
23983
|
-
timestamp: numberToBigInt(info.timestamp),
|
|
23984
|
-
totalLength: numberToBigInt(info.size),
|
|
23985
|
-
attributes: info.attributes,
|
|
23986
|
-
contentHeader: {
|
|
23987
|
-
case: 'textHeader',
|
|
23988
|
-
value: new DataStream_TextHeader({
|
|
23989
|
-
version: options === null || options === void 0 ? void 0 : options.version,
|
|
23990
|
-
attachedStreamIds: info.attachedStreamIds,
|
|
23991
|
-
replyToStreamId: options === null || options === void 0 ? void 0 : options.replyToStreamId,
|
|
23992
|
-
operationType: (options === null || options === void 0 ? void 0 : options.type) === 'update' ? DataStream_OperationType.UPDATE : DataStream_OperationType.CREATE
|
|
23993
|
-
})
|
|
23994
|
-
}
|
|
23995
|
-
});
|
|
23996
|
-
const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
|
|
23997
|
-
const packet = new DataPacket({
|
|
23998
|
-
destinationIdentities,
|
|
23999
|
-
value: {
|
|
24000
|
-
case: 'streamHeader',
|
|
24001
|
-
value: header
|
|
24002
|
-
}
|
|
24003
|
-
});
|
|
24004
|
-
yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
|
|
25081
|
+
const header = buildTextStreamHeader(info, options);
|
|
25082
|
+
const packet = createStreamHeaderPacket(header, destinationIdentities);
|
|
25083
|
+
yield sendHeaderPacket(this.engine, packet);
|
|
24005
25084
|
let chunkId = 0;
|
|
24006
25085
|
const engine = this.engine;
|
|
25086
|
+
// Incremental text streams are never compressed (CompressionStream does not support flushing
|
|
25087
|
+
// mid-stream); one-shot compression lives in sendText.
|
|
25088
|
+
//
|
|
25089
|
+
// Note that a future streamText could send a context-takeover style deflate-raw stream with
|
|
25090
|
+
// intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
|
|
24007
25091
|
const writableStream = new WritableStream({
|
|
24008
|
-
// Implement the sink
|
|
24009
25092
|
write(text) {
|
|
24010
25093
|
return __awaiter(this, void 0, void 0, function* () {
|
|
24011
|
-
for (const textByteChunk of splitUtf8(text,
|
|
25094
|
+
for (const textByteChunk of splitUtf8(text, STREAM_CHUNK_SIZE_BYTES)) {
|
|
24012
25095
|
const chunk = new DataStream_Chunk({
|
|
24013
25096
|
content: textByteChunk,
|
|
24014
25097
|
streamId,
|
|
@@ -24028,17 +25111,7 @@ class OutgoingDataStreamManager {
|
|
|
24028
25111
|
},
|
|
24029
25112
|
close() {
|
|
24030
25113
|
return __awaiter(this, void 0, void 0, function* () {
|
|
24031
|
-
|
|
24032
|
-
streamId
|
|
24033
|
-
});
|
|
24034
|
-
const trailerPacket = new DataPacket({
|
|
24035
|
-
destinationIdentities,
|
|
24036
|
-
value: {
|
|
24037
|
-
case: 'streamTrailer',
|
|
24038
|
-
value: trailer
|
|
24039
|
-
}
|
|
24040
|
-
});
|
|
24041
|
-
yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
|
|
25114
|
+
yield sendStreamTrailer(streamId, destinationIdentities, engine);
|
|
24042
25115
|
});
|
|
24043
25116
|
},
|
|
24044
25117
|
abort(err) {
|
|
@@ -24049,6 +25122,8 @@ class OutgoingDataStreamManager {
|
|
|
24049
25122
|
let onEngineClose = () => __awaiter(this, void 0, void 0, function* () {
|
|
24050
25123
|
yield writer.close();
|
|
24051
25124
|
});
|
|
25125
|
+
// FIXME: make this a global event to ensure "max listener" warning won't get logged for lots of
|
|
25126
|
+
// in flight data streams.
|
|
24052
25127
|
engine.once(EngineEvent.Closing, onEngineClose);
|
|
24053
25128
|
const writer = new TextStreamWriter(writableStream, info, () => this.engine.off(EngineEvent.Closing, onEngineClose));
|
|
24054
25129
|
return writer;
|
|
@@ -24063,29 +25138,47 @@ class OutgoingDataStreamManager {
|
|
|
24063
25138
|
};
|
|
24064
25139
|
});
|
|
24065
25140
|
}
|
|
25141
|
+
/**
|
|
25142
|
+
* Streams a file as a chunked byte stream, compressed (deflate-raw) when the runtime supports it
|
|
25143
|
+
* and every recipient is on data streams v2. The file is piped `file.stream()` →
|
|
25144
|
+
* (`CompressionStream`) → chunk packets via {@link sendChunkedByteStream}, so it is never fully
|
|
25145
|
+
* buffered in memory — unlike {@link sendBytes}, there is no inline single-packet fast path for
|
|
25146
|
+
* files (the compressed size can't be known up front without buffering the whole file).
|
|
25147
|
+
*/
|
|
24066
25148
|
_sendFile(streamId, file, options) {
|
|
24067
25149
|
return __awaiter(this, void 0, void 0, function* () {
|
|
24068
|
-
var _a;
|
|
24069
|
-
const
|
|
24070
|
-
|
|
24071
|
-
|
|
25150
|
+
var _a, _b, _c, _d, _e;
|
|
25151
|
+
const destinationIdentities = options === null || options === void 0 ? void 0 : options.destinationIdentities;
|
|
25152
|
+
const compress = ((_a = options === null || options === void 0 ? void 0 : options.compress) !== null && _a !== void 0 ? _a : true) && isCompressionStreamSupported() && this.allRecipientsSupportV2(destinationIdentities) && this.allRecipientsSupportCompression(destinationIdentities);
|
|
25153
|
+
const info = {
|
|
25154
|
+
id: streamId,
|
|
24072
25155
|
name: file.name,
|
|
24073
|
-
mimeType: (
|
|
24074
|
-
topic: options === null || options === void 0 ? void 0 : options.topic,
|
|
24075
|
-
|
|
25156
|
+
mimeType: (_b = options === null || options === void 0 ? void 0 : options.mimeType) !== null && _b !== void 0 ? _b : file.type,
|
|
25157
|
+
topic: (_c = options === null || options === void 0 ? void 0 : options.topic) !== null && _c !== void 0 ? _c : '',
|
|
25158
|
+
timestamp: Date.now(),
|
|
25159
|
+
size: file.size,
|
|
25160
|
+
encryptionType: ((_d = this.engine.e2eeManager) === null || _d === void 0 ? void 0 : _d.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
|
|
25161
|
+
};
|
|
25162
|
+
// Phase 1: Try to send as a single packet data stream
|
|
25163
|
+
//
|
|
25164
|
+
// This is not being done explictly for files, because it's challenging to determine ahead of
|
|
25165
|
+
// time how well the file contents will compress (and whether the total output will be under the
|
|
25166
|
+
// MTU). Revisit this in the future though.
|
|
25167
|
+
// Phase 2 (compressed) / Phase 3 (uncompressed fallback): header + chunk packets + trailer. Both
|
|
25168
|
+
// funnel through the shared sendChunkedByteStream primitive, differing only by whether the file
|
|
25169
|
+
// stream is wrapped in the deflate-raw compressor; the file is never fully buffered in memory.
|
|
25170
|
+
const header = buildByteStreamHeader(info, {
|
|
25171
|
+
compression: compress ? DataStream_CompressionType.DEFLATE_RAW : DataStream_CompressionType.NONE
|
|
24076
25172
|
});
|
|
24077
|
-
const
|
|
24078
|
-
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
|
|
24084
|
-
}
|
|
24085
|
-
yield writer.write(value);
|
|
25173
|
+
const packet = createStreamHeaderPacket(header, destinationIdentities);
|
|
25174
|
+
const tapped = file.stream().pipeThrough(progressReportingStream(file.size, options === null || options === void 0 ? void 0 : options.onProgress));
|
|
25175
|
+
const source = compress ? tapped.pipeThrough(deflateRawTransform()) : tapped;
|
|
25176
|
+
yield this.sendChunkedByteStream(packet, streamId, destinationIdentities, source);
|
|
25177
|
+
// Ensure there's always a 100% progress event fired, even if the file is zero bytes long
|
|
25178
|
+
if (file.size === 0) {
|
|
25179
|
+
(_e = options === null || options === void 0 ? void 0 : options.onProgress) === null || _e === void 0 ? void 0 : _e.call(options, 1);
|
|
24086
25180
|
}
|
|
24087
|
-
|
|
24088
|
-
return writer.info;
|
|
25181
|
+
return info;
|
|
24089
25182
|
});
|
|
24090
25183
|
}
|
|
24091
25184
|
streamBytes(options) {
|
|
@@ -24103,32 +25196,18 @@ class OutgoingDataStreamManager {
|
|
|
24103
25196
|
name: (_d = options === null || options === void 0 ? void 0 : options.name) !== null && _d !== void 0 ? _d : 'unknown',
|
|
24104
25197
|
encryptionType: ((_e = this.engine.e2eeManager) === null || _e === void 0 ? void 0 : _e.isDataChannelEncryptionEnabled) ? Encryption_Type.GCM : Encryption_Type.NONE
|
|
24105
25198
|
};
|
|
24106
|
-
const header =
|
|
24107
|
-
|
|
24108
|
-
|
|
24109
|
-
streamId,
|
|
24110
|
-
topic: info.topic,
|
|
24111
|
-
timestamp: numberToBigInt(Date.now()),
|
|
24112
|
-
attributes: info.attributes,
|
|
24113
|
-
contentHeader: {
|
|
24114
|
-
case: 'byteHeader',
|
|
24115
|
-
value: new DataStream_ByteHeader({
|
|
24116
|
-
name: info.name
|
|
24117
|
-
})
|
|
24118
|
-
}
|
|
24119
|
-
});
|
|
24120
|
-
const packet = new DataPacket({
|
|
24121
|
-
destinationIdentities,
|
|
24122
|
-
value: {
|
|
24123
|
-
case: 'streamHeader',
|
|
24124
|
-
value: header
|
|
24125
|
-
}
|
|
24126
|
-
});
|
|
24127
|
-
yield this.engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
|
|
25199
|
+
const header = buildByteStreamHeader(info);
|
|
25200
|
+
const packet = createStreamHeaderPacket(header, destinationIdentities);
|
|
25201
|
+
yield sendHeaderPacket(this.engine, packet);
|
|
24128
25202
|
let chunkId = 0;
|
|
24129
25203
|
const writeMutex = new _();
|
|
24130
25204
|
const engine = this.engine;
|
|
24131
25205
|
const logLocal = this.log;
|
|
25206
|
+
// Incremental byte streams are never compressed (CompressionStream does not support flushing
|
|
25207
|
+
// mid-stream); one-shot compression lives in sendFile.
|
|
25208
|
+
//
|
|
25209
|
+
// Note that a future streamBytes could send a context-takeover style deflate-raw stream with
|
|
25210
|
+
// intermedia explicit `Z_SYNC_FLUSH`s - receivers already will handle this properly today.
|
|
24132
25211
|
const writableStream = new WritableStream({
|
|
24133
25212
|
write(chunk) {
|
|
24134
25213
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -24136,7 +25215,7 @@ class OutgoingDataStreamManager {
|
|
|
24136
25215
|
let byteOffset = 0;
|
|
24137
25216
|
try {
|
|
24138
25217
|
while (byteOffset < chunk.byteLength) {
|
|
24139
|
-
const subChunk = chunk.slice(byteOffset, byteOffset +
|
|
25218
|
+
const subChunk = chunk.slice(byteOffset, byteOffset + STREAM_CHUNK_SIZE_BYTES);
|
|
24140
25219
|
const chunkPacket = new DataPacket({
|
|
24141
25220
|
destinationIdentities,
|
|
24142
25221
|
value: {
|
|
@@ -24159,17 +25238,7 @@ class OutgoingDataStreamManager {
|
|
|
24159
25238
|
},
|
|
24160
25239
|
close() {
|
|
24161
25240
|
return __awaiter(this, void 0, void 0, function* () {
|
|
24162
|
-
|
|
24163
|
-
streamId
|
|
24164
|
-
});
|
|
24165
|
-
const trailerPacket = new DataPacket({
|
|
24166
|
-
destinationIdentities,
|
|
24167
|
-
value: {
|
|
24168
|
-
case: 'streamTrailer',
|
|
24169
|
-
value: trailer
|
|
24170
|
-
}
|
|
24171
|
-
});
|
|
24172
|
-
yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
|
|
25241
|
+
yield sendStreamTrailer(streamId, destinationIdentities, engine);
|
|
24173
25242
|
});
|
|
24174
25243
|
},
|
|
24175
25244
|
abort(err) {
|
|
@@ -24180,6 +25249,109 @@ class OutgoingDataStreamManager {
|
|
|
24180
25249
|
return byteWriter;
|
|
24181
25250
|
});
|
|
24182
25251
|
}
|
|
25252
|
+
}
|
|
25253
|
+
/**
|
|
25254
|
+
* Wraps a stream of compressed bytes and defers buffering it into memory until (and only if) the
|
|
25255
|
+
* full length is actually needed.
|
|
25256
|
+
*
|
|
25257
|
+
* The ideal way to send a compressed stream is incrementally: compress each chunk and send it as
|
|
25258
|
+
* soon as it's ready, without ever holding the whole payload in memory. But to decide whether a
|
|
25259
|
+
* payload is small enough to fit in a single-packet data stream, we have to know the compressed
|
|
25260
|
+
* length — and the only way to learn that is to compress everything and add up the bytes. That
|
|
25261
|
+
* forces us to buffer the entire compressed output before we can make the decision.
|
|
25262
|
+
*
|
|
25263
|
+
* This class lets us have it both ways. It holds the compressed stream unread by default and only
|
|
25264
|
+
* collects it into memory ({@link collect}) when a caller needs the length for the single-packet
|
|
25265
|
+
* size check. If that check never happens, the stream is instead {@link stream|passed straight
|
|
25266
|
+
* through} to the downstream consumer without ever being fully buffered. Once collected, later
|
|
25267
|
+
* calls reuse the buffered bytes rather than re-collecting.
|
|
25268
|
+
*/
|
|
25269
|
+
class MaybeCollectedStream {
|
|
25270
|
+
constructor(state) {
|
|
25271
|
+
this.state = state;
|
|
25272
|
+
}
|
|
25273
|
+
static fromStream(stream) {
|
|
25274
|
+
return new MaybeCollectedStream({
|
|
25275
|
+
type: 'stream',
|
|
25276
|
+
stream
|
|
25277
|
+
});
|
|
25278
|
+
}
|
|
25279
|
+
/** Collect data from the stream into memory and return as a Uint8Array. */
|
|
25280
|
+
collect() {
|
|
25281
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25282
|
+
switch (this.state.type) {
|
|
25283
|
+
case 'stream':
|
|
25284
|
+
const bytes = yield collect(this.state.stream);
|
|
25285
|
+
this.state = {
|
|
25286
|
+
type: 'collected',
|
|
25287
|
+
bytes
|
|
25288
|
+
};
|
|
25289
|
+
return bytes;
|
|
25290
|
+
case 'collected':
|
|
25291
|
+
return this.state.bytes;
|
|
25292
|
+
}
|
|
25293
|
+
});
|
|
25294
|
+
}
|
|
25295
|
+
/** Pass wrapped stream through to downstream consumer. */
|
|
25296
|
+
stream() {
|
|
25297
|
+
switch (this.state.type) {
|
|
25298
|
+
case 'stream':
|
|
25299
|
+
return this.state.stream;
|
|
25300
|
+
case 'collected':
|
|
25301
|
+
return readableFromBytes(this.state.bytes);
|
|
25302
|
+
}
|
|
25303
|
+
}
|
|
25304
|
+
}
|
|
25305
|
+
/**
|
|
25306
|
+
* A pass-through byte transform that reports progress as bytes flow through it, measured against
|
|
25307
|
+
* `totalLength`. Used as a "tap" to instrument the source of a chunked send without threading a
|
|
25308
|
+
* progress callback through the send primitive.
|
|
25309
|
+
*
|
|
25310
|
+
* IMPORTANT: This should be upstream of any compression step.
|
|
25311
|
+
*
|
|
25312
|
+
* Emits nothing when the total is unknown or zero.
|
|
25313
|
+
*/
|
|
25314
|
+
function progressReportingStream(totalPreCompressionLength, onProgress) {
|
|
25315
|
+
let sent = 0;
|
|
25316
|
+
return new TransformStream({
|
|
25317
|
+
transform(chunk, controller) {
|
|
25318
|
+
sent += chunk.byteLength;
|
|
25319
|
+
if (onProgress && typeof totalPreCompressionLength === 'number' && totalPreCompressionLength > 0) {
|
|
25320
|
+
onProgress(Math.min(sent / totalPreCompressionLength, 1));
|
|
25321
|
+
}
|
|
25322
|
+
controller.enqueue(chunk);
|
|
25323
|
+
}
|
|
25324
|
+
});
|
|
25325
|
+
}
|
|
25326
|
+
/**
|
|
25327
|
+
* Sends a stream `streamHeader` packet, enforcing that it fits the MTU budget. The header carries
|
|
25328
|
+
* the user attributes (plus topic/framing), and a single `DataPacket` larger than the MTU can't be
|
|
25329
|
+
* reliably sent — so an oversized header (almost always due to large attributes) is a hard error
|
|
25330
|
+
* rather than a malformed packet on the wire. The inline fast path does its own size check and
|
|
25331
|
+
* falls back to the chunked path instead of calling this.
|
|
25332
|
+
*/
|
|
25333
|
+
function sendHeaderPacket(engine, packet) {
|
|
25334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25335
|
+
if (packet.toBinary().byteLength > STREAM_CHUNK_SIZE_BYTES) {
|
|
25336
|
+
throw new DataStreamError("data stream header exceeds the ".concat(STREAM_CHUNK_SIZE_BYTES, "-byte limit; reduce attribute size"), DataStreamErrorReason.HeaderTooLarge);
|
|
25337
|
+
}
|
|
25338
|
+
yield engine.sendDataPacket(packet, DataChannelKind.RELIABLE);
|
|
25339
|
+
});
|
|
25340
|
+
}
|
|
25341
|
+
/** Sends a `streamTrailer` packet, marking the end of a stream. */
|
|
25342
|
+
function sendStreamTrailer(streamId, destinationIdentities, engine) {
|
|
25343
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25344
|
+
const trailerPacket = new DataPacket({
|
|
25345
|
+
destinationIdentities,
|
|
25346
|
+
value: {
|
|
25347
|
+
case: 'streamTrailer',
|
|
25348
|
+
value: new DataStream_Trailer({
|
|
25349
|
+
streamId
|
|
25350
|
+
})
|
|
25351
|
+
}
|
|
25352
|
+
});
|
|
25353
|
+
yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
|
|
25354
|
+
});
|
|
24183
25355
|
}/**
|
|
24184
25356
|
* Implementation of AbortSignal.any
|
|
24185
25357
|
* Creates a signal that will be aborted when any of the given signals is aborted.
|
|
@@ -26670,7 +27842,7 @@ class RpcClientManager extends eventsExports.EventEmitter {
|
|
|
26670
27842
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26671
27843
|
if (remoteClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
|
|
26672
27844
|
// Send payload as a data stream - a "version 2" rpc request.
|
|
26673
|
-
|
|
27845
|
+
yield this.outgoingDataStreamManager.sendText(payload, {
|
|
26674
27846
|
topic: RPC_REQUEST_DATA_STREAM_TOPIC,
|
|
26675
27847
|
destinationIdentities: [destinationIdentity],
|
|
26676
27848
|
attributes: {
|
|
@@ -26680,8 +27852,6 @@ class RpcClientManager extends eventsExports.EventEmitter {
|
|
|
26680
27852
|
[RpcRequestAttrs.RPC_REQUEST_VERSION]: "".concat(RPC_VERSION_V2)
|
|
26681
27853
|
}
|
|
26682
27854
|
});
|
|
26683
|
-
yield writer.write(payload);
|
|
26684
|
-
yield writer.close();
|
|
26685
27855
|
return;
|
|
26686
27856
|
}
|
|
26687
27857
|
// Fallback to sending a literal RpcRequest - a "version 1" rpc request.
|
|
@@ -26955,15 +28125,13 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
26955
28125
|
const callerClientProtocol = this.getRemoteParticipantClientProtocol(destinationIdentity);
|
|
26956
28126
|
if (callerClientProtocol >= CLIENT_PROTOCOL_DATA_STREAM_RPC) {
|
|
26957
28127
|
// Send response as a data stream
|
|
26958
|
-
|
|
28128
|
+
yield this.outgoingDataStreamManager.sendText(payload, {
|
|
26959
28129
|
topic: RPC_RESPONSE_DATA_STREAM_TOPIC,
|
|
26960
28130
|
destinationIdentities: [destinationIdentity],
|
|
26961
28131
|
attributes: {
|
|
26962
28132
|
[RpcRequestAttrs.RPC_REQUEST_ID]: requestId
|
|
26963
28133
|
}
|
|
26964
28134
|
});
|
|
26965
|
-
yield writer.write(payload);
|
|
26966
|
-
yield writer.close();
|
|
26967
28135
|
return;
|
|
26968
28136
|
}
|
|
26969
28137
|
// Legacy client: enforce size limit and send uncompressed payload inline
|
|
@@ -29439,6 +30607,18 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
29439
30607
|
return this.roomOutgoingDataStreamManager.sendFile(file, options);
|
|
29440
30608
|
});
|
|
29441
30609
|
}
|
|
30610
|
+
/**
|
|
30611
|
+
* Sends the given bytes to participants in the room via the data channel.
|
|
30612
|
+
* For files, consider using {@link sendFile}; for longer/incremental payloads, {@link streamBytes}.
|
|
30613
|
+
*
|
|
30614
|
+
* @param bytes The byte payload
|
|
30615
|
+
* @param options.topic Topic identifier used to route the stream to appropriate handlers.
|
|
30616
|
+
*/
|
|
30617
|
+
sendBytes(bytes, options) {
|
|
30618
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30619
|
+
return this.roomOutgoingDataStreamManager.sendBytes(bytes, options);
|
|
30620
|
+
});
|
|
30621
|
+
}
|
|
29442
30622
|
/**
|
|
29443
30623
|
* Stream bytes incrementally to participants in the room via the data channel.
|
|
29444
30624
|
* For sending files, consider using {@link sendFile} instead.
|
|
@@ -29968,7 +31148,7 @@ class DeferrableMap extends Map {
|
|
|
29968
31148
|
return new RemoteDataTrack(info, manager, {
|
|
29969
31149
|
publisherIdentity: pi.identity
|
|
29970
31150
|
});
|
|
29971
|
-
}), pi.clientProtocol);
|
|
31151
|
+
}), pi.clientProtocol, pi.capabilities);
|
|
29972
31152
|
}
|
|
29973
31153
|
get logContext() {
|
|
29974
31154
|
return Object.assign(Object.assign({}, super.logContext), {
|
|
@@ -29981,6 +31161,7 @@ class DeferrableMap extends Map {
|
|
|
29981
31161
|
let kind = arguments.length > 7 && arguments[7] !== undefined ? arguments[7] : ParticipantInfo_Kind.STANDARD;
|
|
29982
31162
|
let remoteDataTracks = arguments.length > 8 && arguments[8] !== undefined ? arguments[8] : [];
|
|
29983
31163
|
let clientProtocol = arguments.length > 9 && arguments[9] !== undefined ? arguments[9] : CLIENT_PROTOCOL_DEFAULT;
|
|
31164
|
+
let capabilities = arguments.length > 10 && arguments[10] !== undefined ? arguments[10] : [];
|
|
29984
31165
|
super(sid, identity || '', name, metadata, attributes, loggerOptions, kind);
|
|
29985
31166
|
this.signalClient = signalClient;
|
|
29986
31167
|
this.trackPublications = new Map();
|
|
@@ -29991,6 +31172,7 @@ class DeferrableMap extends Map {
|
|
|
29991
31172
|
}));
|
|
29992
31173
|
this.volumeMap = new Map();
|
|
29993
31174
|
this.clientProtocol = clientProtocol;
|
|
31175
|
+
this.capabilities = capabilities;
|
|
29994
31176
|
}
|
|
29995
31177
|
addTrackPublication(publication) {
|
|
29996
31178
|
super.addTrackPublication(publication);
|
|
@@ -30402,11 +31584,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
30402
31584
|
return this.connectFuture.promise;
|
|
30403
31585
|
});
|
|
30404
31586
|
this.connectSignal = (url, token, engine, connectOptions, roomOptions, abortController) => __awaiter(this, void 0, void 0, function* () {
|
|
30405
|
-
var _a;
|
|
30406
31587
|
const _yield$engine$join = yield engine.join(url, token, {
|
|
30407
31588
|
autoSubscribe: connectOptions.autoSubscribe,
|
|
30408
31589
|
adaptiveStream: typeof roomOptions.adaptiveStream === 'object' ? true : roomOptions.adaptiveStream,
|
|
30409
|
-
clientInfoCapabilities:
|
|
31590
|
+
clientInfoCapabilities: this.getClientInfoCapabilities(roomOptions),
|
|
30410
31591
|
maxRetries: connectOptions.maxRetries,
|
|
30411
31592
|
e2eeEnabled: !!this.e2eeManager,
|
|
30412
31593
|
websocketTimeout: connectOptions.websocketTimeout
|
|
@@ -30966,6 +32147,13 @@ class Room extends eventsExports.EventEmitter {
|
|
|
30966
32147
|
var _a, _b;
|
|
30967
32148
|
return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.clientProtocol) !== null && _b !== void 0 ? _b : CLIENT_PROTOCOL_DEFAULT;
|
|
30968
32149
|
};
|
|
32150
|
+
this.getRemoteParticipantCapabilities = identity => {
|
|
32151
|
+
var _a, _b;
|
|
32152
|
+
return (_b = (_a = this.remoteParticipants.get(identity)) === null || _a === void 0 ? void 0 : _a.capabilities) !== null && _b !== void 0 ? _b : [];
|
|
32153
|
+
};
|
|
32154
|
+
this.getAllRemoteParticipantIdentities = () => {
|
|
32155
|
+
return Array.from(this.remoteParticipants.keys());
|
|
32156
|
+
};
|
|
30969
32157
|
this.onLocalParticipantMetadataChanged = metadata => {
|
|
30970
32158
|
this.emit(RoomEvent.ParticipantMetadataChanged, metadata, this.localParticipant);
|
|
30971
32159
|
};
|
|
@@ -31042,7 +32230,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31042
32230
|
this.options.publishDefaults = Object.assign(Object.assign({}, publishDefaults), options === null || options === void 0 ? void 0 : options.publishDefaults);
|
|
31043
32231
|
this.maybeCreateEngine();
|
|
31044
32232
|
this.incomingDataStreamManager = new IncomingDataStreamManager();
|
|
31045
|
-
this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log);
|
|
32233
|
+
this.outgoingDataStreamManager = new OutgoingDataStreamManager(this.engine, this.log, this.getRemoteParticipantClientProtocol, this.getRemoteParticipantCapabilities, this.getAllRemoteParticipantIdentities);
|
|
31046
32234
|
this.incomingDataTrackManager = new IncomingDataTrackManager({
|
|
31047
32235
|
e2eeManager: this.e2eeManager
|
|
31048
32236
|
});
|
|
@@ -31079,7 +32267,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31079
32267
|
}).on('packetAvailable', _ref => {
|
|
31080
32268
|
let handle = _ref.handle,
|
|
31081
32269
|
bytes = _ref.bytes;
|
|
31082
|
-
this.engine.
|
|
32270
|
+
this.engine.sendDataTrackFrame(bytes).finally(() => this.outgoingDataTrackManager.handlePacketSendComplete(handle));
|
|
31083
32271
|
});
|
|
31084
32272
|
this.registerRpcDataStreamHandler();
|
|
31085
32273
|
this.rpcClientManager = new RpcClientManager(this.log, this.outgoingDataStreamManager, this.getRemoteParticipantClientProtocol, () => {
|
|
@@ -31328,10 +32516,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
31328
32516
|
this.isResuming = false;
|
|
31329
32517
|
this.log.debug('Resumed signal connection');
|
|
31330
32518
|
this.updateSubscriptions();
|
|
31331
|
-
this.emitBufferedEvents();
|
|
31332
32519
|
if (this.setAndEmitConnectionState(ConnectionState.Connected)) {
|
|
31333
32520
|
this.emit(RoomEvent.Reconnected);
|
|
31334
32521
|
}
|
|
32522
|
+
this.emitBufferedEvents();
|
|
31335
32523
|
}).on(EngineEvent.SignalResumed, () => {
|
|
31336
32524
|
this.bufferedEvents = [];
|
|
31337
32525
|
if (this.state === ConnectionState.Reconnecting || this.isResuming) {
|
|
@@ -32162,6 +33350,20 @@ class Room extends eventsExports.EventEmitter {
|
|
|
32162
33350
|
return this.remoteParticipants.get(identity);
|
|
32163
33351
|
}
|
|
32164
33352
|
}
|
|
33353
|
+
/** The client capabilities this SDK advertises to other participants in its `ClientInfo`. */
|
|
33354
|
+
getClientInfoCapabilities(roomOptions) {
|
|
33355
|
+
var _a;
|
|
33356
|
+
const capabilities = [];
|
|
33357
|
+
if (isFrameMetadataSupported((_a = roomOptions.frameMetadata) !== null && _a !== void 0 ? _a : roomOptions.packetTrailer) || !!this.e2eeManager) {
|
|
33358
|
+
capabilities.push(ClientInfo_Capability.CAP_PACKET_TRAILER);
|
|
33359
|
+
}
|
|
33360
|
+
// Advertise deflate-raw decompression support so peers know they can send us compressed data
|
|
33361
|
+
// streams (gated separately from clientProtocol — see the data streams v2 spec).
|
|
33362
|
+
if (isCompressionStreamSupported()) {
|
|
33363
|
+
capabilities.push(ClientInfo_Capability.CAP_COMPRESSION_DEFLATE_RAW);
|
|
33364
|
+
}
|
|
33365
|
+
return capabilities;
|
|
33366
|
+
}
|
|
32165
33367
|
registerRpcDataStreamHandler() {
|
|
32166
33368
|
this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
|
|
32167
33369
|
var _this4 = this;
|
|
@@ -33781,4 +34983,4 @@ const serializers = {
|
|
|
33781
34983
|
json,
|
|
33782
34984
|
raw,
|
|
33783
34985
|
custom
|
|
33784
|
-
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|
|
34986
|
+
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|