livekit-client 2.13.4 → 2.13.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.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +11 -3
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +329 -76
- 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 +5 -5
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
- 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/worker/FrameCryptor.d.ts +2 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +3 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +3 -3
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +8 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +1 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/utils/dataPacketBuffer.d.ts +15 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -0
- package/dist/src/utils/ttlmap.d.ts +20 -0
- package/dist/src/utils/ttlmap.d.ts.map +1 -0
- package/dist/ts4.2/src/api/SignalClient.d.ts +5 -5
- package/dist/ts4.2/src/e2ee/types.d.ts +1 -0
- package/dist/ts4.2/src/e2ee/worker/FrameCryptor.d.ts +2 -1
- package/dist/ts4.2/src/room/PCTransport.d.ts +3 -2
- package/dist/ts4.2/src/room/PCTransportManager.d.ts +3 -3
- package/dist/ts4.2/src/room/RTCEngine.d.ts +8 -0
- package/dist/ts4.2/src/room/Room.d.ts +1 -1
- package/dist/ts4.2/src/utils/dataPacketBuffer.d.ts +15 -0
- package/dist/ts4.2/src/utils/ttlmap.d.ts +20 -0
- package/package.json +8 -8
- package/src/api/SignalClient.ts +12 -10
- package/src/connectionHelper/checks/publishVideo.ts +1 -0
- package/src/e2ee/E2eeManager.ts +3 -0
- package/src/e2ee/types.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.ts +15 -0
- package/src/e2ee/worker/e2ee.worker.ts +2 -0
- package/src/room/PCTransport.ts +30 -4
- package/src/room/PCTransportManager.ts +10 -7
- package/src/room/RTCEngine.ts +78 -9
- package/src/room/Room.ts +11 -11
- package/src/room/track/LocalVideoTrack.ts +14 -15
- package/src/utils/dataPacketBuffer.ts +52 -0
- package/src/utils/ttlmap.ts +96 -0
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "livekit-client",
|
3
|
-
"version": "2.13.
|
3
|
+
"version": "2.13.6",
|
4
4
|
"description": "JavaScript/TypeScript client SDK for LiveKit",
|
5
5
|
"main": "./dist/livekit-client.umd.js",
|
6
6
|
"unpkg": "./dist/livekit-client.umd.js",
|
@@ -37,7 +37,7 @@
|
|
37
37
|
"license": "Apache-2.0",
|
38
38
|
"dependencies": {
|
39
39
|
"@livekit/mutex": "1.1.1",
|
40
|
-
"@livekit/protocol": "1.
|
40
|
+
"@livekit/protocol": "1.39.2",
|
41
41
|
"events": "^3.3.0",
|
42
42
|
"loglevel": "^1.9.2",
|
43
43
|
"sdp-transform": "^2.15.0",
|
@@ -50,13 +50,13 @@
|
|
50
50
|
"@types/dom-mediacapture-record": "^1"
|
51
51
|
},
|
52
52
|
"devDependencies": {
|
53
|
-
"@babel/core": "7.27.
|
53
|
+
"@babel/core": "7.27.4",
|
54
54
|
"@babel/preset-env": "7.27.2",
|
55
55
|
"@bufbuild/protoc-gen-es": "^1.10.0",
|
56
56
|
"@changesets/cli": "2.29.4",
|
57
57
|
"@livekit/changesets-changelog-github": "^0.0.4",
|
58
58
|
"@rollup/plugin-babel": "6.0.4",
|
59
|
-
"@rollup/plugin-commonjs": "28.0.
|
59
|
+
"@rollup/plugin-commonjs": "28.0.5",
|
60
60
|
"@rollup/plugin-json": "6.1.0",
|
61
61
|
"@rollup/plugin-node-resolve": "16.0.1",
|
62
62
|
"@rollup/plugin-terser": "^0.4.4",
|
@@ -64,25 +64,25 @@
|
|
64
64
|
"@size-limit/webpack": "^11.2.0",
|
65
65
|
"@trivago/prettier-plugin-sort-imports": "^5.0.0",
|
66
66
|
"@types/events": "^3.0.3",
|
67
|
-
"@types/sdp-transform": "2.4.
|
67
|
+
"@types/sdp-transform": "2.4.10",
|
68
68
|
"@types/ua-parser-js": "0.7.39",
|
69
69
|
"@typescript-eslint/eslint-plugin": "7.18.0",
|
70
70
|
"@typescript-eslint/parser": "7.18.0",
|
71
71
|
"downlevel-dts": "^0.11.0",
|
72
72
|
"eslint": "8.57.1",
|
73
73
|
"eslint-config-airbnb-typescript": "18.0.0",
|
74
|
-
"eslint-config-prettier": "
|
74
|
+
"eslint-config-prettier": "10.1.5",
|
75
75
|
"eslint-plugin-ecmascript-compat": "^3.2.1",
|
76
76
|
"eslint-plugin-import": "2.31.0",
|
77
77
|
"gh-pages": "6.3.0",
|
78
78
|
"happy-dom": "^17.2.0",
|
79
79
|
"jsdom": "^26.1.0",
|
80
80
|
"prettier": "^3.4.2",
|
81
|
-
"rollup": "4.
|
81
|
+
"rollup": "4.43.0",
|
82
82
|
"rollup-plugin-delete": "^2.1.0",
|
83
83
|
"rollup-plugin-typescript2": "0.36.0",
|
84
84
|
"size-limit": "^11.2.0",
|
85
|
-
"typedoc": "0.28.
|
85
|
+
"typedoc": "0.28.5",
|
86
86
|
"typedoc-plugin-no-inherit": "1.6.1",
|
87
87
|
"typescript": "5.8.3",
|
88
88
|
"vite": "5.4.19",
|
package/src/api/SignalClient.ts
CHANGED
@@ -110,9 +110,9 @@ export class SignalClient {
|
|
110
110
|
|
111
111
|
onClose?: (reason: string) => void;
|
112
112
|
|
113
|
-
onAnswer?: (sd: RTCSessionDescriptionInit) => void;
|
113
|
+
onAnswer?: (sd: RTCSessionDescriptionInit, offerId: number) => void;
|
114
114
|
|
115
|
-
onOffer?: (sd: RTCSessionDescriptionInit) => void;
|
115
|
+
onOffer?: (sd: RTCSessionDescriptionInit, offerId: number) => void;
|
116
116
|
|
117
117
|
// when a new ICE candidate is made available
|
118
118
|
onTrickle?: (sd: RTCIceCandidateInit, target: SignalTarget) => void;
|
@@ -246,12 +246,12 @@ export class SignalClient {
|
|
246
246
|
// clear ping interval and restart it once reconnected
|
247
247
|
this.clearPingInterval();
|
248
248
|
|
249
|
-
const res = await this.connect(url, token, {
|
249
|
+
const res = (await this.connect(url, token, {
|
250
250
|
...this.options,
|
251
251
|
reconnect: true,
|
252
252
|
sid,
|
253
253
|
reconnectReason: reason,
|
254
|
-
});
|
254
|
+
})) as ReconnectResponse;
|
255
255
|
return res;
|
256
256
|
}
|
257
257
|
|
@@ -506,20 +506,20 @@ export class SignalClient {
|
|
506
506
|
}
|
507
507
|
|
508
508
|
// initial offer after joining
|
509
|
-
sendOffer(offer: RTCSessionDescriptionInit) {
|
509
|
+
sendOffer(offer: RTCSessionDescriptionInit, offerId: number) {
|
510
510
|
this.log.debug('sending offer', { ...this.logContext, offerSdp: offer.sdp });
|
511
511
|
this.sendRequest({
|
512
512
|
case: 'offer',
|
513
|
-
value: toProtoSessionDescription(offer),
|
513
|
+
value: toProtoSessionDescription(offer, offerId),
|
514
514
|
});
|
515
515
|
}
|
516
516
|
|
517
517
|
// answer a server-initiated offer
|
518
|
-
sendAnswer(answer: RTCSessionDescriptionInit) {
|
518
|
+
sendAnswer(answer: RTCSessionDescriptionInit, offerId: number) {
|
519
519
|
this.log.debug('sending answer', { ...this.logContext, answerSdp: answer.sdp });
|
520
520
|
return this.sendRequest({
|
521
521
|
case: 'answer',
|
522
|
-
value: toProtoSessionDescription(answer),
|
522
|
+
value: toProtoSessionDescription(answer, offerId),
|
523
523
|
});
|
524
524
|
}
|
525
525
|
|
@@ -700,12 +700,12 @@ export class SignalClient {
|
|
700
700
|
if (msg.case === 'answer') {
|
701
701
|
const sd = fromProtoSessionDescription(msg.value);
|
702
702
|
if (this.onAnswer) {
|
703
|
-
this.onAnswer(sd);
|
703
|
+
this.onAnswer(sd, msg.value.id);
|
704
704
|
}
|
705
705
|
} else if (msg.case === 'offer') {
|
706
706
|
const sd = fromProtoSessionDescription(msg.value);
|
707
707
|
if (this.onOffer) {
|
708
|
-
this.onOffer(sd);
|
708
|
+
this.onOffer(sd, msg.value.id);
|
709
709
|
}
|
710
710
|
} else if (msg.case === 'trickle') {
|
711
711
|
const candidate: RTCIceCandidateInit = JSON.parse(msg.value.candidateInit!);
|
@@ -888,10 +888,12 @@ function fromProtoSessionDescription(sd: SessionDescription): RTCSessionDescript
|
|
888
888
|
|
889
889
|
export function toProtoSessionDescription(
|
890
890
|
rsd: RTCSessionDescription | RTCSessionDescriptionInit,
|
891
|
+
id?: number,
|
891
892
|
): SessionDescription {
|
892
893
|
const sd = new SessionDescription({
|
893
894
|
sdp: rsd.sdp!,
|
894
895
|
type: rsd.type!,
|
896
|
+
id,
|
895
897
|
});
|
896
898
|
return sd;
|
897
899
|
}
|
package/src/e2ee/E2eeManager.ts
CHANGED
@@ -371,6 +371,7 @@ export class E2EEManager
|
|
371
371
|
let writable: WritableStream = receiver.writableStream;
|
372
372
|
// @ts-ignore
|
373
373
|
let readable: ReadableStream = receiver.readableStream;
|
374
|
+
|
374
375
|
if (!writable || !readable) {
|
375
376
|
// @ts-ignore
|
376
377
|
const receiverStreams = receiver.createEncodedStreams();
|
@@ -390,6 +391,7 @@ export class E2EEManager
|
|
390
391
|
trackId: trackId,
|
391
392
|
codec,
|
392
393
|
participantIdentity: participantIdentity,
|
394
|
+
isReuse: E2EE_FLAG in receiver,
|
393
395
|
},
|
394
396
|
};
|
395
397
|
this.worker.postMessage(msg, [readable, writable]);
|
@@ -435,6 +437,7 @@ export class E2EEManager
|
|
435
437
|
codec,
|
436
438
|
trackId,
|
437
439
|
participantIdentity: this.room.localParticipant.identity,
|
440
|
+
isReuse: false,
|
438
441
|
},
|
439
442
|
};
|
440
443
|
this.worker.postMessage(msg, [senderStreams.readable, senderStreams.writable]);
|
package/src/e2ee/types.ts
CHANGED
@@ -69,6 +69,8 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
69
69
|
|
70
70
|
private detectedCodec?: VideoCodec;
|
71
71
|
|
72
|
+
private isTransformActive: boolean = false;
|
73
|
+
|
72
74
|
constructor(opts: {
|
73
75
|
keys: ParticipantKeyHandler;
|
74
76
|
participantIdentity: string;
|
@@ -159,6 +161,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
159
161
|
readable: ReadableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
160
162
|
writable: WritableStream<RTCEncodedVideoFrame | RTCEncodedAudioFrame>,
|
161
163
|
trackId: string,
|
164
|
+
isReuse: boolean,
|
162
165
|
codec?: VideoCodec,
|
163
166
|
) {
|
164
167
|
if (codec) {
|
@@ -173,11 +176,20 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
173
176
|
...this.logContext,
|
174
177
|
});
|
175
178
|
|
179
|
+
if (isReuse && this.isTransformActive) {
|
180
|
+
workerLogger.debug('reuse transform', {
|
181
|
+
...this.logContext,
|
182
|
+
});
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
|
176
186
|
const transformFn = operation === 'encode' ? this.encodeFunction : this.decodeFunction;
|
177
187
|
const transformStream = new TransformStream({
|
178
188
|
transform: transformFn.bind(this),
|
179
189
|
});
|
180
190
|
|
191
|
+
this.isTransformActive = true;
|
192
|
+
|
181
193
|
readable
|
182
194
|
.pipeThrough(transformStream)
|
183
195
|
.pipeTo(writable)
|
@@ -189,6 +201,9 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
189
201
|
? e
|
190
202
|
: new CryptorError(e.message, undefined, this.participantIdentity),
|
191
203
|
);
|
204
|
+
})
|
205
|
+
.finally(() => {
|
206
|
+
this.isTransformActive = false;
|
192
207
|
});
|
193
208
|
this.trackId = trackId;
|
194
209
|
}
|
@@ -65,6 +65,7 @@ onmessage = (ev) => {
|
|
65
65
|
data.readableStream,
|
66
66
|
data.writableStream,
|
67
67
|
data.trackId,
|
68
|
+
data.isReuse,
|
68
69
|
data.codec,
|
69
70
|
);
|
70
71
|
break;
|
@@ -75,6 +76,7 @@ onmessage = (ev) => {
|
|
75
76
|
data.readableStream,
|
76
77
|
data.writableStream,
|
77
78
|
data.trackId,
|
79
|
+
data.isReuse,
|
78
80
|
data.codec,
|
79
81
|
);
|
80
82
|
break;
|
package/src/room/PCTransport.ts
CHANGED
@@ -50,6 +50,8 @@ export default class PCTransport extends EventEmitter {
|
|
50
50
|
|
51
51
|
private ddExtID = 0;
|
52
52
|
|
53
|
+
private latestOfferId: number = 0;
|
54
|
+
|
53
55
|
pendingCandidates: RTCIceCandidateInit[] = [];
|
54
56
|
|
55
57
|
restartingIce: boolean = false;
|
@@ -62,7 +64,7 @@ export default class PCTransport extends EventEmitter {
|
|
62
64
|
|
63
65
|
remoteNackMids: string[] = [];
|
64
66
|
|
65
|
-
onOffer?: (offer: RTCSessionDescriptionInit) => void;
|
67
|
+
onOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
|
66
68
|
|
67
69
|
onIceCandidate?: (candidate: RTCIceCandidate) => void;
|
68
70
|
|
@@ -137,7 +139,20 @@ export default class PCTransport extends EventEmitter {
|
|
137
139
|
this.pendingCandidates.push(candidate);
|
138
140
|
}
|
139
141
|
|
140
|
-
async setRemoteDescription(sd: RTCSessionDescriptionInit): Promise<
|
142
|
+
async setRemoteDescription(sd: RTCSessionDescriptionInit, offerId: number): Promise<boolean> {
|
143
|
+
if (
|
144
|
+
sd.type === 'answer' &&
|
145
|
+
this.latestOfferId > 0 &&
|
146
|
+
offerId > 0 &&
|
147
|
+
offerId !== this.latestOfferId
|
148
|
+
) {
|
149
|
+
this.log.warn('ignoring answer for old offer', {
|
150
|
+
...this.logContext,
|
151
|
+
offerId,
|
152
|
+
latestOfferId: this.latestOfferId,
|
153
|
+
});
|
154
|
+
return false;
|
155
|
+
}
|
141
156
|
let mungedSDP: string | undefined = undefined;
|
142
157
|
if (sd.type === 'offer') {
|
143
158
|
let { stereoMids, nackMids } = extractStereoAndNackAudioFromOffer(sd);
|
@@ -218,6 +233,7 @@ export default class PCTransport extends EventEmitter {
|
|
218
233
|
});
|
219
234
|
}
|
220
235
|
}
|
236
|
+
return true;
|
221
237
|
}
|
222
238
|
|
223
239
|
// debounced negotiate interface
|
@@ -235,6 +251,9 @@ export default class PCTransport extends EventEmitter {
|
|
235
251
|
}, debounceInterval);
|
236
252
|
|
237
253
|
async createAndSendOffer(options?: RTCOfferOptions) {
|
254
|
+
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
255
|
+
const offerId = this.latestOfferId + 1;
|
256
|
+
this.latestOfferId = offerId;
|
238
257
|
if (this.onOffer === undefined) {
|
239
258
|
return;
|
240
259
|
}
|
@@ -317,9 +336,16 @@ export default class PCTransport extends EventEmitter {
|
|
317
336
|
});
|
318
337
|
}
|
319
338
|
});
|
320
|
-
|
339
|
+
if (this.latestOfferId > offerId) {
|
340
|
+
this.log.warn('latestOfferId mismatch', {
|
341
|
+
...this.logContext,
|
342
|
+
latestOfferId: this.latestOfferId,
|
343
|
+
offerId,
|
344
|
+
});
|
345
|
+
return;
|
346
|
+
}
|
321
347
|
await this.setMungedSDP(offer, write(sdpParsed));
|
322
|
-
this.onOffer(offer);
|
348
|
+
this.onOffer(offer, this.latestOfferId);
|
323
349
|
}
|
324
350
|
|
325
351
|
async createAndSetAnswer(): Promise<RTCSessionDescriptionInit> {
|
@@ -48,7 +48,7 @@ export class PCTransportManager {
|
|
48
48
|
|
49
49
|
public onTrack?: (ev: RTCTrackEvent) => void;
|
50
50
|
|
51
|
-
public onPublisherOffer?: (offer: RTCSessionDescriptionInit) => void;
|
51
|
+
public onPublisherOffer?: (offer: RTCSessionDescriptionInit, offerId: number) => void;
|
52
52
|
|
53
53
|
private isPublisherConnectionRequired: boolean;
|
54
54
|
|
@@ -96,8 +96,8 @@ export class PCTransportManager {
|
|
96
96
|
this.subscriber.onTrack = (ev) => {
|
97
97
|
this.onTrack?.(ev);
|
98
98
|
};
|
99
|
-
this.publisher.onOffer = (offer) => {
|
100
|
-
this.onPublisherOffer?.(offer);
|
99
|
+
this.publisher.onOffer = (offer, offerId) => {
|
100
|
+
this.onPublisherOffer?.(offer, offerId);
|
101
101
|
};
|
102
102
|
|
103
103
|
this.state = PCTransportState.NEW;
|
@@ -126,8 +126,8 @@ export class PCTransportManager {
|
|
126
126
|
return this.publisher.createAndSendOffer(options);
|
127
127
|
}
|
128
128
|
|
129
|
-
setPublisherAnswer(sd: RTCSessionDescriptionInit) {
|
130
|
-
return this.publisher.setRemoteDescription(sd);
|
129
|
+
setPublisherAnswer(sd: RTCSessionDescriptionInit, offerId: number) {
|
130
|
+
return this.publisher.setRemoteDescription(sd, offerId);
|
131
131
|
}
|
132
132
|
|
133
133
|
removeTrack(sender: RTCRtpSender) {
|
@@ -168,7 +168,7 @@ export class PCTransportManager {
|
|
168
168
|
}
|
169
169
|
}
|
170
170
|
|
171
|
-
async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit) {
|
171
|
+
async createSubscriberAnswerFromOffer(sd: RTCSessionDescriptionInit, offerId: number) {
|
172
172
|
this.log.debug('received server offer', {
|
173
173
|
...this.logContext,
|
174
174
|
RTCSdpType: sd.type,
|
@@ -177,7 +177,10 @@ export class PCTransportManager {
|
|
177
177
|
});
|
178
178
|
const unlock = await this.remoteOfferLock.lock();
|
179
179
|
try {
|
180
|
-
await this.subscriber.setRemoteDescription(sd);
|
180
|
+
const success = await this.subscriber.setRemoteDescription(sd, offerId);
|
181
|
+
if (!success) {
|
182
|
+
return undefined;
|
183
|
+
}
|
181
184
|
|
182
185
|
// answer the offer
|
183
186
|
const answer = await this.subscriber.createAndSetAnswer();
|
package/src/room/RTCEngine.ts
CHANGED
@@ -5,6 +5,7 @@ import {
|
|
5
5
|
ClientConfiguration,
|
6
6
|
type ConnectionQualityUpdate,
|
7
7
|
DataChannelInfo,
|
8
|
+
DataChannelReceiveState,
|
8
9
|
DataPacket,
|
9
10
|
DataPacket_Kind,
|
10
11
|
DisconnectReason,
|
@@ -44,6 +45,8 @@ import {
|
|
44
45
|
} from '../api/SignalClient';
|
45
46
|
import log, { LoggerNames, getLogger } from '../logger';
|
46
47
|
import type { InternalRoomOptions } from '../options';
|
48
|
+
import { DataPacketBuffer } from '../utils/dataPacketBuffer';
|
49
|
+
import { TTLMap } from '../utils/ttlmap';
|
47
50
|
import PCTransport, { PCEvents } from './PCTransport';
|
48
51
|
import { PCTransportManager, PCTransportState } from './PCTransportManager';
|
49
52
|
import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
@@ -81,6 +84,7 @@ const lossyDataChannel = '_lossy';
|
|
81
84
|
const reliableDataChannel = '_reliable';
|
82
85
|
const minReconnectWait = 2 * 1000;
|
83
86
|
const leaveReconnect = 'leave-reconnect';
|
87
|
+
const reliabeReceiveStateTTL = 30_000;
|
84
88
|
|
85
89
|
enum PCState {
|
86
90
|
New,
|
@@ -107,6 +111,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
107
111
|
*/
|
108
112
|
latestJoinResponse?: JoinResponse;
|
109
113
|
|
114
|
+
/**
|
115
|
+
* @internal
|
116
|
+
*/
|
117
|
+
latestRemoteOfferId: number = 0;
|
118
|
+
|
110
119
|
get isClosed() {
|
111
120
|
return this._isClosed;
|
112
121
|
}
|
@@ -178,6 +187,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
178
187
|
|
179
188
|
private publisherConnectionPromise: Promise<void> | undefined;
|
180
189
|
|
190
|
+
private reliableDataSequence: number = 1;
|
191
|
+
|
192
|
+
private reliableMessageBuffer = new DataPacketBuffer();
|
193
|
+
|
194
|
+
private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
|
195
|
+
|
181
196
|
constructor(private options: InternalRoomOptions) {
|
182
197
|
super();
|
183
198
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine);
|
@@ -310,6 +325,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
310
325
|
this.lossyDCSub = undefined;
|
311
326
|
this.reliableDC = undefined;
|
312
327
|
this.reliableDCSub = undefined;
|
328
|
+
this.reliableMessageBuffer = new DataPacketBuffer();
|
329
|
+
this.reliableDataSequence = 1;
|
330
|
+
this.reliableReceivedState.clear();
|
313
331
|
}
|
314
332
|
|
315
333
|
async cleanupClient() {
|
@@ -407,8 +425,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
407
425
|
this.client.sendIceCandidate(candidate, target);
|
408
426
|
};
|
409
427
|
|
410
|
-
this.pcManager.onPublisherOffer = (offer) => {
|
411
|
-
this.client.sendOffer(offer);
|
428
|
+
this.pcManager.onPublisherOffer = (offer, offerId) => {
|
429
|
+
this.client.sendOffer(offer, offerId);
|
412
430
|
};
|
413
431
|
|
414
432
|
this.pcManager.onDataChannel = this.handleDataChannel;
|
@@ -463,12 +481,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
463
481
|
|
464
482
|
private setupSignalClientCallbacks() {
|
465
483
|
// configure signaling client
|
466
|
-
this.client.onAnswer = async (sd) => {
|
484
|
+
this.client.onAnswer = async (sd, offerId) => {
|
467
485
|
if (!this.pcManager) {
|
468
486
|
return;
|
469
487
|
}
|
470
488
|
this.log.debug('received server answer', { ...this.logContext, RTCSdpType: sd.type });
|
471
|
-
await this.pcManager.setPublisherAnswer(sd);
|
489
|
+
await this.pcManager.setPublisherAnswer(sd, offerId);
|
472
490
|
};
|
473
491
|
|
474
492
|
// add candidate on trickle
|
@@ -481,12 +499,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
481
499
|
};
|
482
500
|
|
483
501
|
// when server creates an offer for the client
|
484
|
-
this.client.onOffer = async (sd) => {
|
502
|
+
this.client.onOffer = async (sd, offerId) => {
|
503
|
+
this.latestRemoteOfferId = offerId;
|
485
504
|
if (!this.pcManager) {
|
486
505
|
return;
|
487
506
|
}
|
488
|
-
const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd);
|
489
|
-
|
507
|
+
const answer = await this.pcManager.createSubscriberAnswerFromOffer(sd, offerId);
|
508
|
+
if (answer) {
|
509
|
+
this.client.sendAnswer(answer, offerId);
|
510
|
+
}
|
490
511
|
};
|
491
512
|
|
492
513
|
this.client.onLocalTrackPublished = (res: TrackPublishedResponse) => {
|
@@ -677,6 +698,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
677
698
|
}
|
678
699
|
const dp = DataPacket.fromBinary(new Uint8Array(buffer));
|
679
700
|
|
701
|
+
if (dp.sequence > 0 && dp.participantSid !== '') {
|
702
|
+
const lastSeq = this.reliableReceivedState.get(dp.participantSid);
|
703
|
+
if (lastSeq && dp.sequence <= lastSeq) {
|
704
|
+
// ignore duplicate or out-of-order packets in reliable channel
|
705
|
+
return;
|
706
|
+
}
|
707
|
+
this.reliableReceivedState.set(dp.participantSid, dp.sequence);
|
708
|
+
}
|
709
|
+
|
680
710
|
if (dp.value?.case === 'speaker') {
|
681
711
|
// dispatch speaker updates
|
682
712
|
this.emit(EngineEvent.ActiveSpeakersUpdate, dp.value.value.speakers);
|
@@ -1033,6 +1063,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1033
1063
|
if (res) {
|
1034
1064
|
const rtcConfig = this.makeRTCConfiguration(res);
|
1035
1065
|
this.pcManager.updateConfiguration(rtcConfig);
|
1066
|
+
if (this.latestJoinResponse) {
|
1067
|
+
this.latestJoinResponse.serverInfo = res.serverInfo;
|
1068
|
+
}
|
1036
1069
|
} else {
|
1037
1070
|
this.log.warn('Did not receive reconnect response', this.logContext);
|
1038
1071
|
}
|
@@ -1059,6 +1092,10 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1059
1092
|
this.createDataChannels();
|
1060
1093
|
}
|
1061
1094
|
|
1095
|
+
if (res?.lastMessageSeq) {
|
1096
|
+
this.resendReliableMessagesForResume(res.lastMessageSeq);
|
1097
|
+
}
|
1098
|
+
|
1062
1099
|
// resume success
|
1063
1100
|
this.emit(EngineEvent.Resumed);
|
1064
1101
|
}
|
@@ -1151,19 +1188,42 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1151
1188
|
|
1152
1189
|
/* @internal */
|
1153
1190
|
async sendDataPacket(packet: DataPacket, kind: DataPacket_Kind) {
|
1154
|
-
const msg = packet.toBinary();
|
1155
|
-
|
1156
1191
|
// make sure we do have a data connection
|
1157
1192
|
await this.ensurePublisherConnected(kind);
|
1158
1193
|
|
1194
|
+
if (kind === DataPacket_Kind.RELIABLE) {
|
1195
|
+
packet.sequence = this.reliableDataSequence;
|
1196
|
+
this.reliableDataSequence += 1;
|
1197
|
+
}
|
1198
|
+
const msg = packet.toBinary();
|
1159
1199
|
const dc = this.dataChannelForKind(kind);
|
1160
1200
|
if (dc) {
|
1201
|
+
if (kind === DataPacket_Kind.RELIABLE) {
|
1202
|
+
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence });
|
1203
|
+
}
|
1204
|
+
|
1205
|
+
if (this.attemptingReconnect) {
|
1206
|
+
return;
|
1207
|
+
}
|
1208
|
+
|
1161
1209
|
dc.send(msg);
|
1162
1210
|
}
|
1163
1211
|
|
1164
1212
|
this.updateAndEmitDCBufferStatus(kind);
|
1165
1213
|
}
|
1166
1214
|
|
1215
|
+
private async resendReliableMessagesForResume(lastMessageSeq: number) {
|
1216
|
+
await this.ensurePublisherConnected(DataPacket_Kind.RELIABLE);
|
1217
|
+
const dc = this.dataChannelForKind(DataPacket_Kind.RELIABLE);
|
1218
|
+
if (dc) {
|
1219
|
+
this.reliableMessageBuffer.popToSequence(lastMessageSeq);
|
1220
|
+
this.reliableMessageBuffer.getAll().forEach((msg) => {
|
1221
|
+
dc.send(msg.data);
|
1222
|
+
});
|
1223
|
+
}
|
1224
|
+
this.updateAndEmitDCBufferStatus(DataPacket_Kind.RELIABLE);
|
1225
|
+
}
|
1226
|
+
|
1167
1227
|
private updateAndEmitDCBufferStatus = (kind: DataPacket_Kind) => {
|
1168
1228
|
const status = this.isBufferStatusLow(kind);
|
1169
1229
|
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
@@ -1175,6 +1235,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1175
1235
|
private isBufferStatusLow = (kind: DataPacket_Kind): boolean | undefined => {
|
1176
1236
|
const dc = this.dataChannelForKind(kind);
|
1177
1237
|
if (dc) {
|
1238
|
+
if (kind === DataPacket_Kind.RELIABLE) {
|
1239
|
+
this.reliableMessageBuffer.alignBufferedAmount(dc.bufferedAmount);
|
1240
|
+
}
|
1178
1241
|
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
1179
1242
|
}
|
1180
1243
|
};
|
@@ -1409,6 +1472,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
1409
1472
|
publishTracks: getTrackPublicationInfo(localTracks),
|
1410
1473
|
dataChannels: this.dataChannelsInfo(),
|
1411
1474
|
trackSidsDisabled,
|
1475
|
+
datachannelReceiveStates: this.reliableReceivedState.map((seq, sid) => {
|
1476
|
+
return new DataChannelReceiveState({
|
1477
|
+
publisherSid: sid,
|
1478
|
+
lastSeq: seq,
|
1479
|
+
});
|
1480
|
+
}),
|
1412
1481
|
}),
|
1413
1482
|
);
|
1414
1483
|
}
|
package/src/room/Room.ts
CHANGED
@@ -601,7 +601,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
601
601
|
this.handleParticipantDisconnected(identity, participant);
|
602
602
|
});
|
603
603
|
|
604
|
-
this.emit(RoomEvent.Moved, roomMoved.room!.name
|
604
|
+
this.emit(RoomEvent.Moved, roomMoved.room!.name);
|
605
605
|
|
606
606
|
if (roomMoved.participant) {
|
607
607
|
this.handleParticipantUpdates([roomMoved.participant, ...roomMoved.otherParticipants]);
|
@@ -1301,10 +1301,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1301
1301
|
*/
|
1302
1302
|
async switchActiveDevice(kind: MediaDeviceKind, deviceId: string, exact: boolean = true) {
|
1303
1303
|
let success = true;
|
1304
|
-
let
|
1304
|
+
let shouldTriggerImmediateDeviceChange = false;
|
1305
1305
|
const deviceConstraint = exact ? { exact: deviceId } : deviceId;
|
1306
1306
|
if (kind === 'audioinput') {
|
1307
|
-
|
1307
|
+
shouldTriggerImmediateDeviceChange = this.localParticipant.audioTrackPublications.size === 0;
|
1308
1308
|
const prevDeviceId =
|
1309
1309
|
this.getActiveDevice(kind) ?? this.options.audioCaptureDefaults!.deviceId;
|
1310
1310
|
this.options.audioCaptureDefaults!.deviceId = deviceConstraint;
|
@@ -1319,8 +1319,10 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1319
1319
|
this.options.audioCaptureDefaults!.deviceId = prevDeviceId;
|
1320
1320
|
throw e;
|
1321
1321
|
}
|
1322
|
+
const isMuted = tracks.some((t) => t.track?.isMuted ?? false);
|
1323
|
+
if (success && isMuted) shouldTriggerImmediateDeviceChange = true;
|
1322
1324
|
} else if (kind === 'videoinput') {
|
1323
|
-
|
1325
|
+
shouldTriggerImmediateDeviceChange = this.localParticipant.videoTrackPublications.size === 0;
|
1324
1326
|
const prevDeviceId =
|
1325
1327
|
this.getActiveDevice(kind) ?? this.options.videoCaptureDefaults!.deviceId;
|
1326
1328
|
this.options.videoCaptureDefaults!.deviceId = deviceConstraint;
|
@@ -1336,6 +1338,7 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1336
1338
|
throw e;
|
1337
1339
|
}
|
1338
1340
|
} else if (kind === 'audiooutput') {
|
1341
|
+
shouldTriggerImmediateDeviceChange = true;
|
1339
1342
|
if (
|
1340
1343
|
(!supportsSetSinkId() && !this.options.webAudioMix) ||
|
1341
1344
|
(this.options.webAudioMix && this.audioContext && !('setSinkId' in this.audioContext))
|
@@ -1367,12 +1370,9 @@ class Room extends (EventEmitter as new () => TypedEmitter<RoomEventCallbacks>)
|
|
1367
1370
|
throw e;
|
1368
1371
|
}
|
1369
1372
|
}
|
1370
|
-
|
1371
|
-
|
1372
|
-
this.localParticipant.activeDeviceMap.set(
|
1373
|
-
kind,
|
1374
|
-
(kind === 'audiooutput' && this.options.audioOutput?.deviceId) || deviceId,
|
1375
|
-
);
|
1373
|
+
|
1374
|
+
if (shouldTriggerImmediateDeviceChange) {
|
1375
|
+
this.localParticipant.activeDeviceMap.set(kind, deviceId);
|
1376
1376
|
this.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
|
1377
1377
|
}
|
1378
1378
|
|
@@ -2649,7 +2649,7 @@ export type RoomEventCallbacks = {
|
|
2649
2649
|
reconnected: () => void;
|
2650
2650
|
disconnected: (reason?: DisconnectReason) => void;
|
2651
2651
|
connectionStateChanged: (state: ConnectionState) => void;
|
2652
|
-
moved: (name: string
|
2652
|
+
moved: (name: string) => void;
|
2653
2653
|
mediaDevicesChanged: () => void;
|
2654
2654
|
participantConnected: (participant: RemoteParticipant) => void;
|
2655
2655
|
participantDisconnected: (participant: RemoteParticipant) => void;
|