livekit-client 2.20.1 → 2.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/dist/livekit-client.e2ee.worker.js +1 -1
  2. package/dist/livekit-client.e2ee.worker.js.map +1 -1
  3. package/dist/livekit-client.e2ee.worker.mjs +37 -7
  4. package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
  5. package/dist/livekit-client.esm.mjs +2279 -651
  6. package/dist/livekit-client.esm.mjs.map +1 -1
  7. package/dist/livekit-client.fm.worker.js +1 -1
  8. package/dist/livekit-client.fm.worker.js.map +1 -1
  9. package/dist/livekit-client.fm.worker.mjs +37 -7
  10. package/dist/livekit-client.fm.worker.mjs.map +1 -1
  11. package/dist/livekit-client.umd.js +1 -1
  12. package/dist/livekit-client.umd.js.map +1 -1
  13. package/dist/src/api/SignalClient.d.ts.map +1 -1
  14. package/dist/src/index.d.ts +1 -1
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/logger.d.ts +3 -1
  17. package/dist/src/logger.d.ts.map +1 -1
  18. package/dist/src/room/PCTransport.d.ts +2 -0
  19. package/dist/src/room/PCTransport.d.ts.map +1 -1
  20. package/dist/src/room/PCTransportManager.d.ts +1 -0
  21. package/dist/src/room/PCTransportManager.d.ts.map +1 -1
  22. package/dist/src/room/RTCEngine.d.ts +31 -26
  23. package/dist/src/room/RTCEngine.d.ts.map +1 -1
  24. package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
  25. package/dist/src/room/Room.d.ts +4 -0
  26. package/dist/src/room/Room.d.ts.map +1 -1
  27. package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
  28. package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
  29. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  30. package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
  31. package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
  32. package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
  33. package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
  34. package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
  35. package/dist/src/room/data-channel/types.d.ts +12 -0
  36. package/dist/src/room/data-channel/types.d.ts.map +1 -0
  37. package/dist/src/room/data-stream/compression.d.ts +49 -0
  38. package/dist/src/room/data-stream/compression.d.ts.map +1 -0
  39. package/dist/src/room/data-stream/constants.d.ts +18 -0
  40. package/dist/src/room/data-stream/constants.d.ts.map +1 -0
  41. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  42. package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
  43. package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
  44. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  45. package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
  46. package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
  47. package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
  48. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  49. package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
  50. package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
  51. package/dist/src/room/errors.d.ts +3 -1
  52. package/dist/src/room/errors.d.ts.map +1 -1
  53. package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
  54. package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
  55. package/dist/src/room/participant/Participant.d.ts.map +1 -1
  56. package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
  57. package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
  58. package/dist/src/room/participant/publishUtils.d.ts +7 -0
  59. package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
  60. package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
  61. package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
  62. package/dist/src/room/track/LocalAudioTrack.d.ts +1 -0
  63. package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
  64. package/dist/src/room/track/Track.d.ts.map +1 -1
  65. package/dist/src/room/track/options.d.ts +11 -1
  66. package/dist/src/room/track/options.d.ts.map +1 -1
  67. package/dist/src/room/types.d.ts +17 -0
  68. package/dist/src/room/types.d.ts.map +1 -1
  69. package/dist/src/room/utils.d.ts +13 -0
  70. package/dist/src/room/utils.d.ts.map +1 -1
  71. package/dist/src/test/signalToken.d.ts +26 -0
  72. package/dist/src/test/signalToken.d.ts.map +1 -0
  73. package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
  74. package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
  75. package/dist/src/version.d.ts +5 -1
  76. package/dist/src/version.d.ts.map +1 -1
  77. package/dist/ts4.2/index.d.ts +1 -1
  78. package/dist/ts4.2/logger.d.ts +3 -1
  79. package/dist/ts4.2/room/PCTransport.d.ts +2 -0
  80. package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
  81. package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
  82. package/dist/ts4.2/room/Room.d.ts +4 -0
  83. package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
  84. package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
  85. package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
  86. package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
  87. package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
  88. package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
  89. package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
  90. package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
  91. package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
  92. package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
  93. package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
  94. package/dist/ts4.2/room/errors.d.ts +3 -1
  95. package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
  96. package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
  97. package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
  98. package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
  99. package/dist/ts4.2/room/track/options.d.ts +11 -1
  100. package/dist/ts4.2/room/types.d.ts +17 -0
  101. package/dist/ts4.2/room/utils.d.ts +13 -0
  102. package/dist/ts4.2/test/signalToken.d.ts +26 -0
  103. package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
  104. package/dist/ts4.2/version.d.ts +5 -1
  105. package/package.json +8 -4
  106. package/src/api/SignalClient.e2e.test.ts +264 -0
  107. package/src/api/SignalClient.ts +34 -2
  108. package/src/index.ts +2 -0
  109. package/src/logger.ts +2 -0
  110. package/src/room/PCTransport.ts +66 -31
  111. package/src/room/PCTransportManager.ts +10 -6
  112. package/src/room/RTCEngine.test.ts +238 -6
  113. package/src/room/RTCEngine.ts +101 -338
  114. package/src/room/RegionUrlProvider.ts +4 -2
  115. package/src/room/Room.test.ts +4 -1
  116. package/src/room/Room.ts +47 -9
  117. package/src/room/data-channel/DataChannelManager.test.ts +152 -0
  118. package/src/room/data-channel/DataChannelManager.ts +237 -0
  119. package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
  120. package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
  121. package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
  122. package/src/room/data-channel/LossyDataChannel.ts +125 -0
  123. package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
  124. package/src/room/data-channel/ReliableDataChannel.ts +153 -0
  125. package/src/room/data-channel/types.ts +30 -0
  126. package/src/room/data-stream/compression.ts +117 -0
  127. package/src/room/data-stream/constants.ts +18 -0
  128. package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
  129. package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
  130. package/src/room/data-stream/incoming/StreamReader.ts +8 -2
  131. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
  132. package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
  133. package/src/room/data-stream/outgoing/header-utils.ts +87 -0
  134. package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
  135. package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
  136. package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
  137. package/src/room/errors.ts +10 -4
  138. package/src/room/participant/LocalParticipant.ts +29 -6
  139. package/src/room/participant/Participant.ts +5 -0
  140. package/src/room/participant/RemoteParticipant.ts +19 -5
  141. package/src/room/participant/publishUtils.ts +14 -11
  142. package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
  143. package/src/room/rpc/client/RpcClientManager.ts +1 -4
  144. package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
  145. package/src/room/rpc/server/RpcServerManager.ts +1 -3
  146. package/src/room/track/LocalAudioTrack.ts +16 -0
  147. package/src/room/track/Track.ts +10 -6
  148. package/src/room/track/options.ts +11 -1
  149. package/src/room/types.ts +18 -0
  150. package/src/room/utils.ts +67 -0
  151. package/src/test/signalServerSetup.ts +151 -0
  152. package/src/test/signalToken.ts +59 -0
  153. package/src/test/vitest-context.d.ts +10 -0
  154. package/src/utils/dataPacketBuffer.test.ts +100 -0
  155. package/src/utils/dataPacketBuffer.ts +46 -1
  156. package/src/version.ts +5 -1
@@ -0,0 +1,87 @@
1
+ import {
2
+ DataPacket,
3
+ DataStream_ByteHeader,
4
+ DataStream_CompressionType,
5
+ DataStream_Header,
6
+ DataStream_OperationType,
7
+ DataStream_TextHeader,
8
+ } from '@livekit/protocol';
9
+ import type { ByteStreamInfo, StreamTextOptions, TextStreamInfo } from '../../types';
10
+ import { numberToBigInt } from '../../utils';
11
+
12
+ /** The data-streams-v2 wire signals carried directly on the header: the compression flag and the
13
+ * inline single-packet payload. Both used to live in reserved header attributes; they are now
14
+ * first-class protobuf fields on `DataStream.Header`. */
15
+ export interface StreamHeaderV2Fields {
16
+ /** Compression applied to the inline/chunked payload. Defaults to `NONE` when omitted. */
17
+ compression?: DataStream_CompressionType;
18
+ /** The full payload smuggled into the header for single-packet (inline) sends. */
19
+ inlineContent?: Uint8Array;
20
+ }
21
+
22
+ /** Builds the `DataStream_Header` for a text stream from its info and stream options. */
23
+ export function buildTextStreamHeader(
24
+ info: TextStreamInfo,
25
+ options?: Pick<StreamTextOptions, 'version' | 'replyToStreamId' | 'type'>,
26
+ v2?: StreamHeaderV2Fields,
27
+ ): DataStream_Header {
28
+ return new DataStream_Header({
29
+ streamId: info.id,
30
+ mimeType: info.mimeType,
31
+ topic: info.topic,
32
+ timestamp: numberToBigInt(info.timestamp),
33
+ totalLength: numberToBigInt(info.size),
34
+ attributes: info.attributes,
35
+ compression: v2?.compression ?? DataStream_CompressionType.NONE,
36
+ inlineContent: v2?.inlineContent,
37
+ contentHeader: {
38
+ case: 'textHeader',
39
+ value: new DataStream_TextHeader({
40
+ version: options?.version,
41
+ attachedStreamIds: info.attachedStreamIds,
42
+ replyToStreamId: options?.replyToStreamId,
43
+ operationType:
44
+ options?.type === 'update'
45
+ ? DataStream_OperationType.UPDATE
46
+ : DataStream_OperationType.CREATE,
47
+ }),
48
+ },
49
+ });
50
+ }
51
+
52
+ /** Builds the `DataStream_Header` for a byte stream from its info. */
53
+ export function buildByteStreamHeader(
54
+ info: ByteStreamInfo,
55
+ v2?: StreamHeaderV2Fields,
56
+ ): DataStream_Header {
57
+ return new DataStream_Header({
58
+ streamId: info.id,
59
+ mimeType: info.mimeType,
60
+ topic: info.topic,
61
+ timestamp: numberToBigInt(info.timestamp),
62
+ totalLength: numberToBigInt(info.size),
63
+ attributes: info.attributes,
64
+ compression: v2?.compression ?? DataStream_CompressionType.NONE,
65
+ inlineContent: v2?.inlineContent,
66
+ contentHeader: {
67
+ case: 'byteHeader',
68
+ value: new DataStream_ByteHeader({
69
+ name: info.name,
70
+ }),
71
+ },
72
+ });
73
+ }
74
+
75
+ /** Wraps a `DataStream_Header` in a `DataPacket` ready to be sent over a data channel. */
76
+ export function createStreamHeaderPacket(
77
+ header: DataStream_Header,
78
+ destinationIdentities?: Array<string>,
79
+ ): DataPacket {
80
+ return new DataPacket({
81
+ destinationIdentities,
82
+ value: {
83
+ case: 'streamHeader',
84
+ value: header,
85
+ },
86
+ });
87
+ }
@@ -86,6 +86,37 @@ describe('DataTrackIncomingManager', () => {
86
86
  DataTrackHandle.fromNumber(5),
87
87
  ]);
88
88
  });
89
+
90
+ it('should not republish when a track sid is reassigned', async () => {
91
+ const manager = new IncomingDataTrackManager();
92
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
93
+ 'sfuUpdateSubscription',
94
+ 'trackPublished',
95
+ 'trackUnpublished',
96
+ ]);
97
+
98
+ const pubHandle = DataTrackHandle.fromNumber(5);
99
+ const oldSid = 'old sid';
100
+ const newSid = 'new sid';
101
+
102
+ // Simulate track published
103
+ await manager.receiveSfuPublicationUpdates(
104
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
105
+ );
106
+
107
+ const { track } = await managerEvents.waitFor('trackPublished');
108
+ expect(track.info.sid).toStrictEqual(oldSid);
109
+
110
+ // Simulate publisher full reconnect: same track, new SID
111
+ await manager.receiveSfuPublicationUpdates(
112
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
113
+ );
114
+
115
+ // No publish/unpublish should appear
116
+ expect(managerEvents.areThereBufferedEvents('trackPublished')).toStrictEqual(false);
117
+ expect(managerEvents.areThereBufferedEvents('trackUnpublished')).toStrictEqual(false);
118
+ expect(track.info.sid).toStrictEqual(newSid);
119
+ });
89
120
  });
90
121
 
91
122
  describe('Track subscription', () => {
@@ -1064,6 +1095,278 @@ describe('DataTrackIncomingManager', () => {
1064
1095
  expect(onlyFrame.done).toStrictEqual(false);
1065
1096
  expect(onlyFrame.value?.payload).toStrictEqual(new Uint8Array([0xb1, 0xb2]));
1066
1097
  });
1098
+
1099
+ it('should resubscribe an active subscription when a track sid is reassigned', async () => {
1100
+ const manager = new IncomingDataTrackManager();
1101
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
1102
+ 'sfuUpdateSubscription',
1103
+ 'trackPublished',
1104
+ ]);
1105
+
1106
+ const pubHandle = DataTrackHandle.fromNumber(5);
1107
+ const oldSid = 'old sid';
1108
+ const newSid = 'new sid';
1109
+ const oldSubHandle = DataTrackHandle.fromNumber(6);
1110
+ const newSubHandle = DataTrackHandle.fromNumber(7);
1111
+
1112
+ // Simulate track published
1113
+ await manager.receiveSfuPublicationUpdates(
1114
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1115
+ );
1116
+ const { track } = await managerEvents.waitFor('trackPublished');
1117
+
1118
+ // Subscribe to the track
1119
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(oldSid);
1120
+ const reader = stream.getReader();
1121
+ await managerEvents.waitFor('sfuUpdateSubscription');
1122
+
1123
+ // Simulate SFU assigning subscriber handle
1124
+ manager.receivedSfuSubscriberHandles(new Map([[oldSubHandle, oldSid]]));
1125
+
1126
+ await sfuSubscriptionComplete;
1127
+
1128
+ // Simulate publisher full reconnect: same track, new SID
1129
+ await manager.receiveSfuPublicationUpdates(
1130
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1131
+ );
1132
+
1133
+ // Manager should re-subscribe under the new SID
1134
+ const event = await managerEvents.waitFor('sfuUpdateSubscription');
1135
+ expect(event.subscribe).toStrictEqual(true);
1136
+ expect(event.sid).toStrictEqual(newSid);
1137
+ expect(track.info.sid).toStrictEqual(newSid);
1138
+
1139
+ // Simulate SFU assigning a new subscriber handle
1140
+ manager.receivedSfuSubscriberHandles(new Map([[newSubHandle, newSid]]));
1141
+
1142
+ // Frames received on the new handle reach the existing subscriber
1143
+ manager.packetReceived(
1144
+ new DataTrackPacket(
1145
+ new DataTrackPacketHeader({
1146
+ extensions: new DataTrackExtensions(),
1147
+ frameNumber: WrapAroundUnsignedInt.u16(0),
1148
+ marker: FrameMarker.Single,
1149
+ sequence: WrapAroundUnsignedInt.u16(0),
1150
+ timestamp: DataTrackTimestamp.fromRtpTicks(0),
1151
+ trackHandle: newSubHandle,
1152
+ }),
1153
+ new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]),
1154
+ ).toBinary(),
1155
+ );
1156
+
1157
+ const { value, done } = await reader.read();
1158
+ expect(done).toStrictEqual(false);
1159
+ expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05]));
1160
+ });
1161
+
1162
+ it('should stop routing packets on the old handle once a new handle is assigned', async () => {
1163
+ const manager = new IncomingDataTrackManager();
1164
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
1165
+ 'sfuUpdateSubscription',
1166
+ 'trackPublished',
1167
+ ]);
1168
+
1169
+ const pubHandle = DataTrackHandle.fromNumber(5);
1170
+ const oldSid = 'old sid';
1171
+ const newSid = 'new sid';
1172
+ const oldSubHandle = DataTrackHandle.fromNumber(6);
1173
+ const newSubHandle = DataTrackHandle.fromNumber(7);
1174
+
1175
+ // Simulate track published
1176
+ await manager.receiveSfuPublicationUpdates(
1177
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1178
+ );
1179
+ await managerEvents.waitFor('trackPublished');
1180
+
1181
+ // Subscribe to the track
1182
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(oldSid);
1183
+ const reader = stream.getReader();
1184
+ await managerEvents.waitFor('sfuUpdateSubscription');
1185
+
1186
+ // Simulate SFU assigning subscriber handle
1187
+ manager.receivedSfuSubscriberHandles(new Map([[oldSubHandle, oldSid]]));
1188
+ await sfuSubscriptionComplete;
1189
+
1190
+ // Simulate publisher full reconnect: same track, new SID
1191
+ await manager.receiveSfuPublicationUpdates(
1192
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1193
+ );
1194
+ await managerEvents.waitFor('sfuUpdateSubscription');
1195
+
1196
+ // Simulate SFU assigning a new subscriber handle
1197
+ manager.receivedSfuSubscriberHandles(new Map([[newSubHandle, newSid]]));
1198
+
1199
+ const buildPacket = (trackHandle: DataTrackHandle, frameNumber: number, payload: number[]) =>
1200
+ new DataTrackPacket(
1201
+ new DataTrackPacketHeader({
1202
+ extensions: new DataTrackExtensions(),
1203
+ frameNumber: WrapAroundUnsignedInt.u16(frameNumber),
1204
+ marker: FrameMarker.Single,
1205
+ sequence: WrapAroundUnsignedInt.u16(frameNumber),
1206
+ timestamp: DataTrackTimestamp.fromRtpTicks(0),
1207
+ trackHandle,
1208
+ }),
1209
+ new Uint8Array(payload),
1210
+ ).toBinary();
1211
+
1212
+ // A packet received on the old handle must be dropped, not routed to the subscriber
1213
+ await manager.packetReceived(buildPacket(oldSubHandle, 0, [0xaa, 0xbb]));
1214
+
1215
+ // A packet received on the new handle reaches the subscriber
1216
+ await manager.packetReceived(buildPacket(newSubHandle, 1, [0x01, 0x02]));
1217
+
1218
+ const { value, done } = await reader.read();
1219
+ expect(done).toStrictEqual(false);
1220
+ expect(value?.payload).toStrictEqual(new Uint8Array([0x01, 0x02]));
1221
+ });
1222
+
1223
+ it('should terminate the sfu subscription when the stream is cancelled after a sid reassignment', async () => {
1224
+ const manager = new IncomingDataTrackManager();
1225
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
1226
+ 'sfuUpdateSubscription',
1227
+ 'trackPublished',
1228
+ ]);
1229
+
1230
+ const pubHandle = DataTrackHandle.fromNumber(5);
1231
+ const oldSid = 'old sid';
1232
+ const newSid = 'new sid';
1233
+ const oldSubHandle = DataTrackHandle.fromNumber(6);
1234
+ const newSubHandle = DataTrackHandle.fromNumber(7);
1235
+
1236
+ // Simulate track published
1237
+ await manager.receiveSfuPublicationUpdates(
1238
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1239
+ );
1240
+ await managerEvents.waitFor('trackPublished');
1241
+
1242
+ // Subscribe to the track
1243
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(oldSid);
1244
+ const reader = stream.getReader();
1245
+ await managerEvents.waitFor('sfuUpdateSubscription');
1246
+
1247
+ // Simulate SFU assigning subscriber handle
1248
+ manager.receivedSfuSubscriberHandles(new Map([[oldSubHandle, oldSid]]));
1249
+ await sfuSubscriptionComplete;
1250
+
1251
+ // Simulate publisher full reconnect: same track, new SID
1252
+ await manager.receiveSfuPublicationUpdates(
1253
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1254
+ );
1255
+
1256
+ // Manager should re-subscribe under the new SID
1257
+ const resubscribeEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1258
+ expect(resubscribeEvent.subscribe).toStrictEqual(true);
1259
+ expect(resubscribeEvent.sid).toStrictEqual(newSid);
1260
+
1261
+ // Simulate SFU assigning a new subscriber handle
1262
+ manager.receivedSfuSubscriberHandles(new Map([[newSubHandle, newSid]]));
1263
+
1264
+ // Cancel the stream; the SFU unsubscribe must go out under the new SID
1265
+ await reader.cancel();
1266
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1267
+ expect(endEvent.sid).toStrictEqual(newSid);
1268
+ expect(endEvent.subscribe).toStrictEqual(false);
1269
+ });
1270
+
1271
+ it('should terminate the sfu subscription when the abortsignal fires after a sid reassignment', async () => {
1272
+ const manager = new IncomingDataTrackManager();
1273
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
1274
+ 'sfuUpdateSubscription',
1275
+ 'trackPublished',
1276
+ ]);
1277
+
1278
+ const pubHandle = DataTrackHandle.fromNumber(5);
1279
+ const oldSid = 'old sid';
1280
+ const newSid = 'new sid';
1281
+ const oldSubHandle = DataTrackHandle.fromNumber(6);
1282
+ const newSubHandle = DataTrackHandle.fromNumber(7);
1283
+
1284
+ // Simulate track published
1285
+ await manager.receiveSfuPublicationUpdates(
1286
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1287
+ );
1288
+ await managerEvents.waitFor('trackPublished');
1289
+
1290
+ // Subscribe to the track
1291
+ const controller = new AbortController();
1292
+ const [stream, sfuSubscriptionComplete] = manager.openSubscriptionStream(
1293
+ oldSid,
1294
+ controller.signal,
1295
+ );
1296
+ const reader = stream.getReader();
1297
+ await managerEvents.waitFor('sfuUpdateSubscription');
1298
+
1299
+ // Simulate SFU assigning subscriber handle
1300
+ manager.receivedSfuSubscriberHandles(new Map([[oldSubHandle, oldSid]]));
1301
+ await sfuSubscriptionComplete;
1302
+
1303
+ // Simulate publisher full reconnect: same track, new SID
1304
+ await manager.receiveSfuPublicationUpdates(
1305
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1306
+ );
1307
+
1308
+ // Manager should re-subscribe under the new SID
1309
+ const resubscribeEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1310
+ expect(resubscribeEvent.subscribe).toStrictEqual(true);
1311
+ expect(resubscribeEvent.sid).toStrictEqual(newSid);
1312
+
1313
+ // Simulate SFU assigning a new subscriber handle
1314
+ manager.receivedSfuSubscriberHandles(new Map([[newSubHandle, newSid]]));
1315
+
1316
+ // Abort the subscription; the SFU unsubscribe must go out under the new SID
1317
+ const inFlightReadPromise = reader.read();
1318
+ controller.abort();
1319
+ await expect(inFlightReadPromise).rejects.toThrowError(
1320
+ 'Subscription to data track cancelled by caller',
1321
+ );
1322
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1323
+ expect(endEvent.sid).toStrictEqual(newSid);
1324
+ expect(endEvent.subscribe).toStrictEqual(false);
1325
+ });
1326
+
1327
+ it('should cancel a pending subscription under the new sid after a sid reassignment', async () => {
1328
+ const manager = new IncomingDataTrackManager();
1329
+ const managerEvents = subscribeToEvents<DataTrackIncomingManagerCallbacks>(manager, [
1330
+ 'sfuUpdateSubscription',
1331
+ 'trackPublished',
1332
+ ]);
1333
+
1334
+ const pubHandle = DataTrackHandle.fromNumber(5);
1335
+ const oldSid = 'old sid';
1336
+ const newSid = 'new sid';
1337
+
1338
+ // Simulate track published
1339
+ await manager.receiveSfuPublicationUpdates(
1340
+ new Map([['id', [{ sid: oldSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1341
+ );
1342
+ await managerEvents.waitFor('trackPublished');
1343
+
1344
+ // Begin subscribing to the track, leaving the subscription pending
1345
+ const controller = new AbortController();
1346
+ const [, sfuSubscriptionComplete] = manager.openSubscriptionStream(oldSid, controller.signal);
1347
+ const startEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1348
+ expect(startEvent.sid).toStrictEqual(oldSid);
1349
+ expect(startEvent.subscribe).toStrictEqual(true);
1350
+
1351
+ // Simulate publisher full reconnect: same track, new SID
1352
+ await manager.receiveSfuPublicationUpdates(
1353
+ new Map([['id', [{ sid: newSid, pubHandle, name: 'test', usesE2ee: false }]]]),
1354
+ );
1355
+
1356
+ // Manager should re-subscribe under the new SID
1357
+ const resubscribeEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1358
+ expect(resubscribeEvent.subscribe).toStrictEqual(true);
1359
+ expect(resubscribeEvent.sid).toStrictEqual(newSid);
1360
+
1361
+ // Abort the pending subscription; the SFU unsubscribe must go out under the new SID
1362
+ controller.abort();
1363
+ await expect(sfuSubscriptionComplete).rejects.toStrictEqual(
1364
+ DataTrackSubscribeError.cancelled(),
1365
+ );
1366
+ const endEvent = await managerEvents.waitFor('sfuUpdateSubscription');
1367
+ expect(endEvent.sid).toStrictEqual(newSid);
1368
+ expect(endEvent.subscribe).toStrictEqual(false);
1369
+ });
1067
1370
  });
1068
1371
  });
1069
1372
 
@@ -147,6 +147,10 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
147
147
  let streamController: ReadableStreamDefaultController<DataTrackFrame> | null = null;
148
148
  const sfuSubscriptionComplete = new Future<void, DataTrackSubscribeError>();
149
149
 
150
+ // Hold the descriptor by reference: SID reassignment re-keys the map, so lookups
151
+ // with the SID captured at subscribe time would fail.
152
+ const descriptor = this.descriptors.get(sid);
153
+
150
154
  const detachSignal = () => {
151
155
  signal?.removeEventListener('abort', onAbort);
152
156
  };
@@ -158,8 +162,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
158
162
  log.warn(`ReadableStream subscribed to ${sid} was not started.`);
159
163
  return;
160
164
  }
161
- const descriptor = this.descriptors.get(sid);
162
- if (!descriptor) {
165
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
163
166
  log.warn(`Unknown track ${sid}, skipping cancel...`);
164
167
  return;
165
168
  }
@@ -180,9 +183,8 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
180
183
  if (!streamController) {
181
184
  return;
182
185
  }
183
- const currentDescriptor = this.descriptors.get(sid);
184
- if (currentDescriptor?.subscription.type === 'active') {
185
- currentDescriptor.subscription.streamControllers.delete(streamController);
186
+ if (descriptor?.subscription.type === 'active') {
187
+ descriptor.subscription.streamControllers.delete(streamController);
186
188
  }
187
189
 
188
190
  streamController.error(DataTrackSubscribeError.cancelled());
@@ -198,8 +200,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
198
200
 
199
201
  this.subscribeRequest(sid, signal)
200
202
  .then(async () => {
201
- const descriptor = this.descriptors.get(sid);
202
- if (!descriptor) {
203
+ if (!descriptor || this.descriptors.get(descriptor.info.sid) !== descriptor) {
203
204
  log.error(`Unknown track ${sid}`);
204
205
  const err = DataTrackSubscribeError.disconnected();
205
206
  controller.error(err);
@@ -330,7 +331,7 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
330
331
  descriptor.subscription = { type: 'none' };
331
332
 
332
333
  // Let the SFU know that the subscribe has been cancelled
333
- this.emit('sfuUpdateSubscription', { sid, subscribe: false });
334
+ this.emit('sfuUpdateSubscription', { sid: descriptor.info.sid, subscribe: false });
334
335
 
335
336
  if (previousDescriptorSubscription.type === 'pending') {
336
337
  previousDescriptorSubscription.completionFuture.reject?.(
@@ -458,6 +459,9 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
458
459
  if (this.descriptors.has(info.sid)) {
459
460
  continue;
460
461
  }
462
+ if (this.handleSidReassigned(publisherIdentity, info)) {
463
+ continue;
464
+ }
461
465
  await this.handleTrackPublished(publisherIdentity, info);
462
466
  }
463
467
  publisherParticipantToSidsInUpdate.set(publisherIdentity, sidsInUpdate);
@@ -499,6 +503,62 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
499
503
  this.emit('trackPublished', { track });
500
504
  }
501
505
 
506
+ /**
507
+ * Detects and handles SID reassignment, which occurs when the publisher
508
+ * republishes its tracks after a full reconnect.
509
+ *
510
+ * Returns `true` if an SID reassignment occurred, `false` otherwise.
511
+ */
512
+ private handleSidReassigned(
513
+ publisherIdentity: Participant['identity'],
514
+ info: DataTrackInfo,
515
+ ): boolean {
516
+ // Publisher identity and pub handle are stable across republications.
517
+ const existingEntry = Array.from(this.descriptors.entries()).find(
518
+ ([_sid, descriptor]) =>
519
+ descriptor.publisherIdentity === publisherIdentity &&
520
+ descriptor.info.pubHandle === info.pubHandle,
521
+ );
522
+ if (!existingEntry) {
523
+ return false;
524
+ }
525
+ const [oldSid, descriptor] = existingEntry;
526
+
527
+ // Invariant: other than SID, info should not have changed.
528
+ // TODO: consider refactoring to move SID out of info to allow for direct comparison.
529
+ const { name, usesE2ee } = descriptor.info;
530
+ if (name !== info.name || usesE2ee !== info.usesE2ee) {
531
+ log.warn(`Info mismatch for ${oldSid}, treating as new publication`);
532
+ return false;
533
+ }
534
+
535
+ const newSid = info.sid;
536
+ log.debug(`SID reassigned: ${oldSid} -> ${newSid}`);
537
+
538
+ if (!this.descriptors.delete(oldSid)) {
539
+ return false;
540
+ }
541
+ descriptor.info.sid = newSid;
542
+
543
+ switch (descriptor.subscription.type) {
544
+ case 'none':
545
+ break;
546
+ case 'pending':
547
+ case 'active':
548
+ // The SFU does not carry subscriptions across a publisher's full
549
+ // reconnect; re-request the subscription under the new SID.
550
+ this.emit('sfuUpdateSubscription', { sid: newSid, subscribe: true });
551
+ break;
552
+ }
553
+ if (descriptor.subscription.type === 'active') {
554
+ // Keep the routing index consistent until the SFU assigns a new handle
555
+ // (see `registerSubscriberHandle`).
556
+ this.subscriptionHandles.set(descriptor.subscription.subcriptionHandle, newSid);
557
+ }
558
+ this.descriptors.set(newSid, descriptor);
559
+ return true;
560
+ }
561
+
502
562
  handleTrackUnpublished(sid: DataTrackSid) {
503
563
  const descriptor = this.descriptors.get(sid);
504
564
  if (!descriptor) {
@@ -540,11 +600,13 @@ export default class IncomingDataTrackManager extends (EventEmitter as new () =>
540
600
  }
541
601
  case 'active': {
542
602
  // Update handle for an active subscription. This can occur following a full reconnect.
603
+ this.subscriptionHandles.delete(descriptor.subscription.subcriptionHandle);
543
604
  descriptor.subscription.subcriptionHandle = assignedHandle;
544
605
  this.subscriptionHandles.set(assignedHandle, sid);
545
606
  return;
546
607
  }
547
608
  case 'pending': {
609
+ log.debug(`data track subscription activated`, { sid, handle: assignedHandle });
548
610
  const pipeline = new IncomingDataTrackPipeline({
549
611
  info: descriptor.info,
550
612
  publisherIdentity: descriptor.publisherIdentity,
@@ -326,11 +326,13 @@ export default class OutgoingDataTrackManager extends (EventEmitter as new () =>
326
326
  case 'pending': {
327
327
  if (result.type === 'ok') {
328
328
  const info = result.data;
329
+ log.debug(`SFU accepted publish request for handle ${handle}`, { sid: info.sid });
329
330
  const e2eeManager = info.usesE2ee ? this.e2eeManager : null;
330
331
  this.descriptors.set(info.pubHandle, Descriptor.active(info, e2eeManager));
331
332
 
332
333
  descriptor.completionFuture.resolve?.();
333
334
  } else {
335
+ log.debug(`SFU rejected publish request for handle ${handle}`, { error: result.error });
334
336
  descriptor.completionFuture.reject?.(result.error);
335
337
  }
336
338
  return;
@@ -208,7 +208,7 @@ export class UnsupportedServer extends LivekitError {
208
208
  readonly name = 'UnsupportedServer';
209
209
 
210
210
  constructor(message?: string) {
211
- super(10, message ?? 'unsupported server');
211
+ super(10, message || 'unsupported server');
212
212
  }
213
213
  }
214
214
 
@@ -216,7 +216,7 @@ export class UnexpectedConnectionState extends LivekitError {
216
216
  readonly name = 'UnexpectedConnectionState';
217
217
 
218
218
  constructor(message?: string) {
219
- super(12, message ?? 'unexpected connection state');
219
+ super(12, message || 'unexpected connection state');
220
220
  }
221
221
  }
222
222
 
@@ -224,7 +224,7 @@ export class NegotiationError extends LivekitError {
224
224
  readonly name = 'NegotiationError';
225
225
 
226
226
  constructor(message?: string) {
227
- super(13, message ?? 'unable to negotiate');
227
+ super(13, message || 'unable to negotiate');
228
228
  }
229
229
  }
230
230
 
@@ -232,7 +232,7 @@ export class PublishDataError extends LivekitError {
232
232
  readonly name = 'PublishDataError';
233
233
 
234
234
  constructor(message?: string) {
235
- super(14, message ?? 'unable to publish data');
235
+ super(14, message || 'unable to publish data');
236
236
  }
237
237
  }
238
238
 
@@ -287,6 +287,12 @@ export enum DataStreamErrorReason {
287
287
 
288
288
  // Encryption type mismatch.
289
289
  EncryptionTypeMismatch = 8,
290
+
291
+ // The serialized stream header packet (driven mainly by attributes) exceeds the MTU budget.
292
+ HeaderTooLarge = 9,
293
+
294
+ // The stream's (decompressed) payload exceeds the maximum allowed size.
295
+ PayloadTooLarge = 10,
290
296
  }
291
297
 
292
298
  export class DataStreamError extends LivekitReasonedError<DataStreamErrorReason> {
@@ -82,8 +82,10 @@ import {
82
82
  sourceToKind,
83
83
  } from '../track/utils';
84
84
  import {
85
+ type ByteStreamInfo,
85
86
  type ChatMessage,
86
87
  type DataPublishOptions,
88
+ type SendBytesOptions,
87
89
  type SendFileOptions,
88
90
  type SendTextOptions,
89
91
  type StreamBytesOptions,
@@ -100,6 +102,7 @@ import {
100
102
  isLocalVideoTrack,
101
103
  isSVCCodec,
102
104
  isSafari17Based,
105
+ isVideoCodec,
103
106
  isVideoTrack,
104
107
  isWeb,
105
108
  sleep,
@@ -1249,12 +1252,21 @@ export default class LocalParticipant extends Participant {
1249
1252
  maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
1250
1253
  });
1251
1254
  }
1252
- } else if (track.codec && isSVCCodec(track.codec) && encodings[0]?.maxBitrate) {
1253
- this.engine.pcManager.publisher.setTrackCodecBitrate({
1254
- cid: req.cid,
1255
- codec: track.codec,
1256
- maxbr: encodings[0].maxBitrate / 1000,
1257
- });
1255
+ } else if (track.codec && isVideoCodec(track.codec)) {
1256
+ // Apply start bitrate for all video codecs to prevent initial blurriness.
1257
+ // - SVC codecs: use first encoding's bitrate (single stream with built-in layers)
1258
+ // - Simulcast: sum all encoding bitrates (independent streams, BWE needs total)
1259
+ const targetBitrate = isSVCCodec(track.codec)
1260
+ ? (encodings[0]?.maxBitrate ?? 0)
1261
+ : encodings.reduce((sum, enc) => sum + (enc.maxBitrate ?? 0), 0);
1262
+ if (targetBitrate > 0) {
1263
+ this.engine.pcManager.publisher.setTrackCodecBitrate({
1264
+ cid: req.cid,
1265
+ codec: track.codec,
1266
+ maxbr: targetBitrate / 1000,
1267
+ isScreenShare: track.source === Track.Source.ScreenShare,
1268
+ });
1269
+ }
1258
1270
  }
1259
1271
  }
1260
1272
 
@@ -1841,6 +1853,17 @@ export default class LocalParticipant extends Participant {
1841
1853
  return this.roomOutgoingDataStreamManager.sendFile(file, options);
1842
1854
  }
1843
1855
 
1856
+ /**
1857
+ * Sends the given bytes to participants in the room via the data channel.
1858
+ * For files, consider using {@link sendFile}; for longer/incremental payloads, {@link streamBytes}.
1859
+ *
1860
+ * @param bytes The byte payload
1861
+ * @param options.topic Topic identifier used to route the stream to appropriate handlers.
1862
+ */
1863
+ async sendBytes(bytes: Uint8Array, options?: SendBytesOptions): Promise<ByteStreamInfo> {
1864
+ return this.roomOutgoingDataStreamManager.sendBytes(bytes, options);
1865
+ }
1866
+
1844
1867
  /**
1845
1868
  * Stream bytes incrementally to participants in the room via the data channel.
1846
1869
  * For sending files, consider using {@link sendFile} instead.
@@ -371,6 +371,11 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
371
371
  }
372
372
 
373
373
  protected addTrackPublication(publication: TrackPublication) {
374
+ this.log.debug(`adding track publication`, {
375
+ trackSid: publication.trackSid,
376
+ source: publication.source,
377
+ kind: publication.kind,
378
+ });
374
379
  // forward publication driven events
375
380
  publication.on(TrackEvent.Muted, () => {
376
381
  this.emit(ParticipantEvent.TrackMuted, publication);