livekit-client 2.20.2 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- 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 +6 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2588 -1056
- 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 +6 -1
- 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/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +49 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +48 -65
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +6 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts +40 -0
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts +56 -0
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -0
- package/dist/src/room/data-channel/types.d.ts +12 -0
- package/dist/src/room/data-channel/types.d.ts.map +1 -0
- package/dist/src/room/data-stream/compression.d.ts +49 -0
- package/dist/src/room/data-stream/compression.d.ts.map +1 -0
- package/dist/src/room/data-stream/constants.d.ts +18 -0
- package/dist/src/room/data-stream/constants.d.ts.map +1 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/StreamReader.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/src/room/data-stream/outgoing/header-utils.d.ts.map +1 -0
- package/dist/src/room/errors.d.ts +3 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +11 -2
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +19 -10
- package/dist/src/room/token-source/TokenSource.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/src/room/track/RemoteTrack.d.ts +3 -0
- package/dist/src/room/track/RemoteTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +17 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/promiseState.d.ts +12 -0
- package/dist/src/test/promiseState.d.ts.map +1 -0
- package/dist/src/test/signalToken.d.ts.map +1 -1
- package/dist/src/utils/AsyncQueue.d.ts +3 -3
- package/dist/src/utils/AsyncQueue.d.ts.map +1 -1
- package/dist/src/version.d.ts +5 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/index.d.ts +1 -1
- package/dist/ts4.2/room/PCTransport.d.ts +49 -2
- package/dist/ts4.2/room/RTCEngine.d.ts +48 -65
- package/dist/ts4.2/room/Room.d.ts +6 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +62 -0
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +95 -0
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -0
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -0
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -0
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -0
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -0
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +4 -0
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +49 -2
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -0
- package/dist/ts4.2/room/errors.d.ts +3 -1
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +10 -2
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +11 -1
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +17 -8
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +12 -1
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +3 -0
- package/dist/ts4.2/room/types.d.ts +17 -0
- package/dist/ts4.2/room/utils.d.ts +28 -0
- package/dist/ts4.2/test/promiseState.d.ts +12 -0
- package/dist/ts4.2/utils/AsyncQueue.d.ts +3 -3
- package/dist/ts4.2/version.d.ts +5 -1
- package/package.json +14 -13
- package/src/api/SignalClient.e2e.test.ts +16 -9
- package/src/e2ee/utils.ts +1 -1
- package/src/index.ts +2 -0
- package/src/room/PCTransport.test.ts +243 -1
- package/src/room/PCTransport.ts +181 -80
- package/src/room/RTCEngine.test.ts +397 -101
- package/src/room/RTCEngine.ts +244 -545
- package/src/room/Room.test.ts +138 -4
- package/src/room/Room.ts +92 -25
- package/src/room/data-channel/DataChannelManager.test.ts +152 -0
- package/src/room/data-channel/DataChannelManager.ts +237 -0
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +154 -0
- package/src/room/data-channel/FlowControlledDataChannel.ts +216 -0
- package/src/room/data-channel/LossyDataChannel.test.ts +118 -0
- package/src/room/data-channel/LossyDataChannel.ts +125 -0
- package/src/room/data-channel/ReliableDataChannel.test.ts +158 -0
- package/src/room/data-channel/ReliableDataChannel.ts +153 -0
- package/src/room/data-channel/types.ts +30 -0
- package/src/room/data-stream/compression.ts +117 -0
- package/src/room/data-stream/constants.ts +18 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +1601 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +438 -86
- package/src/room/data-stream/incoming/StreamReader.ts +9 -3
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +1292 -0
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +489 -116
- package/src/room/data-stream/outgoing/header-utils.ts +87 -0
- package/src/room/errors.ts +7 -2
- package/src/room/participant/LocalParticipant.ts +13 -0
- package/src/room/participant/RemoteParticipant.ts +19 -5
- package/src/room/rpc/client/RpcClientManager.test.ts +20 -27
- package/src/room/rpc/client/RpcClientManager.ts +1 -4
- package/src/room/rpc/server/RpcServerManager.test.ts +24 -23
- package/src/room/rpc/server/RpcServerManager.ts +1 -3
- package/src/room/token-source/TokenSource.ts +25 -12
- package/src/room/track/LocalVideoTrack.test.ts +105 -2
- package/src/room/track/LocalVideoTrack.ts +36 -10
- package/src/room/track/RemoteTrack.test.ts +144 -0
- package/src/room/track/RemoteTrack.ts +39 -12
- package/src/room/track/Track.ts +2 -1
- package/src/room/types.ts +18 -0
- package/src/room/utils.test.ts +71 -2
- package/src/room/utils.ts +119 -0
- package/src/test/promiseState.ts +23 -0
- package/src/test/signalServerSetup.ts +2 -1
- package/src/test/signalToken.ts +17 -13
- package/src/type-polyfills/header-extensions.d.ts +13 -0
- package/src/utils/AsyncQueue.ts +3 -3
- package/src/version.ts +5 -1
package/src/room/RTCEngine.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
LeaveRequest_Action,
|
|
19
19
|
MediaSectionsRequirement,
|
|
20
20
|
ParticipantInfo,
|
|
21
|
+
ConnectionQuality as ProtoConnectionQuality,
|
|
21
22
|
PublishDataTrackResponse,
|
|
22
23
|
ReconnectReason,
|
|
23
24
|
type ReconnectResponse,
|
|
@@ -44,7 +45,6 @@ import {
|
|
|
44
45
|
type UserPacket,
|
|
45
46
|
} from '@livekit/protocol';
|
|
46
47
|
import { EventEmitter } from 'events';
|
|
47
|
-
import type { Throws } from '@livekit/throws-transformer/throws';
|
|
48
48
|
import type { MediaAttributes } from 'sdp-transform';
|
|
49
49
|
import type TypedEventEmitter from 'typed-emitter';
|
|
50
50
|
import type { SignalOptions } from '../api/SignalClient';
|
|
@@ -64,11 +64,15 @@ import log, { LoggerNames, getLogger } from '../logger';
|
|
|
64
64
|
import type { InternalRoomOptions } from '../options';
|
|
65
65
|
import type { NonSharedUint8Array } from '../type-polyfills/non-shared-typed-arrays';
|
|
66
66
|
import TypedPromise from '../utils/TypedPromise';
|
|
67
|
-
import { DataPacketBuffer } from '../utils/dataPacketBuffer';
|
|
68
67
|
import { TTLMap } from '../utils/ttlmap';
|
|
69
68
|
import PCTransport, { PCEvents } from './PCTransport';
|
|
70
69
|
import { PCTransportManager, PCTransportState } from './PCTransportManager';
|
|
71
70
|
import type { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
|
|
71
|
+
import { DataChannelManager } from './data-channel/DataChannelManager';
|
|
72
|
+
import type { FlowControlledDataChannel } from './data-channel/FlowControlledDataChannel';
|
|
73
|
+
import type { LossyDataChannel } from './data-channel/LossyDataChannel';
|
|
74
|
+
import type { ReliableDataChannel } from './data-channel/ReliableDataChannel';
|
|
75
|
+
import { DataChannelKind } from './data-channel/types';
|
|
72
76
|
import { DataTrackInfo } from './data-track/types';
|
|
73
77
|
import { roomConnectOptionDefaults } from './defaults';
|
|
74
78
|
import {
|
|
@@ -97,29 +101,22 @@ import {
|
|
|
97
101
|
isVideoCodec,
|
|
98
102
|
isVideoTrack,
|
|
99
103
|
isWeb,
|
|
104
|
+
negotiateDependencyDescriptor,
|
|
100
105
|
sleep,
|
|
101
106
|
supportsAddTrack,
|
|
102
107
|
supportsTransceiver,
|
|
103
108
|
toHttpUrl,
|
|
104
109
|
} from './utils';
|
|
105
110
|
|
|
106
|
-
const lossyDataChannel = '_lossy';
|
|
107
|
-
const reliableDataChannel = '_reliable';
|
|
108
|
-
const dataTrackDataChannel = '_data_track';
|
|
109
111
|
const minReconnectWait = 2 * 1000;
|
|
110
112
|
const leaveReconnect = 'leave-reconnect';
|
|
111
|
-
const reliabeReceiveStateTTL = 30_000;
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const reliableDataChannelWaterMarkLow = 64 * 1024;
|
|
120
|
-
const reliableDataChannelWaterMarkHigh = 1024 * 1024;
|
|
121
|
-
const lossyDataChannelWaterMarkLow = 8 * 1024;
|
|
122
|
-
const lossyDataChannelWaterMarkHigh = 256 * 1024;
|
|
114
|
+
/**
|
|
115
|
+
* How long local connection quality must stay `LOST` while connected and publishing before we
|
|
116
|
+
* force a full reconnect — `LOST` is the server's verdict that it isn't receiving our media.
|
|
117
|
+
*/
|
|
118
|
+
const connectionQualityLostTimeout = 10 * 1000;
|
|
119
|
+
const reliabeReceiveStateTTL = 30_000;
|
|
123
120
|
|
|
124
121
|
const initialMediaSectionsAudio = 3;
|
|
125
122
|
const initialMediaSectionsVideo = 3;
|
|
@@ -132,11 +129,7 @@ enum PCState {
|
|
|
132
129
|
Closed,
|
|
133
130
|
}
|
|
134
131
|
|
|
135
|
-
export
|
|
136
|
-
RELIABLE = DataPacket_Kind.RELIABLE,
|
|
137
|
-
LOSSY = DataPacket_Kind.LOSSY,
|
|
138
|
-
DATA_TRACK_LOSSY = 2,
|
|
139
|
-
}
|
|
132
|
+
export { DataChannelKind };
|
|
140
133
|
|
|
141
134
|
// Default data-channel max message size (bytes), used when the remote SDP
|
|
142
135
|
// answer does not advertise an `a=max-message-size` attribute (RFC 8841).
|
|
@@ -180,22 +173,24 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
180
173
|
return !!this.reconnectTimeout;
|
|
181
174
|
}
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
private
|
|
189
|
-
|
|
190
|
-
// @ts-ignore noUnusedLocals
|
|
191
|
-
private reliableDCSub?: RTCDataChannel;
|
|
176
|
+
/**
|
|
177
|
+
* Owns the data channels: the three flow-controlled publisher wrappers (engine-lifetime; the
|
|
178
|
+
* RTCDataChannel handles underneath are attached/detached as peer connections come and go, with
|
|
179
|
+
* waiter invalidation built into the turnover) plus the subscriber-side receive handles.
|
|
180
|
+
*/
|
|
181
|
+
private dataChannels: DataChannelManager;
|
|
192
182
|
|
|
193
|
-
private
|
|
183
|
+
private get reliableChannel(): ReliableDataChannel {
|
|
184
|
+
return this.dataChannels.reliable;
|
|
185
|
+
}
|
|
194
186
|
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
private get lossyChannel(): LossyDataChannel {
|
|
188
|
+
return this.dataChannels.lossy;
|
|
189
|
+
}
|
|
197
190
|
|
|
198
|
-
private
|
|
191
|
+
private get dataTrackChannel(): LossyDataChannel {
|
|
192
|
+
return this.dataChannels.dataTrack;
|
|
193
|
+
}
|
|
199
194
|
|
|
200
195
|
private subscriberPrimary: boolean = false;
|
|
201
196
|
|
|
@@ -252,25 +247,19 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
252
247
|
|
|
253
248
|
private publisherConnectionPromise: Promise<void> | undefined;
|
|
254
249
|
|
|
255
|
-
private reliableDataSequence: number = 1;
|
|
256
|
-
|
|
257
|
-
private reliableMessageBuffer = new DataPacketBuffer();
|
|
258
|
-
|
|
259
250
|
private reliableReceivedState: TTLMap<string, number> = new TTLMap(reliabeReceiveStateTTL);
|
|
260
251
|
|
|
261
|
-
private lossyDataStatCurrentBytes: number = 0;
|
|
262
|
-
|
|
263
|
-
private lossyDataStatByterate: number = 0;
|
|
264
|
-
|
|
265
|
-
private lossyDataStatInterval: ReturnType<typeof setInterval> | undefined;
|
|
266
|
-
|
|
267
|
-
private lossyDataDropCount: number = 0;
|
|
268
|
-
|
|
269
252
|
private midToTrackId: { [key: string]: string } = {};
|
|
270
253
|
|
|
271
254
|
/** used to indicate whether the browser is currently waiting to reconnect */
|
|
272
255
|
private isWaitingForNetworkReconnect: boolean = false;
|
|
273
256
|
|
|
257
|
+
/** set while the local participant's connection quality is `LOST`; forces a full reconnect on timeout */
|
|
258
|
+
private lostQualityTimeout?: ReturnType<typeof setTimeout>;
|
|
259
|
+
|
|
260
|
+
/** timestamp (ms) the primary transport entered `CONNECTING`, used to bound how long we tolerate it */
|
|
261
|
+
private transportConnectingSince?: number;
|
|
262
|
+
|
|
274
263
|
constructor(private options: InternalRoomOptions) {
|
|
275
264
|
super();
|
|
276
265
|
this.log = getLogger(options.loggerName ?? LoggerNames.Engine, () => this.logContext);
|
|
@@ -283,16 +272,23 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
283
272
|
this.reconnectPolicy = this.options.reconnectPolicy;
|
|
284
273
|
this.closingLock = new Mutex();
|
|
285
274
|
this.dataProcessLock = new Mutex();
|
|
286
|
-
this.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
275
|
+
this.dataChannels = new DataChannelManager({
|
|
276
|
+
isEngineClosed: () => this.isClosed,
|
|
277
|
+
isReconnecting: () => this.attemptingReconnect,
|
|
278
|
+
onDataMessage: (message) => this.handleDataMessage(message),
|
|
279
|
+
onDataTrackMessage: (message) => this.handleDataTrackMessage(message),
|
|
280
|
+
onDataError: (event) => this.handleDataError(event),
|
|
281
|
+
onChannelClose: (kind) => this.handleDataChannelClose(kind)(),
|
|
282
|
+
onBufferStatusChanged: (kind, isLow) =>
|
|
283
|
+
this.emit(EngineEvent.DCBufferStatusChanged, isLow, kind),
|
|
284
|
+
});
|
|
291
285
|
|
|
292
286
|
this.client.onParticipantUpdate = (updates) =>
|
|
293
287
|
this.emit(EngineEvent.ParticipantUpdate, updates);
|
|
294
|
-
this.client.onConnectionQuality = (update) =>
|
|
288
|
+
this.client.onConnectionQuality = (update) => {
|
|
289
|
+
this.handleLocalConnectionQuality(update);
|
|
295
290
|
this.emit(EngineEvent.ConnectionQualityUpdate, update);
|
|
291
|
+
};
|
|
296
292
|
this.client.onRoomUpdate = (update) => this.emit(EngineEvent.RoomUpdate, update);
|
|
297
293
|
this.client.onSubscriptionError = (resp) => this.emit(EngineEvent.SubscriptionError, resp);
|
|
298
294
|
this.client.onSubscriptionPermissionUpdate = (update) =>
|
|
@@ -449,6 +445,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
449
445
|
this.removeAllListeners();
|
|
450
446
|
this.deregisterOnLineListener();
|
|
451
447
|
this.clearPendingReconnect();
|
|
448
|
+
this.clearLostQualityTimeout();
|
|
452
449
|
this.cleanupLossyDataStats();
|
|
453
450
|
await this.cleanupPeerConnections();
|
|
454
451
|
await this.cleanupClient();
|
|
@@ -458,60 +455,18 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
458
455
|
}
|
|
459
456
|
|
|
460
457
|
async cleanupPeerConnections() {
|
|
461
|
-
|
|
462
|
-
// to transition data channels to 'closed' without firing events, which would otherwise strand
|
|
463
|
-
// a waiter holding the headroom lock.
|
|
464
|
-
this.invalidateDataChannelWaiters('peer connections cleaned up');
|
|
465
|
-
|
|
466
|
-
const dcCleanup = (dc: RTCDataChannel | undefined) => {
|
|
467
|
-
if (!dc) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
472
|
-
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
473
|
-
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
474
|
-
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
475
|
-
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
476
|
-
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
477
|
-
dc.onbufferedamountlow = null;
|
|
478
|
-
dc.onclose = null;
|
|
479
|
-
dc.onclosing = null;
|
|
480
|
-
dc.onerror = null;
|
|
481
|
-
dc.onmessage = null;
|
|
482
|
-
dc.onopen = null;
|
|
483
|
-
|
|
484
|
-
dc.close();
|
|
485
|
-
};
|
|
486
|
-
dcCleanup(this.lossyDC);
|
|
487
|
-
dcCleanup(this.lossyDCSub);
|
|
488
|
-
dcCleanup(this.reliableDC);
|
|
489
|
-
dcCleanup(this.reliableDCSub);
|
|
490
|
-
dcCleanup(this.dataTrackDC);
|
|
491
|
-
dcCleanup(this.dataTrackDCSub);
|
|
458
|
+
this.dataChannels.teardown();
|
|
492
459
|
|
|
493
460
|
await this.pcManager?.close();
|
|
494
461
|
this.pcManager = undefined;
|
|
462
|
+
// the connecting timestamp belongs to the transports we just tore down
|
|
463
|
+
this.transportConnectingSince = undefined;
|
|
495
464
|
|
|
496
|
-
this.lossyDC = undefined;
|
|
497
|
-
this.lossyDCSub = undefined;
|
|
498
|
-
this.reliableDC = undefined;
|
|
499
|
-
this.reliableDCSub = undefined;
|
|
500
|
-
this.dataTrackDC = undefined;
|
|
501
|
-
this.dataTrackDCSub = undefined;
|
|
502
|
-
this.reliableMessageBuffer = new DataPacketBuffer();
|
|
503
|
-
this.reliableDataSequence = 1;
|
|
504
465
|
this.reliableReceivedState.clear();
|
|
505
466
|
}
|
|
506
467
|
|
|
507
468
|
cleanupLossyDataStats() {
|
|
508
|
-
this.
|
|
509
|
-
this.lossyDataStatCurrentBytes = 0;
|
|
510
|
-
if (this.lossyDataStatInterval) {
|
|
511
|
-
clearInterval(this.lossyDataStatInterval);
|
|
512
|
-
this.lossyDataStatInterval = undefined;
|
|
513
|
-
}
|
|
514
|
-
this.lossyDataDropCount = 0;
|
|
469
|
+
this.lossyChannel.stopThresholdTuning();
|
|
515
470
|
}
|
|
516
471
|
|
|
517
472
|
async cleanupClient() {
|
|
@@ -532,7 +487,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
532
487
|
throw new TrackInvalidError('a track with the same ID has already been published');
|
|
533
488
|
}
|
|
534
489
|
return new Promise<TrackInfo>((resolve, reject) => {
|
|
535
|
-
const publicationTimeout = setTimeout(() => {
|
|
490
|
+
const publicationTimeout = CriticalTimers.setTimeout(() => {
|
|
536
491
|
delete this.pendingTrackResolvers[req.cid];
|
|
537
492
|
reject(
|
|
538
493
|
ConnectionError.timeout('publication of local track timed out, no response from server'),
|
|
@@ -540,11 +495,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
540
495
|
}, 10_000);
|
|
541
496
|
this.pendingTrackResolvers[req.cid] = {
|
|
542
497
|
resolve: (info: TrackInfo) => {
|
|
543
|
-
clearTimeout(publicationTimeout);
|
|
498
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
544
499
|
resolve(info);
|
|
545
500
|
},
|
|
546
501
|
reject: () => {
|
|
547
|
-
clearTimeout(publicationTimeout);
|
|
502
|
+
CriticalTimers.clearTimeout(publicationTimeout);
|
|
548
503
|
reject(new Error('Cancelled publication by calling unpublish'));
|
|
549
504
|
},
|
|
550
505
|
};
|
|
@@ -580,7 +535,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
580
535
|
}
|
|
581
536
|
|
|
582
537
|
get dataSubscriberReadyState(): string | undefined {
|
|
583
|
-
return this.
|
|
538
|
+
return this.dataChannelForKind(DataChannelKind.RELIABLE, true)?.readyState;
|
|
584
539
|
}
|
|
585
540
|
|
|
586
541
|
async getConnectedServerAddress(): Promise<string | undefined> {
|
|
@@ -629,6 +584,16 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
629
584
|
this.pcManager.onStateChange = async (connectionState, publisherState, subscriberState) => {
|
|
630
585
|
this.log.debug(`primary PC state changed ${connectionState}`);
|
|
631
586
|
|
|
587
|
+
// Record when the primary transport actually entered CONNECTING so
|
|
588
|
+
// verifyTransport() can bound how long we tolerate it. Deriving it from the
|
|
589
|
+
// real transition (this handler only fires on state changes) rather than from
|
|
590
|
+
// observation time keeps it from going stale across peer-connection rebuilds.
|
|
591
|
+
if (connectionState === PCTransportState.CONNECTING) {
|
|
592
|
+
this.transportConnectingSince = Date.now();
|
|
593
|
+
} else {
|
|
594
|
+
this.transportConnectingSince = undefined;
|
|
595
|
+
}
|
|
596
|
+
|
|
632
597
|
if (['closed', 'disconnected', 'failed'].includes(publisherState)) {
|
|
633
598
|
// reset publisher connection promise
|
|
634
599
|
this.publisherConnectionPromise = undefined;
|
|
@@ -880,8 +845,15 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
880
845
|
for (let i: number = 0; i < numAudios; i++) {
|
|
881
846
|
this.pcManager?.addPublisherTransceiverOfKind('audio', transceiverInit);
|
|
882
847
|
}
|
|
848
|
+
// media only arrives on these sections when there is no subscriber connection to arrive on
|
|
849
|
+
const receivesMedia = this.pcManager?.mode === 'publisher-only';
|
|
883
850
|
for (let i: number = 0; i < numVideos; i++) {
|
|
884
|
-
this.pcManager?.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
851
|
+
const transceiver = this.pcManager?.addPublisherTransceiverOfKind('video', transceiverInit);
|
|
852
|
+
if (receivesMedia && transceiver) {
|
|
853
|
+
const negotiated = negotiateDependencyDescriptor(transceiver);
|
|
854
|
+
|
|
855
|
+
this.log.debug('dependency descriptor negotiated for received video', { negotiated });
|
|
856
|
+
}
|
|
885
857
|
}
|
|
886
858
|
}
|
|
887
859
|
|
|
@@ -890,121 +862,39 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
890
862
|
return;
|
|
891
863
|
}
|
|
892
864
|
|
|
893
|
-
|
|
894
|
-
// are replaced below — reject them so the headroom lock is released and queued senders
|
|
895
|
-
// re-check against the new channels.
|
|
896
|
-
this.invalidateDataChannelWaiters('data channels recreated');
|
|
897
|
-
|
|
898
|
-
// clear old data channel callbacks if recreate
|
|
899
|
-
if (this.lossyDC) {
|
|
900
|
-
this.lossyDC.onmessage = null;
|
|
901
|
-
this.lossyDC.onerror = null;
|
|
902
|
-
this.lossyDC.onclose = null;
|
|
903
|
-
}
|
|
904
|
-
if (this.reliableDC) {
|
|
905
|
-
this.reliableDC.onmessage = null;
|
|
906
|
-
this.reliableDC.onerror = null;
|
|
907
|
-
this.reliableDC.onclose = null;
|
|
908
|
-
}
|
|
909
|
-
if (this.dataTrackDC) {
|
|
910
|
-
this.dataTrackDC.onmessage = null;
|
|
911
|
-
this.dataTrackDC.onerror = null;
|
|
912
|
-
this.dataTrackDC.onclose = null;
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
// create data channels
|
|
916
|
-
this.lossyDC = this.pcManager.createPublisherDataChannel(lossyDataChannel, {
|
|
917
|
-
ordered: false,
|
|
918
|
-
maxRetransmits: 0,
|
|
919
|
-
});
|
|
920
|
-
this.reliableDC = this.pcManager.createPublisherDataChannel(reliableDataChannel, {
|
|
921
|
-
ordered: true,
|
|
922
|
-
});
|
|
923
|
-
this.dataTrackDC = this.pcManager.createPublisherDataChannel(dataTrackDataChannel, {
|
|
924
|
-
ordered: false,
|
|
925
|
-
maxRetransmits: 0,
|
|
926
|
-
});
|
|
927
|
-
|
|
928
|
-
// also handle messages over the pub channel, for backwards compatibility
|
|
929
|
-
this.lossyDC.onmessage = this.handleDataMessage;
|
|
930
|
-
this.reliableDC.onmessage = this.handleDataMessage;
|
|
931
|
-
this.dataTrackDC.onmessage = this.handleDataTrackMessage;
|
|
932
|
-
|
|
933
|
-
// handle datachannel errors
|
|
934
|
-
this.lossyDC.onerror = this.handleDataError;
|
|
935
|
-
this.reliableDC.onerror = this.handleDataError;
|
|
936
|
-
this.dataTrackDC.onerror = this.handleDataError;
|
|
937
|
-
|
|
938
|
-
// detect unexpected publisher data channel closes
|
|
939
|
-
this.lossyDC.onclose = this.handleDataChannelClose(DataChannelKind.LOSSY);
|
|
940
|
-
this.reliableDC.onclose = this.handleDataChannelClose(DataChannelKind.RELIABLE);
|
|
941
|
-
this.dataTrackDC.onclose = this.handleDataChannelClose(DataChannelKind.DATA_TRACK_LOSSY);
|
|
942
|
-
|
|
943
|
-
// set up dc buffer threshold - if this is not set, it will default to 0
|
|
944
|
-
this.lossyDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
945
|
-
this.reliableDC.bufferedAmountLowThreshold = reliableDataChannelWaterMarkLow;
|
|
946
|
-
this.dataTrackDC.bufferedAmountLowThreshold = lossyDataChannelWaterMarkLow;
|
|
947
|
-
|
|
948
|
-
// handle buffer amount low events
|
|
949
|
-
this.lossyDC.onbufferedamountlow = () => this.handleBufferedAmountLow(DataChannelKind.LOSSY);
|
|
950
|
-
this.reliableDC.onbufferedamountlow = () =>
|
|
951
|
-
this.handleBufferedAmountLow(DataChannelKind.RELIABLE);
|
|
952
|
-
this.dataTrackDC.onbufferedamountlow = () =>
|
|
953
|
-
this.handleBufferedAmountLow(DataChannelKind.DATA_TRACK_LOSSY);
|
|
954
|
-
|
|
955
|
-
this.cleanupLossyDataStats();
|
|
956
|
-
this.lossyDataStatInterval = setInterval(() => {
|
|
957
|
-
this.lossyDataStatByterate = this.lossyDataStatCurrentBytes;
|
|
958
|
-
this.lossyDataStatCurrentBytes = 0;
|
|
959
|
-
|
|
960
|
-
const dc = this.dataChannelForKind(DataChannelKind.LOSSY);
|
|
961
|
-
if (dc) {
|
|
962
|
-
// control buffered latency to ~100ms
|
|
963
|
-
const threshold = this.lossyDataStatByterate / 10;
|
|
964
|
-
dc.bufferedAmountLowThreshold = Math.min(
|
|
965
|
-
Math.max(threshold, lossyDataChannelWaterMarkLow),
|
|
966
|
-
lossyDataChannelWaterMarkHigh,
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
}, 1000);
|
|
865
|
+
this.dataChannels.createPublisherChannels(this.pcManager);
|
|
970
866
|
}
|
|
971
867
|
|
|
972
868
|
private handleDataChannel = async ({ channel }: RTCDataChannelEvent) => {
|
|
973
869
|
if (!channel) {
|
|
974
870
|
return;
|
|
975
871
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
this.reliableDCSub = channel;
|
|
979
|
-
handler = this.handleDataMessage;
|
|
980
|
-
} else if (channel.label === lossyDataChannel) {
|
|
981
|
-
this.lossyDCSub = channel;
|
|
982
|
-
handler = this.handleDataMessage;
|
|
983
|
-
} else if (channel.label === dataTrackDataChannel) {
|
|
984
|
-
this.dataTrackDCSub = channel;
|
|
985
|
-
handler = this.handleDataTrackMessage;
|
|
986
|
-
} else {
|
|
987
|
-
return;
|
|
872
|
+
if (this.dataChannels.adoptSubscriberChannel(channel)) {
|
|
873
|
+
this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
|
|
988
874
|
}
|
|
989
|
-
this.log.debug(`on data channel ${channel.id}, ${channel.label}`);
|
|
990
|
-
channel.onmessage = handler;
|
|
991
875
|
};
|
|
992
876
|
|
|
877
|
+
/** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
|
|
878
|
+
private async decodeDataMessage(message: MessageEvent): Promise<Uint8Array | undefined> {
|
|
879
|
+
if (message.data instanceof ArrayBuffer) {
|
|
880
|
+
return new Uint8Array(message.data);
|
|
881
|
+
}
|
|
882
|
+
if (message.data instanceof Blob) {
|
|
883
|
+
return new Uint8Array(await message.data.arrayBuffer());
|
|
884
|
+
}
|
|
885
|
+
this.log.error('unsupported data type', { data: message.data });
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
|
|
993
889
|
private handleDataMessage = async (message: MessageEvent) => {
|
|
994
890
|
// make sure to respect incoming data message order by processing message events one after the other
|
|
995
891
|
const unlock = await this.dataProcessLock.lock();
|
|
996
892
|
try {
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
if (message.data instanceof ArrayBuffer) {
|
|
1000
|
-
buffer = message.data;
|
|
1001
|
-
} else if (message.data instanceof Blob) {
|
|
1002
|
-
buffer = await message.data.arrayBuffer();
|
|
1003
|
-
} else {
|
|
1004
|
-
this.log.error('unsupported data type', { data: message.data });
|
|
893
|
+
const bytes = await this.decodeDataMessage(message);
|
|
894
|
+
if (!bytes) {
|
|
1005
895
|
return;
|
|
1006
896
|
}
|
|
1007
|
-
const dp = DataPacket.fromBinary(
|
|
897
|
+
const dp = DataPacket.fromBinary(bytes);
|
|
1008
898
|
|
|
1009
899
|
if (dp.sequence > 0 && dp.participantSid !== '') {
|
|
1010
900
|
const lastSeq = this.reliableReceivedState.get(dp.participantSid);
|
|
@@ -1053,18 +943,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1053
943
|
};
|
|
1054
944
|
|
|
1055
945
|
private handleDataTrackMessage = async (message: MessageEvent) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (message.data instanceof ArrayBuffer) {
|
|
1059
|
-
buffer = message.data;
|
|
1060
|
-
} else if (message.data instanceof Blob) {
|
|
1061
|
-
buffer = await message.data.arrayBuffer();
|
|
1062
|
-
} else {
|
|
1063
|
-
this.log.error('unsupported data type', { data: message.data });
|
|
946
|
+
const bytes = await this.decodeDataMessage(message);
|
|
947
|
+
if (!bytes) {
|
|
1064
948
|
return;
|
|
1065
949
|
}
|
|
1066
|
-
|
|
1067
|
-
this.emit('dataTrackPacketReceived', new Uint8Array(buffer));
|
|
950
|
+
this.emit('dataTrackPacketReceived', bytes);
|
|
1068
951
|
};
|
|
1069
952
|
|
|
1070
953
|
private handleDataError = (event: Event) => {
|
|
@@ -1103,10 +986,6 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1103
986
|
}
|
|
1104
987
|
};
|
|
1105
988
|
|
|
1106
|
-
private handleBufferedAmountLow = (channelKind: DataChannelKind) => {
|
|
1107
|
-
this.updateAndEmitDCBufferStatus(channelKind);
|
|
1108
|
-
};
|
|
1109
|
-
|
|
1110
989
|
async createSender(
|
|
1111
990
|
track: LocalTrack,
|
|
1112
991
|
opts: TrackPublishOptions,
|
|
@@ -1252,7 +1131,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1252
1131
|
if (!opts.videoCodec) {
|
|
1253
1132
|
return;
|
|
1254
1133
|
}
|
|
1255
|
-
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
1134
|
+
await track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
|
|
1256
1135
|
return transceiver.sender;
|
|
1257
1136
|
}
|
|
1258
1137
|
|
|
@@ -1314,6 +1193,73 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1314
1193
|
);
|
|
1315
1194
|
};
|
|
1316
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* A sustained local `LOST` while connected and publishing means the server isn't receiving
|
|
1198
|
+
* our media, so force a full reconnect; any non-`LOST` value cancels a pending trigger.
|
|
1199
|
+
*/
|
|
1200
|
+
private handleLocalConnectionQuality(update: ConnectionQualityUpdate) {
|
|
1201
|
+
if (!this.participantSid) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const localUpdate = update.updates.find((u) => u.participantSid === this.participantSid);
|
|
1205
|
+
if (!localUpdate) {
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
if (localUpdate.quality === ProtoConnectionQuality.LOST) {
|
|
1209
|
+
this.scheduleLostQualityReconnect();
|
|
1210
|
+
} else {
|
|
1211
|
+
this.clearLostQualityTimeout();
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
private scheduleLostQualityReconnect() {
|
|
1216
|
+
if (this.lostQualityTimeout) {
|
|
1217
|
+
// already counting down towards a reconnect
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
this.lostQualityTimeout = CriticalTimers.setTimeout(() => {
|
|
1221
|
+
this.lostQualityTimeout = undefined;
|
|
1222
|
+
if (this._isClosed || this.pcState !== PCState.Connected || this.attemptingReconnect) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (!this.hasActivePublisherSenders()) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
this.log.warn(
|
|
1229
|
+
'local connection quality lost while publishing, triggering full reconnect',
|
|
1230
|
+
this.logContext,
|
|
1231
|
+
);
|
|
1232
|
+
this.fullReconnectOnNext = true;
|
|
1233
|
+
this.handleDisconnect('connection quality lost', ReconnectReason.RR_PUBLISHER_FAILED);
|
|
1234
|
+
}, connectionQualityLostTimeout);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private clearLostQualityTimeout() {
|
|
1238
|
+
if (this.lostQualityTimeout) {
|
|
1239
|
+
CriticalTimers.clearTimeout(this.lostQualityTimeout);
|
|
1240
|
+
this.lostQualityTimeout = undefined;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
/** Whether the publisher currently has any sender with a live track. */
|
|
1245
|
+
private hasActivePublisherSenders(): boolean {
|
|
1246
|
+
return (
|
|
1247
|
+
this.pcManager?.publisher
|
|
1248
|
+
.getSenders()
|
|
1249
|
+
.some((sender) => !!sender.track && sender.track.readyState === 'live') ?? false
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Forces a full reconnect while keeping the engine (and its saved credentials) alive. Used by
|
|
1255
|
+
* Room's connection-reconcile safety net when the transport silently died but we looked connected.
|
|
1256
|
+
* @internal
|
|
1257
|
+
*/
|
|
1258
|
+
reconnect(reason: ReconnectReason = ReconnectReason.RR_UNKNOWN) {
|
|
1259
|
+
this.fullReconnectOnNext = true;
|
|
1260
|
+
this.handleDisconnect('reconcile', reason);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1317
1263
|
private async attemptReconnect(reason?: ReconnectReason) {
|
|
1318
1264
|
if (this._isClosed) {
|
|
1319
1265
|
return;
|
|
@@ -1323,6 +1269,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1323
1269
|
this.log.warn('already attempting reconnect, returning early');
|
|
1324
1270
|
return;
|
|
1325
1271
|
}
|
|
1272
|
+
|
|
1273
|
+
// A pending Lost-quality countdown belongs to the session we're now leaving; cancel it so
|
|
1274
|
+
// it can't fire against the reconnected session before the server has evaluated it. (A resume
|
|
1275
|
+
// keeps the peer connections, so cleanupPeerConnections wouldn't cover this path.)
|
|
1276
|
+
this.clearLostQualityTimeout();
|
|
1277
|
+
|
|
1326
1278
|
if (
|
|
1327
1279
|
this.clientConfiguration?.resumeConnection === ClientConfigSetting.DISABLED ||
|
|
1328
1280
|
// signaling state could change to closed due to hardware sleep
|
|
@@ -1332,15 +1284,23 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1332
1284
|
this.fullReconnectOnNext = true;
|
|
1333
1285
|
}
|
|
1334
1286
|
|
|
1287
|
+
// Consume the flag up front: capture whether this attempt is a full reconnect, then reset
|
|
1288
|
+
// it. From here on a `true` value unambiguously represents a *new* full-reconnect request
|
|
1289
|
+
// that arrived while this attempt was running (e.g. a server RECONNECT leave), which the
|
|
1290
|
+
// finally block dispatches — for both the resume and full-reconnect paths.
|
|
1291
|
+
const fullReconnect = this.fullReconnectOnNext;
|
|
1292
|
+
this.fullReconnectOnNext = false;
|
|
1293
|
+
|
|
1294
|
+
let succeeded = false;
|
|
1335
1295
|
try {
|
|
1336
1296
|
this.attemptingReconnect = true;
|
|
1337
|
-
if (
|
|
1297
|
+
if (fullReconnect) {
|
|
1338
1298
|
await this.restartConnection();
|
|
1339
1299
|
} else {
|
|
1340
1300
|
await this.resumeConnection(reason);
|
|
1341
1301
|
}
|
|
1342
1302
|
this.clearPendingReconnect();
|
|
1343
|
-
|
|
1303
|
+
succeeded = true;
|
|
1344
1304
|
} catch (e) {
|
|
1345
1305
|
this.reconnectAttempts += 1;
|
|
1346
1306
|
let recoverable = true;
|
|
@@ -1348,8 +1308,9 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1348
1308
|
this.log.debug('received unrecoverable error', { error: e });
|
|
1349
1309
|
// unrecoverable
|
|
1350
1310
|
recoverable = false;
|
|
1351
|
-
} else if (!(e instanceof SignalReconnectError)) {
|
|
1352
|
-
//
|
|
1311
|
+
} else if (fullReconnect || !(e instanceof SignalReconnectError)) {
|
|
1312
|
+
// a failed full reconnect stays a full reconnect; a failed resume can only be
|
|
1313
|
+
// resumed again for a signal-level error, otherwise it escalates
|
|
1353
1314
|
this.fullReconnectOnNext = true;
|
|
1354
1315
|
}
|
|
1355
1316
|
|
|
@@ -1366,6 +1327,14 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1366
1327
|
}
|
|
1367
1328
|
} finally {
|
|
1368
1329
|
this.attemptingReconnect = false;
|
|
1330
|
+
|
|
1331
|
+
// A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
|
|
1332
|
+
// during a resume or a restart) that a successful attempt didn't act on; dispatch it now
|
|
1333
|
+
// (the failure path already retries).
|
|
1334
|
+
if (succeeded && this.fullReconnectOnNext && !this._isClosed) {
|
|
1335
|
+
this.log.debug('full reconnect requested during in-progress attempt, dispatching');
|
|
1336
|
+
this.handleDisconnect('reconnect');
|
|
1337
|
+
}
|
|
1369
1338
|
}
|
|
1370
1339
|
}
|
|
1371
1340
|
|
|
@@ -1510,7 +1479,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1510
1479
|
|
|
1511
1480
|
// recreate publish datachannel if it's id is null
|
|
1512
1481
|
// (for safari https://bugs.webkit.org/show_bug.cgi?id=184688)
|
|
1513
|
-
|
|
1482
|
+
const reliableDC = this.dataChannelForKind(DataChannelKind.RELIABLE);
|
|
1483
|
+
if (reliableDC?.readyState === 'open' && reliableDC.id === null) {
|
|
1514
1484
|
this.createDataChannels();
|
|
1515
1485
|
}
|
|
1516
1486
|
|
|
@@ -1586,7 +1556,11 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1586
1556
|
}
|
|
1587
1557
|
|
|
1588
1558
|
/* @internal */
|
|
1589
|
-
async sendDataPacket(
|
|
1559
|
+
async sendDataPacket(
|
|
1560
|
+
packet: DataPacket,
|
|
1561
|
+
/** Data-track frames don't come through here — they're sent pre-serialized via {@link sendDataTrackFrame }. */
|
|
1562
|
+
kind: Exclude<DataChannelKind, DataChannelKind.DATA_TRACK_LOSSY>,
|
|
1563
|
+
) {
|
|
1590
1564
|
// make sure we do have a data connection
|
|
1591
1565
|
await this.ensurePublisherConnected(kind);
|
|
1592
1566
|
|
|
@@ -1608,8 +1582,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1608
1582
|
}
|
|
1609
1583
|
|
|
1610
1584
|
if (kind === DataChannelKind.RELIABLE) {
|
|
1611
|
-
packet.sequence = this.
|
|
1612
|
-
this.reliableDataSequence += 1;
|
|
1585
|
+
packet.sequence = this.reliableChannel.nextSequence();
|
|
1613
1586
|
}
|
|
1614
1587
|
|
|
1615
1588
|
const msg = packet.toBinary() as Uint8Array<ArrayBuffer>;
|
|
@@ -1631,314 +1604,46 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
1631
1604
|
);
|
|
1632
1605
|
}
|
|
1633
1606
|
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
1637
|
-
return this.sendLossyBytes(msg, kind);
|
|
1638
|
-
|
|
1639
|
-
case DataChannelKind.RELIABLE: {
|
|
1640
|
-
if (this.attemptingReconnect) {
|
|
1641
|
-
// A reconnect is already underway — queue for the resume replay instead of parking on a
|
|
1642
|
-
// channel that is being torn down. The send resolves; delivery is deferred to the replay.
|
|
1643
|
-
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1644
|
-
return;
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
const dc = this.dataChannelForKind(kind);
|
|
1648
|
-
if (dc) {
|
|
1649
|
-
try {
|
|
1650
|
-
await this.waitForBufferHeadroomWithLock(kind);
|
|
1651
|
-
} catch (error) {
|
|
1652
|
-
if (this.isClosed) {
|
|
1653
|
-
// No replay is coming after an engine close — surface the failure.
|
|
1654
|
-
throw error;
|
|
1655
|
-
}
|
|
1656
|
-
// Transient teardown (the channel closed or was replaced while we waited): the
|
|
1657
|
-
// reliable channel promises delivery across resume, so queue the packet for the
|
|
1658
|
-
// replay instead of rejecting a send the app can't meaningfully retry.
|
|
1659
|
-
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
if (this.attemptingReconnect) {
|
|
1664
|
-
// A reconnect began while we waited for headroom — same deal as above.
|
|
1665
|
-
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: false });
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
this.reliableMessageBuffer.push({ data: msg, sequence: packet.sequence, sent: true });
|
|
1670
|
-
dc.send(msg);
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
this.updateAndEmitDCBufferStatus(kind);
|
|
1674
|
-
break;
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
/* @internal */
|
|
1680
|
-
async sendLossyBytes(
|
|
1681
|
-
bytes: NonSharedUint8Array,
|
|
1682
|
-
kind: Exclude<DataChannelKind, DataChannelKind.RELIABLE>,
|
|
1683
|
-
bufferStatusFullBehavior: 'drop' | 'wait' = 'drop',
|
|
1684
|
-
) {
|
|
1685
|
-
// Make sure we do have a data connection. This matters most for the direct data-track path
|
|
1686
|
-
// (Room's packetAvailable handler), which doesn't go through sendDataPacket: it both waits for
|
|
1687
|
-
// the channel to be open and — on lazily negotiated publisher connections — is what kicks the
|
|
1688
|
-
// negotiation off in the first place. The call is memoized, so the steady-state cost is one
|
|
1689
|
-
// await on an already-resolved promise.
|
|
1690
|
-
await this.ensurePublisherConnected(kind);
|
|
1691
|
-
|
|
1692
|
-
const dc = this.dataChannelForKind(kind);
|
|
1693
|
-
try {
|
|
1694
|
-
if (dc) {
|
|
1695
|
-
// Depending on the exact circumstance that data is being sent, either drop or wait for the
|
|
1696
|
-
// buffer to drain below the high-water mark before continuing.
|
|
1697
|
-
switch (bufferStatusFullBehavior) {
|
|
1698
|
-
case 'wait':
|
|
1699
|
-
if (!this.isBelowHighWaterMark(kind)) {
|
|
1700
|
-
await this.waitForBufferHeadroomWithLock(kind);
|
|
1701
|
-
}
|
|
1702
|
-
break;
|
|
1703
|
-
case 'drop':
|
|
1704
|
-
// we check against the actual threshold on the DC here, as it is dynamic for the lossy DC
|
|
1705
|
-
if (!this.isBelowLowWaterMark(kind)) {
|
|
1706
|
-
// Drop messages to reduce latency
|
|
1707
|
-
this.lossyDataDropCount += 1;
|
|
1708
|
-
if (this.lossyDataDropCount % 100 === 0) {
|
|
1709
|
-
this.log.warn(
|
|
1710
|
-
`dropping lossy data channel messages, total dropped: ${this.lossyDataDropCount}`,
|
|
1711
|
-
);
|
|
1712
|
-
}
|
|
1713
|
-
return;
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
if (kind === DataChannelKind.LOSSY) {
|
|
1717
|
-
// The byterate stat tunes the LOSSY channel's dynamic drop threshold; counting data-track
|
|
1718
|
-
// bytes here would inflate it and let the lossy channel buffer far more latency than the
|
|
1719
|
-
// ~100ms the tuning targets.
|
|
1720
|
-
this.lossyDataStatCurrentBytes += bytes.byteLength;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (this.attemptingReconnect) {
|
|
1724
|
-
return;
|
|
1725
|
-
}
|
|
1726
|
-
|
|
1727
|
-
dc.send(bytes);
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
this.updateAndEmitDCBufferStatus(kind);
|
|
1731
|
-
} catch (error: unknown) {
|
|
1732
|
-
// ensure same surface behaviour as before with missing data channel being silently ignored, just log an error message for clarity
|
|
1733
|
-
if (error instanceof TypeError) {
|
|
1734
|
-
this.log.error(error);
|
|
1735
|
-
} else {
|
|
1736
|
-
throw error;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
private async resendReliableMessagesForResume(lastMessageSeq: number) {
|
|
1742
|
-
await this.ensurePublisherConnected(DataChannelKind.RELIABLE);
|
|
1743
|
-
const dc = this.dataChannelForKind(DataChannelKind.RELIABLE);
|
|
1744
|
-
if (dc) {
|
|
1745
|
-
this.reliableMessageBuffer.popToSequence(lastMessageSeq);
|
|
1746
|
-
// Hold the headroom lock across the whole replay. Releasing it between messages would let a
|
|
1747
|
-
// concurrent send — whose (newer) sequence was already assigned in sendDataPacket before it
|
|
1748
|
-
// queued on the lock — hit the wire mid-replay, and receivers would then discard the
|
|
1749
|
-
// remaining lower-sequence resent messages as duplicates.
|
|
1750
|
-
const unlock = await this.getBufferHeadroomLock(DataChannelKind.RELIABLE).lock();
|
|
1751
|
-
try {
|
|
1752
|
-
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
1753
|
-
this.reliableMessageBuffer.markAllUnsent();
|
|
1754
|
-
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
1755
|
-
// sent:false) during our own awaits appends after this pass started, so we pick it up on
|
|
1756
|
-
// the next one. We mark each packet only once we've actually handed it to the channel — a
|
|
1757
|
-
// blanket "mark all sent" would flip such a late arrival to sent without transmitting it,
|
|
1758
|
-
// and a later alignBufferedAmount would then drop it for good. If the loop throws
|
|
1759
|
-
// mid-drain, unsent entries keep their flag and the next replay picks them up.
|
|
1760
|
-
for (
|
|
1761
|
-
let batch = this.reliableMessageBuffer.getUnsent();
|
|
1762
|
-
batch.length > 0;
|
|
1763
|
-
batch = this.reliableMessageBuffer.getUnsent()
|
|
1764
|
-
) {
|
|
1765
|
-
for (const item of batch) {
|
|
1766
|
-
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
1767
|
-
// We already hold the lock across the whole replay, so use the lock-free wait.
|
|
1768
|
-
await this.waitForBufferHeadroomWithoutLock(DataChannelKind.RELIABLE);
|
|
1769
|
-
dc.send(item.data);
|
|
1770
|
-
this.reliableMessageBuffer.markSent(item);
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
} finally {
|
|
1774
|
-
unlock();
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
this.updateAndEmitDCBufferStatus(DataChannelKind.RELIABLE);
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
private updateAndEmitDCBufferStatus = (kind: DataChannelKind) => {
|
|
1607
|
+
// The full-buffer policy (drop for lossy, wait/replay for reliable) is the channel's own, as
|
|
1608
|
+
// is emitting the buffer-status change once the send settles.
|
|
1781
1609
|
if (kind === DataChannelKind.RELIABLE) {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
try {
|
|
1788
|
-
const status = this.isBelowLowWaterMark(kind);
|
|
1789
|
-
if (typeof status !== 'undefined' && status !== this.dcBufferStatus.get(kind)) {
|
|
1790
|
-
this.dcBufferStatus.set(kind, status);
|
|
1791
|
-
this.emit(EngineEvent.DCBufferStatusChanged, status, kind);
|
|
1792
|
-
}
|
|
1793
|
-
} catch (e) {
|
|
1794
|
-
this.log.warn('could not update buffer status', { error: e });
|
|
1795
|
-
}
|
|
1796
|
-
};
|
|
1797
|
-
|
|
1798
|
-
/**
|
|
1799
|
-
* Whether the send buffer has room to accept more data (the send gate). Senders proceed while
|
|
1800
|
-
* this is true and block once it goes false.
|
|
1801
|
-
*/
|
|
1802
|
-
private isBelowHighWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
|
|
1803
|
-
const dc = this.dataChannelForKind(kind);
|
|
1804
|
-
if (!dc) {
|
|
1805
|
-
throw new TypeError(`Could not get data channel for kind ${kind}`);
|
|
1806
|
-
}
|
|
1807
|
-
// because RTCDatachannel has no high water mark built in we read the statically defined versions as constants
|
|
1808
|
-
const highMark =
|
|
1809
|
-
kind === DataChannelKind.RELIABLE
|
|
1810
|
-
? reliableDataChannelWaterMarkHigh
|
|
1811
|
-
: lossyDataChannelWaterMarkHigh;
|
|
1812
|
-
return dc.bufferedAmount <= highMark;
|
|
1813
|
-
};
|
|
1814
|
-
|
|
1815
|
-
/**
|
|
1816
|
-
* Whether the send buffer has drained to its low-water mark. Drives the public
|
|
1817
|
-
* {@link EngineEvent.DCBufferStatusChanged} event.
|
|
1818
|
-
*/
|
|
1819
|
-
private isBelowLowWaterMark = (kind: DataChannelKind): Throws<boolean, TypeError> => {
|
|
1820
|
-
const dc = this.dataChannelForKind(kind);
|
|
1821
|
-
if (!dc) {
|
|
1822
|
-
throw new TypeError(`Could not get data channel for kind ${kind}`);
|
|
1610
|
+
await this.reliableChannel.send(msg, packet.sequence);
|
|
1611
|
+
} else {
|
|
1612
|
+
await this.lossyChannel.send(msg);
|
|
1823
1613
|
}
|
|
1824
|
-
|
|
1825
|
-
return dc.bufferedAmount <= dc.bufferedAmountLowThreshold;
|
|
1826
|
-
};
|
|
1827
|
-
|
|
1828
|
-
/** Per-kind lock serializing senders that have to wait for the buffer to drain. */
|
|
1829
|
-
private waitForBufferHeadroomLocks = new Map<DataChannelKind, Mutex>();
|
|
1614
|
+
}
|
|
1830
1615
|
|
|
1831
1616
|
/**
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
1835
|
-
*
|
|
1836
|
-
*
|
|
1617
|
+
* Sends pre-serialized bytes on the data-track channel. This is the one send path that doesn't
|
|
1618
|
+
* go through {@link sendDataPacket} — Room's `packetAvailable` handler calls it directly with
|
|
1619
|
+
* bytes the data-track pipeline already serialized.
|
|
1620
|
+
*
|
|
1621
|
+
* @internal
|
|
1837
1622
|
*/
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
this.waiterAbortControllers.set(kind, controller);
|
|
1845
|
-
}
|
|
1846
|
-
return controller.signal;
|
|
1623
|
+
async sendDataTrackFrame(bytes: NonSharedUint8Array) {
|
|
1624
|
+
// Make sure we do have a data connection: on lazily negotiated publisher connections this is
|
|
1625
|
+
// what kicks negotiation off, and it waits for the channel to open. Memoized, so the
|
|
1626
|
+
// steady-state cost is one await on an already-resolved promise.
|
|
1627
|
+
await this.ensurePublisherConnected(DataChannelKind.DATA_TRACK_LOSSY);
|
|
1628
|
+
await this.dataTrackChannel.send(bytes);
|
|
1847
1629
|
}
|
|
1848
1630
|
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
controller.abort(reason);
|
|
1853
|
-
}
|
|
1854
|
-
this.waiterAbortControllers.clear();
|
|
1631
|
+
private async resendReliableMessagesForResume(lastMessageSeq: number) {
|
|
1632
|
+
await this.ensurePublisherConnected(DataChannelKind.RELIABLE);
|
|
1633
|
+
await this.reliableChannel.replay(lastMessageSeq);
|
|
1855
1634
|
}
|
|
1856
1635
|
|
|
1857
|
-
/**
|
|
1858
|
-
private
|
|
1859
|
-
|
|
1860
|
-
if (!lock) {
|
|
1861
|
-
lock = new Mutex();
|
|
1862
|
-
this.waitForBufferHeadroomLocks.set(kind, lock);
|
|
1863
|
-
}
|
|
1864
|
-
return lock;
|
|
1636
|
+
/** The flow-control gate for `kind` — see {@link FlowControlledDataChannel}. */
|
|
1637
|
+
private flowControlFor(kind: DataChannelKind): FlowControlledDataChannel {
|
|
1638
|
+
return this.dataChannels.channelFor(kind);
|
|
1865
1639
|
}
|
|
1866
1640
|
|
|
1867
1641
|
/**
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
* resume replay) acquire it via {@link getBufferHeadroomLock} and call
|
|
1871
|
-
* {@link waitForBufferHeadroomWithoutLock} per message instead.
|
|
1642
|
+
* Resolves once the caller may send on the `kind` channel — see
|
|
1643
|
+
* {@link FlowControlledDataChannel.waitForHeadroomWithLock}.
|
|
1872
1644
|
*/
|
|
1873
|
-
async
|
|
1874
|
-
|
|
1875
|
-
try {
|
|
1876
|
-
await this.waitForBufferHeadroomWithoutLock(kind);
|
|
1877
|
-
} finally {
|
|
1878
|
-
unlock();
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
/**
|
|
1883
|
-
* Waits until the send buffer for `kind` is at or below the high-water mark (draining, if
|
|
1884
|
-
* needed, via the `bufferedamountlow` event). Does no locking of its own — the caller must
|
|
1885
|
-
* already hold the kind's headroom lock (via {@link getBufferHeadroomLock}). The resume replay
|
|
1886
|
-
* holds the lock across its whole batch and calls this per message so no other sender can
|
|
1887
|
-
* interleave; the single-send path goes through {@link waitForBufferHeadroomWithLock}.
|
|
1888
|
-
*
|
|
1889
|
-
* Serializing through the per-kind lock means that, once the buffer drains, queued callers
|
|
1890
|
-
* refill it one at a time (up to the high-water mark) rather than all at once and overflowing
|
|
1891
|
-
* the SCTP send buffer (see livekit/client-sdk-js#1995).
|
|
1892
|
-
*/
|
|
1893
|
-
private async waitForBufferHeadroomWithoutLock(
|
|
1894
|
-
kind: DataChannelKind,
|
|
1895
|
-
): Promise<Throws<void, UnexpectedConnectionState | TypeError>> {
|
|
1896
|
-
if (this.isClosed) {
|
|
1897
|
-
throw new UnexpectedConnectionState('engine closed');
|
|
1898
|
-
}
|
|
1899
|
-
if (this.isBelowHighWaterMark(kind)) {
|
|
1900
|
-
return;
|
|
1901
|
-
}
|
|
1902
|
-
const dc = this.dataChannelForKind(kind);
|
|
1903
|
-
if (!dc) {
|
|
1904
|
-
throw new UnexpectedConnectionState(`DataChannel not found, kind: ${kind}`);
|
|
1905
|
-
}
|
|
1906
|
-
const abortSignal = this.waiterAbortSignal(kind);
|
|
1907
|
-
await new TypedPromise<void, UnexpectedConnectionState>((resolve, reject) => {
|
|
1908
|
-
const onBufferedAmountLow = () => {
|
|
1909
|
-
cleanup();
|
|
1910
|
-
resolve();
|
|
1911
|
-
};
|
|
1912
|
-
const onDCClose = () => {
|
|
1913
|
-
cleanup();
|
|
1914
|
-
reject(
|
|
1915
|
-
new UnexpectedConnectionState(`DataChannel ${kind} closed while draining the buffer`),
|
|
1916
|
-
);
|
|
1917
|
-
};
|
|
1918
|
-
const onAbort = () => {
|
|
1919
|
-
cleanup();
|
|
1920
|
-
reject(
|
|
1921
|
-
new UnexpectedConnectionState(
|
|
1922
|
-
`DataChannel ${kind} was replaced or torn down while waiting for headroom`,
|
|
1923
|
-
),
|
|
1924
|
-
);
|
|
1925
|
-
};
|
|
1926
|
-
const cleanup = () => {
|
|
1927
|
-
dc.removeEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
1928
|
-
dc.removeEventListener('close', onDCClose);
|
|
1929
|
-
abortSignal.removeEventListener('abort', onAbort);
|
|
1930
|
-
};
|
|
1931
|
-
if (abortSignal.aborted) {
|
|
1932
|
-
onAbort();
|
|
1933
|
-
return;
|
|
1934
|
-
}
|
|
1935
|
-
dc.addEventListener('bufferedamountlow', onBufferedAmountLow);
|
|
1936
|
-
// Proxy along any error caused by the data channel closing while we wait.
|
|
1937
|
-
dc.addEventListener('close', onDCClose);
|
|
1938
|
-
// The channel object we're parked on can be abandoned without ever firing another event
|
|
1939
|
-
// (e.g. createDataChannels replacing it); the abort is our way out.
|
|
1940
|
-
abortSignal.addEventListener('abort', onAbort);
|
|
1941
|
-
});
|
|
1645
|
+
async waitForBufferHeadroom(kind: DataChannelKind) {
|
|
1646
|
+
return this.flowControlFor(kind).waitForHeadroomWithLock();
|
|
1942
1647
|
}
|
|
1943
1648
|
|
|
1944
1649
|
/**
|
|
@@ -2012,11 +1717,12 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
2012
1717
|
if (!this.pcManager) {
|
|
2013
1718
|
return false;
|
|
2014
1719
|
}
|
|
1720
|
+
const state = this.pcManager.currentState;
|
|
2015
1721
|
const allowedConnectionStates: PCTransportState[] = [
|
|
2016
1722
|
PCTransportState.CONNECTING,
|
|
2017
1723
|
PCTransportState.CONNECTED,
|
|
2018
1724
|
];
|
|
2019
|
-
if (!allowedConnectionStates.includes(
|
|
1725
|
+
if (!allowedConnectionStates.includes(state)) {
|
|
2020
1726
|
return false;
|
|
2021
1727
|
}
|
|
2022
1728
|
|
|
@@ -2024,6 +1730,20 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
2024
1730
|
if (!this.client.ws || this.client.ws.readyState === WebSocket.CLOSED) {
|
|
2025
1731
|
return false;
|
|
2026
1732
|
}
|
|
1733
|
+
|
|
1734
|
+
// A transport stuck in CONNECTING never reaches CONNECTED nor reports FAILED, so it would
|
|
1735
|
+
// otherwise look healthy forever; bound how long we tolerate it. The entry time is recorded
|
|
1736
|
+
// in the pcManager state-change handler (see configure()), so this is a pure read — an
|
|
1737
|
+
// unrecorded CONNECTING fails open rather than measuring against a stale timestamp.
|
|
1738
|
+
if (
|
|
1739
|
+
state === PCTransportState.CONNECTING &&
|
|
1740
|
+
this.transportConnectingSince !== undefined &&
|
|
1741
|
+
Date.now() - this.transportConnectingSince > this.peerConnectionTimeout
|
|
1742
|
+
) {
|
|
1743
|
+
this.log.warn('transport stuck in connecting state', this.logContext);
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
2027
1747
|
return true;
|
|
2028
1748
|
}
|
|
2029
1749
|
|
|
@@ -2040,9 +1760,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
2040
1760
|
// don't negotiate without any transceivers or data channel, it will generate sdp without ice frag then negotiate failed
|
|
2041
1761
|
if (
|
|
2042
1762
|
this.pcManager.publisher.getTransceivers().length == 0 &&
|
|
2043
|
-
!this.
|
|
2044
|
-
!this.reliableDC &&
|
|
2045
|
-
!this.dataTrackDC
|
|
1763
|
+
!this.dataChannels.hasPublisherChannels
|
|
2046
1764
|
) {
|
|
2047
1765
|
this.createDataChannels();
|
|
2048
1766
|
}
|
|
@@ -2091,26 +1809,7 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
|
|
|
2091
1809
|
}
|
|
2092
1810
|
|
|
2093
1811
|
dataChannelForKind(kind: DataChannelKind, sub?: boolean): RTCDataChannel | undefined {
|
|
2094
|
-
|
|
2095
|
-
case DataChannelKind.RELIABLE:
|
|
2096
|
-
if (!sub) {
|
|
2097
|
-
return this.reliableDC;
|
|
2098
|
-
} else {
|
|
2099
|
-
return this.reliableDCSub;
|
|
2100
|
-
}
|
|
2101
|
-
case DataChannelKind.LOSSY:
|
|
2102
|
-
if (!sub) {
|
|
2103
|
-
return this.lossyDC;
|
|
2104
|
-
} else {
|
|
2105
|
-
return this.lossyDCSub;
|
|
2106
|
-
}
|
|
2107
|
-
case DataChannelKind.DATA_TRACK_LOSSY:
|
|
2108
|
-
if (!sub) {
|
|
2109
|
-
return this.dataTrackDC;
|
|
2110
|
-
} else {
|
|
2111
|
-
return this.dataTrackDCSub;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
1812
|
+
return this.dataChannels.getHandle(kind, sub);
|
|
2114
1813
|
}
|
|
2115
1814
|
|
|
2116
1815
|
/** @internal */
|