livekit-client 2.18.4 → 2.18.6
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.esm.mjs +451 -227
- package/dist/livekit-client.esm.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/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +10 -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/events.d.ts +3 -1
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +8 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +7 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/ts4.2/room/RTCEngine.d.ts +10 -4
- package/dist/ts4.2/room/Room.d.ts +1 -0
- package/dist/ts4.2/room/events.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -0
- package/dist/ts4.2/room/track/LocalTrack.d.ts +7 -0
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/package.json +2 -2
- package/src/api/SignalClient.ts +4 -0
- package/src/room/PCTransport.ts +6 -5
- package/src/room/RTCEngine.ts +41 -29
- package/src/room/RegionUrlProvider.ts +7 -0
- package/src/room/Room.ts +21 -3
- package/src/room/data-track/packet/index.test.ts +16 -21
- package/src/room/data-track/packet/index.ts +3 -3
- package/src/room/events.ts +2 -0
- package/src/room/participant/LocalParticipant.ts +70 -5
- package/src/room/token-source/TokenSource.test.ts +337 -0
- package/src/room/token-source/test-tokens.ts +28 -0
- package/src/room/token-source/utils.test.ts +12 -20
- package/src/room/track/LocalTrack.ts +15 -1
- package/src/room/track/LocalVideoTrack.ts +126 -2
- package/src/room/track/RemoteVideoTrack.ts +8 -2
package/src/room/RTCEngine.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
PublishDataTrackResponse,
|
|
22
22
|
ReconnectReason,
|
|
23
23
|
type ReconnectResponse,
|
|
24
|
+
type RegionSettings,
|
|
24
25
|
RequestResponse,
|
|
25
26
|
Room as RoomModel,
|
|
26
27
|
RoomMovedResponse,
|
|
@@ -62,7 +63,6 @@ import { TTLMap } from '../utils/ttlmap';
|
|
|
62
63
|
import PCTransport, { PCEvents } from './PCTransport';
|
|
63
64
|
import { PCTransportManager, PCTransportState } from './PCTransportManager';
|
|
64
65
|
import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
|
65
|
-
import { DEFAULT_MAX_AGE_MS, type RegionUrlProvider } from './RegionUrlProvider';
|
|
66
66
|
import { DataTrackInfo } from './data-track/types';
|
|
67
67
|
import { roomConnectOptionDefaults } from './defaults';
|
|
68
68
|
import {
|
|
@@ -86,6 +86,7 @@ import type { TrackPublishOptions, VideoCodec } from './track/options';
|
|
|
86
86
|
import { getTrackPublicationInfo } from './track/utils';
|
|
87
87
|
import type { LoggerOptions } from './types';
|
|
88
88
|
import {
|
|
89
|
+
Future,
|
|
89
90
|
isCompressionStreamSupported,
|
|
90
91
|
isVideoCodec,
|
|
91
92
|
isVideoTrack,
|
|
@@ -222,7 +223,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
222
223
|
|
|
223
224
|
private shouldFailOnV1Path: boolean = false;
|
|
224
225
|
|
|
225
|
-
private
|
|
226
|
+
private regionStrategy?: RegionStrategy;
|
|
226
227
|
|
|
227
228
|
private log = log;
|
|
228
229
|
|
|
@@ -249,6 +250,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
249
250
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
250
251
|
private isWaitingForNetworkReconnect: boolean = false;
|
|
251
252
|
|
|
253
|
+
private bufferStatusLowClosingFuture = new Future<never, UnexpectedConnectionState>();
|
|
254
|
+
|
|
252
255
|
constructor(private options: InternalRoomOptions) {
|
|
253
256
|
super();
|
|
254
257
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
|
|
@@ -282,6 +285,13 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
282
285
|
this.client.onParticipantUpdate = (updates) =>
|
|
283
286
|
this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
284
287
|
this.client.onJoined = (joinResponse) => this.emit(EngineEvent.Joined, joinResponse);
|
|
288
|
+
|
|
289
|
+
this.on(EngineEvent.Closing, () => {
|
|
290
|
+
this.bufferStatusLowClosingFuture.reject?.(new UnexpectedConnectionState('engine closed'));
|
|
291
|
+
});
|
|
292
|
+
// Swallow the rejection at the source so it doesn't surface as an unhandled promise rejection
|
|
293
|
+
// when no waitForBufferStatusLow callers are attached.
|
|
294
|
+
this.bufferStatusLowClosingFuture.promise.catch(() => {});
|
|
285
295
|
}
|
|
286
296
|
|
|
287
297
|
/** @internal */
|
|
@@ -332,7 +342,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
332
342
|
this.shouldFailOnV1Path = false;
|
|
333
343
|
throw ConnectionError.serviceNotFound('Simulated v1 path failure', 'v0-rtc');
|
|
334
344
|
}
|
|
335
|
-
log.warn('joining signal with ', url);
|
|
336
345
|
const joinResponse = await this.client.join(
|
|
337
346
|
url,
|
|
338
347
|
token,
|
|
@@ -469,6 +478,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
469
478
|
async cleanupClient() {
|
|
470
479
|
await this.client.close();
|
|
471
480
|
this.client.resetCallbacks();
|
|
481
|
+
// Any in-flight addTrack requests are orphaned by the signal reconnect — the new session
|
|
482
|
+
// won't deliver `trackPublishedResponse` for them, so reject the pending resolvers and
|
|
483
|
+
// clear the map. Otherwise a subsequent `addTrack` call with the same client id (e.g. a
|
|
484
|
+
// publish retry after a `NegotiationError`) throws `TrackInvalidError`.
|
|
485
|
+
for (const cid of Object.keys(this.pendingTrackResolvers)) {
|
|
486
|
+
this.pendingTrackResolvers[cid].reject();
|
|
487
|
+
}
|
|
488
|
+
this.pendingTrackResolvers = {};
|
|
472
489
|
}
|
|
473
490
|
|
|
474
491
|
addTrack(req: AddTrackRequest): Promise<TrackInfo> {
|
|
@@ -532,8 +549,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
532
549
|
}
|
|
533
550
|
|
|
534
551
|
/* @internal */
|
|
535
|
-
|
|
536
|
-
this.
|
|
552
|
+
setRegionStrategy(strategy: RegionStrategy | undefined) {
|
|
553
|
+
this.regionStrategy = strategy;
|
|
537
554
|
}
|
|
538
555
|
|
|
539
556
|
private async configure(joinResponse?: JoinResponse, useSinglePeerConnection?: boolean) {
|
|
@@ -684,7 +701,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
684
701
|
|
|
685
702
|
this.client.onTokenRefresh = (token: string) => {
|
|
686
703
|
this.token = token;
|
|
687
|
-
this.
|
|
704
|
+
this.emit(EngineEvent.TokenRefreshed, token);
|
|
688
705
|
};
|
|
689
706
|
|
|
690
707
|
this.client.onRemoteMuteChanged = (trackSid: string, muted: boolean) => {
|
|
@@ -726,13 +743,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
726
743
|
|
|
727
744
|
this.client.onLeave = (leave: LeaveRequest) => {
|
|
728
745
|
this.log.debug('client leave request', { ...this.logContext, reason: leave?.reason });
|
|
729
|
-
if (leave.regions
|
|
746
|
+
if (leave.regions) {
|
|
730
747
|
this.log.debug('updating regions', this.logContext);
|
|
731
|
-
this.
|
|
732
|
-
updatedAtInMs: Date.now(),
|
|
733
|
-
maxAgeInMs: DEFAULT_MAX_AGE_MS,
|
|
734
|
-
regionSettings: leave.regions,
|
|
735
|
-
});
|
|
748
|
+
this.emit(EngineEvent.ServerRegionsReported, leave.regions);
|
|
736
749
|
}
|
|
737
750
|
switch (leave.action) {
|
|
738
751
|
case LeaveRequest_Action.DISCONNECT:
|
|
@@ -1137,10 +1150,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1137
1150
|
this.log.debug(`reconnecting in ${delay}ms`, this.logContext);
|
|
1138
1151
|
|
|
1139
1152
|
this.clearReconnectTimeout();
|
|
1140
|
-
if (this.token
|
|
1153
|
+
if (this.token) {
|
|
1141
1154
|
// token may have been refreshed, we do not want to recreate the regionUrlProvider
|
|
1142
1155
|
// since the current engine may have inherited a regional url
|
|
1143
|
-
this.
|
|
1156
|
+
this.emit(EngineEvent.TokenRefreshed, this.token);
|
|
1144
1157
|
}
|
|
1145
1158
|
this.reconnectTimeout = CriticalTimers.setTimeout(
|
|
1146
1159
|
() =>
|
|
@@ -1273,17 +1286,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1273
1286
|
throw new SignalReconnectError('Signal connection got severed during reconnect');
|
|
1274
1287
|
}
|
|
1275
1288
|
|
|
1276
|
-
this.
|
|
1289
|
+
this.regionStrategy?.resetAttempts();
|
|
1277
1290
|
// reconnect success
|
|
1278
1291
|
this.emit(EngineEvent.Restarted);
|
|
1279
1292
|
} catch (error) {
|
|
1280
|
-
const nextRegionUrl = await this.
|
|
1293
|
+
const nextRegionUrl = await this.regionStrategy?.getNextUrl();
|
|
1281
1294
|
if (nextRegionUrl) {
|
|
1282
1295
|
await this.restartConnection(nextRegionUrl);
|
|
1283
1296
|
return;
|
|
1284
1297
|
} else {
|
|
1285
1298
|
// no more regions to try (or we're not on cloud)
|
|
1286
|
-
this.
|
|
1299
|
+
this.regionStrategy?.resetAttempts();
|
|
1287
1300
|
throw error;
|
|
1288
1301
|
}
|
|
1289
1302
|
}
|
|
@@ -1578,23 +1591,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1578
1591
|
if (this.isBufferStatusLow(kind)) {
|
|
1579
1592
|
resolve();
|
|
1580
1593
|
} else {
|
|
1581
|
-
const onClosing = () => reject(new UnexpectedConnectionState('engine closed'));
|
|
1582
|
-
this.once(EngineEvent.Closing, onClosing);
|
|
1583
1594
|
const dc = this.dataChannelForKind(kind);
|
|
1584
1595
|
if (!dc) {
|
|
1585
1596
|
reject(new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`));
|
|
1586
1597
|
return;
|
|
1587
1598
|
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
resolve();
|
|
1593
|
-
},
|
|
1594
|
-
{
|
|
1595
|
-
once: true,
|
|
1596
|
-
},
|
|
1597
|
-
);
|
|
1599
|
+
this.bufferStatusLowClosingFuture.promise.catch((e) => reject(e));
|
|
1600
|
+
dc.addEventListener('bufferedamountlow', () => resolve(), {
|
|
1601
|
+
once: true,
|
|
1602
|
+
});
|
|
1598
1603
|
}
|
|
1599
1604
|
});
|
|
1600
1605
|
}
|
|
@@ -2023,8 +2028,15 @@ export type EngineEventCallbacks = {
|
|
|
2023
2028
|
dataTrackSubscriberHandles: (event: DataTrackSubscriberHandles) => void;
|
|
2024
2029
|
dataTrackPacketReceived: (packet: Uint8Array) => void;
|
|
2025
2030
|
joined: (joinResponse: JoinResponse) => void;
|
|
2031
|
+
tokenRefreshed: (token: string) => void;
|
|
2032
|
+
serverRegionsReported: (regions: RegionSettings) => void;
|
|
2026
2033
|
};
|
|
2027
2034
|
|
|
2035
|
+
export interface RegionStrategy {
|
|
2036
|
+
getNextUrl(abortSignal?: AbortSignal): Promise<string | null>;
|
|
2037
|
+
resetAttempts(): void;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2028
2040
|
function applyUserDataCompat(newObj: DataPacket, oldObj: UserPacket) {
|
|
2029
2041
|
const participantIdentity = newObj.participantIdentity
|
|
2030
2042
|
? newObj.participantIdentity
|
|
@@ -189,6 +189,13 @@ export class RegionUrlProvider {
|
|
|
189
189
|
|
|
190
190
|
updateToken(token: string) {
|
|
191
191
|
this.token = token;
|
|
192
|
+
const url = this.getServerUrl();
|
|
193
|
+
const settings = RegionUrlProvider.cache.get(url.hostname);
|
|
194
|
+
RegionUrlProvider.scheduleRefetch(
|
|
195
|
+
this.serverUrl,
|
|
196
|
+
this.token,
|
|
197
|
+
settings?.maxAgeInMs ?? DEFAULT_MAX_AGE_MS,
|
|
198
|
+
);
|
|
192
199
|
}
|
|
193
200
|
|
|
194
201
|
isCloud() {
|
package/src/room/Room.ts
CHANGED
|
@@ -47,8 +47,8 @@ import TypedPromise from '../utils/TypedPromise';
|
|
|
47
47
|
import { getBrowser } from '../utils/browserParser';
|
|
48
48
|
import { BackOffStrategy } from './BackOffStrategy';
|
|
49
49
|
import DeviceManager from './DeviceManager';
|
|
50
|
-
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
51
|
-
import { RegionUrlProvider } from './RegionUrlProvider';
|
|
50
|
+
import RTCEngine, { DataChannelKind, type RegionStrategy } from './RTCEngine';
|
|
51
|
+
import { DEFAULT_MAX_AGE_MS, RegionUrlProvider } from './RegionUrlProvider';
|
|
52
52
|
import IncomingDataStreamManager from './data-stream/incoming/IncomingDataStreamManager';
|
|
53
53
|
import {
|
|
54
54
|
type ByteStreamHandler,
|
|
@@ -668,6 +668,16 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
668
668
|
}),
|
|
669
669
|
);
|
|
670
670
|
this.incomingDataTrackManager.receiveSfuPublicationUpdates(mapped);
|
|
671
|
+
})
|
|
672
|
+
.on(EngineEvent.TokenRefreshed, (token) => {
|
|
673
|
+
this.regionUrlProvider?.updateToken(token);
|
|
674
|
+
})
|
|
675
|
+
.on(EngineEvent.ServerRegionsReported, (regions) => {
|
|
676
|
+
this.regionUrlProvider?.setServerReportedRegions({
|
|
677
|
+
regionSettings: regions,
|
|
678
|
+
updatedAtInMs: Date.now(),
|
|
679
|
+
maxAgeInMs: DEFAULT_MAX_AGE_MS,
|
|
680
|
+
});
|
|
671
681
|
});
|
|
672
682
|
|
|
673
683
|
if (this.localParticipant) {
|
|
@@ -681,6 +691,14 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
681
691
|
}
|
|
682
692
|
}
|
|
683
693
|
|
|
694
|
+
private createRegionStrategy(): RegionStrategy {
|
|
695
|
+
return {
|
|
696
|
+
getNextUrl: async (signal?: AbortSignal) =>
|
|
697
|
+
this.regionUrlProvider ? this.regionUrlProvider.getNextBestRegionUrl(signal) : null,
|
|
698
|
+
resetAttempts: () => this.regionUrlProvider?.resetAttempts(),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
684
702
|
/**
|
|
685
703
|
* getLocalDevices abstracts navigator.mediaDevices.enumerateDevices.
|
|
686
704
|
* In particular, it requests device permissions by default if needed
|
|
@@ -965,7 +983,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
|
965
983
|
this.maybeCreateEngine();
|
|
966
984
|
}
|
|
967
985
|
if (this.regionUrlProvider?.isCloud()) {
|
|
968
|
-
this.engine.
|
|
986
|
+
this.engine.setRegionStrategy(this.createRegionStrategy());
|
|
969
987
|
}
|
|
970
988
|
|
|
971
989
|
this.acquireAudioContext();
|
|
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
import { DataTrackPacket, DataTrackPacketHeader, FrameMarker } from '.';
|
|
4
4
|
import { DataTrackHandle } from '../handle';
|
|
5
5
|
import { DataTrackTimestamp, WrapAroundUnsignedInt } from '../utils';
|
|
6
|
-
import { EXT_FLAG_SHIFT } from './constants';
|
|
6
|
+
import { EXT_FLAG_SHIFT, EXT_WORDS_INDICATOR_SIZE } from './constants';
|
|
7
7
|
import {
|
|
8
8
|
DataTrackE2eeExtension,
|
|
9
9
|
DataTrackExtensionTag,
|
|
@@ -72,7 +72,7 @@ describe('DataTrackPacket', () => {
|
|
|
72
72
|
|
|
73
73
|
const packet = new DataTrackPacket(header, payloadBytes);
|
|
74
74
|
|
|
75
|
-
expect(packet.toBinaryLengthBytes()).toStrictEqual(
|
|
75
|
+
expect(packet.toBinaryLengthBytes()).toStrictEqual(72);
|
|
76
76
|
expect(packet.toBinary()).toStrictEqual(
|
|
77
77
|
new Uint8Array([
|
|
78
78
|
0xc, // Version 0, final, extension
|
|
@@ -120,8 +120,6 @@ describe('DataTrackPacket', () => {
|
|
|
120
120
|
17,
|
|
121
121
|
|
|
122
122
|
0, // Extension padding
|
|
123
|
-
0,
|
|
124
|
-
0,
|
|
125
123
|
|
|
126
124
|
0xfa, // Payload
|
|
127
125
|
0xfa,
|
|
@@ -174,7 +172,7 @@ describe('DataTrackPacket', () => {
|
|
|
174
172
|
|
|
175
173
|
const packet = new DataTrackPacket(header, payloadBytes);
|
|
176
174
|
|
|
177
|
-
expect(packet.toBinaryLengthBytes()).toStrictEqual(
|
|
175
|
+
expect(packet.toBinaryLengthBytes()).toStrictEqual(64);
|
|
178
176
|
expect(packet.toBinary()).toStrictEqual(
|
|
179
177
|
new Uint8Array([
|
|
180
178
|
0x14, // Version 0, start, extension
|
|
@@ -190,7 +188,7 @@ describe('DataTrackPacket', () => {
|
|
|
190
188
|
0,
|
|
191
189
|
104,
|
|
192
190
|
0, // RTP oriented extension words (big endian)
|
|
193
|
-
|
|
191
|
+
4,
|
|
194
192
|
|
|
195
193
|
// E2ee extension
|
|
196
194
|
1, // ID 1
|
|
@@ -210,6 +208,8 @@ describe('DataTrackPacket', () => {
|
|
|
210
208
|
0x3c,
|
|
211
209
|
|
|
212
210
|
0, // Extension padding
|
|
211
|
+
0,
|
|
212
|
+
0,
|
|
213
213
|
|
|
214
214
|
0xfa, // Payload
|
|
215
215
|
0xfa,
|
|
@@ -350,8 +350,8 @@ describe('DataTrackPacket', () => {
|
|
|
350
350
|
const packetBytes = new Uint8Array([
|
|
351
351
|
...VALID_PACKET_BYTES,
|
|
352
352
|
|
|
353
|
-
0, // Extension word (big endian)
|
|
354
|
-
|
|
353
|
+
0, // Extension word (big endian) — data_budget = (0+1)*4 - 2 = 2, but no data follows
|
|
354
|
+
0,
|
|
355
355
|
]);
|
|
356
356
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag - should have ext word indicator here
|
|
357
357
|
|
|
@@ -409,7 +409,7 @@ describe('DataTrackPacket', () => {
|
|
|
409
409
|
0, // Extension words (big endian)
|
|
410
410
|
extensionWords,
|
|
411
411
|
|
|
412
|
-
...new Array((extensionWords + 1)
|
|
412
|
+
...new Array((extensionWords + 1) * 4 - EXT_WORDS_INDICATOR_SIZE).fill(0), // Padding
|
|
413
413
|
]);
|
|
414
414
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag
|
|
415
415
|
|
|
@@ -423,11 +423,11 @@ describe('DataTrackPacket', () => {
|
|
|
423
423
|
...VALID_PACKET_BYTES,
|
|
424
424
|
|
|
425
425
|
0, // RTP oriented extension words (big endian)
|
|
426
|
-
|
|
426
|
+
4,
|
|
427
427
|
|
|
428
428
|
// E2ee extension
|
|
429
429
|
1, // ID 1
|
|
430
|
-
|
|
430
|
+
13, // Length 13
|
|
431
431
|
0xfa, // Key index
|
|
432
432
|
0x3c, // Iv array
|
|
433
433
|
0x3c,
|
|
@@ -443,6 +443,8 @@ describe('DataTrackPacket', () => {
|
|
|
443
443
|
0x3c,
|
|
444
444
|
|
|
445
445
|
0, // Padding
|
|
446
|
+
0,
|
|
447
|
+
0,
|
|
446
448
|
]);
|
|
447
449
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag
|
|
448
450
|
|
|
@@ -465,7 +467,7 @@ describe('DataTrackPacket', () => {
|
|
|
465
467
|
|
|
466
468
|
// User timestamp extension
|
|
467
469
|
2, // ID 2
|
|
468
|
-
|
|
470
|
+
8, // Length 8
|
|
469
471
|
0x44, // Timestamp (big endian)
|
|
470
472
|
0x11,
|
|
471
473
|
0x22,
|
|
@@ -474,9 +476,6 @@ describe('DataTrackPacket', () => {
|
|
|
474
476
|
0x11,
|
|
475
477
|
0x88,
|
|
476
478
|
0x11,
|
|
477
|
-
|
|
478
|
-
0, // Padding
|
|
479
|
-
0,
|
|
480
479
|
]);
|
|
481
480
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag
|
|
482
481
|
|
|
@@ -505,11 +504,9 @@ describe('DataTrackPacket', () => {
|
|
|
505
504
|
0x4,
|
|
506
505
|
0x5,
|
|
507
506
|
0x6,
|
|
508
|
-
0x0,
|
|
509
507
|
|
|
510
508
|
0x0, // Padding
|
|
511
509
|
0x0,
|
|
512
|
-
0x0,
|
|
513
510
|
]);
|
|
514
511
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag
|
|
515
512
|
|
|
@@ -525,12 +522,10 @@ describe('DataTrackPacket', () => {
|
|
|
525
522
|
const packetBytes = new Uint8Array([
|
|
526
523
|
...VALID_PACKET_BYTES,
|
|
527
524
|
|
|
528
|
-
0, // RTP oriented extension words (big endian)
|
|
525
|
+
0, // RTP oriented extension words (big endian, data_budget = 2)
|
|
529
526
|
0,
|
|
530
527
|
|
|
531
|
-
0x0, //
|
|
532
|
-
0x0,
|
|
533
|
-
0x0,
|
|
528
|
+
0x0, // Only 1 byte, but 2 needed
|
|
534
529
|
]);
|
|
535
530
|
packetBytes[0] |= 1 << EXT_FLAG_SHIFT; // Extension flag
|
|
536
531
|
|
|
@@ -65,8 +65,8 @@ export class DataTrackPacketHeader extends Serializable {
|
|
|
65
65
|
|
|
66
66
|
private extensionsMetrics() {
|
|
67
67
|
const lengthBytes = this.extensions.toBinaryLengthBytes();
|
|
68
|
-
const lengthWords = Math.ceil(lengthBytes / 4);
|
|
69
|
-
const paddingLengthBytes = lengthWords * 4 - lengthBytes;
|
|
68
|
+
const lengthWords = Math.ceil((EXT_WORDS_INDICATOR_SIZE + lengthBytes) / 4);
|
|
69
|
+
const paddingLengthBytes = lengthWords * 4 - EXT_WORDS_INDICATOR_SIZE - lengthBytes;
|
|
70
70
|
|
|
71
71
|
return { lengthBytes, lengthWords, paddingLengthBytes };
|
|
72
72
|
}
|
|
@@ -242,7 +242,7 @@ export class DataTrackPacketHeader extends Serializable {
|
|
|
242
242
|
// potentially unintuitive so I wanted to call it out.
|
|
243
243
|
const extensionWords = rtpOrientedExtensionWords + 1;
|
|
244
244
|
|
|
245
|
-
let extensionLengthBytes = 4 * extensionWords;
|
|
245
|
+
let extensionLengthBytes = 4 * extensionWords - EXT_WORDS_INDICATOR_SIZE;
|
|
246
246
|
|
|
247
247
|
if (byteIndex + extensionLengthBytes > dataView.byteLength) {
|
|
248
248
|
throw DataTrackDeserializeError.headerOverrun();
|
package/src/room/events.ts
CHANGED
|
@@ -631,6 +631,8 @@ export enum EngineEvent {
|
|
|
631
631
|
DataTrackSubscriberHandles = 'dataTrackSubscriberHandles',
|
|
632
632
|
DataTrackPacketReceived = 'dataTrackPacketReceived',
|
|
633
633
|
Joined = 'joined',
|
|
634
|
+
TokenRefreshed = 'tokenRefreshed',
|
|
635
|
+
ServerRegionsReported = 'serverRegionsReported',
|
|
634
636
|
}
|
|
635
637
|
|
|
636
638
|
export enum TrackEvent {
|
|
@@ -39,6 +39,7 @@ import { defaultVideoCodec } from '../defaults';
|
|
|
39
39
|
import {
|
|
40
40
|
DeviceUnsupportedError,
|
|
41
41
|
LivekitError,
|
|
42
|
+
NegotiationError,
|
|
42
43
|
PublishTrackError,
|
|
43
44
|
SignalRequestError,
|
|
44
45
|
TrackInvalidError,
|
|
@@ -656,11 +657,12 @@ export default class LocalParticipant extends Participant {
|
|
|
656
657
|
if (track && track.track) {
|
|
657
658
|
// screenshare cannot be muted, unpublish instead
|
|
658
659
|
if (source === Track.Source.ScreenShare) {
|
|
659
|
-
|
|
660
|
+
const unpublishPromises = [this.unpublishTrack(track.track)];
|
|
660
661
|
const screenAudioTrack = this.getTrackPublication(Track.Source.ScreenShareAudio);
|
|
661
662
|
if (screenAudioTrack && screenAudioTrack.track) {
|
|
662
|
-
this.unpublishTrack(screenAudioTrack.track);
|
|
663
|
+
unpublishPromises.push(this.unpublishTrack(screenAudioTrack.track));
|
|
663
664
|
}
|
|
665
|
+
[track] = await Promise.all(unpublishPromises);
|
|
664
666
|
} else {
|
|
665
667
|
await track.mute();
|
|
666
668
|
}
|
|
@@ -806,10 +808,42 @@ export default class LocalParticipant extends Participant {
|
|
|
806
808
|
return this.publishOrRepublishTrack(track, options);
|
|
807
809
|
}
|
|
808
810
|
|
|
811
|
+
/**
|
|
812
|
+
* Waits for the engine's next `Restarted` event. Unlike `engine.waitForRestarted`, this does
|
|
813
|
+
* not short-circuit when `pcState === Connected` — at the point this is called (right after a
|
|
814
|
+
* `NegotiationError`) the PC transport is still connected, but `fullReconnectOnNext` has been
|
|
815
|
+
* set and `attemptReconnect` is queued via setTimeout. We need to wait for that restart to
|
|
816
|
+
* actually complete (which clears `pendingTrackResolvers` via `cleanupClient`) before retrying.
|
|
817
|
+
*/
|
|
818
|
+
private waitForNextEngineRestart(timeoutMs = 15_000): Promise<void> {
|
|
819
|
+
return new Promise<void>((resolve, reject) => {
|
|
820
|
+
const cleanup = () => {
|
|
821
|
+
clearTimeout(timeout);
|
|
822
|
+
this.engine.off(EngineEvent.Restarted, onRestarted);
|
|
823
|
+
this.engine.off(EngineEvent.Closing, onClosing);
|
|
824
|
+
};
|
|
825
|
+
const onRestarted = () => {
|
|
826
|
+
cleanup();
|
|
827
|
+
resolve();
|
|
828
|
+
};
|
|
829
|
+
const onClosing = () => {
|
|
830
|
+
cleanup();
|
|
831
|
+
reject(new Error('engine closed before restart completed'));
|
|
832
|
+
};
|
|
833
|
+
const timeout = setTimeout(() => {
|
|
834
|
+
cleanup();
|
|
835
|
+
reject(new Error('timed out waiting for engine restart'));
|
|
836
|
+
}, timeoutMs);
|
|
837
|
+
this.engine.once(EngineEvent.Restarted, onRestarted);
|
|
838
|
+
this.engine.once(EngineEvent.Closing, onClosing);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
809
842
|
private async publishOrRepublishTrack(
|
|
810
843
|
track: LocalTrack | MediaStreamTrack,
|
|
811
844
|
options?: TrackPublishOptions,
|
|
812
845
|
isRepublish = false,
|
|
846
|
+
hasRetriedAfterNegotiationError = false,
|
|
813
847
|
): Promise<LocalTrackPublication> {
|
|
814
848
|
if (isLocalAudioTrack(track)) {
|
|
815
849
|
track.setAudioContext(this.audioContext);
|
|
@@ -978,6 +1012,15 @@ export default class LocalParticipant extends Participant {
|
|
|
978
1012
|
const publication = await publishPromise;
|
|
979
1013
|
return publication;
|
|
980
1014
|
} catch (e) {
|
|
1015
|
+
if (!hasRetriedAfterNegotiationError && e instanceof NegotiationError) {
|
|
1016
|
+
this.log.warn('negotiation due to track publish failed, retrying after reconnect', {
|
|
1017
|
+
...this.logContext,
|
|
1018
|
+
error: e,
|
|
1019
|
+
});
|
|
1020
|
+
this.pendingPublishPromises.delete(track);
|
|
1021
|
+
await this.waitForNextEngineRestart();
|
|
1022
|
+
return await this.publishOrRepublishTrack(track, options, isRepublish, true);
|
|
1023
|
+
}
|
|
981
1024
|
throw e;
|
|
982
1025
|
} finally {
|
|
983
1026
|
this.pendingPublishPromises.delete(track);
|
|
@@ -1272,7 +1315,11 @@ export default class LocalParticipant extends Participant {
|
|
|
1272
1315
|
resolve(ti);
|
|
1273
1316
|
} catch (err) {
|
|
1274
1317
|
if (track.sender && this.engine.pcManager?.publisher) {
|
|
1275
|
-
|
|
1318
|
+
try {
|
|
1319
|
+
this.engine.pcManager.publisher.removeTrack(track.sender);
|
|
1320
|
+
} catch (e) {
|
|
1321
|
+
this.log.error(e, this.logContext);
|
|
1322
|
+
}
|
|
1276
1323
|
await this.engine.negotiate().catch((negotiateErr) => {
|
|
1277
1324
|
this.log.error(
|
|
1278
1325
|
'failed to negotiate after removing track due to failed add track request',
|
|
@@ -1333,6 +1380,17 @@ export default class LocalParticipant extends Participant {
|
|
|
1333
1380
|
publication.options = opts;
|
|
1334
1381
|
track.sid = ti.sid;
|
|
1335
1382
|
|
|
1383
|
+
// keep publish options on the video track so that it can recompute encoding
|
|
1384
|
+
// parameters when the MediaStreamTrack is restarted (e.g. after switching cameras).
|
|
1385
|
+
// Seed the dimensions we encoded at publish time so the first no-op restart
|
|
1386
|
+
// (e.g. unmute with unchanged constraints) can skip the recompute.
|
|
1387
|
+
if (isLocalVideoTrack(track)) {
|
|
1388
|
+
track.publishOptions = opts;
|
|
1389
|
+
if (req.width && req.height) {
|
|
1390
|
+
track.lastEncodedDimensions = { width: req.width, height: req.height };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1336
1394
|
this.log.debug(`publishing ${track.kind} with encodings`, {
|
|
1337
1395
|
...this.logContext,
|
|
1338
1396
|
encodings,
|
|
@@ -1587,13 +1645,20 @@ export default class LocalParticipant extends Participant {
|
|
|
1587
1645
|
negotiationNeeded = true;
|
|
1588
1646
|
}
|
|
1589
1647
|
}
|
|
1590
|
-
|
|
1648
|
+
try {
|
|
1649
|
+
negotiationNeeded = this.engine.removeTrack(trackSender);
|
|
1650
|
+
} catch (e) {
|
|
1651
|
+
this.log.warn(e, this.logContext);
|
|
1591
1652
|
negotiationNeeded = true;
|
|
1592
1653
|
}
|
|
1654
|
+
|
|
1593
1655
|
if (isLocalVideoTrack(track)) {
|
|
1594
1656
|
for (const [, trackInfo] of track.simulcastCodecs) {
|
|
1595
1657
|
if (trackInfo.sender) {
|
|
1596
|
-
|
|
1658
|
+
try {
|
|
1659
|
+
negotiationNeeded = this.engine.removeTrack(trackInfo.sender);
|
|
1660
|
+
} catch (e) {
|
|
1661
|
+
this.log.warn(e, this.logContext);
|
|
1597
1662
|
negotiationNeeded = true;
|
|
1598
1663
|
}
|
|
1599
1664
|
trackInfo.sender = undefined;
|