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