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.
- 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 +37 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2279 -651
- 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 +37 -7
- 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/api/SignalClient.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +3 -1
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +2 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +31 -26
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/RegionUrlProvider.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +4 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.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/participant/publishUtils.d.ts +7 -0
- package/dist/src/room/participant/publishUtils.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/track/LocalAudioTrack.d.ts +1 -0
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +11 -1
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +13 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/signalToken.d.ts +26 -0
- package/dist/src/test/signalToken.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/src/utils/dataPacketBuffer.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/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +2 -0
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +31 -26
- package/dist/ts4.2/room/Room.d.ts +4 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +9 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/participant/publishUtils.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +13 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +18 -0
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +34 -2
- package/src/index.ts +2 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.ts +66 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +238 -6
- package/src/room/RTCEngine.ts +101 -338
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.test.ts +4 -1
- package/src/room/Room.ts +47 -9
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +8 -2
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +303 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +70 -8
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +2 -0
- package/src/room/errors.ts +10 -4
- package/src/room/participant/LocalParticipant.ts +29 -6
- package/src/room/participant/Participant.ts +5 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/participant/publishUtils.ts +14 -11
- 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/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +10 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.ts +67 -0
- package/src/test/signalServerSetup.ts +151 -0
- package/src/test/signalToken.ts +59 -0
- package/src/test/vitest-context.d.ts +10 -0
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +46 -1
- 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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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;
|
package/src/room/errors.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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 &&
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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);
|