livekit-client 2.20.0 → 2.20.2
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 +41 -11
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +744 -178
- 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 +41 -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/e2ee/E2eeManager.d.ts +1 -0
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +1 -0
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -1
- package/dist/src/frameMetadata/types.d.ts +1 -0
- package/dist/src/frameMetadata/types.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 +42 -1
- 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 +46 -4
- 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 +1 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +1 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- 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/data-track/outgoing/types.d.ts +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +1 -0
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.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/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 +1 -0
- 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/track/record.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -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/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +19 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +1 -0
- package/dist/ts4.2/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/e2ee/utils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +1 -0
- package/dist/ts4.2/frameMetadata/types.d.ts +1 -0
- package/dist/ts4.2/logger.d.ts +3 -1
- package/dist/ts4.2/room/PCTransport.d.ts +42 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +46 -4
- package/dist/ts4.2/room/Room.d.ts +1 -0
- package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +7 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +1 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
- 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/Track.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +11 -1
- package/dist/ts4.2/room/utils.d.ts +1 -0
- package/dist/ts4.2/test/signalToken.d.ts +26 -0
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +19 -0
- package/package.json +8 -4
- package/src/api/SignalClient.e2e.test.ts +264 -0
- package/src/api/SignalClient.ts +35 -2
- package/src/e2ee/E2eeManager.ts +3 -1
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/utils.ts +1 -0
- package/src/e2ee/worker/DataCryptor.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +1 -0
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
- package/src/e2ee/worker/e2ee.worker.ts +1 -0
- package/src/e2ee/worker/naluUtils.ts +1 -0
- package/src/e2ee/worker/sifPayload.ts +1 -0
- package/src/frameMetadata/frameMetadata.test.ts +83 -0
- package/src/frameMetadata/frameMetadata.ts +8 -0
- package/src/frameMetadata/types.ts +1 -0
- package/src/logger.ts +2 -0
- package/src/room/PCTransport.test.ts +150 -0
- package/src/room/PCTransport.ts +179 -31
- package/src/room/PCTransportManager.ts +10 -6
- package/src/room/RTCEngine.test.ts +275 -2
- package/src/room/RTCEngine.ts +277 -71
- package/src/room/RegionUrlProvider.ts +4 -2
- package/src/room/Room.ts +8 -1
- package/src/room/data-track/depacketizer.ts +1 -0
- package/src/room/data-track/frame.ts +1 -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/data-track/outgoing/types.ts +1 -0
- package/src/room/data-track/packet/extensions.ts +1 -0
- package/src/room/data-track/packet/index.ts +1 -0
- package/src/room/data-track/packet/serializable.ts +1 -0
- package/src/room/errors.ts +4 -4
- package/src/room/participant/LocalParticipant.ts +17 -6
- package/src/room/participant/Participant.ts +6 -0
- package/src/room/participant/publishUtils.ts +14 -11
- package/src/room/track/LocalAudioTrack.ts +16 -0
- package/src/room/track/Track.ts +11 -6
- package/src/room/track/options.ts +11 -1
- package/src/room/track/record.ts +1 -0
- package/src/room/utils.ts +1 -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/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
- package/src/utils/dataPacketBuffer.test.ts +100 -0
- package/src/utils/dataPacketBuffer.ts +48 -1
|
@@ -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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { coerceToDataView } from '../utils';
|
|
3
4
|
import { EXT_TAG_PADDING, U8_LENGTH_BYTES, U64_LENGTH_BYTES } from './constants';
|
|
4
5
|
import { DataTrackDeserializeError, DataTrackDeserializeErrorReason } from './errors';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { DataTrackHandle, DataTrackHandleError, DataTrackHandleErrorReason } from '../handle';
|
|
3
4
|
import {
|
|
4
5
|
DataTrackTimestamp,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Throws } from '@livekit/throws-transformer/throws';
|
|
2
|
+
import type { NonSharedUint8Array } from '../../../type-polyfills/non-shared-typed-arrays';
|
|
2
3
|
import { DataTrackSerializeError } from './errors';
|
|
3
4
|
|
|
4
5
|
/** An abstract class implementing common behavior related to data track binary serialization. */
|
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
|
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
isFrameMetadataSupported,
|
|
30
30
|
} from '../../frameMetadata/utils';
|
|
31
31
|
import type { InternalRoomOptions } from '../../options';
|
|
32
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
32
33
|
import TypedPromise from '../../utils/TypedPromise';
|
|
33
34
|
import { PCTransportState } from '../PCTransportManager';
|
|
34
35
|
import type RTCEngine from '../RTCEngine';
|
|
@@ -99,6 +100,7 @@ import {
|
|
|
99
100
|
isLocalVideoTrack,
|
|
100
101
|
isSVCCodec,
|
|
101
102
|
isSafari17Based,
|
|
103
|
+
isVideoCodec,
|
|
102
104
|
isVideoTrack,
|
|
103
105
|
isWeb,
|
|
104
106
|
sleep,
|
|
@@ -1248,12 +1250,21 @@ export default class LocalParticipant extends Participant {
|
|
|
1248
1250
|
maxbr: encodings[0]?.maxBitrate ? encodings[0].maxBitrate / 1000 : 0,
|
|
1249
1251
|
});
|
|
1250
1252
|
}
|
|
1251
|
-
} else if (track.codec &&
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1253
|
+
} else if (track.codec && isVideoCodec(track.codec)) {
|
|
1254
|
+
// Apply start bitrate for all video codecs to prevent initial blurriness.
|
|
1255
|
+
// - SVC codecs: use first encoding's bitrate (single stream with built-in layers)
|
|
1256
|
+
// - Simulcast: sum all encoding bitrates (independent streams, BWE needs total)
|
|
1257
|
+
const targetBitrate = isSVCCodec(track.codec)
|
|
1258
|
+
? (encodings[0]?.maxBitrate ?? 0)
|
|
1259
|
+
: encodings.reduce((sum, enc) => sum + (enc.maxBitrate ?? 0), 0);
|
|
1260
|
+
if (targetBitrate > 0) {
|
|
1261
|
+
this.engine.pcManager.publisher.setTrackCodecBitrate({
|
|
1262
|
+
cid: req.cid,
|
|
1263
|
+
codec: track.codec,
|
|
1264
|
+
maxbr: targetBitrate / 1000,
|
|
1265
|
+
isScreenShare: track.source === Track.Source.ScreenShare,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1257
1268
|
}
|
|
1258
1269
|
}
|
|
1259
1270
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { EventEmitter } from 'events';
|
|
13
13
|
import type TypedEmitter from 'typed-emitter';
|
|
14
14
|
import log, { LoggerNames, type StructuredLogger, getLogger } from '../../logger';
|
|
15
|
+
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
15
16
|
import { ParticipantEvent, TrackEvent } from '../events';
|
|
16
17
|
import type LocalTrackPublication from '../track/LocalTrackPublication';
|
|
17
18
|
import type LocalVideoTrack from '../track/LocalVideoTrack';
|
|
@@ -370,6 +371,11 @@ export default class Participant extends (EventEmitter as new () => TypedEmitter
|
|
|
370
371
|
}
|
|
371
372
|
|
|
372
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
|
+
});
|
|
373
379
|
// forward publication driven events
|
|
374
380
|
publication.on(TrackEvent.Muted, () => {
|
|
375
381
|
this.emit(ParticipantEvent.TrackMuted, publication);
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
isSVCCodec,
|
|
20
20
|
isSafariBased,
|
|
21
21
|
isSafariSvcApi,
|
|
22
|
-
unwrapConstraint,
|
|
23
22
|
} from '../utils';
|
|
24
23
|
|
|
25
24
|
/** @internal */
|
|
@@ -468,16 +467,20 @@ export class ScalabilityMode {
|
|
|
468
467
|
}
|
|
469
468
|
}
|
|
470
469
|
|
|
470
|
+
/**
|
|
471
|
+
* Returns the appropriate degradation preference for a video track based on its source.
|
|
472
|
+
*
|
|
473
|
+
* - Camera: 'maintain-framerate' (smoother video for real-time communication)
|
|
474
|
+
* - Screen share: 'maintain-resolution' (clarity is critical for reading text/UI)
|
|
475
|
+
* - Other/unknown: 'balanced'
|
|
476
|
+
*/
|
|
471
477
|
export function getDefaultDegradationPreference(track: LocalVideoTrack): RTCDegradationPreference {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return 'maintain-resolution';
|
|
480
|
-
} else {
|
|
481
|
-
return 'balanced';
|
|
478
|
+
switch (track.source) {
|
|
479
|
+
case Track.Source.Camera:
|
|
480
|
+
return 'maintain-framerate';
|
|
481
|
+
case Track.Source.ScreenShare:
|
|
482
|
+
return 'maintain-resolution';
|
|
483
|
+
default:
|
|
484
|
+
return 'balanced';
|
|
482
485
|
}
|
|
483
486
|
}
|
|
@@ -103,6 +103,22 @@ export default class LocalAudioTrack extends LocalTrack<Track.Kind.Audio> {
|
|
|
103
103
|
await this.restart(constraints);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
async applyConstraints(
|
|
107
|
+
constraints: Pick<
|
|
108
|
+
AudioCaptureOptions,
|
|
109
|
+
'autoGainControl' | 'noiseSuppression' | 'echoCancellation' | 'voiceIsolation'
|
|
110
|
+
>,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const unlock = await this.trackChangeLock.lock();
|
|
113
|
+
try {
|
|
114
|
+
const res = await this._mediaStreamTrack.applyConstraints(constraints);
|
|
115
|
+
this._constraints = { ...this._constraints, ...constraints };
|
|
116
|
+
return res;
|
|
117
|
+
} finally {
|
|
118
|
+
unlock();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
106
122
|
protected async restart(
|
|
107
123
|
constraints?: MediaTrackConstraints,
|
|
108
124
|
isUnmuting?: boolean,
|