livekit-client 2.22.4 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +69 -7
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1133 -289
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -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/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +26 -4
- package/dist/src/room/PCTransport.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 +53 -0
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/rpc/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +18 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +11 -0
- package/dist/src/room/track/processor/types.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +28 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/ts4.2/api/WebSocketStream.d.ts +1 -0
- package/dist/ts4.2/api/utils.d.ts +1 -1
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/ts4.2/index.d.ts +2 -2
- package/dist/ts4.2/room/PCTransport.d.ts +26 -4
- package/dist/ts4.2/room/RTCEngine.d.ts +8 -0
- package/dist/ts4.2/room/Room.d.ts +53 -0
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +4 -0
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/ts4.2/room/track/LocalTrack.d.ts +18 -0
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +7 -0
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/ts4.2/room/track/processor/types.d.ts +11 -0
- package/dist/ts4.2/room/utils.d.ts +28 -1
- package/package.json +1 -1
- package/src/api/SignalClient.test.ts +234 -0
- package/src/api/SignalClient.ts +58 -23
- package/src/api/WebSocketStream.test.ts +11 -0
- package/src/api/WebSocketStream.ts +22 -1
- package/src/api/utils.ts +1 -1
- package/src/e2ee/E2eeManager.test.ts +43 -0
- package/src/e2ee/E2eeManager.ts +6 -2
- package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
- package/src/e2ee/worker/FrameCryptor.ts +21 -4
- package/src/e2ee/worker/naluUtils.test.ts +123 -0
- package/src/e2ee/worker/naluUtils.ts +57 -2
- package/src/index.ts +2 -0
- package/src/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +144 -2
- package/src/room/PCTransport.ts +151 -9
- package/src/room/RTCEngine.test.ts +234 -2
- package/src/room/RTCEngine.ts +102 -7
- package/src/room/Room.test.ts +116 -0
- package/src/room/Room.ts +286 -74
- package/src/room/data-channel/DataChannelManager.test.ts +24 -7
- package/src/room/data-channel/DataChannelManager.ts +43 -38
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
- package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
- package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
- package/src/room/data-channel/LossyDataChannel.ts +10 -1
- package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
- package/src/room/data-channel/ReliableDataChannel.ts +16 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +31 -0
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +11 -0
- package/src/room/participant/LocalParticipant.test.ts +136 -1
- package/src/room/participant/LocalParticipant.ts +66 -17
- package/src/room/participant/Participant.ts +6 -3
- package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
- package/src/room/rpc/utils.test.ts +32 -0
- package/src/room/rpc/utils.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +101 -10
- package/src/room/track/LocalTrack.ts +70 -13
- package/src/room/track/LocalVideoTrack.ts +7 -0
- package/src/room/track/RemoteAudioTrack.ts +56 -11
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +5 -0
- package/src/room/track/utils.ts +29 -5
- package/src/room/utils.test.ts +126 -0
- package/src/room/utils.ts +84 -21
|
@@ -8708,6 +8708,11 @@ function onWorkerLogLevelChanged(cb) {
|
|
|
8708
8708
|
workerLogLevelListeners.delete(cb);
|
|
8709
8709
|
};
|
|
8710
8710
|
}const maxRetryDelay = 7000;
|
|
8711
|
+
/**
|
|
8712
|
+
* Upper bound of the random delay added to the first retry, whose base delay is 0. It keeps a
|
|
8713
|
+
* brief network blip recovering quickly while spreading out clients that disconnect together.
|
|
8714
|
+
*/
|
|
8715
|
+
const maxFirstRetryJitterInMs = 500;
|
|
8711
8716
|
const DEFAULT_RETRY_DELAYS_IN_MS = [0, 300, 2 * 2 * 300, 3 * 3 * 300, 4 * 4 * 300, maxRetryDelay, maxRetryDelay, maxRetryDelay, maxRetryDelay, maxRetryDelay];
|
|
8712
8717
|
class DefaultReconnectPolicy {
|
|
8713
8718
|
constructor(retryDelays) {
|
|
@@ -8716,8 +8721,13 @@ class DefaultReconnectPolicy {
|
|
|
8716
8721
|
nextRetryDelayInMs(context) {
|
|
8717
8722
|
if (context.retryCount >= this._retryDelays.length) return null;
|
|
8718
8723
|
const retryDelay = this._retryDelays[context.retryCount];
|
|
8719
|
-
|
|
8720
|
-
|
|
8724
|
+
// The first retry has no base delay to scale, so it gets a fixed window instead. A zero at a
|
|
8725
|
+
// later index is an explicit request for an immediate retry and passes through the formula as zero.
|
|
8726
|
+
if (context.retryCount === 0 && retryDelay === 0) {
|
|
8727
|
+
return Math.random() * maxFirstRetryJitterInMs;
|
|
8728
|
+
}
|
|
8729
|
+
// Jitter scales with the delay (+/-50%) so clients spread out further on later retries.
|
|
8730
|
+
return Math.round(retryDelay * (0.5 + Math.random()));
|
|
8721
8731
|
}
|
|
8722
8732
|
}/******************************************************************************
|
|
8723
8733
|
Copyright (c) Microsoft Corporation.
|
|
@@ -12290,7 +12300,7 @@ function getMatch(exp, ua) {
|
|
|
12290
12300
|
}
|
|
12291
12301
|
function getOSVersion(ua) {
|
|
12292
12302
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
|
12293
|
-
}var version$1 = "2.
|
|
12303
|
+
}var version$1 = "2.23.0";const version = version$1;
|
|
12294
12304
|
const protocolVersion = 17;
|
|
12295
12305
|
/** Initial client protocol. */
|
|
12296
12306
|
const CLIENT_PROTOCOL_DEFAULT = 0;
|
|
@@ -13558,20 +13568,44 @@ function detectSilence(track_1) {
|
|
|
13558
13568
|
let timeOffset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 200;
|
|
13559
13569
|
return function* () {
|
|
13560
13570
|
const ctx = getNewAudioContext();
|
|
13561
|
-
if (ctx) {
|
|
13562
|
-
|
|
13571
|
+
if (!ctx) {
|
|
13572
|
+
return false;
|
|
13573
|
+
}
|
|
13574
|
+
let source;
|
|
13575
|
+
let analyser;
|
|
13576
|
+
try {
|
|
13577
|
+
analyser = ctx.createAnalyser();
|
|
13563
13578
|
analyser.fftSize = 2048;
|
|
13564
13579
|
const bufferLength = analyser.frequencyBinCount;
|
|
13565
13580
|
const dataArray = new Uint8Array(bufferLength);
|
|
13566
|
-
|
|
13581
|
+
source = ctx.createMediaStreamSource(new MediaStream([track.mediaStreamTrack]));
|
|
13567
13582
|
source.connect(analyser);
|
|
13583
|
+
// read through a function so that `state` isn't narrowed to its value from before `resume`
|
|
13584
|
+
const isRunning = () => ctx.state === 'running';
|
|
13585
|
+
if (!isRunning()) {
|
|
13586
|
+
try {
|
|
13587
|
+
// `resume` can stay pending indefinitely on a blocked page rather than rejecting, which
|
|
13588
|
+
// would keep the context and its nodes alive past the `finally` below
|
|
13589
|
+
yield Promise.race([ctx.resume(), sleep(200)]);
|
|
13590
|
+
} catch (_a) {
|
|
13591
|
+
// autoplay policies can keep us from resuming the context
|
|
13592
|
+
}
|
|
13593
|
+
if (!isRunning()) {
|
|
13594
|
+
// a context that isn't running doesn't process any audio, so the analyser would only ever
|
|
13595
|
+
// read silence. Report the track as not silent rather than raising a false positive.
|
|
13596
|
+
return false;
|
|
13597
|
+
}
|
|
13598
|
+
}
|
|
13568
13599
|
yield sleep(timeOffset);
|
|
13569
13600
|
analyser.getByteTimeDomainData(dataArray);
|
|
13570
13601
|
const someNoise = dataArray.some(sample => sample !== 128 && sample !== 0);
|
|
13571
|
-
ctx.close();
|
|
13572
13602
|
return !someNoise;
|
|
13603
|
+
} finally {
|
|
13604
|
+
source === null || source === void 0 ? void 0 : source.disconnect();
|
|
13605
|
+
analyser === null || analyser === void 0 ? void 0 : analyser.disconnect();
|
|
13606
|
+
// don't let a failing teardown mask the detection result
|
|
13607
|
+
yield ctx.close().catch(() => {});
|
|
13573
13608
|
}
|
|
13574
|
-
return false;
|
|
13575
13609
|
}();
|
|
13576
13610
|
});
|
|
13577
13611
|
}
|
|
@@ -14710,6 +14744,15 @@ function createDummyVideoStreamTrack() {
|
|
|
14710
14744
|
return dummyTrack;
|
|
14711
14745
|
}
|
|
14712
14746
|
let emptyAudioStreamTrack;
|
|
14747
|
+
let emptyAudioStreamContext;
|
|
14748
|
+
/** tracks handed out by `getEmptyAudioStreamTrack` that haven't been released yet */
|
|
14749
|
+
const outstandingEmptyAudioStreamTracks = new Set();
|
|
14750
|
+
/**
|
|
14751
|
+
* Returns a silent audio track, cloned off a shared `AudioContext` created on first use. Hand the
|
|
14752
|
+
* clone back to `releaseEmptyAudioStreamTrack` rather than stopping it, so the context can close.
|
|
14753
|
+
* A clone that is only stopped stays counted as outstanding, which holds the shared context open
|
|
14754
|
+
* for the lifetime of the page.
|
|
14755
|
+
*/
|
|
14713
14756
|
function getEmptyAudioStreamTrack() {
|
|
14714
14757
|
if (!emptyAudioStreamTrack) {
|
|
14715
14758
|
// implementation adapted from https://blog.mozilla.org/webrtc/warm-up-with-replacetrack/
|
|
@@ -14725,11 +14768,38 @@ function getEmptyAudioStreamTrack() {
|
|
|
14725
14768
|
var _dst$stream$getAudioT2 = _slicedToArray(_dst$stream$getAudioT, 1);
|
|
14726
14769
|
emptyAudioStreamTrack = _dst$stream$getAudioT2[0];
|
|
14727
14770
|
if (!emptyAudioStreamTrack) {
|
|
14771
|
+
// don't hold on to a context we can't get a track out of
|
|
14772
|
+
ctx.close().catch(() => {});
|
|
14728
14773
|
throw Error('Could not get empty media stream audio track');
|
|
14729
14774
|
}
|
|
14730
14775
|
emptyAudioStreamTrack.enabled = false;
|
|
14776
|
+
emptyAudioStreamContext = ctx;
|
|
14731
14777
|
}
|
|
14732
|
-
|
|
14778
|
+
const track = emptyAudioStreamTrack.clone();
|
|
14779
|
+
outstandingEmptyAudioStreamTracks.add(track);
|
|
14780
|
+
return track;
|
|
14781
|
+
}
|
|
14782
|
+
/**
|
|
14783
|
+
* Stops a track obtained from `getEmptyAudioStreamTrack` and closes the shared `AudioContext`
|
|
14784
|
+
* backing it once every track it handed out has been released. Does nothing for a track this
|
|
14785
|
+
* module never handed out, or one that has been released already.
|
|
14786
|
+
*/
|
|
14787
|
+
function releaseEmptyAudioStreamTrack(track) {
|
|
14788
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
14789
|
+
if (!outstandingEmptyAudioStreamTracks.delete(track)) {
|
|
14790
|
+
// the track wasn't obtained from `getEmptyAudioStreamTrack`, or has been released before
|
|
14791
|
+
return;
|
|
14792
|
+
}
|
|
14793
|
+
track.stop();
|
|
14794
|
+
if (outstandingEmptyAudioStreamTracks.size > 0) {
|
|
14795
|
+
return;
|
|
14796
|
+
}
|
|
14797
|
+
const context = emptyAudioStreamContext;
|
|
14798
|
+
emptyAudioStreamTrack === null || emptyAudioStreamTrack === void 0 ? void 0 : emptyAudioStreamTrack.stop();
|
|
14799
|
+
emptyAudioStreamTrack = undefined;
|
|
14800
|
+
emptyAudioStreamContext = undefined;
|
|
14801
|
+
yield context === null || context === void 0 ? void 0 : context.close();
|
|
14802
|
+
});
|
|
14733
14803
|
}
|
|
14734
14804
|
/** An object that represents a serialized version of a `new Promise((resolve, reject) => {})`
|
|
14735
14805
|
* constructor. Wait for a promise resolution with `await future.promise` and explicitly resolve or
|
|
@@ -14793,11 +14863,19 @@ function createAudioAnalyser(track, options) {
|
|
|
14793
14863
|
const volume = Math.sqrt(sum / dataArray.length);
|
|
14794
14864
|
return volume;
|
|
14795
14865
|
};
|
|
14866
|
+
let isCleanedUp = false;
|
|
14796
14867
|
const cleanup = () => __awaiter(this, void 0, void 0, function* () {
|
|
14797
|
-
|
|
14868
|
+
if (isCleanedUp) {
|
|
14869
|
+
// closing an already closed context would reject, make repeated cleanups a no-op instead
|
|
14870
|
+
return;
|
|
14871
|
+
}
|
|
14872
|
+
isCleanedUp = true;
|
|
14873
|
+
mediaStreamSource.disconnect();
|
|
14874
|
+
analyser.disconnect();
|
|
14798
14875
|
if (opts.cloneTrack) {
|
|
14799
14876
|
streamTrack.stop();
|
|
14800
14877
|
}
|
|
14878
|
+
yield audioContext.close();
|
|
14801
14879
|
});
|
|
14802
14880
|
return {
|
|
14803
14881
|
calculateVolume,
|
|
@@ -14832,6 +14910,31 @@ function unwrapConstraint(constraint) {
|
|
|
14832
14910
|
}
|
|
14833
14911
|
throw Error('could not unwrap constraint');
|
|
14834
14912
|
}
|
|
14913
|
+
/**
|
|
14914
|
+
* The getUserMedia rejections that no softer constraint can recover from, because the user or the
|
|
14915
|
+
* document refused capture outright. Retrying one of these would put a second prompt in front of
|
|
14916
|
+
* someone who already said no. The last two names are legacy spellings still reported by older
|
|
14917
|
+
* engines. Matched by `name` rather than with `instanceof`, because these are not reliably distinct
|
|
14918
|
+
* constructors across environments. happy-dom, which the tests run under, aliases them to `Event`.
|
|
14919
|
+
*/
|
|
14920
|
+
function isPermissionDeniedError(e) {
|
|
14921
|
+
return typeof e === 'object' && e !== null && 'name' in e && (e.name === 'NotAllowedError' || e.name === 'SecurityError' || e.name === 'PermissionDeniedError' || e.name === 'PermissionDismissedError');
|
|
14922
|
+
}
|
|
14923
|
+
const deviceAcquisitionFailures = new WeakSet();
|
|
14924
|
+
/** Records that `e` is the rejection getUserMedia itself produced. */
|
|
14925
|
+
function markDeviceAcquisitionFailure(e) {
|
|
14926
|
+
if (typeof e === 'object' && e !== null) {
|
|
14927
|
+
deviceAcquisitionFailures.add(e);
|
|
14928
|
+
}
|
|
14929
|
+
}
|
|
14930
|
+
/**
|
|
14931
|
+
* Whether a track restart failed at getUserMedia rather than in the work that adopts the new track.
|
|
14932
|
+
* Recorded on the side rather than by wrapping, because callers of `restartTrack` match on the
|
|
14933
|
+
* rejection's own name.
|
|
14934
|
+
*/
|
|
14935
|
+
function isDeviceAcquisitionFailure(e) {
|
|
14936
|
+
return typeof e === 'object' && e !== null && deviceAcquisitionFailures.has(e);
|
|
14937
|
+
}
|
|
14835
14938
|
function toWebsocketUrl(url) {
|
|
14836
14939
|
if (url.startsWith('http')) {
|
|
14837
14940
|
return url.replace(/^(http)/, 'ws');
|
|
@@ -16545,12 +16648,16 @@ class E2EEManager extends eventsExports.EventEmitter {
|
|
|
16545
16648
|
this.handleReceiver(track.receiver, track.mediaStreamID, remoteId, track.kind === 'video' ? mimeTypeToVideoCodecString(trackInfo.mimeType) : undefined, hasPacketTrailer);
|
|
16546
16649
|
}
|
|
16547
16650
|
setupE2EESender(track, sender) {
|
|
16548
|
-
var _a, _b, _c;
|
|
16651
|
+
var _a, _b, _c, _d;
|
|
16549
16652
|
if (!isLocalTrack(track) || !sender) {
|
|
16550
16653
|
if (!sender) this.log.warn('early return because sender is not ready');
|
|
16551
16654
|
return;
|
|
16552
16655
|
}
|
|
16553
|
-
|
|
16656
|
+
const isVideo = isVideoTrack(track);
|
|
16657
|
+
// Safari exposes no per-frame codec info until trackInfo arrives. Seed only h264/h265:
|
|
16658
|
+
// a wrong vp8 seed would skip the NALU path, a wrong NALU seed only costs a logged fallback.
|
|
16659
|
+
const publishedCodec = isVideo ? (_a = track.publishOptions) === null || _a === void 0 ? void 0 : _a.videoCodec : undefined;
|
|
16660
|
+
this.handleSender(sender, track.mediaStreamID, publishedCodec === 'h264' || publishedCodec === 'h265' ? publishedCodec : undefined, isVideo ? (_c = (_b = track.publishOptions) === null || _b === void 0 ? void 0 : _b.frameMetadata) !== null && _c !== void 0 ? _c : (_d = track.publishOptions) === null || _d === void 0 ? void 0 : _d.packetTrailer : undefined);
|
|
16554
16661
|
}
|
|
16555
16662
|
/**
|
|
16556
16663
|
* Handles the given {@code RTCRtpReceiver} by creating a {@code TransformStream} which will inject
|
|
@@ -17320,6 +17427,58 @@ class AsyncQueue {
|
|
|
17320
17427
|
snapshot() {
|
|
17321
17428
|
return Array.from(this.pendingTasks.values());
|
|
17322
17429
|
}
|
|
17430
|
+
}/**
|
|
17431
|
+
* Implementation of AbortSignal.any
|
|
17432
|
+
* Creates a signal that will be aborted when any of the given signals is aborted.
|
|
17433
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any
|
|
17434
|
+
*/
|
|
17435
|
+
function abortSignalAny(signals) {
|
|
17436
|
+
// Handle empty signals array
|
|
17437
|
+
if (signals.length === 0) {
|
|
17438
|
+
const controller = new AbortController();
|
|
17439
|
+
return controller.signal;
|
|
17440
|
+
}
|
|
17441
|
+
// Fast path for single signal
|
|
17442
|
+
if (signals.length === 1) {
|
|
17443
|
+
return signals[0];
|
|
17444
|
+
}
|
|
17445
|
+
// Check if any signal is already aborted
|
|
17446
|
+
for (const signal of signals) {
|
|
17447
|
+
if (signal.aborted) {
|
|
17448
|
+
return signal;
|
|
17449
|
+
}
|
|
17450
|
+
}
|
|
17451
|
+
// Create a new controller for the combined signal
|
|
17452
|
+
const controller = new AbortController();
|
|
17453
|
+
const unlisteners = Array(signals.length);
|
|
17454
|
+
// Function to clean up all event listeners
|
|
17455
|
+
const cleanup = () => {
|
|
17456
|
+
for (const unsubscribe of unlisteners) {
|
|
17457
|
+
unsubscribe();
|
|
17458
|
+
}
|
|
17459
|
+
};
|
|
17460
|
+
// Add event listeners to each signal
|
|
17461
|
+
signals.forEach((signal, index) => {
|
|
17462
|
+
const handler = () => {
|
|
17463
|
+
controller.abort(signal.reason);
|
|
17464
|
+
cleanup();
|
|
17465
|
+
};
|
|
17466
|
+
signal.addEventListener('abort', handler);
|
|
17467
|
+
unlisteners[index] = () => signal.removeEventListener('abort', handler);
|
|
17468
|
+
});
|
|
17469
|
+
return controller.signal;
|
|
17470
|
+
}
|
|
17471
|
+
/**
|
|
17472
|
+
* Implementation of AbortSignal.timeout
|
|
17473
|
+
* Creates a signal that will be aborted after the specified timeout.
|
|
17474
|
+
* @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
|
|
17475
|
+
*/
|
|
17476
|
+
function abortSignalTimeout(ms) {
|
|
17477
|
+
const controller = new AbortController();
|
|
17478
|
+
setTimeout(() => {
|
|
17479
|
+
controller.abort(new DOMException("signal timed out after ".concat(ms, " ms"), 'TimeoutError'));
|
|
17480
|
+
}, ms);
|
|
17481
|
+
return controller.signal;
|
|
17323
17482
|
}/**
|
|
17324
17483
|
* Opt-in registry that lets a development tool observe the state machines driving the connection
|
|
17325
17484
|
* layer.
|
|
@@ -18477,7 +18636,7 @@ class WebSocketStream {
|
|
|
18477
18636
|
let _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
|
|
18478
18637
|
code = _ref.closeCode,
|
|
18479
18638
|
reason = _ref.reason;
|
|
18480
|
-
return ws.close(code, reason);
|
|
18639
|
+
return ws.close(code, reason === undefined ? undefined : truncateCloseReason(reason));
|
|
18481
18640
|
};
|
|
18482
18641
|
this.opened = new TypedPromise((resolve, reject) => {
|
|
18483
18642
|
const rejectHandler = () => {
|
|
@@ -18553,6 +18712,25 @@ class WebSocketStream {
|
|
|
18553
18712
|
}
|
|
18554
18713
|
this.close = closeWithInfo;
|
|
18555
18714
|
}
|
|
18715
|
+
}
|
|
18716
|
+
/** `WebSocket.close` throws a `SyntaxError` for a reason longer than this many UTF-8 bytes. */
|
|
18717
|
+
const MAX_CLOSE_REASON_BYTES = 123;
|
|
18718
|
+
function truncateCloseReason(reason) {
|
|
18719
|
+
const encoder = new TextEncoder();
|
|
18720
|
+
if (encoder.encode(reason).length <= MAX_CLOSE_REASON_BYTES) {
|
|
18721
|
+
return reason;
|
|
18722
|
+
}
|
|
18723
|
+
// cut on code point boundaries, so a multi-byte character is never split
|
|
18724
|
+
let truncated = '';
|
|
18725
|
+
let bytes = 0;
|
|
18726
|
+
for (const char of reason) {
|
|
18727
|
+
bytes += encoder.encode(char).length;
|
|
18728
|
+
if (bytes > MAX_CLOSE_REASON_BYTES) {
|
|
18729
|
+
break;
|
|
18730
|
+
}
|
|
18731
|
+
truncated += char;
|
|
18732
|
+
}
|
|
18733
|
+
return truncated;
|
|
18556
18734
|
}const passThroughQueueSignals = ['syncState', 'trickle', 'offer', 'answer', 'simulate', 'leave'];
|
|
18557
18735
|
/**
|
|
18558
18736
|
* Whether a failed resume may be followed by another one. Mirrors how `RTCEngine` classifies these
|
|
@@ -18768,12 +18946,25 @@ class SignalClient {
|
|
|
18768
18946
|
let publisherOffer = arguments.length > 5 ? arguments[5] : undefined;
|
|
18769
18947
|
return function* () {
|
|
18770
18948
|
const unlock = yield _this2.connectionLock.lock();
|
|
18771
|
-
|
|
18772
|
-
|
|
18773
|
-
|
|
18774
|
-
|
|
18775
|
-
|
|
18776
|
-
|
|
18949
|
+
let rtcUrl;
|
|
18950
|
+
let validateUrl;
|
|
18951
|
+
try {
|
|
18952
|
+
_this2.connectOptions = opts;
|
|
18953
|
+
_this2.useV0SignalPath = useV0Path;
|
|
18954
|
+
const clientInfo = getClientInfo(opts.clientInfoCapabilities);
|
|
18955
|
+
const params = useV0Path ? createConnectionParams(token, clientInfo, opts) : yield createJoinRequestConnectionParams(token, clientInfo, opts, publisherOffer);
|
|
18956
|
+
rtcUrl = createRtcUrl(url, params, useV0Path).toString();
|
|
18957
|
+
validateUrl = createValidateUrl(rtcUrl).toString();
|
|
18958
|
+
} catch (e) {
|
|
18959
|
+
// the executor below owns the lock from here on
|
|
18960
|
+
unlock();
|
|
18961
|
+
throw e;
|
|
18962
|
+
}
|
|
18963
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
18964
|
+
unlock();
|
|
18965
|
+
_this2.log.debug('signal connect abandoned before opening a transport');
|
|
18966
|
+
throw ConnectionError.cancelled('Connection aborted');
|
|
18967
|
+
}
|
|
18777
18968
|
return new Promise((resolve, reject) => __awaiter(_this2, void 0, void 0, function* () {
|
|
18778
18969
|
var _a, _b, _c, _d, _e;
|
|
18779
18970
|
try {
|
|
@@ -18787,12 +18978,12 @@ class SignalClient {
|
|
|
18787
18978
|
const reason = getAbortReasonAsString(target, 'Abort handler called');
|
|
18788
18979
|
// send leave if we have an active stream writer (connection is open)
|
|
18789
18980
|
if (this.streamWriter && !this.isDisconnected) {
|
|
18790
|
-
this.sendLeave().then(() => this.close(reason)).catch(e => {
|
|
18981
|
+
this.sendLeave().then(() => this.close(true, reason)).catch(e => {
|
|
18791
18982
|
this.log.error(e);
|
|
18792
|
-
this.close();
|
|
18983
|
+
this.close(true, reason);
|
|
18793
18984
|
});
|
|
18794
18985
|
} else {
|
|
18795
|
-
this.close();
|
|
18986
|
+
this.close(true, reason);
|
|
18796
18987
|
}
|
|
18797
18988
|
cleanupAbortHandlers();
|
|
18798
18989
|
// The caller may already have classified this failure: the connect timeout hands us a
|
|
@@ -18818,6 +19009,10 @@ class SignalClient {
|
|
|
18818
19009
|
yield this.teardownTransport('replaced by a new connection attempt');
|
|
18819
19010
|
this.log.debug("closed previous ws connection in ".concat(performance.now() - startClose, "ms"));
|
|
18820
19011
|
}
|
|
19012
|
+
if (alreadyAborted) {
|
|
19013
|
+
this.log.debug('signal connect abandoned after tearing down the previous transport');
|
|
19014
|
+
return;
|
|
19015
|
+
}
|
|
18821
19016
|
// the transport created below belongs to this attempt; events arriving from it after a
|
|
18822
19017
|
// newer attempt has started are dropped by the machine
|
|
18823
19018
|
const attemptId = this.attemptId;
|
|
@@ -18862,6 +19057,12 @@ class SignalClient {
|
|
|
18862
19057
|
}
|
|
18863
19058
|
});
|
|
18864
19059
|
const connection = yield this.ws.opened.catch(reason => __awaiter(this, void 0, void 0, function* () {
|
|
19060
|
+
if (alreadyAborted) {
|
|
19061
|
+
// the abort handler has already rejected this attempt; don't probe the validate
|
|
19062
|
+
// endpoint for a failure nobody is waiting on
|
|
19063
|
+
this.log.debug('signal connect abandoned while the websocket was opening');
|
|
19064
|
+
return;
|
|
19065
|
+
}
|
|
18865
19066
|
if (this.lifecycleState !== 'connected') {
|
|
18866
19067
|
// claimed synchronously, before the await below: the socket's close event is already
|
|
18867
19068
|
// on its way and would otherwise reject first with a less useful error
|
|
@@ -18924,6 +19125,13 @@ class SignalClient {
|
|
|
18924
19125
|
return;
|
|
18925
19126
|
}
|
|
18926
19127
|
signalReader.releaseLock();
|
|
19128
|
+
if (alreadyAborted) {
|
|
19129
|
+
// The abort handler has already rejected this attempt and is closing the transport.
|
|
19130
|
+
// Don't act on a join that raced it: `onJoined` would publish an abandoned session's
|
|
19131
|
+
// state to the room.
|
|
19132
|
+
this.log.debug('signal connect abandoned while waiting for the first message');
|
|
19133
|
+
return;
|
|
19134
|
+
}
|
|
18927
19135
|
// Validate the first message
|
|
18928
19136
|
const validation = this.validateFirstMessage(firstSignalResponse, (_d = opts.reconnect) !== null && _d !== void 0 ? _d : false);
|
|
18929
19137
|
if (!validation.isValid) {
|
|
@@ -19020,16 +19228,17 @@ class SignalClient {
|
|
|
19020
19228
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19021
19229
|
try {
|
|
19022
19230
|
this.clearPingInterval();
|
|
19023
|
-
|
|
19024
|
-
|
|
19231
|
+
const ws = this.ws;
|
|
19232
|
+
if (ws) {
|
|
19233
|
+
// released before closing, so a close that throws cannot leave the transport attached
|
|
19234
|
+
this.ws = undefined;
|
|
19235
|
+
this.streamWriter = undefined;
|
|
19236
|
+
ws.close({
|
|
19025
19237
|
closeCode: 1000,
|
|
19026
19238
|
reason
|
|
19027
19239
|
});
|
|
19028
19240
|
// calling `ws.close()` only starts the closing handshake (CLOSING state), prefer to wait until state is actually CLOSED
|
|
19029
|
-
|
|
19030
|
-
this.ws = undefined;
|
|
19031
|
-
this.streamWriter = undefined;
|
|
19032
|
-
yield Promise.race([closePromise, sleep(MAX_WS_CLOSE_TIME)]);
|
|
19241
|
+
yield Promise.race([ws.closed, sleep(MAX_WS_CLOSE_TIME)]);
|
|
19033
19242
|
}
|
|
19034
19243
|
} catch (e) {
|
|
19035
19244
|
this.log.debug('websocket error while closing', {
|
|
@@ -19552,7 +19761,9 @@ class SignalClient {
|
|
|
19552
19761
|
handleConnectionError(reason, validateUrl) {
|
|
19553
19762
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19554
19763
|
try {
|
|
19555
|
-
const resp = yield fetch(validateUrl
|
|
19764
|
+
const resp = yield fetch(validateUrl, {
|
|
19765
|
+
signal: abortSignalTimeout(3000)
|
|
19766
|
+
});
|
|
19556
19767
|
switch (resp.status) {
|
|
19557
19768
|
case 404:
|
|
19558
19769
|
const errorMsg = yield resp.text();
|
|
@@ -19576,7 +19787,11 @@ class SignalClient {
|
|
|
19576
19787
|
});
|
|
19577
19788
|
}
|
|
19578
19789
|
} catch (e) {
|
|
19579
|
-
|
|
19790
|
+
if (e instanceof ConnectionError) {
|
|
19791
|
+
return e;
|
|
19792
|
+
} else {
|
|
19793
|
+
return ConnectionError.serverUnreachable(e instanceof Error ? e.message : 'server was not reachable');
|
|
19794
|
+
}
|
|
19580
19795
|
}
|
|
19581
19796
|
});
|
|
19582
19797
|
}
|
|
@@ -20502,15 +20717,32 @@ function requireLib() {
|
|
|
20502
20717
|
* Why same for all codecs: Target bitrate already accounts for codec efficiency
|
|
20503
20718
|
* (e.g., users set lower targets for VP9/AV1 knowing they're more efficient).
|
|
20504
20719
|
* Why cap at 1 Mbps: Prevents BWE from starting too aggressively on high bitrate tracks.
|
|
20720
|
+
* The cap is lowered further for connections that were slow to set up, see
|
|
20721
|
+
* `computeStartBitrateCap`, or that the browser reports as slow, see
|
|
20722
|
+
* `connectionEffectiveTypeStartBitrateCap`.
|
|
20505
20723
|
*/
|
|
20506
20724
|
const startBitrateMultiplier = 0.9;
|
|
20507
20725
|
/** Maximum x-google-start-bitrate in kbps. 1 Mbps prevents BWE from starting too aggressively. */
|
|
20508
20726
|
const maxStartBitrateKbps = 1000;
|
|
20509
20727
|
/**
|
|
20510
|
-
* Minimum
|
|
20511
|
-
* real capacity costs more than the ramp it
|
|
20728
|
+
* Minimum x-google-start-bitrate in kbps: libwebrtc's own default starting estimate. A target
|
|
20729
|
+
* below this gets no hint, since seeding above the real capacity costs more than the ramp it
|
|
20730
|
+
* saves, and a slow connection is seeded no higher than this.
|
|
20512
20731
|
*/
|
|
20513
20732
|
const minTargetBitrateKbps = 300;
|
|
20733
|
+
/**
|
|
20734
|
+
* Connection setup times (ms) bounding the ramp in `computeStartBitrateCap`: at or below the
|
|
20735
|
+
* first the hint is capped at `maxStartBitrateKbps`, at or above the second at
|
|
20736
|
+
* `minTargetBitrateKbps`. Measured on shaped links (CLT-3380): healthy links set up in under
|
|
20737
|
+
* 860 ms, a 1 Mbps link with 150 ms RTT took 1.1–1.7 s, a 500 kbps link ~2.3 s at the median,
|
|
20738
|
+
* and a 300 kbps link never under 3 s. The first sits above the 1 Mbps link, which a lower seed
|
|
20739
|
+
* only slows down, and the second above the 500 kbps link's median, so only links that cannot
|
|
20740
|
+
* carry more land at the floor.
|
|
20741
|
+
*/
|
|
20742
|
+
const setupTimeForMaxStartBitrateMs = 1500;
|
|
20743
|
+
const setupTimeForMinStartBitrateMs = 3500;
|
|
20744
|
+
/** Cap on the start bitrate hint, in kbps, when the browser reports a `3g` connection. */
|
|
20745
|
+
const effectiveType3gStartBitrateKbps = 500;
|
|
20514
20746
|
const debounceInterval = 20;
|
|
20515
20747
|
/**
|
|
20516
20748
|
* Codec payload for `codec` in this media section, when the section carries `cid`.
|
|
@@ -20537,17 +20769,66 @@ function findTrackCodecPayload(media, cid, codec) {
|
|
|
20537
20769
|
* high-bitrate track; screen share is exempt, because its content needs the bitrate
|
|
20538
20770
|
* immediately to stay legible.
|
|
20539
20771
|
*
|
|
20540
|
-
*
|
|
20541
|
-
*
|
|
20772
|
+
* The cap is lowered for a connection that was slow to set up (`computeStartBitrateCap`), and
|
|
20773
|
+
* for one the browser reports as slow (`connectionEffectiveTypeStartBitrateCap`), whichever is
|
|
20774
|
+
* lower. Screen share is exempt from both, however slow the connection.
|
|
20542
20775
|
*
|
|
20543
20776
|
* @internal
|
|
20544
20777
|
*/
|
|
20545
|
-
function computeTrackStartBitrate(trackbr) {
|
|
20778
|
+
function computeTrackStartBitrate(trackbr, connectionSetupTimeMs) {
|
|
20546
20779
|
if (trackbr.maxbr < minTargetBitrateKbps) {
|
|
20547
20780
|
return undefined;
|
|
20548
20781
|
}
|
|
20549
|
-
const calculated = Math.round(trackbr.maxbr * startBitrateMultiplier);
|
|
20550
|
-
|
|
20782
|
+
const calculated = Math.min(Math.round(trackbr.maxbr * startBitrateMultiplier), trackbr.maxbr);
|
|
20783
|
+
if (trackbr.isScreenShare) {
|
|
20784
|
+
return calculated;
|
|
20785
|
+
}
|
|
20786
|
+
const cap = Math.min(computeStartBitrateCap(connectionSetupTimeMs), connectionEffectiveTypeStartBitrateCap());
|
|
20787
|
+
return Math.min(calculated, cap);
|
|
20788
|
+
}
|
|
20789
|
+
/**
|
|
20790
|
+
* Cap on the start bitrate hint, in kbps, from the browser's effective connection type
|
|
20791
|
+
* (`navigator.connection.effectiveType`, Chromium only): `slow-2g` and `2g` cap at
|
|
20792
|
+
* `minTargetBitrateKbps`, `3g` at `effectiveType3gStartBitrateKbps`.
|
|
20793
|
+
*
|
|
20794
|
+
* It catches links that have low bandwidth but low latency, which set up quickly and so slip
|
|
20795
|
+
* past the setup time ramp. `4g`, and browsers without the API, give no guidance and stay at the
|
|
20796
|
+
* `maxStartBitrateKbps` ceiling, as `computeStartBitrateCap` does without a setup time. The
|
|
20797
|
+
* connection RTT the API also reports is not used: it lags behind network changes, and the setup
|
|
20798
|
+
* time already reflects latency.
|
|
20799
|
+
*/
|
|
20800
|
+
function connectionEffectiveTypeStartBitrateCap() {
|
|
20801
|
+
// A `typeof` guard, since `globalThis` is missing from some supported browsers (Chrome < 71,
|
|
20802
|
+
// Firefox < 65, Safari < 12.1) and the build does not polyfill it.
|
|
20803
|
+
const connection = typeof navigator !== 'undefined' ? navigator.connection : undefined;
|
|
20804
|
+
switch (connection === null || connection === void 0 ? void 0 : connection.effectiveType) {
|
|
20805
|
+
case 'slow-2g':
|
|
20806
|
+
case '2g':
|
|
20807
|
+
return minTargetBitrateKbps;
|
|
20808
|
+
case '3g':
|
|
20809
|
+
return effectiveType3gStartBitrateKbps;
|
|
20810
|
+
default:
|
|
20811
|
+
return maxStartBitrateKbps;
|
|
20812
|
+
}
|
|
20813
|
+
}
|
|
20814
|
+
/**
|
|
20815
|
+
* Cap on the start bitrate hint, in kbps, from how long the connection took to set up.
|
|
20816
|
+
*
|
|
20817
|
+
* Connection setup time (signaling join plus ICE/DTLS) is the network signal every platform has
|
|
20818
|
+
* before the first video offer, since libwebrtc cannot probe the path until a video sender
|
|
20819
|
+
* exists. It grows with round-trip time and loss, which also mark the links where a 1 Mbps
|
|
20820
|
+
* seed overshoots, so the cap ramps linearly from `maxStartBitrateKbps` at
|
|
20821
|
+
* `setupTimeForMaxStartBitrateMs` down to `minTargetBitrateKbps` at
|
|
20822
|
+
* `setupTimeForMinStartBitrateMs`. A video offer built while the connection is still being set
|
|
20823
|
+
* up (publishing during connect) passes the time elapsed so far, a lower bound on the setup time.
|
|
20824
|
+
* Without any timing the cap stays at the 1 Mbps ceiling.
|
|
20825
|
+
*/
|
|
20826
|
+
function computeStartBitrateCap(connectionSetupTimeMs) {
|
|
20827
|
+
if (connectionSetupTimeMs === undefined) {
|
|
20828
|
+
return maxStartBitrateKbps;
|
|
20829
|
+
}
|
|
20830
|
+
const ramp = Math.min(Math.max((connectionSetupTimeMs - setupTimeForMaxStartBitrateMs) / (setupTimeForMinStartBitrateMs - setupTimeForMaxStartBitrateMs), 0), 1);
|
|
20831
|
+
return Math.round(maxStartBitrateKbps - ramp * (maxStartBitrateKbps - minTargetBitrateKbps));
|
|
20551
20832
|
}
|
|
20552
20833
|
/**
|
|
20553
20834
|
* The single start bitrate for this peer connection: the largest hint among the video
|
|
@@ -20576,7 +20857,7 @@ function computeTrackStartBitrate(trackbr) {
|
|
|
20576
20857
|
*
|
|
20577
20858
|
* @internal
|
|
20578
20859
|
*/
|
|
20579
|
-
function computeConnectionStartBitrate(media, trackBitrates) {
|
|
20860
|
+
function computeConnectionStartBitrate(media, trackBitrates, connectionSetupTimeMs) {
|
|
20580
20861
|
let connectionStartBitrate;
|
|
20581
20862
|
for (const m of media) {
|
|
20582
20863
|
if (m.type !== 'video' || m.direction === 'recvonly' || m.direction === 'inactive') {
|
|
@@ -20590,7 +20871,7 @@ function computeConnectionStartBitrate(media, trackBitrates) {
|
|
|
20590
20871
|
if (codecPayload === undefined) {
|
|
20591
20872
|
continue;
|
|
20592
20873
|
}
|
|
20593
|
-
const startBitrate = codecPayload > 0 ? computeTrackStartBitrate(trackbr) : undefined;
|
|
20874
|
+
const startBitrate = codecPayload > 0 ? computeTrackStartBitrate(trackbr, connectionSetupTimeMs) : undefined;
|
|
20594
20875
|
if (startBitrate !== undefined && (connectionStartBitrate === undefined || startBitrate > connectionStartBitrate)) {
|
|
20595
20876
|
connectionStartBitrate = startBitrate;
|
|
20596
20877
|
}
|
|
@@ -20989,7 +21270,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
20989
21270
|
// local video: the hint is connection-level in libwebrtc, so differing per-section
|
|
20990
21271
|
// values would be last-writer-wins on m-section order. Offers before any video is
|
|
20991
21272
|
// published (data channel or audio only) find no target and leave the latch unset.
|
|
20992
|
-
const connectionStartBitrate = this.hasAppliedVideoStartBitrate ? undefined : computeConnectionStartBitrate(sdpParsed.media, this.trackBitrates);
|
|
21273
|
+
const connectionStartBitrate = this.hasAppliedVideoStartBitrate ? undefined : computeConnectionStartBitrate(sdpParsed.media, this.trackBitrates, this.connectionSetupTimeEstimateMs());
|
|
20993
21274
|
let appliedVideoStartBitrate = false;
|
|
20994
21275
|
sdpParsed.media.forEach(media => {
|
|
20995
21276
|
ensureIPAddrMatchVersion(media);
|
|
@@ -21038,6 +21319,10 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
21038
21319
|
// one-shot hint once the SDP carrying it has been accepted locally.
|
|
21039
21320
|
if (appliedVideoStartBitrate && offer.sdp === mungedSdp) {
|
|
21040
21321
|
this.hasAppliedVideoStartBitrate = true;
|
|
21322
|
+
this.log.info('applied x-google-start-bitrate', {
|
|
21323
|
+
startBitrateKbps: connectionStartBitrate,
|
|
21324
|
+
connectionSetupTimeMs: this.connectionSetupTimeEstimateMs()
|
|
21325
|
+
});
|
|
21041
21326
|
}
|
|
21042
21327
|
this.onOffer(offer, this.latestOfferId);
|
|
21043
21328
|
} finally {
|
|
@@ -21089,6 +21374,27 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
|
21089
21374
|
setTrackCodecBitrate(info) {
|
|
21090
21375
|
this.trackBitrates.push(info);
|
|
21091
21376
|
}
|
|
21377
|
+
/**
|
|
21378
|
+
* Record how long this peer connection took to set up. Called once, when the initial connect
|
|
21379
|
+
* succeeds; resumes and ICE restarts keep the estimator they have and never call this.
|
|
21380
|
+
*/
|
|
21381
|
+
setConnectionSetupTime(ms) {
|
|
21382
|
+
this.connectionSetupTimeMs = ms;
|
|
21383
|
+
}
|
|
21384
|
+
/** Record when the connection attempt that built this peer connection began. */
|
|
21385
|
+
setConnectionStartedAt(startedAt) {
|
|
21386
|
+
this.connectionStartedAt = startedAt;
|
|
21387
|
+
}
|
|
21388
|
+
/** The measured setup time, or while still connecting, the time elapsed so far. */
|
|
21389
|
+
connectionSetupTimeEstimateMs() {
|
|
21390
|
+
if (this.connectionSetupTimeMs !== undefined) {
|
|
21391
|
+
return this.connectionSetupTimeMs;
|
|
21392
|
+
}
|
|
21393
|
+
if (this.connectionStartedAt === undefined) {
|
|
21394
|
+
return undefined;
|
|
21395
|
+
}
|
|
21396
|
+
return Math.round(performance.now() - this.connectionStartedAt);
|
|
21397
|
+
}
|
|
21092
21398
|
setConfiguration(rtcConfig) {
|
|
21093
21399
|
var _a;
|
|
21094
21400
|
if (!this._pc) {
|
|
@@ -21905,7 +22211,8 @@ class PCTransportManager {
|
|
|
21905
22211
|
* Waiters are parked on the channel object captured at wait entry. If that object stops being
|
|
21906
22212
|
* current — replaced or torn down — its events may never fire again, so the owner must call
|
|
21907
22213
|
* {@link invalidateWaiters}, which aborts parked waiters (releasing the gate); the next waiter
|
|
21908
|
-
* gets a fresh controller.
|
|
22214
|
+
* gets a fresh controller. {@link attach} and {@link detach} do this, and close the released
|
|
22215
|
+
* handle, as part of handle turnover.
|
|
21909
22216
|
*/
|
|
21910
22217
|
class FlowControlledDataChannel {
|
|
21911
22218
|
constructor(opts) {
|
|
@@ -21925,25 +22232,38 @@ class FlowControlledDataChannel {
|
|
|
21925
22232
|
return this.handle;
|
|
21926
22233
|
}
|
|
21927
22234
|
/**
|
|
21928
|
-
* Attaches the channel handle this wrapper controls. Replacing an existing handle
|
|
21929
|
-
* parked waiters — their events would never fire again on the abandoned object — and
|
|
21930
|
-
* fresh controller
|
|
21931
|
-
*
|
|
21932
|
-
* waiter structurally impossible.
|
|
22235
|
+
* Attaches the channel handle this wrapper controls. Replacing an existing handle closes it and
|
|
22236
|
+
* rejects parked waiters — their events would never fire again on the abandoned object — and
|
|
22237
|
+
* installs a fresh controller. Wrappers outlive their handles: this is the one place handle
|
|
22238
|
+
* turnover happens, so a handle the wrapper lets go of is never left open, and stranding a
|
|
22239
|
+
* waiter is structurally impossible.
|
|
21933
22240
|
*/
|
|
21934
22241
|
attach(dc) {
|
|
21935
|
-
|
|
21936
|
-
this.invalidateWaiters('data channel replaced');
|
|
21937
|
-
}
|
|
22242
|
+
const old = this.handle;
|
|
21938
22243
|
this.handle = dc;
|
|
22244
|
+
if (old && old !== dc) {
|
|
22245
|
+
this.release(old, 'data channel replaced');
|
|
22246
|
+
}
|
|
21939
22247
|
}
|
|
21940
|
-
/** Detaches the handle on teardown, rejecting parked waiters. */
|
|
22248
|
+
/** Detaches and closes the handle on teardown, rejecting parked waiters. */
|
|
21941
22249
|
detach() {
|
|
21942
22250
|
let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'data channel torn down';
|
|
21943
|
-
|
|
21944
|
-
this.invalidateWaiters(reason);
|
|
21945
|
-
}
|
|
22251
|
+
const old = this.handle;
|
|
21946
22252
|
this.handle = undefined;
|
|
22253
|
+
if (old) {
|
|
22254
|
+
this.release(old, reason);
|
|
22255
|
+
}
|
|
22256
|
+
}
|
|
22257
|
+
/**
|
|
22258
|
+
* Closes a handle the wrapper no longer controls. `close()` moves it to `closing` synchronously,
|
|
22259
|
+
* so senders still holding it (queued on the headroom lock) fail the open check once they get
|
|
22260
|
+
* the lock. Parked waiters are aborted directly instead of relying on the `close` event, which
|
|
22261
|
+
* fires asynchronously, if at all (the spec lets `pc.close()` skip it). Strip any handlers that
|
|
22262
|
+
* must not observe the close before calling this.
|
|
22263
|
+
*/
|
|
22264
|
+
release(dc, reason) {
|
|
22265
|
+
this.invalidateWaiters(reason);
|
|
22266
|
+
dc.close();
|
|
21947
22267
|
}
|
|
21948
22268
|
getChannel() {
|
|
21949
22269
|
return this.handle;
|
|
@@ -21974,32 +22294,35 @@ class FlowControlledDataChannel {
|
|
|
21974
22294
|
return this.headroomLock.lock();
|
|
21975
22295
|
}
|
|
21976
22296
|
/**
|
|
21977
|
-
* Resolves once the caller may send on
|
|
21978
|
-
*
|
|
22297
|
+
* Resolves once the caller may send on `dc`: immediately while its send buffer is at or below
|
|
22298
|
+
* the high-water mark, otherwise once the buffer has drained to the low-water mark (the
|
|
21979
22299
|
* `bufferedamountlow` event). Callers are serialized through the headroom lock so that, when
|
|
21980
22300
|
* the buffer drains, they refill it one at a time (up to the high-water mark) rather than all
|
|
21981
22301
|
* sending at once and overflowing the SCTP send buffer (see livekit/client-sdk-js#1995). The
|
|
21982
|
-
*
|
|
22302
|
+
* checks run inside the lock so queued callers proceed in FIFO order.
|
|
22303
|
+
*
|
|
22304
|
+
* The caller passes the handle it resolved before queueing, and the wait is against that one
|
|
22305
|
+
* handle throughout: if it was replaced meanwhile, it has been closed, so the wait rejects
|
|
22306
|
+
* rather than letting the caller send on the replacement out of turn.
|
|
21983
22307
|
*/
|
|
21984
|
-
waitForHeadroomWithLock() {
|
|
22308
|
+
waitForHeadroomWithLock(dc) {
|
|
21985
22309
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21986
22310
|
const unlock = yield this.lockHeadroom();
|
|
21987
22311
|
try {
|
|
21988
|
-
yield this.waitForHeadroomWithoutLock();
|
|
22312
|
+
yield this.waitForHeadroomWithoutLock(dc);
|
|
21989
22313
|
} finally {
|
|
21990
22314
|
unlock();
|
|
21991
22315
|
}
|
|
21992
22316
|
});
|
|
21993
22317
|
}
|
|
21994
22318
|
/** Core wait of {@link waitForHeadroomWithLock}. The caller must hold the headroom lock. */
|
|
21995
|
-
waitForHeadroomWithoutLock() {
|
|
22319
|
+
waitForHeadroomWithoutLock(dc) {
|
|
21996
22320
|
return __awaiter(this, void 0, void 0, function* () {
|
|
21997
22321
|
if (this.isEngineClosed()) {
|
|
21998
22322
|
throw new UnexpectedConnectionState('engine closed');
|
|
21999
22323
|
}
|
|
22000
|
-
|
|
22001
|
-
|
|
22002
|
-
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
|
|
22324
|
+
if (dc.readyState !== 'open') {
|
|
22325
|
+
throw new UnexpectedConnectionState("DataChannel ".concat(this.kind, " is ").concat(dc.readyState));
|
|
22003
22326
|
}
|
|
22004
22327
|
if (this.isBelowHighWaterMark(dc)) {
|
|
22005
22328
|
return;
|
|
@@ -22089,7 +22412,16 @@ class LossyDataChannel extends FlowControlledDataChannel {
|
|
|
22089
22412
|
switch (this.bufferFullBehavior) {
|
|
22090
22413
|
case 'wait':
|
|
22091
22414
|
if (!this.isBelowHighWaterMark(dc)) {
|
|
22092
|
-
|
|
22415
|
+
try {
|
|
22416
|
+
yield this.waitForHeadroomWithLock(dc);
|
|
22417
|
+
} catch (error) {
|
|
22418
|
+
if (this.isEngineClosed()) {
|
|
22419
|
+
throw error;
|
|
22420
|
+
}
|
|
22421
|
+
// The channel closed or was replaced while this send waited. The replacement may
|
|
22422
|
+
// still be connecting, so drop the packet, as sends during a reconnect are.
|
|
22423
|
+
return;
|
|
22424
|
+
}
|
|
22093
22425
|
}
|
|
22094
22426
|
break;
|
|
22095
22427
|
case 'drop':
|
|
@@ -22271,7 +22603,7 @@ class ReliableDataChannel extends FlowControlledDataChannel {
|
|
|
22271
22603
|
return;
|
|
22272
22604
|
}
|
|
22273
22605
|
try {
|
|
22274
|
-
yield this.waitForHeadroomWithLock();
|
|
22606
|
+
yield this.waitForHeadroomWithLock(dc);
|
|
22275
22607
|
} catch (error) {
|
|
22276
22608
|
if (this.isEngineClosed()) {
|
|
22277
22609
|
// No replay is coming after an engine close — surface the failure.
|
|
@@ -22279,7 +22611,9 @@ class ReliableDataChannel extends FlowControlledDataChannel {
|
|
|
22279
22611
|
}
|
|
22280
22612
|
// Transient teardown (the channel closed or was replaced while we waited): the reliable
|
|
22281
22613
|
// channel promises delivery across resume, so queue the packet for the replay instead of
|
|
22282
|
-
// rejecting a send the app can't meaningfully retry.
|
|
22614
|
+
// rejecting a send the app can't meaningfully retry. This also keeps the order: a send
|
|
22615
|
+
// queued behind one aborted by a replacement fails the same way (the old handle is closed),
|
|
22616
|
+
// so it can't reach the replacement ahead of the lower sequence the replay will deliver.
|
|
22283
22617
|
this.messageBuffer.push({
|
|
22284
22618
|
data: msg,
|
|
22285
22619
|
sequence,
|
|
@@ -22314,13 +22648,18 @@ class ReliableDataChannel extends FlowControlledDataChannel {
|
|
|
22314
22648
|
*/
|
|
22315
22649
|
replay(lastMessageSeq) {
|
|
22316
22650
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22317
|
-
|
|
22318
|
-
if (!dc) {
|
|
22651
|
+
if (!this.getChannel()) {
|
|
22319
22652
|
return;
|
|
22320
22653
|
}
|
|
22321
22654
|
this.messageBuffer.popToSequence(lastMessageSeq);
|
|
22322
22655
|
const unlock = yield this.lockHeadroom();
|
|
22323
22656
|
try {
|
|
22657
|
+
// Resolved only once the lock is held: the handle can be replaced while the replay waits
|
|
22658
|
+
// for it.
|
|
22659
|
+
const dc = this.getChannel();
|
|
22660
|
+
if (!dc) {
|
|
22661
|
+
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(this.kind));
|
|
22662
|
+
}
|
|
22324
22663
|
// Everything left after the ack cutoff must be re-handed to the current channel.
|
|
22325
22664
|
this.messageBuffer.markAllUnsent();
|
|
22326
22665
|
// Drain in passes, re-scanning the live buffer each time: a send that arrives (deferred,
|
|
@@ -22332,7 +22671,10 @@ class ReliableDataChannel extends FlowControlledDataChannel {
|
|
|
22332
22671
|
for (let batch = this.messageBuffer.getUnsent(); batch.length > 0; batch = this.messageBuffer.getUnsent()) {
|
|
22333
22672
|
for (const item of batch) {
|
|
22334
22673
|
// Respect flow control on resume too, so a large resend doesn't overflow the buffer.
|
|
22335
|
-
|
|
22674
|
+
// Against the handle resolved under the lock: if it is replaced mid-replay (another
|
|
22675
|
+
// resume), it has been closed, so this throws and that resume's replay picks up the
|
|
22676
|
+
// entries still flagged unsent.
|
|
22677
|
+
yield this.waitForHeadroomWithoutLock(dc);
|
|
22336
22678
|
dc.send(item.data);
|
|
22337
22679
|
this.messageBuffer.markSent(item);
|
|
22338
22680
|
}
|
|
@@ -22387,13 +22729,23 @@ function dataChannelHighWaterMark(kind) {
|
|
|
22387
22729
|
}const lossyDataChannelLabel = '_lossy';
|
|
22388
22730
|
const reliableDataChannelLabel = '_reliable';
|
|
22389
22731
|
const dataTrackDataChannelLabel = '_data_track';
|
|
22732
|
+
/** Unhooks every handler the manager may have wired onto `dc`, ahead of closing it. */
|
|
22733
|
+
function clearHandlers(dc) {
|
|
22734
|
+
dc.onbufferedamountlow = null;
|
|
22735
|
+
dc.onclose = null;
|
|
22736
|
+
dc.onclosing = null;
|
|
22737
|
+
dc.onerror = null;
|
|
22738
|
+
dc.onmessage = null;
|
|
22739
|
+
dc.onopen = null;
|
|
22740
|
+
}
|
|
22390
22741
|
/**
|
|
22391
22742
|
* Owns the engine's data channels: the three flow-controlled publisher channel wrappers (which
|
|
22392
22743
|
* live for the engine's lifetime and have RTCDataChannel handles attached/detached as peer
|
|
22393
22744
|
* connections come and go) plus the subscriber-side receive handles adopted by label.
|
|
22394
22745
|
*
|
|
22395
|
-
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which
|
|
22396
|
-
* parked headroom waiters as a built-in — there is no separate
|
|
22746
|
+
* Handle turnover goes through {@link FlowControlledDataChannel.attach}/`detach`, which close the
|
|
22747
|
+
* released handle and reject parked headroom waiters as a built-in — there is no separate
|
|
22748
|
+
* invalidation step to forget.
|
|
22397
22749
|
*/
|
|
22398
22750
|
class DataChannelManager {
|
|
22399
22751
|
constructor(opts) {
|
|
@@ -22451,17 +22803,16 @@ class DataChannelManager {
|
|
|
22451
22803
|
}
|
|
22452
22804
|
/**
|
|
22453
22805
|
* Creates the three publisher data channels on the given transport, wires their handlers, and
|
|
22454
|
-
* attaches them to the wrappers — attaching
|
|
22455
|
-
*
|
|
22806
|
+
* attaches them to the wrappers — attaching closes any replaced channel objects and rejects the
|
|
22807
|
+
* waiters still parked on them.
|
|
22456
22808
|
*/
|
|
22457
22809
|
createPublisherChannels(pcManager) {
|
|
22458
|
-
//
|
|
22810
|
+
// Recreating (the Safari null-id path): attaching the new handles below closes the old ones,
|
|
22811
|
+
// so unhook them first — the old channel's close must not report an unexpected close.
|
|
22459
22812
|
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
22460
22813
|
const old = channel.channelHandle;
|
|
22461
22814
|
if (old) {
|
|
22462
|
-
old
|
|
22463
|
-
old.onerror = null;
|
|
22464
|
-
old.onclose = null;
|
|
22815
|
+
clearHandlers(old);
|
|
22465
22816
|
}
|
|
22466
22817
|
}
|
|
22467
22818
|
const wire = (channel, dc, onMessage) => {
|
|
@@ -22512,38 +22863,35 @@ class DataChannelManager {
|
|
|
22512
22863
|
return true;
|
|
22513
22864
|
}
|
|
22514
22865
|
/**
|
|
22515
|
-
* Tears down all channels for a peer-connection cleanup:
|
|
22516
|
-
*
|
|
22517
|
-
*
|
|
22518
|
-
*
|
|
22866
|
+
* Tears down all channels for a peer-connection cleanup: strips handlers, closes the handles,
|
|
22867
|
+
* and resets the reliable session state. Detaching the publisher handles closes them and
|
|
22868
|
+
* rejects parked waiters (the spec allows `pc.close()` to transition channels to 'closed'
|
|
22869
|
+
* without firing events, so waiting for browser close events is not an option).
|
|
22519
22870
|
*/
|
|
22520
22871
|
teardown() {
|
|
22521
|
-
|
|
22522
|
-
|
|
22523
|
-
|
|
22524
|
-
|
|
22525
|
-
|
|
22526
|
-
|
|
22527
|
-
|
|
22528
|
-
|
|
22529
|
-
|
|
22530
|
-
|
|
22531
|
-
dc.onbufferedamountlow = null;
|
|
22532
|
-
dc.onclose = null;
|
|
22533
|
-
dc.onclosing = null;
|
|
22534
|
-
dc.onerror = null;
|
|
22535
|
-
dc.onmessage = null;
|
|
22536
|
-
dc.onopen = null;
|
|
22537
|
-
dc.close();
|
|
22538
|
-
};
|
|
22539
|
-
for (const channel of [this.lossy, this.reliable, this.dataTrack]) {
|
|
22872
|
+
// Detach the data channel handlers before closing anything. Closing a peer connection tears
|
|
22873
|
+
// down the SCTP transport, which can dispatch `error`/`close` events on the still-open data
|
|
22874
|
+
// channels; if our handlers are still attached at that point, handleDataError logs a spurious
|
|
22875
|
+
// "Unknown DataChannel error" during an otherwise graceful disconnect. Removing the handlers
|
|
22876
|
+
// before dc.close()/pcManager.close() makes this deterministic regardless of how/when the
|
|
22877
|
+
// browser dispatches those teardown events. See livekit/client-sdk-js#1953.
|
|
22878
|
+
const publisherDataChannels = [this.lossy, this.reliable, this.dataTrack];
|
|
22879
|
+
const subscriberDataChannels = [this.lossySub, this.reliableSub, this.dataTrackSub];
|
|
22880
|
+
// Publisher handles are closed by detach, which also rejects parked waiters.
|
|
22881
|
+
for (const channel of publisherDataChannels) {
|
|
22540
22882
|
const dc = channel.channelHandle;
|
|
22883
|
+
if (dc) {
|
|
22884
|
+
clearHandlers(dc);
|
|
22885
|
+
}
|
|
22541
22886
|
channel.detach('peer connections cleaned up');
|
|
22542
|
-
dcCleanup(dc);
|
|
22543
22887
|
}
|
|
22544
|
-
|
|
22545
|
-
|
|
22546
|
-
|
|
22888
|
+
// Subscriber handles have no wrapper, so close them directly.
|
|
22889
|
+
for (const dc of subscriberDataChannels) {
|
|
22890
|
+
if (dc) {
|
|
22891
|
+
clearHandlers(dc);
|
|
22892
|
+
dc.close();
|
|
22893
|
+
}
|
|
22894
|
+
}
|
|
22547
22895
|
this.lossySub = undefined;
|
|
22548
22896
|
this.reliableSub = undefined;
|
|
22549
22897
|
this.dataTrackSub = undefined;
|
|
@@ -22705,6 +23053,9 @@ class LocalTrack extends Track {
|
|
|
22705
23053
|
get isUserProvided() {
|
|
22706
23054
|
return this.providedByUser;
|
|
22707
23055
|
}
|
|
23056
|
+
get isStopped() {
|
|
23057
|
+
return this.manuallyStopped;
|
|
23058
|
+
}
|
|
22708
23059
|
get mediaStreamTrack() {
|
|
22709
23060
|
var _a, _b;
|
|
22710
23061
|
return (_b = (_a = this.processor) === null || _a === void 0 ? void 0 : _a.processedTrack) !== null && _b !== void 0 ? _b : this._mediaStreamTrack;
|
|
@@ -22762,6 +23113,9 @@ class LocalTrack extends Track {
|
|
|
22762
23113
|
track: newTrack,
|
|
22763
23114
|
kind: this.kind,
|
|
22764
23115
|
element: this.processorElement,
|
|
23116
|
+
// audio processors need the context they were initialised with in order to rebuild
|
|
23117
|
+
// their graph, omitting it here would leave them without one
|
|
23118
|
+
audioContext: this.audioContext,
|
|
22765
23119
|
localTrack: this
|
|
22766
23120
|
});
|
|
22767
23121
|
processedTrack = this.processor.processedTrack;
|
|
@@ -22785,6 +23139,18 @@ class LocalTrack extends Track {
|
|
|
22785
23139
|
attachToElement(processedTrack !== null && processedTrack !== void 0 ? processedTrack : newTrack, el);
|
|
22786
23140
|
});
|
|
22787
23141
|
}
|
|
23142
|
+
yield this.onMediaStreamTrackChanged();
|
|
23143
|
+
});
|
|
23144
|
+
}
|
|
23145
|
+
/**
|
|
23146
|
+
* Hook invoked after the underlying MediaStreamTrack has been swapped and the processor has
|
|
23147
|
+
* been restarted around it, so `processor.processedTrack` already holds the new track.
|
|
23148
|
+
* Unlike `onSenderTrackSwapped` this also fires on the restart and unmute paths.
|
|
23149
|
+
*/
|
|
23150
|
+
onMediaStreamTrackChanged() {
|
|
23151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23152
|
+
// base implementation is a no-op; LocalAudioTrack overrides this to move the krisp
|
|
23153
|
+
// feature listeners onto the new processed track.
|
|
22788
23154
|
});
|
|
22789
23155
|
}
|
|
22790
23156
|
waitForDimensions() {
|
|
@@ -22823,7 +23189,7 @@ class LocalTrack extends Track {
|
|
|
22823
23189
|
}
|
|
22824
23190
|
setDeviceId(deviceId) {
|
|
22825
23191
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22826
|
-
if (this._constraints.deviceId === deviceId && this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId)) {
|
|
23192
|
+
if (this._constraints.deviceId === deviceId && this._mediaStreamTrack.getSettings().deviceId === unwrapConstraint(deviceId) && this._mediaStreamTrack.readyState === 'live') {
|
|
22827
23193
|
return true;
|
|
22828
23194
|
}
|
|
22829
23195
|
this._constraints.deviceId = deviceId;
|
|
@@ -22951,24 +23317,40 @@ class LocalTrack extends Track {
|
|
|
22951
23317
|
// 'A MediaStreamTrack ended due to a capture failure`
|
|
22952
23318
|
this._mediaStreamTrack.stop();
|
|
22953
23319
|
// create new track and attach
|
|
22954
|
-
|
|
23320
|
+
let mediaStream;
|
|
23321
|
+
try {
|
|
23322
|
+
mediaStream = yield navigator.mediaDevices.getUserMedia(streamConstraints);
|
|
23323
|
+
} catch (e) {
|
|
23324
|
+
markDeviceAcquisitionFailure(e);
|
|
23325
|
+
throw e;
|
|
23326
|
+
}
|
|
22955
23327
|
const newTrack = mediaStream.getTracks()[0];
|
|
22956
|
-
|
|
22957
|
-
|
|
22958
|
-
|
|
23328
|
+
try {
|
|
23329
|
+
if (this.kind === Track.Kind.Video) {
|
|
23330
|
+
// we already captured the audio track with the constraints, so we only need to apply the video constraints
|
|
23331
|
+
yield newTrack.applyConstraints(otherConstraints);
|
|
23332
|
+
}
|
|
23333
|
+
newTrack.addEventListener('ended', this.handleEnded);
|
|
23334
|
+
this.log.debug('re-acquired MediaStreamTrack', this.logContext);
|
|
23335
|
+
yield this.setMediaStreamTrack(newTrack, false, isUnmuting);
|
|
23336
|
+
} catch (e) {
|
|
23337
|
+
// setMediaStreamTrack can throw after acquisition but before it adopts the new track,
|
|
23338
|
+
// which would leave the capture device live with nothing referencing it
|
|
23339
|
+
if (this._mediaStreamTrack !== newTrack) {
|
|
23340
|
+
newTrack.stop();
|
|
23341
|
+
}
|
|
23342
|
+
throw e;
|
|
22959
23343
|
}
|
|
22960
|
-
newTrack.addEventListener('ended', this.handleEnded);
|
|
22961
|
-
this.log.debug('re-acquired MediaStreamTrack', this.logContext);
|
|
22962
|
-
yield this.setMediaStreamTrack(newTrack, false, isUnmuting);
|
|
22963
23344
|
this._constraints = constraints;
|
|
22964
23345
|
this.pendingDeviceChange = false;
|
|
22965
23346
|
this.emit(TrackEvent.Restarted, this);
|
|
23347
|
+
return this;
|
|
23348
|
+
} finally {
|
|
23349
|
+
// in the finally because a throw can also leave the new track adopted, and so still live
|
|
22966
23350
|
if (this.manuallyStopped) {
|
|
22967
23351
|
this.log.warn('track was stopped during a restart, stopping restarted track', this.logContext);
|
|
22968
23352
|
this.stop();
|
|
22969
23353
|
}
|
|
22970
|
-
return this;
|
|
22971
|
-
} finally {
|
|
22972
23354
|
unlock();
|
|
22973
23355
|
}
|
|
22974
23356
|
});
|
|
@@ -23009,7 +23391,13 @@ class LocalTrack extends Track {
|
|
|
23009
23391
|
this._mediaStreamTrack.removeEventListener('ended', this.handleEnded);
|
|
23010
23392
|
this._mediaStreamTrack.removeEventListener('mute', this.handleTrackMuteEvent);
|
|
23011
23393
|
this._mediaStreamTrack.removeEventListener('unmute', this.handleTrackUnmuteEvent);
|
|
23012
|
-
|
|
23394
|
+
// `stop` is synchronous, so we can't await the teardown of the processor here.
|
|
23395
|
+
// make sure a failing teardown doesn't surface as an unhandled rejection.
|
|
23396
|
+
(_a = this.processor) === null || _a === void 0 ? void 0 : _a.destroy().catch(error => {
|
|
23397
|
+
this.log.error('failed to destroy processor', Object.assign(Object.assign({}, this.logContext), {
|
|
23398
|
+
error
|
|
23399
|
+
}));
|
|
23400
|
+
});
|
|
23013
23401
|
this.processor = undefined;
|
|
23014
23402
|
}
|
|
23015
23403
|
/**
|
|
@@ -23088,6 +23476,11 @@ class LocalTrack extends Track {
|
|
|
23088
23476
|
* Sets a processor on this track.
|
|
23089
23477
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
23090
23478
|
*
|
|
23479
|
+
* The processor lives on the track, so stopping the track destroys it. Unpublishing and
|
|
23480
|
+
* disconnecting both stop the track by default, which means a processor has to be set again on
|
|
23481
|
+
* the track published after a reconnect. See {@link TrackProcessor} for the full lifecycle,
|
|
23482
|
+
* including what `stopLocalTrackOnUnpublish: false` changes.
|
|
23483
|
+
*
|
|
23091
23484
|
* @param processor
|
|
23092
23485
|
* @param showProcessedStreamLocally
|
|
23093
23486
|
* @returns
|
|
@@ -23157,6 +23550,10 @@ class LocalTrack extends Track {
|
|
|
23157
23550
|
}();
|
|
23158
23551
|
});
|
|
23159
23552
|
}
|
|
23553
|
+
/**
|
|
23554
|
+
* Returns the processor currently set on this track, or `undefined` if there is none. A track
|
|
23555
|
+
* that was stopped by an unpublish or a disconnect has none. See {@link TrackProcessor}.
|
|
23556
|
+
*/
|
|
23160
23557
|
getProcessor() {
|
|
23161
23558
|
return this.processor;
|
|
23162
23559
|
}
|
|
@@ -23164,6 +23561,8 @@ class LocalTrack extends Track {
|
|
|
23164
23561
|
* Stops the track processor
|
|
23165
23562
|
* See https://github.com/livekit/track-processors-js for example usage
|
|
23166
23563
|
*
|
|
23564
|
+
* Only needed to remove a processor from a track that stays published. Unpublishing or
|
|
23565
|
+
* disconnecting already destroys it. See {@link TrackProcessor}.
|
|
23167
23566
|
*/
|
|
23168
23567
|
stopProcessor() {
|
|
23169
23568
|
return __awaiter(this, arguments, void 0, function () {
|
|
@@ -23404,6 +23803,38 @@ class LocalTrack extends Track {
|
|
|
23404
23803
|
this.monitorSender();
|
|
23405
23804
|
}, monitorFrequency);
|
|
23406
23805
|
}
|
|
23806
|
+
/**
|
|
23807
|
+
* registers the krisp feature listeners on the given processed track, moving them over from
|
|
23808
|
+
* a previously observed one. Passing `undefined` just removes the existing listeners.
|
|
23809
|
+
*/
|
|
23810
|
+
observeProcessedTrack(processedTrack) {
|
|
23811
|
+
if (this.observedProcessedTrack === processedTrack) {
|
|
23812
|
+
return;
|
|
23813
|
+
}
|
|
23814
|
+
if (this.observedProcessedTrack) {
|
|
23815
|
+
this.observedProcessedTrack.removeEventListener('enable-lk-krisp-noise-filter', this.handleKrispNoiseFilterEnable);
|
|
23816
|
+
this.observedProcessedTrack.removeEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
|
|
23817
|
+
this.observedProcessedTrack = undefined;
|
|
23818
|
+
if (this.isKrispNoiseFilterEnabled) {
|
|
23819
|
+
// we're no longer observing the track that had the filter enabled, so the feature
|
|
23820
|
+
// can't be active anymore. Make sure the state doesn't get stuck on `true`.
|
|
23821
|
+
this.handleKrispNoiseFilterDisable();
|
|
23822
|
+
}
|
|
23823
|
+
}
|
|
23824
|
+
if (!processedTrack) {
|
|
23825
|
+
return;
|
|
23826
|
+
}
|
|
23827
|
+
processedTrack.addEventListener('enable-lk-krisp-noise-filter', this.handleKrispNoiseFilterEnable);
|
|
23828
|
+
processedTrack.addEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
|
|
23829
|
+
this.observedProcessedTrack = processedTrack;
|
|
23830
|
+
}
|
|
23831
|
+
/**
|
|
23832
|
+
* Sets a processor on this track.
|
|
23833
|
+
*
|
|
23834
|
+
* The processor is destroyed when the track is stopped, and also when the audio context goes
|
|
23835
|
+
* away, so a disconnect ends it on both counts and it has to be set again after reconnecting.
|
|
23836
|
+
* See {@link TrackProcessor} for the full lifecycle.
|
|
23837
|
+
*/
|
|
23407
23838
|
setProcessor(processor) {
|
|
23408
23839
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23409
23840
|
var _a;
|
|
@@ -23427,8 +23858,7 @@ class LocalTrack extends Track {
|
|
|
23427
23858
|
this.processor = processor;
|
|
23428
23859
|
if (this.processor.processedTrack) {
|
|
23429
23860
|
yield (_a = this.sender) === null || _a === void 0 ? void 0 : _a.replaceTrack(this.processor.processedTrack);
|
|
23430
|
-
this.processor.processedTrack
|
|
23431
|
-
this.processor.processedTrack.addEventListener('disable-lk-krisp-noise-filter', this.handleKrispNoiseFilterDisable);
|
|
23861
|
+
this.observeProcessedTrack(this.processor.processedTrack);
|
|
23432
23862
|
}
|
|
23433
23863
|
this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
|
|
23434
23864
|
} finally {
|
|
@@ -23436,12 +23866,73 @@ class LocalTrack extends Track {
|
|
|
23436
23866
|
}
|
|
23437
23867
|
});
|
|
23438
23868
|
}
|
|
23869
|
+
onMediaStreamTrackChanged() {
|
|
23870
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23871
|
+
var _a;
|
|
23872
|
+
// the processor rebuilds its graph around the new track, so the krisp listeners have to
|
|
23873
|
+
// follow it onto whatever it hands back
|
|
23874
|
+
this.observeProcessedTrack((_a = this.processor) === null || _a === void 0 ? void 0 : _a.processedTrack);
|
|
23875
|
+
});
|
|
23876
|
+
}
|
|
23877
|
+
internalStopProcessor() {
|
|
23878
|
+
const _super = Object.create(null, {
|
|
23879
|
+
internalStopProcessor: {
|
|
23880
|
+
get: () => super.internalStopProcessor
|
|
23881
|
+
}
|
|
23882
|
+
});
|
|
23883
|
+
return __awaiter(this, arguments, void 0, function () {
|
|
23884
|
+
var _this = this;
|
|
23885
|
+
let keepElement = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
|
|
23886
|
+
return function* () {
|
|
23887
|
+
_this.observeProcessedTrack(undefined);
|
|
23888
|
+
yield _super.internalStopProcessor.call(_this, keepElement);
|
|
23889
|
+
}();
|
|
23890
|
+
});
|
|
23891
|
+
}
|
|
23892
|
+
stop() {
|
|
23893
|
+
this.observeProcessedTrack(undefined);
|
|
23894
|
+
super.stop();
|
|
23895
|
+
}
|
|
23439
23896
|
/**
|
|
23440
23897
|
* @internal
|
|
23441
23898
|
* @experimental
|
|
23899
|
+
*
|
|
23900
|
+
* A processor's nodes (`AudioWorkletNode`, ...) can't move between contexts, so the processor
|
|
23901
|
+
* is rebuilt whenever the context changes and torn down when there is none left to run in.
|
|
23442
23902
|
*/
|
|
23443
23903
|
setAudioContext(audioContext) {
|
|
23444
|
-
this
|
|
23904
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
23905
|
+
var _a;
|
|
23906
|
+
if (this.audioContext === audioContext) {
|
|
23907
|
+
return;
|
|
23908
|
+
}
|
|
23909
|
+
const unlock = yield this.trackChangeLock.lock();
|
|
23910
|
+
try {
|
|
23911
|
+
this.audioContext = audioContext;
|
|
23912
|
+
if (!this.processor || isReactNative()) {
|
|
23913
|
+
return;
|
|
23914
|
+
}
|
|
23915
|
+
if (!audioContext) {
|
|
23916
|
+
this.log.debug('stopping audio processor, audio context has been removed', this.logContext);
|
|
23917
|
+
yield this.internalStopProcessor();
|
|
23918
|
+
return;
|
|
23919
|
+
}
|
|
23920
|
+
this.log.debug('restarting audio processor on new audio context', this.logContext);
|
|
23921
|
+
yield this.processor.restart({
|
|
23922
|
+
kind: this.kind,
|
|
23923
|
+
track: this._mediaStreamTrack,
|
|
23924
|
+
audioContext,
|
|
23925
|
+
localTrack: this
|
|
23926
|
+
});
|
|
23927
|
+
if (this.processor.processedTrack) {
|
|
23928
|
+
yield (_a = this.sender) === null || _a === void 0 ? void 0 : _a.replaceTrack(this.processor.processedTrack);
|
|
23929
|
+
this.observeProcessedTrack(this.processor.processedTrack);
|
|
23930
|
+
}
|
|
23931
|
+
this.emit(TrackEvent.TrackProcessorUpdate, this.processor);
|
|
23932
|
+
} finally {
|
|
23933
|
+
unlock();
|
|
23934
|
+
}
|
|
23935
|
+
});
|
|
23445
23936
|
}
|
|
23446
23937
|
getSenderStats() {
|
|
23447
23938
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -24271,7 +24762,14 @@ class LocalVideoTrack extends LocalTrack {
|
|
|
24271
24762
|
yield sender.setParameters(params);
|
|
24272
24763
|
});
|
|
24273
24764
|
}
|
|
24274
|
-
|
|
24765
|
+
/**
|
|
24766
|
+
* Sets a processor on this track, and wires its processed track into the simulcast codecs.
|
|
24767
|
+
*
|
|
24768
|
+
* The processor is destroyed when the track is stopped, which unpublishing and disconnecting
|
|
24769
|
+
* both do by default, so it has to be set again after reconnecting. See {@link TrackProcessor}
|
|
24770
|
+
* for the full lifecycle.
|
|
24771
|
+
*/
|
|
24772
|
+
setProcessor(processor_1) {
|
|
24275
24773
|
const _super = Object.create(null, {
|
|
24276
24774
|
setProcessor: {
|
|
24277
24775
|
get: () => super.setProcessor
|
|
@@ -24872,7 +25370,17 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
24872
25370
|
return;
|
|
24873
25371
|
}
|
|
24874
25372
|
if (connection === leaveReconnect) {
|
|
24875
|
-
|
|
25373
|
+
// The server asked for a reconnect, so skip any remaining backoff. Use the policy's first
|
|
25374
|
+
// retry delay instead of 0 so that clients told to reconnect together don't all reconnect
|
|
25375
|
+
// at the same instant.
|
|
25376
|
+
delay = this.getNextRetryDelay({
|
|
25377
|
+
elapsedMs: duration,
|
|
25378
|
+
retryCount: 0
|
|
25379
|
+
});
|
|
25380
|
+
if (delay === null) {
|
|
25381
|
+
disconnect(duration);
|
|
25382
|
+
return;
|
|
25383
|
+
}
|
|
24876
25384
|
}
|
|
24877
25385
|
this.log.debug("reconnecting in ".concat(delay, "ms"));
|
|
24878
25386
|
this.clearReconnectTimeout();
|
|
@@ -25005,7 +25513,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25005
25513
|
var _this2 = this;
|
|
25006
25514
|
let useV0Path = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
|
|
25007
25515
|
return function* () {
|
|
25008
|
-
var _a, _b, _c;
|
|
25516
|
+
var _a, _b, _c, _d;
|
|
25009
25517
|
_this2._isNewlyCreated = false;
|
|
25010
25518
|
_this2.url = url;
|
|
25011
25519
|
_this2.token = token;
|
|
@@ -25013,6 +25521,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25013
25521
|
_this2.maxJoinAttempts = opts.maxRetries;
|
|
25014
25522
|
try {
|
|
25015
25523
|
_this2.joinAttempts += 1;
|
|
25524
|
+
_this2.joinStartedAt = performance.now();
|
|
25016
25525
|
_this2.setupSignalClientCallbacks();
|
|
25017
25526
|
// Whether the initial publisher offer is bundled with the join request. Computed once and
|
|
25018
25527
|
// reused after the join below. Only the (non-Firefox) offer-with-join path does this.
|
|
@@ -25042,6 +25551,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25042
25551
|
throw ConnectionError.serviceNotFound('Simulated v1 path failure', 'v0-rtc');
|
|
25043
25552
|
}
|
|
25044
25553
|
const joinResponse = yield _this2.client.join(url, token, opts, abortSignal, useV0Path, offerProto);
|
|
25554
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
25555
|
+
_this2.log.info('join cancelled after the signal join response');
|
|
25556
|
+
throw ConnectionError.cancelled('Connection aborted');
|
|
25557
|
+
}
|
|
25045
25558
|
_this2._isClosed = false;
|
|
25046
25559
|
_this2.latestJoinResponse = joinResponse;
|
|
25047
25560
|
_this2.participantSid = (_b = joinResponse.participant) === null || _b === void 0 ? void 0 : _b.sid;
|
|
@@ -25054,6 +25567,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25054
25567
|
// the server's ICE servers and topology, then negotiate separately rather than bundling
|
|
25055
25568
|
// the offer with the join.
|
|
25056
25569
|
yield _this2.configure(joinResponse, !useV0Path);
|
|
25570
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
|
25571
|
+
_this2.log.info('join cancelled after configuring peer connections');
|
|
25572
|
+
throw ConnectionError.cancelled('Connection aborted');
|
|
25573
|
+
}
|
|
25057
25574
|
if (!useV0Path) {
|
|
25058
25575
|
// The V1 first offer must carry the media layout so Firefox binds receive decoders for
|
|
25059
25576
|
// subscribed tracks — without it, subscribed audio/video arrive as RTP but
|
|
@@ -25068,6 +25585,11 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25068
25585
|
});
|
|
25069
25586
|
}
|
|
25070
25587
|
}
|
|
25588
|
+
if (_this2.joinStartedAt !== undefined) {
|
|
25589
|
+
// Lets a video published before the transports connect be seeded from the time elapsed
|
|
25590
|
+
// so far, rather than delaying its offer until the setup time is measured.
|
|
25591
|
+
(_d = _this2.pcManager) === null || _d === void 0 ? void 0 : _d.publisher.setConnectionStartedAt(_this2.joinStartedAt);
|
|
25592
|
+
}
|
|
25071
25593
|
_this2.registerOnLineListener();
|
|
25072
25594
|
_this2.clientConfiguration = joinResponse.clientConfiguration;
|
|
25073
25595
|
_this2.emit(EngineEvent.SignalConnected, joinResponse);
|
|
@@ -25116,6 +25638,9 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25116
25638
|
*/
|
|
25117
25639
|
close(reason) {
|
|
25118
25640
|
return __awaiter(this, void 0, void 0, function* () {
|
|
25641
|
+
var _a;
|
|
25642
|
+
(_a = this.reconnectAbortController) === null || _a === void 0 ? void 0 : _a.abort();
|
|
25643
|
+
this.reconnectAbortController = undefined;
|
|
25119
25644
|
const unlock = yield this.closingLock.lock();
|
|
25120
25645
|
if (this.isClosed) {
|
|
25121
25646
|
unlock();
|
|
@@ -25124,6 +25649,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25124
25649
|
try {
|
|
25125
25650
|
this._isClosed = true;
|
|
25126
25651
|
this.joinAttempts = 0;
|
|
25652
|
+
this.joinStartedAt = undefined;
|
|
25127
25653
|
this.emit(EngineEvent.Closing);
|
|
25128
25654
|
this.removeAllListeners();
|
|
25129
25655
|
this.deregisterOnLineListener();
|
|
@@ -25252,6 +25778,7 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25252
25778
|
};
|
|
25253
25779
|
this.pcManager.onDataChannel = this.handleDataChannel;
|
|
25254
25780
|
this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
|
|
25781
|
+
var _a;
|
|
25255
25782
|
this.log.debug("primary PC state changed ".concat(connectionState));
|
|
25256
25783
|
// Record when the primary transport actually entered CONNECTING so
|
|
25257
25784
|
// verifyTransport() can bound how long we tolerate it. Deriving it from the
|
|
@@ -25267,6 +25794,16 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25267
25794
|
this.publisherConnectionPromise = undefined;
|
|
25268
25795
|
}
|
|
25269
25796
|
if (connectionState === PCTransportState.CONNECTED) {
|
|
25797
|
+
if (this.joinStartedAt !== undefined) {
|
|
25798
|
+
// Connection setup time is the only network signal there is before the first video
|
|
25799
|
+
// offer. Set synchronously, so it is in place before `room.connect()` resolves and the
|
|
25800
|
+
// app can publish. A full restart joins again and builds new transports, so it is
|
|
25801
|
+
// measured again; a resume keeps its estimator and never restarts the timer.
|
|
25802
|
+
const setupTimeMs = Math.round(performance.now() - this.joinStartedAt);
|
|
25803
|
+
this.joinStartedAt = undefined;
|
|
25804
|
+
this.log.info("connection setup took ".concat(setupTimeMs, " ms"));
|
|
25805
|
+
(_a = this.pcManager) === null || _a === void 0 ? void 0 : _a.publisher.setConnectionSetupTime(setupTimeMs);
|
|
25806
|
+
}
|
|
25270
25807
|
const shouldEmit = this.pcState === PCState.New;
|
|
25271
25808
|
this.pcState = PCState.Connected;
|
|
25272
25809
|
if (shouldEmit) {
|
|
@@ -25501,6 +26038,12 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25501
26038
|
if (!this.pcManager) {
|
|
25502
26039
|
return;
|
|
25503
26040
|
}
|
|
26041
|
+
if (this.dataChannels.hasPublisherChannels) {
|
|
26042
|
+
// Replacing live channels (the Safari null-id path in resumeConnection): the new ones start
|
|
26043
|
+
// out connecting, so drop the memoized readiness check and make the next sender wait for
|
|
26044
|
+
// them to open.
|
|
26045
|
+
this.publisherConnectionPromise = undefined;
|
|
26046
|
+
}
|
|
25504
26047
|
this.dataChannels.createPublisherChannels(this.pcManager);
|
|
25505
26048
|
}
|
|
25506
26049
|
/** Normalizes an incoming data-channel message into bytes, or logs and returns undefined. */
|
|
@@ -25733,16 +26276,28 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25733
26276
|
const fullReconnect = this.fullReconnectOnNext;
|
|
25734
26277
|
this.fullReconnectOnNext = false;
|
|
25735
26278
|
let succeeded = false;
|
|
26279
|
+
const abortController = new AbortController();
|
|
26280
|
+
this.reconnectAbortController = abortController;
|
|
25736
26281
|
try {
|
|
25737
26282
|
this.attemptingReconnect = true;
|
|
25738
26283
|
if (fullReconnect) {
|
|
25739
|
-
yield this.restartConnection();
|
|
26284
|
+
yield this.restartConnection(abortController.signal);
|
|
25740
26285
|
} else {
|
|
25741
26286
|
yield this.resumeConnection(reason);
|
|
25742
26287
|
}
|
|
26288
|
+
if (abortController.signal.aborted) {
|
|
26289
|
+
this.log.info('reconnect attempt cancelled, not marking it as succeeded');
|
|
26290
|
+
return;
|
|
26291
|
+
}
|
|
25743
26292
|
this.clearPendingReconnect();
|
|
25744
26293
|
succeeded = true;
|
|
25745
26294
|
} catch (e) {
|
|
26295
|
+
if (abortController.signal.aborted) {
|
|
26296
|
+
this.log.info('reconnect attempt cancelled, not retrying', {
|
|
26297
|
+
error: e
|
|
26298
|
+
});
|
|
26299
|
+
return;
|
|
26300
|
+
}
|
|
25746
26301
|
this.reconnectAttempts += 1;
|
|
25747
26302
|
let recoverable = true;
|
|
25748
26303
|
if (e instanceof UnexpectedConnectionState) {
|
|
@@ -25764,6 +26319,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25764
26319
|
yield this.close("gave up reconnecting after ".concat(this.reconnectAttempts, " attempts, ").concat(Date.now() - this.reconnectStart, "ms"));
|
|
25765
26320
|
}
|
|
25766
26321
|
} finally {
|
|
26322
|
+
// close() may already have cleared this (or a later attempt replaced it); only release our own
|
|
26323
|
+
if (this.reconnectAbortController === abortController) {
|
|
26324
|
+
this.reconnectAbortController = undefined;
|
|
26325
|
+
}
|
|
25767
26326
|
this.attemptingReconnect = false;
|
|
25768
26327
|
// A full reconnect requested while this attempt was running (e.g. a `RECONNECT` leave
|
|
25769
26328
|
// during a resume or a restart) that a successful attempt didn't act on; dispatch it now
|
|
@@ -25786,10 +26345,16 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25786
26345
|
// error in user code with provided reconnect policy, stop reconnecting
|
|
25787
26346
|
return null;
|
|
25788
26347
|
}
|
|
25789
|
-
restartConnection(regionUrl) {
|
|
26348
|
+
restartConnection(abortSignal, regionUrl) {
|
|
25790
26349
|
return __awaiter(this, void 0, void 0, function* () {
|
|
25791
26350
|
var _a, _b, _c;
|
|
25792
26351
|
try {
|
|
26352
|
+
if (abortSignal.aborted) {
|
|
26353
|
+
this.log.info('full reconnect cancelled before starting', {
|
|
26354
|
+
regionUrl
|
|
26355
|
+
});
|
|
26356
|
+
return;
|
|
26357
|
+
}
|
|
25793
26358
|
if (!this.url || !this.token) {
|
|
25794
26359
|
// permanent failure, don't attempt reconnection
|
|
25795
26360
|
throw new UnexpectedConnectionState('could not reconnect, url or token not saved');
|
|
@@ -25801,6 +26366,12 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25801
26366
|
}
|
|
25802
26367
|
yield this.cleanupPeerConnections();
|
|
25803
26368
|
yield this.cleanupClient();
|
|
26369
|
+
if (abortSignal.aborted) {
|
|
26370
|
+
this.log.info('full reconnect cancelled after cleaning up the previous session', {
|
|
26371
|
+
regionUrl
|
|
26372
|
+
});
|
|
26373
|
+
return;
|
|
26374
|
+
}
|
|
25804
26375
|
let joinResponse;
|
|
25805
26376
|
try {
|
|
25806
26377
|
if (!this.signalOpts) {
|
|
@@ -25808,13 +26379,19 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25808
26379
|
throw new SignalReconnectError();
|
|
25809
26380
|
}
|
|
25810
26381
|
// in case a regionUrl is passed, the region URL takes precedence
|
|
25811
|
-
joinResponse = (yield this.join(regionUrl !== null && regionUrl !== void 0 ? regionUrl : this.url, this.token, this.signalOpts,
|
|
26382
|
+
joinResponse = (yield this.join(regionUrl !== null && regionUrl !== void 0 ? regionUrl : this.url, this.token, this.signalOpts, abortSignal, !this.options.singlePeerConnection)).joinResponse;
|
|
25812
26383
|
} catch (e) {
|
|
25813
26384
|
if (e instanceof ConnectionError && e.reason === ConnectionErrorReason.NotAllowed) {
|
|
25814
26385
|
throw new UnexpectedConnectionState('could not reconnect, token might be expired');
|
|
25815
26386
|
}
|
|
25816
26387
|
throw new SignalReconnectError();
|
|
25817
26388
|
}
|
|
26389
|
+
if (abortSignal.aborted) {
|
|
26390
|
+
this.log.info('full reconnect cancelled after the signal join response', {
|
|
26391
|
+
regionUrl
|
|
26392
|
+
});
|
|
26393
|
+
return;
|
|
26394
|
+
}
|
|
25818
26395
|
if (this.shouldFailNext) {
|
|
25819
26396
|
this.shouldFailNext = false;
|
|
25820
26397
|
throw new Error('simulated failure');
|
|
@@ -25822,6 +26399,12 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25822
26399
|
this.client.setReconnected();
|
|
25823
26400
|
this.emit(EngineEvent.SignalRestarted, joinResponse);
|
|
25824
26401
|
yield this.waitForPCReconnected();
|
|
26402
|
+
if (abortSignal.aborted) {
|
|
26403
|
+
this.log.info('full reconnect cancelled after peer connections reconnected', {
|
|
26404
|
+
regionUrl
|
|
26405
|
+
});
|
|
26406
|
+
return;
|
|
26407
|
+
}
|
|
25825
26408
|
// re-check signal connection state before setting engine as resumed
|
|
25826
26409
|
if (this.client.currentState !== SignalConnectionState.CONNECTED) {
|
|
25827
26410
|
throw new SignalReconnectError('Signal connection got severed during reconnect');
|
|
@@ -25830,9 +26413,21 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
25830
26413
|
// reconnect success
|
|
25831
26414
|
this.emit(EngineEvent.Restarted);
|
|
25832
26415
|
} catch (error) {
|
|
25833
|
-
|
|
26416
|
+
if (abortSignal.aborted) {
|
|
26417
|
+
this.log.info('full reconnect cancelled after a failed attempt', {
|
|
26418
|
+
regionUrl
|
|
26419
|
+
});
|
|
26420
|
+
return;
|
|
26421
|
+
}
|
|
26422
|
+
const nextRegionUrl = yield (_b = this.regionStrategy) === null || _b === void 0 ? void 0 : _b.getNextUrl(abortSignal);
|
|
26423
|
+
if (abortSignal.aborted) {
|
|
26424
|
+
this.log.info('full reconnect cancelled during region selection', {
|
|
26425
|
+
regionUrl
|
|
26426
|
+
});
|
|
26427
|
+
return;
|
|
26428
|
+
}
|
|
25834
26429
|
if (nextRegionUrl) {
|
|
25835
|
-
yield this.restartConnection(nextRegionUrl);
|
|
26430
|
+
yield this.restartConnection(abortSignal, nextRegionUrl);
|
|
25836
26431
|
return;
|
|
25837
26432
|
} else {
|
|
25838
26433
|
// no more regions to try (or we're not on cloud)
|
|
@@ -26027,7 +26622,11 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
|
26027
26622
|
*/
|
|
26028
26623
|
waitForBufferHeadroom(kind) {
|
|
26029
26624
|
return __awaiter(this, void 0, void 0, function* () {
|
|
26030
|
-
|
|
26625
|
+
const dc = this.dataChannelForKind(kind);
|
|
26626
|
+
if (!dc) {
|
|
26627
|
+
throw new UnexpectedConnectionState("DataChannel not found, kind: ".concat(kind));
|
|
26628
|
+
}
|
|
26629
|
+
return this.flowControlFor(kind).waitForHeadroomWithLock(dc);
|
|
26031
26630
|
});
|
|
26032
26631
|
}
|
|
26033
26632
|
/**
|
|
@@ -26887,6 +27486,16 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26887
27486
|
this.byteStreamHandlers.delete(topic);
|
|
26888
27487
|
}
|
|
26889
27488
|
clearControllers() {
|
|
27489
|
+
// Error any in flight streams so readers do not wait forever for chunks that can no longer arrive.
|
|
27490
|
+
const abnormalEndError = new DataStreamError('Room disconnected in the middle of receiving data', DataStreamErrorReason.AbnormalEnd);
|
|
27491
|
+
for (const _ref2 of this.byteStreamControllers.values()) {
|
|
27492
|
+
const controller = _ref2.controller;
|
|
27493
|
+
controller.error(abnormalEndError);
|
|
27494
|
+
}
|
|
27495
|
+
for (const _ref3 of this.textStreamControllers.values()) {
|
|
27496
|
+
const controller = _ref3.controller;
|
|
27497
|
+
controller.error(abnormalEndError);
|
|
27498
|
+
}
|
|
26890
27499
|
this.byteStreamControllers.clear();
|
|
26891
27500
|
this.textStreamControllers.clear();
|
|
26892
27501
|
this.bufferedPackets = [];
|
|
@@ -26897,18 +27506,18 @@ class TextStreamReader extends BaseStreamReader {
|
|
|
26897
27506
|
const byteStreamsBeingSentByDisconnectingParticipant = Array.from(this.byteStreamControllers.entries()).filter(entry => entry[1].sendingParticipantIdentity === participantIdentity);
|
|
26898
27507
|
if (textStreamsBeingSentByDisconnectingParticipant.length > 0 || byteStreamsBeingSentByDisconnectingParticipant.length > 0) {
|
|
26899
27508
|
const abnormalEndError = new DataStreamError("Participant ".concat(participantIdentity, " unexpectedly disconnected in the middle of sending data"), DataStreamErrorReason.AbnormalEnd);
|
|
26900
|
-
for (const
|
|
26901
|
-
var _ref3 = _slicedToArray(_ref2, 2);
|
|
26902
|
-
const id = _ref3[0];
|
|
26903
|
-
const controller = _ref3[1];
|
|
26904
|
-
controller.controller.error(abnormalEndError);
|
|
26905
|
-
this.byteStreamControllers.delete(id);
|
|
26906
|
-
}
|
|
26907
|
-
for (const _ref4 of textStreamsBeingSentByDisconnectingParticipant) {
|
|
27509
|
+
for (const _ref4 of byteStreamsBeingSentByDisconnectingParticipant) {
|
|
26908
27510
|
var _ref5 = _slicedToArray(_ref4, 2);
|
|
26909
27511
|
const id = _ref5[0];
|
|
26910
27512
|
const controller = _ref5[1];
|
|
26911
27513
|
controller.controller.error(abnormalEndError);
|
|
27514
|
+
this.byteStreamControllers.delete(id);
|
|
27515
|
+
}
|
|
27516
|
+
for (const _ref6 of textStreamsBeingSentByDisconnectingParticipant) {
|
|
27517
|
+
var _ref7 = _slicedToArray(_ref6, 2);
|
|
27518
|
+
const id = _ref7[0];
|
|
27519
|
+
const controller = _ref7[1];
|
|
27520
|
+
controller.controller.error(abnormalEndError);
|
|
26912
27521
|
this.textStreamControllers.delete(id);
|
|
26913
27522
|
}
|
|
26914
27523
|
}
|
|
@@ -27905,58 +28514,6 @@ function sendStreamTrailer(streamId, destinationIdentities, engine) {
|
|
|
27905
28514
|
});
|
|
27906
28515
|
yield engine.sendDataPacket(trailerPacket, DataChannelKind.RELIABLE);
|
|
27907
28516
|
});
|
|
27908
|
-
}/**
|
|
27909
|
-
* Implementation of AbortSignal.any
|
|
27910
|
-
* Creates a signal that will be aborted when any of the given signals is aborted.
|
|
27911
|
-
* @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/any
|
|
27912
|
-
*/
|
|
27913
|
-
function abortSignalAny(signals) {
|
|
27914
|
-
// Handle empty signals array
|
|
27915
|
-
if (signals.length === 0) {
|
|
27916
|
-
const controller = new AbortController();
|
|
27917
|
-
return controller.signal;
|
|
27918
|
-
}
|
|
27919
|
-
// Fast path for single signal
|
|
27920
|
-
if (signals.length === 1) {
|
|
27921
|
-
return signals[0];
|
|
27922
|
-
}
|
|
27923
|
-
// Check if any signal is already aborted
|
|
27924
|
-
for (const signal of signals) {
|
|
27925
|
-
if (signal.aborted) {
|
|
27926
|
-
return signal;
|
|
27927
|
-
}
|
|
27928
|
-
}
|
|
27929
|
-
// Create a new controller for the combined signal
|
|
27930
|
-
const controller = new AbortController();
|
|
27931
|
-
const unlisteners = Array(signals.length);
|
|
27932
|
-
// Function to clean up all event listeners
|
|
27933
|
-
const cleanup = () => {
|
|
27934
|
-
for (const unsubscribe of unlisteners) {
|
|
27935
|
-
unsubscribe();
|
|
27936
|
-
}
|
|
27937
|
-
};
|
|
27938
|
-
// Add event listeners to each signal
|
|
27939
|
-
signals.forEach((signal, index) => {
|
|
27940
|
-
const handler = () => {
|
|
27941
|
-
controller.abort(signal.reason);
|
|
27942
|
-
cleanup();
|
|
27943
|
-
};
|
|
27944
|
-
signal.addEventListener('abort', handler);
|
|
27945
|
-
unlisteners[index] = () => signal.removeEventListener('abort', handler);
|
|
27946
|
-
});
|
|
27947
|
-
return controller.signal;
|
|
27948
|
-
}
|
|
27949
|
-
/**
|
|
27950
|
-
* Implementation of AbortSignal.timeout
|
|
27951
|
-
* Creates a signal that will be aborted after the specified timeout.
|
|
27952
|
-
* @link https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout
|
|
27953
|
-
*/
|
|
27954
|
-
function abortSignalTimeout(ms) {
|
|
27955
|
-
const controller = new AbortController();
|
|
27956
|
-
setTimeout(() => {
|
|
27957
|
-
controller.abort(new DOMException("signal timed out after ".concat(ms, " ms"), 'TimeoutError'));
|
|
27958
|
-
}, ms);
|
|
27959
|
-
return controller.signal;
|
|
27960
28517
|
}// Number type sizes
|
|
27961
28518
|
const U8_LENGTH_BYTES = 1;
|
|
27962
28519
|
const U16_LENGTH_BYTES = 2;
|
|
@@ -30313,6 +30870,12 @@ function truncateBytes(str, maxBytes) {
|
|
|
30313
30870
|
high = mid - 1;
|
|
30314
30871
|
}
|
|
30315
30872
|
}
|
|
30873
|
+
// Never end on the first half of a surrogate pair: it would be encoded as a replacement
|
|
30874
|
+
// character, and the truncated string would no longer round trip.
|
|
30875
|
+
const lastCode = low > 0 ? str.charCodeAt(low - 1) : 0;
|
|
30876
|
+
if (lastCode >= 0xd800 && lastCode <= 0xdbff) {
|
|
30877
|
+
low -= 1;
|
|
30878
|
+
}
|
|
30316
30879
|
return str.slice(0, low);
|
|
30317
30880
|
}/**
|
|
30318
30881
|
* Manages the client (caller) side of RPC: sending requests, tracking pending
|
|
@@ -30717,6 +31280,12 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30717
31280
|
}class RemoteAudioTrack extends RemoteTrack {
|
|
30718
31281
|
constructor(mediaTrack, sid, receiver, audioContext, audioOutput, loggerOptions) {
|
|
30719
31282
|
super(mediaTrack, sid, Track.Kind.Audio, receiver, loggerOptions);
|
|
31283
|
+
/**
|
|
31284
|
+
* the nodes of the graph currently wired up, in order, so teardown can undo exactly the edges
|
|
31285
|
+
* `connectWebAudio` created. Reading `webAudioPluginNodes` instead would miss the old plugins
|
|
31286
|
+
* once `setWebAudioPlugins` has replaced them.
|
|
31287
|
+
*/
|
|
31288
|
+
this.connectedNodes = [];
|
|
30720
31289
|
this.monitorReceiver = () => __awaiter(this, void 0, void 0, function* () {
|
|
30721
31290
|
if (!this.receiver) {
|
|
30722
31291
|
this._currentBitrate = 0;
|
|
@@ -30739,10 +31308,11 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30739
31308
|
*/
|
|
30740
31309
|
setVolume(volume) {
|
|
30741
31310
|
var _a;
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
31311
|
+
if (this.audioContext) {
|
|
31312
|
+
// playback goes through the web audio graph, the attached elements themselves stay silent
|
|
31313
|
+
(_a = this.gainNode) === null || _a === void 0 ? void 0 : _a.gain.setTargetAtTime(volume, 0, 0.1);
|
|
31314
|
+
} else {
|
|
31315
|
+
for (const el of this.attachedElements) {
|
|
30746
31316
|
el.volume = volume;
|
|
30747
31317
|
}
|
|
30748
31318
|
}
|
|
@@ -30788,7 +31358,9 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30788
31358
|
});
|
|
30789
31359
|
}
|
|
30790
31360
|
attach(element) {
|
|
30791
|
-
|
|
31361
|
+
// the graph is fed by the track rather than by a specific element, so one connection covers
|
|
31362
|
+
// all attached elements
|
|
31363
|
+
const needsNewWebAudioConnection = !this.sourceNode;
|
|
30792
31364
|
if (!element) {
|
|
30793
31365
|
element = super.attach();
|
|
30794
31366
|
} else {
|
|
@@ -30799,11 +31371,15 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30799
31371
|
this.log.error('Failed to set sink id on remote audio track', e, this.logContext);
|
|
30800
31372
|
});
|
|
30801
31373
|
}
|
|
30802
|
-
if (this.audioContext
|
|
30803
|
-
|
|
30804
|
-
|
|
31374
|
+
if (this.audioContext) {
|
|
31375
|
+
// audio is routed through the web audio graph, so every attached element has to stay
|
|
31376
|
+
// silent, no matter whether it is the one the graph was built from
|
|
30805
31377
|
element.volume = 0;
|
|
30806
31378
|
element.muted = true;
|
|
31379
|
+
if (needsNewWebAudioConnection) {
|
|
31380
|
+
this.log.debug('using audio context mapping', this.logContext);
|
|
31381
|
+
this.connectWebAudio(this.audioContext, element);
|
|
31382
|
+
}
|
|
30807
31383
|
}
|
|
30808
31384
|
if (this.elementVolume !== undefined) {
|
|
30809
31385
|
// make sure volume setting is being applied to the newly attached element
|
|
@@ -30835,11 +31411,27 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30835
31411
|
* @experimental
|
|
30836
31412
|
*/
|
|
30837
31413
|
setAudioContext(audioContext) {
|
|
31414
|
+
var _a;
|
|
31415
|
+
if (this.audioContext === audioContext) {
|
|
31416
|
+
return;
|
|
31417
|
+
}
|
|
30838
31418
|
this.audioContext = audioContext;
|
|
30839
|
-
if (audioContext
|
|
30840
|
-
|
|
30841
|
-
|
|
31419
|
+
if (audioContext) {
|
|
31420
|
+
// playback moves into the web audio graph, mute the elements to avoid double playback
|
|
31421
|
+
for (const element of this.attachedElements) {
|
|
31422
|
+
element.volume = 0;
|
|
31423
|
+
element.muted = true;
|
|
31424
|
+
}
|
|
31425
|
+
if (this.attachedElements.length > 0) {
|
|
31426
|
+
this.connectWebAudio(audioContext, this.attachedElements[0]);
|
|
31427
|
+
}
|
|
31428
|
+
} else {
|
|
30842
31429
|
this.disconnectWebAudio();
|
|
31430
|
+
// playback moves back into the elements, which `attach` had muted in favour of the graph
|
|
31431
|
+
for (const element of this.attachedElements) {
|
|
31432
|
+
element.muted = false;
|
|
31433
|
+
element.volume = (_a = this.elementVolume) !== null && _a !== void 0 ? _a : 1;
|
|
31434
|
+
}
|
|
30843
31435
|
}
|
|
30844
31436
|
}
|
|
30845
31437
|
/**
|
|
@@ -30858,12 +31450,15 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30858
31450
|
// @ts-ignore attached elements always have a srcObject set
|
|
30859
31451
|
this.sourceNode = context.createMediaStreamSource(element.srcObject);
|
|
30860
31452
|
let lastNode = this.sourceNode;
|
|
31453
|
+
this.connectedNodes = [this.sourceNode];
|
|
30861
31454
|
this.webAudioPluginNodes.forEach(node => {
|
|
30862
31455
|
lastNode.connect(node);
|
|
30863
31456
|
lastNode = node;
|
|
31457
|
+
this.connectedNodes.push(node);
|
|
30864
31458
|
});
|
|
30865
31459
|
this.gainNode = context.createGain();
|
|
30866
31460
|
lastNode.connect(this.gainNode);
|
|
31461
|
+
this.connectedNodes.push(this.gainNode);
|
|
30867
31462
|
this.gainNode.connect(context.destination);
|
|
30868
31463
|
if (this.elementVolume !== undefined) {
|
|
30869
31464
|
this.gainNode.gain.setTargetAtTime(this.elementVolume, 0, 0.1);
|
|
@@ -30881,6 +31476,19 @@ class RpcServerManager extends eventsExports.EventEmitter {
|
|
|
30881
31476
|
}
|
|
30882
31477
|
disconnectWebAudio() {
|
|
30883
31478
|
var _a, _b;
|
|
31479
|
+
// the plugin nodes belong to whoever passed them in and may feed graphs of their own, so only
|
|
31480
|
+
// the edges this track created come down. A bare `node.disconnect()` would drop the
|
|
31481
|
+
// application's own routing with them
|
|
31482
|
+
for (let i = 0; i < this.connectedNodes.length - 1; i += 1) {
|
|
31483
|
+
try {
|
|
31484
|
+
this.connectedNodes[i].disconnect(this.connectedNodes[i + 1]);
|
|
31485
|
+
} catch (_c) {
|
|
31486
|
+
// a plugin node the application already disconnected itself, the rest of the chain still
|
|
31487
|
+
// has to come apart
|
|
31488
|
+
}
|
|
31489
|
+
}
|
|
31490
|
+
this.connectedNodes = [];
|
|
31491
|
+
// the source and gain nodes are ours alone, so every remaining edge of theirs is ours to drop
|
|
30884
31492
|
(_a = this.gainNode) === null || _a === void 0 ? void 0 : _a.disconnect();
|
|
30885
31493
|
(_b = this.sourceNode) === null || _b === void 0 ? void 0 : _b.disconnect();
|
|
30886
31494
|
this.gainNode = undefined;
|
|
@@ -31542,8 +32150,10 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
31542
32150
|
* @internal
|
|
31543
32151
|
*/
|
|
31544
32152
|
setAudioContext(ctx) {
|
|
31545
|
-
this
|
|
31546
|
-
|
|
32153
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32154
|
+
this.audioContext = ctx;
|
|
32155
|
+
yield Promise.all(Array.from(this.audioTrackPublications.values()).map(publication => publication.track).filter(isAudioTrack).map(track => track.setAudioContext(ctx)));
|
|
32156
|
+
});
|
|
31547
32157
|
}
|
|
31548
32158
|
addTrackPublication(publication) {
|
|
31549
32159
|
this.log.debug("adding track publication", {
|
|
@@ -31802,10 +32412,7 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
31802
32412
|
if (!track.isMuted) {
|
|
31803
32413
|
this.log.debug('track ended, attempting to use a different device', getLogContextFromTrack(track));
|
|
31804
32414
|
if (isLocalAudioTrack(track)) {
|
|
31805
|
-
|
|
31806
|
-
yield track.restartTrack({
|
|
31807
|
-
deviceId: 'default'
|
|
31808
|
-
});
|
|
32415
|
+
yield this.restartOnDefaultAudioDevice(track);
|
|
31809
32416
|
} else {
|
|
31810
32417
|
yield track.restartTrack();
|
|
31811
32418
|
}
|
|
@@ -32152,10 +32759,10 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
32152
32759
|
loggerName: this.roomOptions.loggerName,
|
|
32153
32760
|
loggerContextCb: () => this.logContext
|
|
32154
32761
|
});
|
|
32155
|
-
const localTracks = tracks.map(track => {
|
|
32762
|
+
const localTracks = yield Promise.all(tracks.map(track => __awaiter(this, void 0, void 0, function* () {
|
|
32156
32763
|
if (isAudioTrack(track)) {
|
|
32157
32764
|
this.microphoneError = undefined;
|
|
32158
|
-
track.setAudioContext(this.audioContext);
|
|
32765
|
+
yield track.setAudioContext(this.audioContext);
|
|
32159
32766
|
track.source = Track.Source.Microphone;
|
|
32160
32767
|
this.emit(ParticipantEvent.AudioStreamAcquired);
|
|
32161
32768
|
}
|
|
@@ -32164,7 +32771,7 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
32164
32771
|
track.source = Track.Source.Camera;
|
|
32165
32772
|
}
|
|
32166
32773
|
return track;
|
|
32167
|
-
});
|
|
32774
|
+
})));
|
|
32168
32775
|
return localTracks;
|
|
32169
32776
|
} catch (err) {
|
|
32170
32777
|
if (err instanceof Error) {
|
|
@@ -32274,7 +32881,7 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
32274
32881
|
return function* () {
|
|
32275
32882
|
var _a, _b, _c, _d;
|
|
32276
32883
|
if (isLocalAudioTrack(track)) {
|
|
32277
|
-
track.setAudioContext(_this2.audioContext);
|
|
32884
|
+
yield track.setAudioContext(_this2.audioContext);
|
|
32278
32885
|
}
|
|
32279
32886
|
yield (_a = _this2.reconnectFuture) === null || _a === void 0 ? void 0 : _a.promise;
|
|
32280
32887
|
if (_this2.republishPromise && !isRepublish) {
|
|
@@ -32874,19 +33481,36 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
32874
33481
|
return;
|
|
32875
33482
|
}
|
|
32876
33483
|
const packetTrailerFeatures = this.normalizeRequestedFrameMetadataOptions(track, opts);
|
|
33484
|
+
// the backup sender encodes the processed track, not the raw capture track.dimensions reports
|
|
33485
|
+
const _track$mediaStreamTra = track.mediaStreamTrack.getSettings(),
|
|
33486
|
+
width = _track$mediaStreamTra.width,
|
|
33487
|
+
height = _track$mediaStreamTra.height;
|
|
33488
|
+
const dims = width && height ? {
|
|
33489
|
+
width,
|
|
33490
|
+
height
|
|
33491
|
+
} : undefined;
|
|
33492
|
+
if (!dims) {
|
|
33493
|
+
this.log.warn('could not determine track dimensions, publishing backup codec without layers', getLogContextFromTrack(track));
|
|
33494
|
+
}
|
|
33495
|
+
// the server reads the layers off the codec entry for a request that carries a sid, and
|
|
33496
|
+
// falls back to the top level ones when it predates per codec layers
|
|
33497
|
+
const layers = dims ? videoLayersFromEncodings(dims.width, dims.height, encodings) : [];
|
|
32877
33498
|
const req = new AddTrackRequest({
|
|
32878
33499
|
cid: simulcastTrack.mediaStreamTrack.id,
|
|
32879
33500
|
type: Track.kindToProto(track.kind),
|
|
32880
33501
|
muted: track.isMuted,
|
|
32881
33502
|
source: Track.sourceToProto(track.source),
|
|
32882
33503
|
sid: track.sid,
|
|
33504
|
+
width: dims === null || dims === void 0 ? void 0 : dims.width,
|
|
33505
|
+
height: dims === null || dims === void 0 ? void 0 : dims.height,
|
|
32883
33506
|
packetTrailerFeatures,
|
|
33507
|
+
layers,
|
|
32884
33508
|
simulcastCodecs: [{
|
|
32885
33509
|
codec: opts.videoCodec,
|
|
32886
|
-
cid: simulcastTrack.mediaStreamTrack.id
|
|
33510
|
+
cid: simulcastTrack.mediaStreamTrack.id,
|
|
33511
|
+
layers
|
|
32887
33512
|
}]
|
|
32888
33513
|
});
|
|
32889
|
-
req.layers = videoLayersFromEncodings(req.width, req.height, encodings);
|
|
32890
33514
|
if (!this.engine || this.engine.isClosed) {
|
|
32891
33515
|
throw new UnexpectedConnectionState('cannot publish track when not connected');
|
|
32892
33516
|
}
|
|
@@ -33306,6 +33930,35 @@ class Participant extends eventsExports.EventEmitter {
|
|
|
33306
33930
|
}
|
|
33307
33931
|
return this.activeAgentFuture.promise;
|
|
33308
33932
|
}
|
|
33933
|
+
/**
|
|
33934
|
+
* @internal
|
|
33935
|
+
*/
|
|
33936
|
+
restartOnDefaultAudioDevice(track) {
|
|
33937
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
33938
|
+
var _a, _b;
|
|
33939
|
+
try {
|
|
33940
|
+
if (isChromiumBased()) {
|
|
33941
|
+
// only chrome has the notion of a literal `default` device
|
|
33942
|
+
yield track.setDeviceId({
|
|
33943
|
+
exact: 'default'
|
|
33944
|
+
});
|
|
33945
|
+
} else {
|
|
33946
|
+
// just try to restart for other browsers and try to change the device id as a fallback in the catch clause
|
|
33947
|
+
yield track.restartTrack();
|
|
33948
|
+
}
|
|
33949
|
+
} catch (e) {
|
|
33950
|
+
// restart() clears the manually-stopped flag on entry, so retrying a track the user stopped
|
|
33951
|
+
// while the first attempt was in flight would re-open the capture device behind their back
|
|
33952
|
+
if (!isDeviceAcquisitionFailure(e) || isPermissionDeniedError(e) || track.isStopped) {
|
|
33953
|
+
throw e;
|
|
33954
|
+
}
|
|
33955
|
+
this.log.debug('could not reacquire the audio device, retrying with an enumerated device', getLogContextFromTrack(track));
|
|
33956
|
+
// permission is already granted at this point, so skip the probe that would re-acquire media
|
|
33957
|
+
const allDevices = yield DeviceManager.getInstance().getDevices('audioinput', false);
|
|
33958
|
+
yield track.setDeviceId((_b = (_a = allDevices === null || allDevices === void 0 ? void 0 : allDevices[0]) === null || _a === void 0 ? void 0 : _a.deviceId) !== null && _b !== void 0 ? _b : 'default');
|
|
33959
|
+
}
|
|
33960
|
+
});
|
|
33961
|
+
}
|
|
33309
33962
|
getPublicationForTrack(track) {
|
|
33310
33963
|
let publication;
|
|
33311
33964
|
this.trackPublications.forEach(pub => {
|
|
@@ -34016,6 +34669,30 @@ class DeferrableMap extends Map {
|
|
|
34016
34669
|
})(ConnectionState || (ConnectionState = {}));
|
|
34017
34670
|
const CONNECTION_RECONCILE_FREQUENCY_MS = 4 * 1000;
|
|
34018
34671
|
const STATS_LOG_FREQUENCY_MS = 30 * 1000;
|
|
34672
|
+
const DUMMY_AUDIO_ELEMENT_ID = 'livekit-dummy-audio-el';
|
|
34673
|
+
/**
|
|
34674
|
+
* One silent audio element serves every room on the page, so it is reference counted: a room
|
|
34675
|
+
* disconnecting must not pull it out from under the rooms still playing through it. The element
|
|
34676
|
+
* and its track go away once the last room hands its reference back.
|
|
34677
|
+
*/
|
|
34678
|
+
let sharedDummyAudioElement;
|
|
34679
|
+
function createSharedDummyAudioElement() {
|
|
34680
|
+
const element = document.createElement('audio');
|
|
34681
|
+
element.id = DUMMY_AUDIO_ELEMENT_ID;
|
|
34682
|
+
element.autoplay = true;
|
|
34683
|
+
element.hidden = true;
|
|
34684
|
+
const track = getEmptyAudioStreamTrack();
|
|
34685
|
+
track.enabled = true;
|
|
34686
|
+
const stream = new MediaStream([track]);
|
|
34687
|
+
element.srcObject = stream;
|
|
34688
|
+
document.body.append(element);
|
|
34689
|
+
return {
|
|
34690
|
+
element,
|
|
34691
|
+
stream,
|
|
34692
|
+
track,
|
|
34693
|
+
refCount: 0
|
|
34694
|
+
};
|
|
34695
|
+
}
|
|
34019
34696
|
/**
|
|
34020
34697
|
* In LiveKit, a room is the logical grouping for a list of participants.
|
|
34021
34698
|
* Participants in a room can publish tracks, and subscribe to others' tracks.
|
|
@@ -34046,6 +34723,24 @@ class Room extends eventsExports.EventEmitter {
|
|
|
34046
34723
|
/** reflects the sender encryption status of the local participant */
|
|
34047
34724
|
this.isE2EEEnabled = false;
|
|
34048
34725
|
this.audioEnabled = true;
|
|
34726
|
+
/**
|
|
34727
|
+
* whether `audioContext` was created by the SDK and therefore has to be closed by it.
|
|
34728
|
+
* A context handed in through `webAudioMix` is owned by the application instead.
|
|
34729
|
+
*/
|
|
34730
|
+
this.ownsAudioContext = false;
|
|
34731
|
+
/**
|
|
34732
|
+
* set once the audio context has been released as part of a disconnect. Guards against
|
|
34733
|
+
* creating a new context outside of a connection lifecycle, where nothing would close it again.
|
|
34734
|
+
*/
|
|
34735
|
+
this.audioContextReleased = false;
|
|
34736
|
+
/**
|
|
34737
|
+
* serialises `acquireAudioContext` against `releaseAudioContext`. Both await per-track graph
|
|
34738
|
+
* rebuilds, so without it a reconnect landing inside a disconnect's teardown leaves the two
|
|
34739
|
+
* racing to set the participants' context, and the loser's value is the one that sticks.
|
|
34740
|
+
*/
|
|
34741
|
+
this.audioContextMutex = new _();
|
|
34742
|
+
/** silent audio tracks this room acquired, handed back to the shared source on disconnect */
|
|
34743
|
+
this.acquiredEmptyAudioTracks = new Set();
|
|
34049
34744
|
this.e2eeStateMutex = new _();
|
|
34050
34745
|
this.isVideoPlaybackBlocked = false;
|
|
34051
34746
|
this.log = livekitLogger;
|
|
@@ -34218,7 +34913,9 @@ class Room extends eventsExports.EventEmitter {
|
|
|
34218
34913
|
if ((_b = this.regionUrlProvider) === null || _b === void 0 ? void 0 : _b.isCloud()) {
|
|
34219
34914
|
this.engine.setRegionStrategy(this.createRegionStrategy());
|
|
34220
34915
|
}
|
|
34221
|
-
this.acquireAudioContext()
|
|
34916
|
+
this.acquireAudioContext(true).catch(error => this.log.warn('Could not acquire audio context', Object.assign(Object.assign({}, this.logContext), {
|
|
34917
|
+
error
|
|
34918
|
+
})));
|
|
34222
34919
|
this.connOptions = Object.assign(Object.assign({}, roomConnectOptionDefaults), opts);
|
|
34223
34920
|
if (this.connOptions.rtcConfig) {
|
|
34224
34921
|
this.engine.rtcConfig = this.connOptions.rtcConfig;
|
|
@@ -34337,44 +35034,8 @@ class Room extends eventsExports.EventEmitter {
|
|
|
34337
35034
|
this.startAudio = () => __awaiter(this, void 0, void 0, function* () {
|
|
34338
35035
|
const elements = [];
|
|
34339
35036
|
const browser = getBrowser();
|
|
34340
|
-
if (browser && browser.os === 'iOS') {
|
|
34341
|
-
|
|
34342
|
-
* iOS blocks audio element playback if
|
|
34343
|
-
* - user is not publishing audio themselves and
|
|
34344
|
-
* - no other audio source is playing
|
|
34345
|
-
*
|
|
34346
|
-
* as a workaround, we create an audio element with an empty track, so that
|
|
34347
|
-
* silent audio is always playing
|
|
34348
|
-
*/
|
|
34349
|
-
const audioId = 'livekit-dummy-audio-el';
|
|
34350
|
-
let dummyAudioEl = document.getElementById(audioId);
|
|
34351
|
-
if (!dummyAudioEl) {
|
|
34352
|
-
dummyAudioEl = document.createElement('audio');
|
|
34353
|
-
dummyAudioEl.id = audioId;
|
|
34354
|
-
dummyAudioEl.autoplay = true;
|
|
34355
|
-
dummyAudioEl.hidden = true;
|
|
34356
|
-
const track = getEmptyAudioStreamTrack();
|
|
34357
|
-
track.enabled = true;
|
|
34358
|
-
const stream = new MediaStream([track]);
|
|
34359
|
-
dummyAudioEl.srcObject = stream;
|
|
34360
|
-
document.addEventListener('visibilitychange', () => {
|
|
34361
|
-
if (!dummyAudioEl) {
|
|
34362
|
-
return;
|
|
34363
|
-
}
|
|
34364
|
-
// set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
|
|
34365
|
-
dummyAudioEl.srcObject = document.hidden ? null : stream;
|
|
34366
|
-
if (!document.hidden) {
|
|
34367
|
-
this.log.debug('page visible again, triggering startAudio to resume playback and update playback status');
|
|
34368
|
-
this.startAudio();
|
|
34369
|
-
}
|
|
34370
|
-
});
|
|
34371
|
-
document.body.append(dummyAudioEl);
|
|
34372
|
-
this.once(RoomEvent.Disconnected, () => {
|
|
34373
|
-
dummyAudioEl === null || dummyAudioEl === void 0 ? void 0 : dummyAudioEl.remove();
|
|
34374
|
-
dummyAudioEl = null;
|
|
34375
|
-
});
|
|
34376
|
-
}
|
|
34377
|
-
elements.push(dummyAudioEl);
|
|
35037
|
+
if (browser && browser.os === 'iOS' && !this.audioContextReleased) {
|
|
35038
|
+
elements.push(this.acquireDummyAudioElement());
|
|
34378
35039
|
}
|
|
34379
35040
|
this.remoteParticipants.forEach(p => {
|
|
34380
35041
|
p.audioTrackPublications.forEach(t => {
|
|
@@ -34386,7 +35047,10 @@ class Room extends eventsExports.EventEmitter {
|
|
|
34386
35047
|
});
|
|
34387
35048
|
});
|
|
34388
35049
|
try {
|
|
34389
|
-
yield Promise.all([
|
|
35050
|
+
yield Promise.all([
|
|
35051
|
+
// once the audio context has been released as part of a disconnect there is nothing left
|
|
35052
|
+
// to unlock, and creating a new context here would leak it as no disconnect will follow
|
|
35053
|
+
...(this.audioContextReleased ? [] : [this.acquireAudioContext()]), ...elements.map(e => {
|
|
34390
35054
|
// when webAudioMix is enabled, attached elements are deliberately kept muted by
|
|
34391
35055
|
// RemoteAudioTrack.attach() and audio is routed through the web audio graph instead.
|
|
34392
35056
|
// unmuting them here would cause double playback on platforms where element.volume
|
|
@@ -35430,6 +36094,78 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35430
36094
|
}
|
|
35431
36095
|
});
|
|
35432
36096
|
}
|
|
36097
|
+
/**
|
|
36098
|
+
* iOS blocks audio element playback if
|
|
36099
|
+
* - user is not publishing audio themselves and
|
|
36100
|
+
* - no other audio source is playing
|
|
36101
|
+
*
|
|
36102
|
+
* as a workaround, we create an audio element with an empty track, so that
|
|
36103
|
+
* silent audio is always playing
|
|
36104
|
+
*
|
|
36105
|
+
* The element is shared with any other room on the page and handed back on disconnect, so it
|
|
36106
|
+
* outlives whichever room happened to create it.
|
|
36107
|
+
*/
|
|
36108
|
+
acquireDummyAudioElement() {
|
|
36109
|
+
if (!sharedDummyAudioElement) {
|
|
36110
|
+
sharedDummyAudioElement = createSharedDummyAudioElement();
|
|
36111
|
+
}
|
|
36112
|
+
const shared = sharedDummyAudioElement;
|
|
36113
|
+
if (this.releaseDummyAudioElement) {
|
|
36114
|
+
// this room already holds a reference, a second one would never be handed back
|
|
36115
|
+
return shared.element;
|
|
36116
|
+
}
|
|
36117
|
+
shared.refCount += 1;
|
|
36118
|
+
// the listener is per room rather than shared, so that the room it resumes playback for is
|
|
36119
|
+
// still connected, and so that it goes away with the room that installed it
|
|
36120
|
+
const handleVisibilityChange = () => {
|
|
36121
|
+
// set the srcObject to null on page hide in order to prevent lock screen controls to show up for it
|
|
36122
|
+
shared.element.srcObject = document.hidden ? null : shared.stream;
|
|
36123
|
+
if (!document.hidden) {
|
|
36124
|
+
this.log.debug('page visible again, triggering startAudio to resume playback and update playback status');
|
|
36125
|
+
this.startAudio().catch(error => this.log.warn('Could not restart audio on page becoming visible', Object.assign(Object.assign({}, this.logContext), {
|
|
36126
|
+
error
|
|
36127
|
+
})));
|
|
36128
|
+
}
|
|
36129
|
+
};
|
|
36130
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
36131
|
+
this.releaseDummyAudioElement = () => {
|
|
36132
|
+
this.releaseDummyAudioElement = undefined;
|
|
36133
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
36134
|
+
shared.refCount -= 1;
|
|
36135
|
+
if (shared.refCount > 0) {
|
|
36136
|
+
return;
|
|
36137
|
+
}
|
|
36138
|
+
shared.element.srcObject = null;
|
|
36139
|
+
shared.element.remove();
|
|
36140
|
+
sharedDummyAudioElement = undefined;
|
|
36141
|
+
releaseEmptyAudioStreamTrack(shared.track).catch(error => this.log.warn('Could not release the dummy audio element track', Object.assign(Object.assign({}, this.logContext), {
|
|
36142
|
+
error
|
|
36143
|
+
})));
|
|
36144
|
+
};
|
|
36145
|
+
return shared.element;
|
|
36146
|
+
}
|
|
36147
|
+
/**
|
|
36148
|
+
* Obtains a silent audio track from the shared source and keeps track of it, so that it can be
|
|
36149
|
+
* handed back on disconnect and the shared `AudioContext` behind it can be closed again.
|
|
36150
|
+
*/
|
|
36151
|
+
acquireEmptyAudioStreamTrack() {
|
|
36152
|
+
const track = getEmptyAudioStreamTrack();
|
|
36153
|
+
this.acquiredEmptyAudioTracks.add(track);
|
|
36154
|
+
return track;
|
|
36155
|
+
}
|
|
36156
|
+
releaseAcquiredEmptyAudioTrack(track) {
|
|
36157
|
+
if (!this.acquiredEmptyAudioTracks.delete(track)) {
|
|
36158
|
+
return;
|
|
36159
|
+
}
|
|
36160
|
+
releaseEmptyAudioStreamTrack(track).catch(error => this.log.warn('Could not release empty audio stream track', Object.assign(Object.assign({}, this.logContext), {
|
|
36161
|
+
error
|
|
36162
|
+
})));
|
|
36163
|
+
}
|
|
36164
|
+
releaseAcquiredEmptyAudioTracks() {
|
|
36165
|
+
for (const track of this.acquiredEmptyAudioTracks) {
|
|
36166
|
+
this.releaseAcquiredEmptyAudioTrack(track);
|
|
36167
|
+
}
|
|
36168
|
+
}
|
|
35433
36169
|
/**
|
|
35434
36170
|
* Returns true if audio playback is enabled
|
|
35435
36171
|
*/
|
|
@@ -35712,7 +36448,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35712
36448
|
handleDisconnect() {
|
|
35713
36449
|
let shouldStopTracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
|
|
35714
36450
|
let reason = arguments.length > 1 ? arguments[1] : undefined;
|
|
35715
|
-
var _a, _b;
|
|
36451
|
+
var _a, _b, _c;
|
|
35716
36452
|
this.clearConnectionReconcile();
|
|
35717
36453
|
this.isResuming = false;
|
|
35718
36454
|
this.bufferedEvents = [];
|
|
@@ -35729,6 +36465,9 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35729
36465
|
this.regionUrlProvider.notifyDisconnected();
|
|
35730
36466
|
}
|
|
35731
36467
|
try {
|
|
36468
|
+
// audio tracks that outlive this room are about to become unreachable through the local
|
|
36469
|
+
// participant, collect them while we can so they can be detached from the context below
|
|
36470
|
+
const retainedAudioTracks = shouldStopTracks ? [] : Array.from(this.localParticipant.audioTrackPublications.values()).map(pub => pub.track).filter(isLocalAudioTrack);
|
|
35732
36471
|
this.remoteParticipants.forEach(p => {
|
|
35733
36472
|
p.trackPublications.forEach(pub => {
|
|
35734
36473
|
p.unpublishTrack(pub.trackSid);
|
|
@@ -35753,15 +36492,18 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35753
36492
|
this.remoteParticipants.clear();
|
|
35754
36493
|
this.sidToIdentity.clear();
|
|
35755
36494
|
this.activeSpeakers = [];
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
36495
|
+
(_a = this.releaseDummyAudioElement) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
36496
|
+
this.releaseAcquiredEmptyAudioTracks();
|
|
36497
|
+
// teardown is asynchronous (processors need to release their web audio nodes), but
|
|
36498
|
+
// `handleDisconnect` is not, so let it finish in the background
|
|
36499
|
+
this.releaseAudioContext(retainedAudioTracks).catch(error => this.log.warn('Could not release audio context', Object.assign(Object.assign({}, this.logContext), {
|
|
36500
|
+
error
|
|
36501
|
+
})));
|
|
35760
36502
|
if (isWeb()) {
|
|
35761
36503
|
window.removeEventListener('beforeunload', this.onPageLeave);
|
|
35762
36504
|
window.removeEventListener('pagehide', this.onPageLeave);
|
|
35763
36505
|
window.removeEventListener('freeze', this.onPageLeave);
|
|
35764
|
-
(
|
|
36506
|
+
(_c = (_b = navigator.mediaDevices) === null || _b === void 0 ? void 0 : _b.removeEventListener) === null || _c === void 0 ? void 0 : _c.call(_b, 'devicechange', this.handleDeviceChange);
|
|
35765
36507
|
}
|
|
35766
36508
|
} finally {
|
|
35767
36509
|
this.setAndEmitConnectionState(ConnectionState.Disconnected);
|
|
@@ -35836,39 +36578,139 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35836
36578
|
}
|
|
35837
36579
|
});
|
|
35838
36580
|
}
|
|
36581
|
+
/**
|
|
36582
|
+
* @param forConnect whether this acquisition belongs to a connect, which is the only caller
|
|
36583
|
+
* allowed to put a released room back on an audio context.
|
|
36584
|
+
*/
|
|
35839
36585
|
acquireAudioContext() {
|
|
35840
|
-
return __awaiter(this,
|
|
35841
|
-
var
|
|
35842
|
-
|
|
35843
|
-
|
|
35844
|
-
|
|
35845
|
-
|
|
35846
|
-
// by using an AudioContext, it reduces lag on audio elements
|
|
35847
|
-
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
|
35848
|
-
this.audioContext = (_a = getNewAudioContext()) !== null && _a !== void 0 ? _a : undefined;
|
|
35849
|
-
}
|
|
35850
|
-
if (this.options.webAudioMix) {
|
|
35851
|
-
this.remoteParticipants.forEach(participant => participant.setAudioContext(this.audioContext));
|
|
35852
|
-
}
|
|
35853
|
-
this.localParticipant.setAudioContext(this.audioContext);
|
|
35854
|
-
if (this.audioContext && this.audioContext.state === 'suspended') {
|
|
35855
|
-
// for iOS a newly created AudioContext is always in `suspended` state.
|
|
35856
|
-
// we try our best to resume the context here, if that doesn't work, we just continue with regular processing
|
|
36586
|
+
return __awaiter(this, arguments, void 0, function () {
|
|
36587
|
+
var _this4 = this;
|
|
36588
|
+
let forConnect = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
|
|
36589
|
+
return function* () {
|
|
36590
|
+
var _a, _b;
|
|
36591
|
+
const unlock = yield _this4.audioContextMutex.lock();
|
|
35857
36592
|
try {
|
|
35858
|
-
|
|
35859
|
-
|
|
35860
|
-
|
|
35861
|
-
|
|
35862
|
-
}
|
|
36593
|
+
if (_this4.audioContextReleased && !forConnect) {
|
|
36594
|
+
// a disconnect set the flag and took the lock while this call was queued behind it.
|
|
36595
|
+
// Creating a context now would leave one open on a room nothing tears down again
|
|
36596
|
+
return;
|
|
36597
|
+
}
|
|
36598
|
+
_this4.audioContextReleased = false;
|
|
36599
|
+
const providedAudioContext = typeof _this4.options.webAudioMix !== 'boolean' ? _this4.options.webAudioMix.audioContext : undefined;
|
|
36600
|
+
if (providedAudioContext) {
|
|
36601
|
+
// override audio context with custom audio context if supplied by user
|
|
36602
|
+
yield _this4.setRoomAudioContext(providedAudioContext, false);
|
|
36603
|
+
} else if (!_this4.audioContext || _this4.audioContext.state === 'closed') {
|
|
36604
|
+
// by using an AudioContext, it reduces lag on audio elements
|
|
36605
|
+
// https://stackoverflow.com/questions/9811429/html5-audio-tag-on-safari-has-a-delay/54119854#54119854
|
|
36606
|
+
yield _this4.setRoomAudioContext((_a = getNewAudioContext()) !== null && _a !== void 0 ? _a : undefined, true);
|
|
36607
|
+
}
|
|
36608
|
+
if (_this4.audioContext && _this4.audioContext.state === 'suspended') {
|
|
36609
|
+
// for iOS a newly created AudioContext is always in `suspended` state.
|
|
36610
|
+
// we try our best to resume the context here, if that doesn't work, we just continue with regular processing
|
|
36611
|
+
try {
|
|
36612
|
+
yield Promise.race([_this4.audioContext.resume(), sleep(200)]);
|
|
36613
|
+
} catch (e) {
|
|
36614
|
+
_this4.log.warn('Could not resume audio context', {
|
|
36615
|
+
error: e
|
|
36616
|
+
});
|
|
36617
|
+
}
|
|
36618
|
+
}
|
|
36619
|
+
const newContextIsRunning = ((_b = _this4.audioContext) === null || _b === void 0 ? void 0 : _b.state) === 'running';
|
|
36620
|
+
if (newContextIsRunning !== _this4.canPlaybackAudio) {
|
|
36621
|
+
_this4.audioEnabled = newContextIsRunning;
|
|
36622
|
+
_this4.emit(RoomEvent.AudioPlaybackStatusChanged, newContextIsRunning);
|
|
36623
|
+
}
|
|
36624
|
+
} finally {
|
|
36625
|
+
unlock();
|
|
35863
36626
|
}
|
|
36627
|
+
}();
|
|
36628
|
+
});
|
|
36629
|
+
}
|
|
36630
|
+
/**
|
|
36631
|
+
* Points the room and its participants at `audioContext`, rebuilding the web audio graph of
|
|
36632
|
+
* every audio track in it. Closes the previous context if the SDK `owned` it.
|
|
36633
|
+
*/
|
|
36634
|
+
setRoomAudioContext(audioContext, owned) {
|
|
36635
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
36636
|
+
const ownsAudioContext = !!audioContext && owned;
|
|
36637
|
+
if (this.audioContext === audioContext) {
|
|
36638
|
+
this.ownsAudioContext = ownsAudioContext;
|
|
36639
|
+
return;
|
|
35864
36640
|
}
|
|
35865
|
-
const
|
|
35866
|
-
|
|
35867
|
-
|
|
35868
|
-
|
|
36641
|
+
const contextToClose = this.ownsAudioContext ? this.audioContext : undefined;
|
|
36642
|
+
this.audioContext = audioContext;
|
|
36643
|
+
this.ownsAudioContext = ownsAudioContext;
|
|
36644
|
+
try {
|
|
36645
|
+
// every audio track rebuilds its nodes in the new context, which has to happen before the
|
|
36646
|
+
// previous one is closed so that processors can move off it while it is still alive
|
|
36647
|
+
if (this.options.webAudioMix) {
|
|
36648
|
+
yield Promise.all(Array.from(this.remoteParticipants.values()).map(participant => participant.setAudioContext(audioContext)));
|
|
36649
|
+
}
|
|
36650
|
+
yield this.localParticipant.setAudioContext(audioContext);
|
|
36651
|
+
} catch (error) {
|
|
36652
|
+
// the context is being abandoned either way, so a track that failed to move off it must
|
|
36653
|
+
// not stop us closing it — that would leak the context this whole path exists to release
|
|
36654
|
+
this.log.warn('Could not move audio tracks to the new audio context', Object.assign(Object.assign({}, this.logContext), {
|
|
36655
|
+
error
|
|
36656
|
+
}));
|
|
36657
|
+
} finally {
|
|
36658
|
+
if (contextToClose && contextToClose.state !== 'closed') {
|
|
36659
|
+
try {
|
|
36660
|
+
yield contextToClose.close();
|
|
36661
|
+
} catch (error) {
|
|
36662
|
+
this.log.warn('Could not close audio context', Object.assign(Object.assign({}, this.logContext), {
|
|
36663
|
+
error
|
|
36664
|
+
}));
|
|
36665
|
+
}
|
|
36666
|
+
}
|
|
35869
36667
|
}
|
|
35870
36668
|
});
|
|
35871
36669
|
}
|
|
36670
|
+
/**
|
|
36671
|
+
* Detaches the web audio graph the SDK built from the current audio context and closes that
|
|
36672
|
+
* context if the SDK created it. A context provided through `webAudioMix` is left untouched.
|
|
36673
|
+
*/
|
|
36674
|
+
releaseAudioContext() {
|
|
36675
|
+
return __awaiter(this, arguments, void 0, function () {
|
|
36676
|
+
var _this5 = this;
|
|
36677
|
+
let retainedAudioTracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
|
|
36678
|
+
return function* () {
|
|
36679
|
+
// both reads happen before the first await, so they describe the room as it was when the
|
|
36680
|
+
// disconnect started rather than after a reconnect has had a chance to run
|
|
36681
|
+
_this5.audioContextReleased = true;
|
|
36682
|
+
const contextToRelease = _this5.audioContext;
|
|
36683
|
+
const unlock = yield _this5.audioContextMutex.lock();
|
|
36684
|
+
try {
|
|
36685
|
+
if (!_this5.audioContextReleased || _this5.audioContext !== contextToRelease) {
|
|
36686
|
+
// a reconnect acquired a new context while we were waiting, closing it would leave the
|
|
36687
|
+
// reconnected room without one
|
|
36688
|
+
return;
|
|
36689
|
+
}
|
|
36690
|
+
if (!_this5.ownsAudioContext) {
|
|
36691
|
+
// a provided context stays open, so tracks that outlive the room stay functional on it.
|
|
36692
|
+
// The tracks this room detached have released their nodes during track teardown already.
|
|
36693
|
+
// read under the lock: an acquire queued behind an in-flight release flips this flag
|
|
36694
|
+
return;
|
|
36695
|
+
}
|
|
36696
|
+
try {
|
|
36697
|
+
// tracks that outlive the room aren't reachable through the local participant anymore,
|
|
36698
|
+
// but still hold web audio nodes in the context we're about to close
|
|
36699
|
+
yield Promise.all(retainedAudioTracks.map(track => track.setAudioContext(undefined)));
|
|
36700
|
+
} catch (error) {
|
|
36701
|
+
_this5.log.warn('Could not detach retained audio tracks from the audio context', Object.assign(Object.assign({}, _this5.logContext), {
|
|
36702
|
+
error
|
|
36703
|
+
}));
|
|
36704
|
+
}
|
|
36705
|
+
// the participant maps have already been cleared by `handleDisconnect`, so this reaches no
|
|
36706
|
+
// tracks of its own — `retainedAudioTracks` above is what detaches them
|
|
36707
|
+
yield _this5.setRoomAudioContext(undefined, false);
|
|
36708
|
+
} finally {
|
|
36709
|
+
unlock();
|
|
36710
|
+
}
|
|
36711
|
+
}();
|
|
36712
|
+
});
|
|
36713
|
+
}
|
|
35872
36714
|
createParticipant(identity, info) {
|
|
35873
36715
|
var _a;
|
|
35874
36716
|
let participant;
|
|
@@ -35884,7 +36726,9 @@ class Room extends eventsExports.EventEmitter {
|
|
|
35884
36726
|
});
|
|
35885
36727
|
}
|
|
35886
36728
|
if (this.options.webAudioMix) {
|
|
35887
|
-
participant.setAudioContext(this.audioContext)
|
|
36729
|
+
participant.setAudioContext(this.audioContext).catch(error => this.log.warn('Could not set audio context', Object.assign(Object.assign({}, this.logContext), {
|
|
36730
|
+
error
|
|
36731
|
+
})));
|
|
35888
36732
|
}
|
|
35889
36733
|
if ((_a = this.options.audioOutput) === null || _a === void 0 ? void 0 : _a.deviceId) {
|
|
35890
36734
|
participant.setAudioOutput(this.options.audioOutput).catch(e => this.log.warn("Could not set audio output: ".concat(e.message)));
|
|
@@ -36004,21 +36848,21 @@ class Room extends eventsExports.EventEmitter {
|
|
|
36004
36848
|
}
|
|
36005
36849
|
registerRpcDataStreamHandler() {
|
|
36006
36850
|
this.incomingDataStreamManager.registerTextStreamHandler(RPC_REQUEST_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref7) {
|
|
36007
|
-
var
|
|
36851
|
+
var _this6 = this;
|
|
36008
36852
|
let identity = _ref7.identity;
|
|
36009
36853
|
return function* () {
|
|
36010
36854
|
var _b;
|
|
36011
36855
|
const attributes = (_b = reader.info.attributes) !== null && _b !== void 0 ? _b : {};
|
|
36012
|
-
yield
|
|
36856
|
+
yield _this6.rpcServerManager.handleIncomingDataStream(reader, identity, attributes);
|
|
36013
36857
|
}();
|
|
36014
36858
|
}));
|
|
36015
36859
|
this.incomingDataStreamManager.registerTextStreamHandler(RPC_RESPONSE_DATA_STREAM_TOPIC, (reader_1, _a) => __awaiter(this, [reader_1, _a], void 0, function (reader, _ref8) {
|
|
36016
|
-
var
|
|
36860
|
+
var _this7 = this;
|
|
36017
36861
|
let identity = _ref8.identity;
|
|
36018
36862
|
return function* () {
|
|
36019
36863
|
var _b;
|
|
36020
36864
|
const attributes = (_b = reader.info.attributes) !== null && _b !== void 0 ? _b : {};
|
|
36021
|
-
yield
|
|
36865
|
+
yield _this7.rpcClientManager.handleIncomingDataStream(reader, identity, attributes);
|
|
36022
36866
|
}();
|
|
36023
36867
|
}));
|
|
36024
36868
|
}
|
|
@@ -36177,7 +37021,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
36177
37021
|
type: TrackType.AUDIO
|
|
36178
37022
|
}), new LocalAudioTrack(publishOptions.useRealTracks && ((_c = navigator.mediaDevices) === null || _c === void 0 ? void 0 : _c.getUserMedia) ? (yield navigator.mediaDevices.getUserMedia({
|
|
36179
37023
|
audio: true
|
|
36180
|
-
})).getAudioTracks()[0] :
|
|
37024
|
+
})).getAudioTracks()[0] : this.acquireEmptyAudioStreamTrack(), undefined, false, this.audioContext, {
|
|
36181
37025
|
loggerName: this.options.loggerName,
|
|
36182
37026
|
loggerContextCb: () => this.logContext
|
|
36183
37027
|
}), {
|
|
@@ -36208,7 +37052,7 @@ class Room extends eventsExports.EventEmitter {
|
|
|
36208
37052
|
info.tracks = [...info.tracks, videoTrack];
|
|
36209
37053
|
}
|
|
36210
37054
|
if (participantOptions.audio) {
|
|
36211
|
-
const dummyTrack =
|
|
37055
|
+
const dummyTrack = this.acquireEmptyAudioStreamTrack();
|
|
36212
37056
|
const audioTrack = new TrackInfo({
|
|
36213
37057
|
source: TrackSource.MICROPHONE,
|
|
36214
37058
|
sid: Math.floor(Math.random() * 10000).toString(),
|
|
@@ -37634,4 +38478,4 @@ const serializers = {
|
|
|
37634
38478
|
json,
|
|
37635
38479
|
raw,
|
|
37636
38480
|
custom
|
|
37637
|
-
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,canFailOverToAnotherRegion,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|
|
38481
|
+
};export{AudioPresets,BackupCodecPolicy,BaseKeyProvider,CLIENT_PROTOCOL_DATA_STREAM_RPC,CLIENT_PROTOCOL_DATA_STREAM_V2,CLIENT_PROTOCOL_DEFAULT,CheckStatus,Checker,ConnectionCheck,ConnectionError,ConnectionErrorReason,ConnectionQuality,ConnectionState,CriticalTimers,CryptorError,CryptorErrorReason,CryptorEvent,DataPacket_Kind,DataStreamError,DataStreamErrorReason,DataTrackPacket,DefaultReconnectPolicy,DeviceUnsupportedError,DisconnectReason,EncryptionEvent,Encryption_Type,EngineEvent,ExternalE2EEKeyProvider,FrameMetadataManager,KeyHandlerEvent,KeyProviderEvent,LivekitError,LivekitReasonedError,LocalAudioTrack,LocalDataTrack,LocalParticipant,LocalTrack,LocalTrackPublication,LocalTrackRecorder,LocalVideoTrack,LogLevel,LoggerNames,MediaDeviceFailure,_ as Mutex,NegotiationError,PacketTrailerManager,Participant,ParticipantEvent,ParticipantInfo_Kind as ParticipantKind,PublishDataError,PublishTrackError,RemoteAudioTrack,RemoteDataTrack,RemoteParticipant,RemoteTrack,RemoteTrackPublication,RemoteVideoTrack,Room,RoomEvent,RpcError,ScreenSharePresets,SignalReconnectError,SignalRequestError,SimulatedError,SubscriptionError,TokenSource,TokenSourceConfigurable,TokenSourceFixed,Track,TrackEvent,TrackInvalidError,TrackPublication,TrackType,UnexpectedConnectionState,UnsupportedServer,VideoPreset,VideoPresets,VideoPresets43,VideoQuality,areTokenSourceFetchOptionsEqual,asEncryptablePacket,attachToElement,attributeTypings as attributes,audioCodecs,canFailOverToAnotherRegion,clientProtocol,compareVersions,createAudioAnalyser,createE2EEKey,createKeyMaterialFromBuffer,createKeyMaterialFromString,createLocalAudioTrack,createLocalScreenTracks,createLocalTracks,createLocalVideoTrack,decodeTokenPayload,deriveKeys,detachTrack,facingModeFromDeviceLabel,facingModeFromLocalTrack,getBrowser,getEmptyAudioStreamTrack,getEmptyVideoStreamTrack,getLogger,importKey,isAudioCodec,isAudioTrack,isBackupCodec,isBackupVideoCodec,isBrowserSupported,isE2EESupported,isInsertableStreamSupported,isLocalParticipant,isLocalTrack,isRemoteParticipant,isRemoteTrack,isSVCCodec,isScriptTransformSupported,isSerializer,isVideoCodec,isVideoFrame,isVideoTrack,needsRbspUnescaping,parseRbsp,protocolVersion,ratchet,releaseEmptyAudioStreamTrack,serializers,setLogExtension,setLogLevel,supportsAV1,supportsAdaptiveStream,supportsAudioOutputSelection,supportsDynacast,supportsH265,supportsVP9,version,videoCodecs,writeRbsp};//# sourceMappingURL=livekit-client.esm.mjs.map
|