livekit-client 2.13.5 → 2.13.7
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.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +82 -74
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +5 -5
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +3 -2
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +3 -3
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/ts4.2/src/api/SignalClient.d.ts +5 -5
- package/dist/ts4.2/src/room/PCTransport.d.ts +3 -2
- package/dist/ts4.2/src/room/PCTransportManager.d.ts +3 -3
- package/dist/ts4.2/src/room/RTCEngine.d.ts +4 -0
- package/package.json +8 -8
- package/src/api/SignalClient.ts +13 -14
- package/src/connectionHelper/checks/publishVideo.ts +1 -0
- package/src/room/PCTransport.ts +30 -4
- package/src/room/PCTransportManager.ts +10 -7
- package/src/room/RTCEngine.ts +15 -7
- package/src/room/Room.ts +9 -9
- package/src/room/track/LocalVideoTrack.ts +14 -15
@@ -5744,6 +5744,12 @@ const SessionDescription = /* @__PURE__ */proto3.makeMessageType("livekit.Sessio
|
|
5744
5744
|
kind: "scalar",
|
5745
5745
|
T: 9
|
5746
5746
|
/* ScalarType.STRING */
|
5747
|
+
}, {
|
5748
|
+
no: 3,
|
5749
|
+
name: "id",
|
5750
|
+
kind: "scalar",
|
5751
|
+
T: 13
|
5752
|
+
/* ScalarType.UINT32 */
|
5747
5753
|
}]);
|
5748
5754
|
const ParticipantUpdate = /* @__PURE__ */proto3.makeMessageType("livekit.ParticipantUpdate", () => [{
|
5749
5755
|
no: 1,
|
@@ -11325,7 +11331,7 @@ function getOSVersion(ua) {
|
|
11325
11331
|
return ua.includes('mac os') ? getMatch(/\(.+?(\d+_\d+(:?_\d+)?)/, ua, 1).replace(/_/g, '.') : undefined;
|
11326
11332
|
}
|
11327
11333
|
|
11328
|
-
var version$1 = "2.13.
|
11334
|
+
var version$1 = "2.13.7";
|
11329
11335
|
|
11330
11336
|
const version = version$1;
|
11331
11337
|
const protocolVersion = 16;
|
@@ -13244,10 +13250,7 @@ class SignalClient {
|
|
13244
13250
|
sid,
|
13245
13251
|
reconnectReason: reason
|
13246
13252
|
}));
|
13247
|
-
|
13248
|
-
return res;
|
13249
|
-
}
|
13250
|
-
return;
|
13253
|
+
return res;
|
13251
13254
|
});
|
13252
13255
|
}
|
13253
13256
|
connect(url, token, opts, abortSignal) {
|
@@ -13425,23 +13428,23 @@ class SignalClient {
|
|
13425
13428
|
});
|
13426
13429
|
}
|
13427
13430
|
// initial offer after joining
|
13428
|
-
sendOffer(offer) {
|
13431
|
+
sendOffer(offer, offerId) {
|
13429
13432
|
this.log.debug('sending offer', Object.assign(Object.assign({}, this.logContext), {
|
13430
13433
|
offerSdp: offer.sdp
|
13431
13434
|
}));
|
13432
13435
|
this.sendRequest({
|
13433
13436
|
case: 'offer',
|
13434
|
-
value: toProtoSessionDescription(offer)
|
13437
|
+
value: toProtoSessionDescription(offer, offerId)
|
13435
13438
|
});
|
13436
13439
|
}
|
13437
13440
|
// answer a server-initiated offer
|
13438
|
-
sendAnswer(answer) {
|
13441
|
+
sendAnswer(answer, offerId) {
|
13439
13442
|
this.log.debug('sending answer', Object.assign(Object.assign({}, this.logContext), {
|
13440
13443
|
answerSdp: answer.sdp
|
13441
13444
|
}));
|
13442
13445
|
return this.sendRequest({
|
13443
13446
|
case: 'answer',
|
13444
|
-
value: toProtoSessionDescription(answer)
|
13447
|
+
value: toProtoSessionDescription(answer, offerId)
|
13445
13448
|
});
|
13446
13449
|
}
|
13447
13450
|
sendIceCandidate(candidate, target) {
|
@@ -13617,12 +13620,12 @@ class SignalClient {
|
|
13617
13620
|
if (msg.case === 'answer') {
|
13618
13621
|
const sd = fromProtoSessionDescription(msg.value);
|
13619
13622
|
if (this.onAnswer) {
|
13620
|
-
this.onAnswer(sd);
|
13623
|
+
this.onAnswer(sd, msg.value.id);
|
13621
13624
|
}
|
13622
13625
|
} else if (msg.case === 'offer') {
|
13623
13626
|
const sd = fromProtoSessionDescription(msg.value);
|
13624
13627
|
if (this.onOffer) {
|
13625
|
-
this.onOffer(sd);
|
13628
|
+
this.onOffer(sd, msg.value.id);
|
13626
13629
|
}
|
13627
13630
|
} else if (msg.case === 'trickle') {
|
13628
13631
|
const candidate = JSON.parse(msg.value.candidateInit);
|
@@ -13793,10 +13796,11 @@ function fromProtoSessionDescription(sd) {
|
|
13793
13796
|
}
|
13794
13797
|
return rsd;
|
13795
13798
|
}
|
13796
|
-
function toProtoSessionDescription(rsd) {
|
13799
|
+
function toProtoSessionDescription(rsd, id) {
|
13797
13800
|
const sd = new SessionDescription({
|
13798
13801
|
sdp: rsd.sdp,
|
13799
|
-
type: rsd.type
|
13802
|
+
type: rsd.type,
|
13803
|
+
id
|
13800
13804
|
});
|
13801
13805
|
return sd;
|
13802
13806
|
}
|
@@ -14741,6 +14745,7 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
14741
14745
|
super();
|
14742
14746
|
this.log = livekitLogger;
|
14743
14747
|
this.ddExtID = 0;
|
14748
|
+
this.latestOfferId = 0;
|
14744
14749
|
this.pendingCandidates = [];
|
14745
14750
|
this.restartingIce = false;
|
14746
14751
|
this.renegotiate = false;
|
@@ -14831,9 +14836,16 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
14831
14836
|
this.pendingCandidates.push(candidate);
|
14832
14837
|
});
|
14833
14838
|
}
|
14834
|
-
setRemoteDescription(sd) {
|
14839
|
+
setRemoteDescription(sd, offerId) {
|
14835
14840
|
return __awaiter(this, void 0, void 0, function* () {
|
14836
14841
|
var _a;
|
14842
|
+
if (sd.type === 'answer' && this.latestOfferId > 0 && offerId > 0 && offerId !== this.latestOfferId) {
|
14843
|
+
this.log.warn('ignoring answer for old offer', Object.assign(Object.assign({}, this.logContext), {
|
14844
|
+
offerId,
|
14845
|
+
latestOfferId: this.latestOfferId
|
14846
|
+
}));
|
14847
|
+
return false;
|
14848
|
+
}
|
14837
14849
|
let mungedSDP = undefined;
|
14838
14850
|
if (sd.type === 'offer') {
|
14839
14851
|
let {
|
@@ -14907,11 +14919,15 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
14907
14919
|
});
|
14908
14920
|
}
|
14909
14921
|
}
|
14922
|
+
return true;
|
14910
14923
|
});
|
14911
14924
|
}
|
14912
14925
|
createAndSendOffer(options) {
|
14913
14926
|
return __awaiter(this, void 0, void 0, function* () {
|
14914
14927
|
var _a;
|
14928
|
+
// increase the offer id at the start to ensure the offer is always > 0 so that we can use 0 as a default value for legacy behavior
|
14929
|
+
const offerId = this.latestOfferId + 1;
|
14930
|
+
this.latestOfferId = offerId;
|
14915
14931
|
if (this.onOffer === undefined) {
|
14916
14932
|
return;
|
14917
14933
|
}
|
@@ -14986,8 +15002,15 @@ class PCTransport extends eventsExports.EventEmitter {
|
|
14986
15002
|
});
|
14987
15003
|
}
|
14988
15004
|
});
|
15005
|
+
if (this.latestOfferId > offerId) {
|
15006
|
+
this.log.warn('latestOfferId mismatch', Object.assign(Object.assign({}, this.logContext), {
|
15007
|
+
latestOfferId: this.latestOfferId,
|
15008
|
+
offerId
|
15009
|
+
}));
|
15010
|
+
return;
|
15011
|
+
}
|
14989
15012
|
yield this.setMungedSDP(offer, libExports.write(sdpParsed));
|
14990
|
-
this.onOffer(offer);
|
15013
|
+
this.onOffer(offer, this.latestOfferId);
|
14991
15014
|
});
|
14992
15015
|
}
|
14993
15016
|
createAndSetAnswer() {
|
@@ -15382,9 +15405,9 @@ class PCTransportManager {
|
|
15382
15405
|
var _a;
|
15383
15406
|
(_a = this.onTrack) === null || _a === void 0 ? void 0 : _a.call(this, ev);
|
15384
15407
|
};
|
15385
|
-
this.publisher.onOffer = offer => {
|
15408
|
+
this.publisher.onOffer = (offer, offerId) => {
|
15386
15409
|
var _a;
|
15387
|
-
(_a = this.onPublisherOffer) === null || _a === void 0 ? void 0 : _a.call(this, offer);
|
15410
|
+
(_a = this.onPublisherOffer) === null || _a === void 0 ? void 0 : _a.call(this, offer, offerId);
|
15388
15411
|
};
|
15389
15412
|
this.state = PCTransportState.NEW;
|
15390
15413
|
this.connectionLock = new _();
|
@@ -15407,8 +15430,8 @@ class PCTransportManager {
|
|
15407
15430
|
createAndSendPublisherOffer(options) {
|
15408
15431
|
return this.publisher.createAndSendOffer(options);
|
15409
15432
|
}
|
15410
|
-
setPublisherAnswer(sd) {
|
15411
|
-
return this.publisher.setRemoteDescription(sd);
|
15433
|
+
setPublisherAnswer(sd, offerId) {
|
15434
|
+
return this.publisher.setRemoteDescription(sd, offerId);
|
15412
15435
|
}
|
15413
15436
|
removeTrack(sender) {
|
15414
15437
|
return this.publisher.removeTrack(sender);
|
@@ -15454,7 +15477,7 @@ class PCTransportManager {
|
|
15454
15477
|
}
|
15455
15478
|
});
|
15456
15479
|
}
|
15457
|
-
createSubscriberAnswerFromOffer(sd) {
|
15480
|
+
createSubscriberAnswerFromOffer(sd, offerId) {
|
15458
15481
|
return __awaiter(this, void 0, void 0, function* () {
|
15459
15482
|
this.log.debug('received server offer', Object.assign(Object.assign({}, this.logContext), {
|
15460
15483
|
RTCSdpType: sd.type,
|
@@ -15463,7 +15486,10 @@ class PCTransportManager {
|
|
15463
15486
|
}));
|
15464
15487
|
const unlock = yield this.remoteOfferLock.lock();
|
15465
15488
|
try {
|
15466
|
-
yield this.subscriber.setRemoteDescription(sd);
|
15489
|
+
const success = yield this.subscriber.setRemoteDescription(sd, offerId);
|
15490
|
+
if (!success) {
|
15491
|
+
return undefined;
|
15492
|
+
}
|
15467
15493
|
// answer the offer
|
15468
15494
|
const answer = yield this.subscriber.createAndSetAnswer();
|
15469
15495
|
return answer;
|
@@ -17403,44 +17429,13 @@ function setPublishingLayersForSender(sender, senderEncodings, qualities, sender
|
|
17403
17429
|
return;
|
17404
17430
|
}
|
17405
17431
|
let hasChanged = false;
|
17406
|
-
|
17407
|
-
|
17432
|
+
/* disable closable spatial layer as it has video blur / frozen issue with current server / client
|
17433
|
+
1. chrome 113: when switching to up layer with scalability Mode change, it will generate a
|
17434
|
+
low resolution frame and recover very quickly, but noticable
|
17435
|
+
2. livekit sfu: additional pli request cause video frozen for a few frames, also noticable */
|
17436
|
+
const closableSpatial = false;
|
17408
17437
|
/* @ts-ignore */
|
17409
|
-
if (closableSpatial && encodings[0].scalabilityMode) {
|
17410
|
-
// svc dynacast encodings
|
17411
|
-
const encoding = encodings[0];
|
17412
|
-
/* @ts-ignore */
|
17413
|
-
const mode = new ScalabilityMode(encoding.scalabilityMode);
|
17414
|
-
let maxQuality = VideoQuality$1.OFF;
|
17415
|
-
qualities.forEach(q => {
|
17416
|
-
if (q.enabled && (maxQuality === VideoQuality$1.OFF || q.quality > maxQuality)) {
|
17417
|
-
maxQuality = q.quality;
|
17418
|
-
}
|
17419
|
-
});
|
17420
|
-
if (maxQuality === VideoQuality$1.OFF) {
|
17421
|
-
if (encoding.active) {
|
17422
|
-
encoding.active = false;
|
17423
|
-
hasChanged = true;
|
17424
|
-
}
|
17425
|
-
} else if (!encoding.active || mode.spatial !== maxQuality + 1) {
|
17426
|
-
hasChanged = true;
|
17427
|
-
encoding.active = true;
|
17428
|
-
/* @ts-ignore */
|
17429
|
-
const originalMode = new ScalabilityMode(senderEncodings[0].scalabilityMode);
|
17430
|
-
mode.spatial = maxQuality + 1;
|
17431
|
-
mode.suffix = originalMode.suffix;
|
17432
|
-
if (mode.spatial === 1) {
|
17433
|
-
// no suffix for L1Tx
|
17434
|
-
mode.suffix = undefined;
|
17435
|
-
}
|
17436
|
-
/* @ts-ignore */
|
17437
|
-
encoding.scalabilityMode = mode.toString();
|
17438
|
-
encoding.scaleResolutionDownBy = Math.pow(2, 2 - maxQuality);
|
17439
|
-
if (senderEncodings[0].maxBitrate) {
|
17440
|
-
encoding.maxBitrate = senderEncodings[0].maxBitrate / (encoding.scaleResolutionDownBy * encoding.scaleResolutionDownBy);
|
17441
|
-
}
|
17442
|
-
}
|
17443
|
-
} else {
|
17438
|
+
if (closableSpatial && encodings[0].scalabilityMode) ; else {
|
17444
17439
|
if (isSVC) {
|
17445
17440
|
const hasEnabledEncoding = qualities.some(q => q.enabled);
|
17446
17441
|
if (hasEnabledEncoding) {
|
@@ -17577,6 +17572,10 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
17577
17572
|
this.rtcConfig = {};
|
17578
17573
|
this.peerConnectionTimeout = roomConnectOptionDefaults.peerConnectionTimeout;
|
17579
17574
|
this.fullReconnectOnNext = false;
|
17575
|
+
/**
|
17576
|
+
* @internal
|
17577
|
+
*/
|
17578
|
+
this.latestRemoteOfferId = 0;
|
17580
17579
|
this.subscriberPrimary = false;
|
17581
17580
|
this.pcState = PCState.New;
|
17582
17581
|
this._isClosed = true;
|
@@ -17953,8 +17952,8 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
17953
17952
|
this.pcManager.onIceCandidate = (candidate, target) => {
|
17954
17953
|
this.client.sendIceCandidate(candidate, target);
|
17955
17954
|
};
|
17956
|
-
this.pcManager.onPublisherOffer = offer => {
|
17957
|
-
this.client.sendOffer(offer);
|
17955
|
+
this.pcManager.onPublisherOffer = (offer, offerId) => {
|
17956
|
+
this.client.sendOffer(offer, offerId);
|
17958
17957
|
};
|
17959
17958
|
this.pcManager.onDataChannel = this.handleDataChannel;
|
17960
17959
|
this.pcManager.onStateChange = (connectionState, publisherState, subscriberState) => __awaiter(this, void 0, void 0, function* () {
|
@@ -17993,14 +17992,14 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
17993
17992
|
}
|
17994
17993
|
setupSignalClientCallbacks() {
|
17995
17994
|
// configure signaling client
|
17996
|
-
this.client.onAnswer = sd => __awaiter(this, void 0, void 0, function* () {
|
17995
|
+
this.client.onAnswer = (sd, offerId) => __awaiter(this, void 0, void 0, function* () {
|
17997
17996
|
if (!this.pcManager) {
|
17998
17997
|
return;
|
17999
17998
|
}
|
18000
17999
|
this.log.debug('received server answer', Object.assign(Object.assign({}, this.logContext), {
|
18001
18000
|
RTCSdpType: sd.type
|
18002
18001
|
}));
|
18003
|
-
yield this.pcManager.setPublisherAnswer(sd);
|
18002
|
+
yield this.pcManager.setPublisherAnswer(sd, offerId);
|
18004
18003
|
});
|
18005
18004
|
// add candidate on trickle
|
18006
18005
|
this.client.onTrickle = (candidate, target) => {
|
@@ -18014,12 +18013,15 @@ class RTCEngine extends eventsExports.EventEmitter {
|
|
18014
18013
|
this.pcManager.addIceCandidate(candidate, target);
|
18015
18014
|
};
|
18016
18015
|
// when server creates an offer for the client
|
18017
|
-
this.client.onOffer = sd => __awaiter(this, void 0, void 0, function* () {
|
18016
|
+
this.client.onOffer = (sd, offerId) => __awaiter(this, void 0, void 0, function* () {
|
18017
|
+
this.latestRemoteOfferId = offerId;
|
18018
18018
|
if (!this.pcManager) {
|
18019
18019
|
return;
|
18020
18020
|
}
|
18021
|
-
const answer = yield this.pcManager.createSubscriberAnswerFromOffer(sd);
|
18022
|
-
|
18021
|
+
const answer = yield this.pcManager.createSubscriberAnswerFromOffer(sd, offerId);
|
18022
|
+
if (answer) {
|
18023
|
+
this.client.sendAnswer(answer, offerId);
|
18024
|
+
}
|
18023
18025
|
});
|
18024
18026
|
this.client.onLocalTrackPublished = res => {
|
18025
18027
|
var _a;
|
@@ -24061,15 +24063,15 @@ class Room extends eventsExports.EventEmitter {
|
|
24061
24063
|
var _this3 = this;
|
24062
24064
|
let exact = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
24063
24065
|
return function* () {
|
24064
|
-
var _a, _b, _c, _d, _e, _f
|
24065
|
-
var
|
24066
|
+
var _a, _b, _c, _d, _e, _f;
|
24067
|
+
var _g;
|
24066
24068
|
let success = true;
|
24067
|
-
let
|
24069
|
+
let shouldTriggerImmediateDeviceChange = false;
|
24068
24070
|
const deviceConstraint = exact ? {
|
24069
24071
|
exact: deviceId
|
24070
24072
|
} : deviceId;
|
24071
24073
|
if (kind === 'audioinput') {
|
24072
|
-
|
24074
|
+
shouldTriggerImmediateDeviceChange = _this3.localParticipant.audioTrackPublications.size === 0;
|
24073
24075
|
const prevDeviceId = (_a = _this3.getActiveDevice(kind)) !== null && _a !== void 0 ? _a : _this3.options.audioCaptureDefaults.deviceId;
|
24074
24076
|
_this3.options.audioCaptureDefaults.deviceId = deviceConstraint;
|
24075
24077
|
const tracks = Array.from(_this3.localParticipant.audioTrackPublications.values()).filter(track => track.source === Track.Source.Microphone);
|
@@ -24082,8 +24084,13 @@ class Room extends eventsExports.EventEmitter {
|
|
24082
24084
|
_this3.options.audioCaptureDefaults.deviceId = prevDeviceId;
|
24083
24085
|
throw e;
|
24084
24086
|
}
|
24087
|
+
const isMuted = tracks.some(t => {
|
24088
|
+
var _a, _b;
|
24089
|
+
return (_b = (_a = t.track) === null || _a === void 0 ? void 0 : _a.isMuted) !== null && _b !== void 0 ? _b : false;
|
24090
|
+
});
|
24091
|
+
if (success && isMuted) shouldTriggerImmediateDeviceChange = true;
|
24085
24092
|
} else if (kind === 'videoinput') {
|
24086
|
-
|
24093
|
+
shouldTriggerImmediateDeviceChange = _this3.localParticipant.videoTrackPublications.size === 0;
|
24087
24094
|
const prevDeviceId = (_b = _this3.getActiveDevice(kind)) !== null && _b !== void 0 ? _b : _this3.options.videoCaptureDefaults.deviceId;
|
24088
24095
|
_this3.options.videoCaptureDefaults.deviceId = deviceConstraint;
|
24089
24096
|
const tracks = Array.from(_this3.localParticipant.videoTrackPublications.values()).filter(track => track.source === Track.Source.Camera);
|
@@ -24097,6 +24104,7 @@ class Room extends eventsExports.EventEmitter {
|
|
24097
24104
|
throw e;
|
24098
24105
|
}
|
24099
24106
|
} else if (kind === 'audiooutput') {
|
24107
|
+
shouldTriggerImmediateDeviceChange = true;
|
24100
24108
|
if (!supportsSetSinkId() && !_this3.options.webAudioMix || _this3.options.webAudioMix && _this3.audioContext && !('setSinkId' in _this3.audioContext)) {
|
24101
24109
|
throw new Error('cannot switch audio output, setSinkId not supported');
|
24102
24110
|
}
|
@@ -24104,7 +24112,7 @@ class Room extends eventsExports.EventEmitter {
|
|
24104
24112
|
// setting `default` for web audio output doesn't work, so we need to normalize the id before
|
24105
24113
|
deviceId = (_c = yield DeviceManager.getInstance().normalizeDeviceId('audiooutput', deviceId)) !== null && _c !== void 0 ? _c : '';
|
24106
24114
|
}
|
24107
|
-
(_d = (
|
24115
|
+
(_d = (_g = _this3.options).audioOutput) !== null && _d !== void 0 ? _d : _g.audioOutput = {};
|
24108
24116
|
const prevDeviceId = (_e = _this3.getActiveDevice(kind)) !== null && _e !== void 0 ? _e : _this3.options.audioOutput.deviceId;
|
24109
24117
|
_this3.options.audioOutput.deviceId = deviceId;
|
24110
24118
|
try {
|
@@ -24122,9 +24130,8 @@ class Room extends eventsExports.EventEmitter {
|
|
24122
24130
|
throw e;
|
24123
24131
|
}
|
24124
24132
|
}
|
24125
|
-
if (
|
24126
|
-
|
24127
|
-
_this3.localParticipant.activeDeviceMap.set(kind, kind === 'audiooutput' && ((_g = _this3.options.audioOutput) === null || _g === void 0 ? void 0 : _g.deviceId) || deviceId);
|
24133
|
+
if (shouldTriggerImmediateDeviceChange) {
|
24134
|
+
_this3.localParticipant.activeDeviceMap.set(kind, deviceId);
|
24128
24135
|
_this3.emit(RoomEvent.ActiveDeviceChanged, kind, deviceId);
|
24129
24136
|
}
|
24130
24137
|
return success;
|
@@ -25249,6 +25256,7 @@ class PublishVideoCheck extends Checker {
|
|
25249
25256
|
};
|
25250
25257
|
video.play();
|
25251
25258
|
});
|
25259
|
+
stream.getTracks().forEach(t => t.stop());
|
25252
25260
|
video.remove();
|
25253
25261
|
});
|
25254
25262
|
}
|