@stream-io/video-client 1.55.2 → 1.57.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/CHANGELOG.md +22 -0
- package/dist/index.browser.es.js +216 -119
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +216 -119
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +216 -119
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/BlockedAudioTracker.d.ts +16 -1
- package/dist/src/helpers/MediaPlaybackWatchdog.d.ts +4 -0
- package/dist/src/reporting/ClientEventReporter.d.ts +1 -0
- package/dist/src/rtc/Publisher.d.ts +1 -0
- package/dist/src/rtc/types.d.ts +9 -2
- package/dist/src/store/rxUtils.d.ts +5 -0
- package/package.json +11 -11
- package/src/Call.ts +1 -0
- package/src/devices/__tests__/MicrophoneManagerRN.test.ts +1 -2
- package/src/devices/devicePersistence.ts +1 -2
- package/src/helpers/BlockedAudioTracker.ts +71 -13
- package/src/helpers/DynascaleManager.ts +14 -3
- package/src/helpers/MediaPlaybackWatchdog.ts +35 -16
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +54 -6
- package/src/helpers/__tests__/DynascaleManager.test.ts +45 -0
- package/src/helpers/__tests__/MediaPlaybackWatchdog.test.ts +137 -8
- package/src/helpers/__tests__/ViewportTracker.test.ts +1 -2
- package/src/reporting/ClientEventReporter.ts +39 -39
- package/src/reporting/__tests__/ClientEventReporter.test.ts +159 -44
- package/src/rtc/BasePeerConnection.ts +1 -0
- package/src/rtc/Publisher.ts +78 -58
- package/src/rtc/__tests__/Publisher.test.ts +297 -0
- package/src/rtc/types.ts +14 -13
- package/src/stats/rtc/types.ts +8 -2
- package/src/store/CallState.ts +6 -19
- package/src/store/rxUtils.ts +15 -0
- package/src/types.ts +1 -4
package/dist/index.cjs.js
CHANGED
|
@@ -4946,7 +4946,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4946
4946
|
}
|
|
4947
4947
|
};
|
|
4948
4948
|
|
|
4949
|
-
const version = "1.
|
|
4949
|
+
const version = "1.57.0";
|
|
4950
4950
|
const [major, minor, patch] = version.split('.');
|
|
4951
4951
|
let sdkInfo = {
|
|
4952
4952
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5275,6 +5275,23 @@ function wrapWithCancellation(tag, cb) {
|
|
|
5275
5275
|
return { cb: wrapped, onContinued };
|
|
5276
5276
|
}
|
|
5277
5277
|
|
|
5278
|
+
/**
|
|
5279
|
+
* Performs shallow comparison of two arrays.
|
|
5280
|
+
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
5281
|
+
*/
|
|
5282
|
+
const isShallowArrayEqual = (a, b) => {
|
|
5283
|
+
if (a.length !== b.length)
|
|
5284
|
+
return false;
|
|
5285
|
+
for (const item of a) {
|
|
5286
|
+
if (!b.includes(item))
|
|
5287
|
+
return false;
|
|
5288
|
+
}
|
|
5289
|
+
for (const item of b) {
|
|
5290
|
+
if (!a.includes(item))
|
|
5291
|
+
return false;
|
|
5292
|
+
}
|
|
5293
|
+
return true;
|
|
5294
|
+
};
|
|
5278
5295
|
/**
|
|
5279
5296
|
* Checks if the provided update is a function patch.
|
|
5280
5297
|
*
|
|
@@ -5387,6 +5404,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
|
|
|
5387
5404
|
createSafeAsyncSubscription: createSafeAsyncSubscription,
|
|
5388
5405
|
createSubscription: createSubscription,
|
|
5389
5406
|
getCurrentValue: getCurrentValue,
|
|
5407
|
+
isShallowArrayEqual: isShallowArrayEqual,
|
|
5390
5408
|
setCurrentValue: setCurrentValue,
|
|
5391
5409
|
setCurrentValueAsync: setCurrentValueAsync,
|
|
5392
5410
|
updateValue: updateValue
|
|
@@ -6127,23 +6145,6 @@ class CallState {
|
|
|
6127
6145
|
this.endedBy$ = this.endedBySubject.asObservable();
|
|
6128
6146
|
this.thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
6129
6147
|
this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
6130
|
-
/**
|
|
6131
|
-
* Performs shallow comparison of two arrays.
|
|
6132
|
-
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
6133
|
-
*/
|
|
6134
|
-
const isShallowEqual = (a, b) => {
|
|
6135
|
-
if (a.length !== b.length)
|
|
6136
|
-
return false;
|
|
6137
|
-
for (const item of a) {
|
|
6138
|
-
if (!b.includes(item))
|
|
6139
|
-
return false;
|
|
6140
|
-
}
|
|
6141
|
-
for (const item of b) {
|
|
6142
|
-
if (!a.includes(item))
|
|
6143
|
-
return false;
|
|
6144
|
-
}
|
|
6145
|
-
return true;
|
|
6146
|
-
};
|
|
6147
6148
|
/**
|
|
6148
6149
|
* Creates an Observable from the given subject by piping to the
|
|
6149
6150
|
* `distinctUntilChanged()` operator.
|
|
@@ -6151,7 +6152,7 @@ class CallState {
|
|
|
6151
6152
|
const duc = (subject, comparator) => subject.pipe(rxjs.distinctUntilChanged(comparator));
|
|
6152
6153
|
// primitive values should only emit once the value they hold changes
|
|
6153
6154
|
this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
|
|
6154
|
-
this.blockedUserIds$ = duc(this.blockedUserIdsSubject,
|
|
6155
|
+
this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
|
|
6155
6156
|
this.backstage$ = duc(this.backstageSubject);
|
|
6156
6157
|
this.callingState$ = duc(this.callingStateSubject);
|
|
6157
6158
|
this.ownCapabilities$ = rxjs.combineLatest([
|
|
@@ -6180,7 +6181,7 @@ class CallState {
|
|
|
6180
6181
|
}
|
|
6181
6182
|
}
|
|
6182
6183
|
return nextCapabilities;
|
|
6183
|
-
}), rxjs.distinctUntilChanged(
|
|
6184
|
+
}), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
6184
6185
|
this.participantCount$ = duc(this.participantCountSubject);
|
|
6185
6186
|
this.recording$ = duc(this.recordingSubject);
|
|
6186
6187
|
this.individualRecording$ = duc(this.individualRecordingSubject);
|
|
@@ -8491,6 +8492,7 @@ class BasePeerConnection {
|
|
|
8491
8492
|
try {
|
|
8492
8493
|
this.onPeerConnectionStateChange?.({
|
|
8493
8494
|
peerType: this.peerType,
|
|
8495
|
+
iceConnectionState: this.pc.iceConnectionState,
|
|
8494
8496
|
...event,
|
|
8495
8497
|
});
|
|
8496
8498
|
}
|
|
@@ -8945,6 +8947,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8945
8947
|
clonedTracks = new Set();
|
|
8946
8948
|
publishOptions;
|
|
8947
8949
|
selfSubEnabled;
|
|
8950
|
+
transceiverLockKey = `pub.tx.${this.lock}`;
|
|
8948
8951
|
/**
|
|
8949
8952
|
* Constructs a new `Publisher` instance.
|
|
8950
8953
|
*/
|
|
@@ -9000,32 +9003,34 @@ class Publisher extends BasePeerConnection {
|
|
|
9000
9003
|
* @param options the publish options to use.
|
|
9001
9004
|
*/
|
|
9002
9005
|
publish = async (track, trackType, options = {}) => {
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
if (publishOption.trackType !== trackType)
|
|
9008
|
-
continue;
|
|
9009
|
-
// create a clone of the track as otherwise the same trackId will
|
|
9010
|
-
// appear in the SDP in multiple transceivers
|
|
9011
|
-
const trackToPublish = this.cloneTrack(track);
|
|
9012
|
-
const bundle = this.transceiverCache.get(publishOption);
|
|
9013
|
-
if (!bundle) {
|
|
9014
|
-
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
9006
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
9007
|
+
const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
|
|
9008
|
+
if (!publishOptions.length) {
|
|
9009
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
9015
9010
|
}
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
|
|
9019
|
-
|
|
9020
|
-
|
|
9011
|
+
for (const publishOption of publishOptions) {
|
|
9012
|
+
// create a clone of the track as otherwise the same trackId will
|
|
9013
|
+
// appear in the SDP in multiple transceivers
|
|
9014
|
+
const trackToPublish = this.cloneTrack(track);
|
|
9015
|
+
const bundle = this.transceiverCache.get(publishOption);
|
|
9016
|
+
if (!bundle) {
|
|
9017
|
+
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
9018
|
+
}
|
|
9019
|
+
else {
|
|
9020
|
+
const previousTrack = bundle.transceiver.sender.track;
|
|
9021
|
+
await this.updateTransceiver(bundle, trackToPublish, options);
|
|
9022
|
+
if (!isReactNative()) {
|
|
9023
|
+
this.stopTrack(previousTrack);
|
|
9024
|
+
}
|
|
9021
9025
|
}
|
|
9022
9026
|
}
|
|
9023
|
-
}
|
|
9027
|
+
});
|
|
9024
9028
|
};
|
|
9025
9029
|
/**
|
|
9026
9030
|
* Adds a new transceiver carrying the given track to the peer connection.
|
|
9027
9031
|
*/
|
|
9028
9032
|
addTransceiver = async (track, publishOption, options) => {
|
|
9033
|
+
const trackType = publishOption.trackType;
|
|
9029
9034
|
const encodings = isAudioTrackType(publishOption.trackType)
|
|
9030
9035
|
? computeAudioLayers(publishOption, options)
|
|
9031
9036
|
: computeVideoLayers(track, publishOption);
|
|
@@ -9036,15 +9041,17 @@ class Publisher extends BasePeerConnection {
|
|
|
9036
9041
|
direction: 'sendonly',
|
|
9037
9042
|
sendEncodings,
|
|
9038
9043
|
});
|
|
9044
|
+
// claim the cache entry in the same task that created the transceiver:
|
|
9045
|
+
// an await in between would let a concurrent caller add a second one
|
|
9046
|
+
// for the same publish option
|
|
9047
|
+
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9048
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
9049
|
+
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9039
9050
|
const params = transceiver.sender.getParameters();
|
|
9040
9051
|
params.degradationPreference =
|
|
9041
9052
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9042
9053
|
'maintain-framerate';
|
|
9043
9054
|
await transceiver.sender.setParameters(params);
|
|
9044
|
-
const trackType = publishOption.trackType;
|
|
9045
|
-
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9046
|
-
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9047
|
-
this.trackIdToTrackType.set(track.id, trackType);
|
|
9048
9055
|
await this.negotiate();
|
|
9049
9056
|
};
|
|
9050
9057
|
/**
|
|
@@ -9101,33 +9108,35 @@ class Publisher extends BasePeerConnection {
|
|
|
9101
9108
|
* Synchronizes the current Publisher state with the provided publish options.
|
|
9102
9109
|
*/
|
|
9103
9110
|
syncPublishOptions = async () => {
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
const
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
i.
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
const
|
|
9123
|
-
|
|
9124
|
-
option.
|
|
9125
|
-
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9111
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
9112
|
+
// enable publishing with new options -> [av1, vp9]
|
|
9113
|
+
for (const publishOption of this.publishOptions) {
|
|
9114
|
+
const { trackType } = publishOption;
|
|
9115
|
+
if (!this.isPublishing(trackType))
|
|
9116
|
+
continue;
|
|
9117
|
+
if (this.transceiverCache.has(publishOption))
|
|
9118
|
+
continue;
|
|
9119
|
+
const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
|
|
9120
|
+
i.publishOption.trackType === trackType);
|
|
9121
|
+
if (!item)
|
|
9122
|
+
continue;
|
|
9123
|
+
// take the track from the existing transceiver for the same track type,
|
|
9124
|
+
// clone it and publish it with the new publish options
|
|
9125
|
+
const track = this.cloneTrack(item.transceiver.sender.track);
|
|
9126
|
+
await this.addTransceiver(track, publishOption, item.options);
|
|
9127
|
+
}
|
|
9128
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
9129
|
+
for (const item of this.transceiverCache.items()) {
|
|
9130
|
+
const { publishOption, transceiver } = item;
|
|
9131
|
+
const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
|
|
9132
|
+
option.trackType === publishOption.trackType);
|
|
9133
|
+
if (hasPublishOption)
|
|
9134
|
+
continue;
|
|
9135
|
+
// it is safe to stop the track here, it is a clone
|
|
9136
|
+
this.stopTrack(transceiver.sender.track);
|
|
9137
|
+
await this.updateTransceiver(item, null);
|
|
9138
|
+
}
|
|
9139
|
+
});
|
|
9131
9140
|
};
|
|
9132
9141
|
/**
|
|
9133
9142
|
* Returns true if the given track type is currently being published to the SFU.
|
|
@@ -9295,12 +9304,23 @@ class Publisher extends BasePeerConnection {
|
|
|
9295
9304
|
negotiate = async (options) => {
|
|
9296
9305
|
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
9297
9306
|
const offer = await this.pc.createOffer(options);
|
|
9298
|
-
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9299
|
-
if (!tracks.length)
|
|
9300
|
-
throw new Error(`Can't negotiate without any tracks`);
|
|
9301
9307
|
try {
|
|
9302
9308
|
this.isIceRestarting = options?.iceRestart ?? false;
|
|
9303
9309
|
await this.pc.setLocalDescription(offer);
|
|
9310
|
+
// the announced tracks have to be read after `setLocalDescription`.
|
|
9311
|
+
// `transceiver.mid` is null until the offer is applied, and reading the
|
|
9312
|
+
// tracks earlier makes `extractMid` guess the mid from the offer SDP.
|
|
9313
|
+
// That guess goes stale after a rolled-back negotiation, and a mid that
|
|
9314
|
+
// doesn't match the m-section carrying the track stops the SFU from
|
|
9315
|
+
// correlating the two: it then answers with every offered codec instead
|
|
9316
|
+
// of the announced one, and we publish the first codec of that list.
|
|
9317
|
+
// Per https://www.w3.org/TR/webrtc/#set-description, applying a local
|
|
9318
|
+
// offer associates the transceiver with its m-section and sets
|
|
9319
|
+
// `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
|
|
9320
|
+
// that wasn't associated before and resets both slots to null.
|
|
9321
|
+
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9322
|
+
if (!tracks.length)
|
|
9323
|
+
throw new Error(`Can't negotiate without any tracks`);
|
|
9304
9324
|
const { sdp: baseSdp = '' } = offer;
|
|
9305
9325
|
const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
|
|
9306
9326
|
let sdp = dangerouslyForceCodec
|
|
@@ -10925,34 +10945,67 @@ class AudioBindingsWatchdog {
|
|
|
10925
10945
|
class BlockedAudioTracker {
|
|
10926
10946
|
logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
|
|
10927
10947
|
tracer;
|
|
10928
|
-
blockedElementsSubject = new rxjs.BehaviorSubject(
|
|
10948
|
+
blockedElementsSubject = new rxjs.BehaviorSubject([]);
|
|
10929
10949
|
/**
|
|
10930
10950
|
* Whether the browser's autoplay policy is blocking audio playback.
|
|
10931
10951
|
* Will be `true` when at least one audio element is currently blocked.
|
|
10932
10952
|
* Use {@link resumeAudio} within a user gesture to unblock.
|
|
10933
10953
|
*/
|
|
10934
|
-
autoplayBlocked$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => elements.
|
|
10954
|
+
autoplayBlocked$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => elements.length > 0), rxjs.distinctUntilChanged());
|
|
10955
|
+
/**
|
|
10956
|
+
* The list of participant `sessionId`s whose audio element is currently
|
|
10957
|
+
* blocked by the browser's autoplay policy. Only some participants may be
|
|
10958
|
+
* blocked (e.g. one joined while a gesture was active and a later one was
|
|
10959
|
+
* not), so use this to render a per-participant affordance rather than a
|
|
10960
|
+
* call-wide one. Call {@link resumeAudio} within a user gesture to unblock
|
|
10961
|
+
* all of them.
|
|
10962
|
+
*
|
|
10963
|
+
* Session ids are registered together with the audio element.
|
|
10964
|
+
*/
|
|
10965
|
+
blockedSessionIds$ = this.blockedElementsSubject.pipe(rxjs.map((elements) => {
|
|
10966
|
+
const ids = [];
|
|
10967
|
+
elements.forEach(({ sessionId }) => {
|
|
10968
|
+
if (sessionId && !ids.includes(sessionId))
|
|
10969
|
+
ids.push(sessionId);
|
|
10970
|
+
});
|
|
10971
|
+
return ids;
|
|
10972
|
+
}), rxjs.distinctUntilChanged(isShallowArrayEqual), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
10935
10973
|
constructor(tracer) {
|
|
10936
10974
|
this.tracer = tracer;
|
|
10937
10975
|
}
|
|
10938
10976
|
/**
|
|
10939
10977
|
* Registers an audio element as blocked by the browser's autoplay policy.
|
|
10940
10978
|
*/
|
|
10941
|
-
markBlocked = (audioElement, blocked) => {
|
|
10979
|
+
markBlocked = (audioElement, blocked, sessionId) => {
|
|
10942
10980
|
setCurrentValue(this.blockedElementsSubject, (elements) => {
|
|
10943
|
-
if (blocked)
|
|
10944
|
-
elements.
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
|
|
10981
|
+
if (!blocked) {
|
|
10982
|
+
return elements.filter(({ element }) => element !== audioElement);
|
|
10983
|
+
}
|
|
10984
|
+
const existing = elements.find(({ element }) => element === audioElement);
|
|
10985
|
+
if (existing) {
|
|
10986
|
+
return elements.map((entry) => entry.element === audioElement
|
|
10987
|
+
? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
|
|
10988
|
+
: entry);
|
|
10989
|
+
}
|
|
10990
|
+
return [...elements, { element: audioElement, sessionId }];
|
|
10948
10991
|
});
|
|
10949
10992
|
};
|
|
10993
|
+
/**
|
|
10994
|
+
* Clears all tracked elements. The tracker stays usable afterwards.
|
|
10995
|
+
*/
|
|
10996
|
+
reset = () => {
|
|
10997
|
+
if (this.blockedElementsSubject.getValue().length > 0) {
|
|
10998
|
+
this.blockedElementsSubject.next([]);
|
|
10999
|
+
}
|
|
11000
|
+
};
|
|
10950
11001
|
/**
|
|
10951
11002
|
* Returns whether the given audio element is currently flagged as blocked
|
|
10952
11003
|
* by the browser's autoplay policy.
|
|
10953
11004
|
*/
|
|
10954
11005
|
isBlocked = (audioElement) => {
|
|
10955
|
-
return this.blockedElementsSubject
|
|
11006
|
+
return this.blockedElementsSubject
|
|
11007
|
+
.getValue()
|
|
11008
|
+
.some(({ element }) => element === audioElement);
|
|
10956
11009
|
};
|
|
10957
11010
|
/**
|
|
10958
11011
|
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
@@ -10961,17 +11014,18 @@ class BlockedAudioTracker {
|
|
|
10961
11014
|
resumeAudio = async () => {
|
|
10962
11015
|
this.tracer.trace('resumeAudio', null);
|
|
10963
11016
|
await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
|
|
10964
|
-
|
|
11017
|
+
let next = elements;
|
|
11018
|
+
await Promise.all(elements.map(async ({ element }) => {
|
|
10965
11019
|
try {
|
|
10966
11020
|
if (element.srcObject)
|
|
10967
11021
|
await timeboxed([element.play()], 2000);
|
|
10968
|
-
|
|
11022
|
+
next = next.filter((entry) => entry.element !== element);
|
|
10969
11023
|
}
|
|
10970
11024
|
catch (err) {
|
|
10971
11025
|
this.logger.warn(`Can't resume audio for element`, element, err);
|
|
10972
11026
|
}
|
|
10973
11027
|
}));
|
|
10974
|
-
return
|
|
11028
|
+
return next;
|
|
10975
11029
|
});
|
|
10976
11030
|
};
|
|
10977
11031
|
}
|
|
@@ -11162,8 +11216,10 @@ class MediaPlaybackWatchdog {
|
|
|
11162
11216
|
tracer;
|
|
11163
11217
|
controller = new AbortController();
|
|
11164
11218
|
pendingTimer;
|
|
11219
|
+
settleTimer;
|
|
11165
11220
|
attempt = 0;
|
|
11166
11221
|
disposed = false;
|
|
11222
|
+
stopped = false;
|
|
11167
11223
|
constructor(opts) {
|
|
11168
11224
|
this.element = opts.element;
|
|
11169
11225
|
this.kind = opts.kind;
|
|
@@ -11184,33 +11240,49 @@ class MediaPlaybackWatchdog {
|
|
|
11184
11240
|
return;
|
|
11185
11241
|
this.disposed = true;
|
|
11186
11242
|
this.controller.abort();
|
|
11243
|
+
this.clearTimers();
|
|
11244
|
+
};
|
|
11245
|
+
clearTimers = () => {
|
|
11187
11246
|
if (this.pendingTimer)
|
|
11188
11247
|
clearTimeout(this.pendingTimer);
|
|
11189
11248
|
this.pendingTimer = undefined;
|
|
11249
|
+
if (this.settleTimer)
|
|
11250
|
+
clearTimeout(this.settleTimer);
|
|
11251
|
+
this.settleTimer = undefined;
|
|
11190
11252
|
};
|
|
11191
11253
|
onPlaying = () => {
|
|
11192
|
-
if (this.attempt > 0) {
|
|
11193
|
-
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11194
|
-
kind: this.kind,
|
|
11195
|
-
attempts: this.attempt,
|
|
11196
|
-
});
|
|
11197
|
-
}
|
|
11198
|
-
this.attempt = 0;
|
|
11199
11254
|
if (this.pendingTimer)
|
|
11200
11255
|
clearTimeout(this.pendingTimer);
|
|
11201
11256
|
this.pendingTimer = undefined;
|
|
11257
|
+
if (this.attempt === 0 || this.settleTimer)
|
|
11258
|
+
return;
|
|
11259
|
+
this.settleTimer ??= setTimeout(this.onSettled, 1000);
|
|
11260
|
+
};
|
|
11261
|
+
onSettled = () => {
|
|
11262
|
+
this.settleTimer = undefined;
|
|
11263
|
+
if (this.element.paused)
|
|
11264
|
+
return;
|
|
11265
|
+
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11266
|
+
kind: this.kind,
|
|
11267
|
+
attempts: this.attempt,
|
|
11268
|
+
});
|
|
11269
|
+
this.attempt = 0;
|
|
11270
|
+
this.stopped = false;
|
|
11202
11271
|
};
|
|
11203
11272
|
onPauseOrSuspend = (event) => {
|
|
11204
|
-
if (this.disposed)
|
|
11273
|
+
if (this.disposed || this.stopped)
|
|
11205
11274
|
return;
|
|
11206
11275
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11207
11276
|
kind: this.kind,
|
|
11208
11277
|
reason: event.type,
|
|
11209
11278
|
});
|
|
11279
|
+
if (this.settleTimer)
|
|
11280
|
+
clearTimeout(this.settleTimer);
|
|
11281
|
+
this.settleTimer = undefined;
|
|
11210
11282
|
this.scheduleRecovery();
|
|
11211
11283
|
};
|
|
11212
11284
|
scheduleRecovery = () => {
|
|
11213
|
-
if (this.disposed || this.pendingTimer)
|
|
11285
|
+
if (this.disposed || this.stopped || this.pendingTimer)
|
|
11214
11286
|
return;
|
|
11215
11287
|
const skipReason = this.computeSkipReason();
|
|
11216
11288
|
if (skipReason) {
|
|
@@ -11220,6 +11292,15 @@ class MediaPlaybackWatchdog {
|
|
|
11220
11292
|
});
|
|
11221
11293
|
return;
|
|
11222
11294
|
}
|
|
11295
|
+
if (this.attempt >= 10) {
|
|
11296
|
+
this.stopped = true;
|
|
11297
|
+
this.clearTimers();
|
|
11298
|
+
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11299
|
+
kind: this.kind,
|
|
11300
|
+
attempts: this.attempt,
|
|
11301
|
+
});
|
|
11302
|
+
return;
|
|
11303
|
+
}
|
|
11223
11304
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
11224
11305
|
this.pendingTimer = setTimeout(this.attemptPlay, delay);
|
|
11225
11306
|
};
|
|
@@ -11255,13 +11336,6 @@ class MediaPlaybackWatchdog {
|
|
|
11255
11336
|
if (this.disposed)
|
|
11256
11337
|
return;
|
|
11257
11338
|
this.logger.warn(`Failed to recover ${this.kind} playback`, err);
|
|
11258
|
-
if (this.attempt >= 10) {
|
|
11259
|
-
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11260
|
-
kind: this.kind,
|
|
11261
|
-
attempts: this.attempt,
|
|
11262
|
-
});
|
|
11263
|
-
return;
|
|
11264
|
-
}
|
|
11265
11339
|
this.scheduleRecovery();
|
|
11266
11340
|
}
|
|
11267
11341
|
};
|
|
@@ -11503,6 +11577,12 @@ class DynascaleManager {
|
|
|
11503
11577
|
let sourceNode = undefined;
|
|
11504
11578
|
let gainNode = undefined;
|
|
11505
11579
|
let audioWatchdog = undefined;
|
|
11580
|
+
const clearBlockedAudio = () => {
|
|
11581
|
+
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
11582
|
+
return;
|
|
11583
|
+
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
11584
|
+
};
|
|
11585
|
+
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
11506
11586
|
const isAudioTrack = trackType === 'audioTrack';
|
|
11507
11587
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
11508
11588
|
const updateMediaStreamSubscription = participant$
|
|
@@ -11516,7 +11596,7 @@ class DynascaleManager {
|
|
|
11516
11596
|
audioWatchdog?.dispose();
|
|
11517
11597
|
audioWatchdog = undefined;
|
|
11518
11598
|
if (!source) {
|
|
11519
|
-
|
|
11599
|
+
clearBlockedAudio();
|
|
11520
11600
|
return;
|
|
11521
11601
|
}
|
|
11522
11602
|
// Safari has a special quirk that prevents playing audio until the user
|
|
@@ -11544,7 +11624,7 @@ class DynascaleManager {
|
|
|
11544
11624
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
11545
11625
|
if (e.name === 'NotAllowedError') {
|
|
11546
11626
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
11547
|
-
this.blockedAudioTracker.markBlocked(audioElement, true);
|
|
11627
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
11548
11628
|
}
|
|
11549
11629
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
11550
11630
|
});
|
|
@@ -11577,7 +11657,8 @@ class DynascaleManager {
|
|
|
11577
11657
|
});
|
|
11578
11658
|
audioElement.autoplay = true;
|
|
11579
11659
|
return () => {
|
|
11580
|
-
|
|
11660
|
+
audioElement.removeEventListener('playing', clearBlockedAudio);
|
|
11661
|
+
clearBlockedAudio();
|
|
11581
11662
|
sinkIdSubscription?.unsubscribe();
|
|
11582
11663
|
volumeSubscription.unsubscribe();
|
|
11583
11664
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -14752,6 +14833,7 @@ class Call {
|
|
|
14752
14833
|
this.screenShare = new ScreenShareManager(this);
|
|
14753
14834
|
this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
|
|
14754
14835
|
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
14836
|
+
this.leaveCallHooks.add(this.blockedAudioTracker.reset);
|
|
14755
14837
|
if (typeof document !== 'undefined') {
|
|
14756
14838
|
this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
|
|
14757
14839
|
this.viewportTracker = new ViewportTracker(this.state);
|
|
@@ -18448,11 +18530,11 @@ class StreamClient {
|
|
|
18448
18530
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18449
18531
|
};
|
|
18450
18532
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18451
|
-
"1.
|
|
18533
|
+
"1.57.0";
|
|
18452
18534
|
getUserAgent = () => {
|
|
18453
18535
|
if (!this.cachedUserAgent) {
|
|
18454
18536
|
const { clientAppIdentifier = {} } = this.options;
|
|
18455
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18537
|
+
const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
|
|
18456
18538
|
this.cachedUserAgent = [
|
|
18457
18539
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18458
18540
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18789,8 +18871,8 @@ class ClientEventReporter {
|
|
|
18789
18871
|
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
18790
18872
|
this.failCoordinator(cid);
|
|
18791
18873
|
this.failWs(cid);
|
|
18792
|
-
this.emitPeerConnectionFailure(cid, 'publish', code, reason
|
|
18793
|
-
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason
|
|
18874
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason);
|
|
18875
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
|
|
18794
18876
|
}
|
|
18795
18877
|
catch (err) {
|
|
18796
18878
|
this.logger.warn('Failed to report abort', err);
|
|
@@ -18802,7 +18884,7 @@ class ClientEventReporter {
|
|
|
18802
18884
|
if (this.wsPairs.get(cid))
|
|
18803
18885
|
this.failWs(cid);
|
|
18804
18886
|
for (const role of ['publish', 'subscribe']) {
|
|
18805
|
-
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt'
|
|
18887
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
|
|
18806
18888
|
}
|
|
18807
18889
|
};
|
|
18808
18890
|
emitJoinInitiated = (cid) => {
|
|
@@ -18964,21 +19046,24 @@ class ClientEventReporter {
|
|
|
18964
19046
|
};
|
|
18965
19047
|
onPeerConnectionStateChange = (cid, event) => {
|
|
18966
19048
|
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18967
|
-
|
|
18968
|
-
|
|
19049
|
+
const pair = this.peerConnectionPairs.get(pcKey(cid, role));
|
|
19050
|
+
if (pair)
|
|
19051
|
+
pair.lastIceState = event.iceConnectionState;
|
|
19052
|
+
if (event.stateType === 'ice') {
|
|
19053
|
+
if (event.state === 'failed') {
|
|
19054
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
|
|
19055
|
+
}
|
|
18969
19056
|
return;
|
|
18970
19057
|
}
|
|
18971
|
-
if (event.
|
|
18972
|
-
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed'
|
|
19058
|
+
if (event.state === 'failed') {
|
|
19059
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
|
|
18973
19060
|
return;
|
|
18974
19061
|
}
|
|
18975
|
-
if (event.stateType !== 'peerConnection')
|
|
18976
|
-
return;
|
|
18977
19062
|
switch (event.state) {
|
|
18978
19063
|
case 'connecting':
|
|
18979
|
-
if (
|
|
19064
|
+
if (pair)
|
|
18980
19065
|
return;
|
|
18981
|
-
this.openPeerConnectionPair(cid, role);
|
|
19066
|
+
this.openPeerConnectionPair(cid, role, event.iceConnectionState);
|
|
18982
19067
|
break;
|
|
18983
19068
|
case 'connected':
|
|
18984
19069
|
this.emitPeerConnectionSuccess(cid, role);
|
|
@@ -18986,7 +19071,7 @@ class ClientEventReporter {
|
|
|
18986
19071
|
break;
|
|
18987
19072
|
}
|
|
18988
19073
|
};
|
|
18989
|
-
openPeerConnectionPair = (cid, role) => {
|
|
19074
|
+
openPeerConnectionPair = (cid, role, iceConnectionState) => {
|
|
18990
19075
|
const key = pcKey(cid, role);
|
|
18991
19076
|
const pair = {
|
|
18992
19077
|
sid: generateUUIDv4(),
|
|
@@ -18996,6 +19081,7 @@ class ClientEventReporter {
|
|
|
18996
19081
|
sfuId: this.getSfuId(cid),
|
|
18997
19082
|
userSessionId: this.getUserSessionId(cid),
|
|
18998
19083
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19084
|
+
lastIceState: iceConnectionState,
|
|
18999
19085
|
};
|
|
19000
19086
|
this.peerConnectionPairs.set(key, pair);
|
|
19001
19087
|
this.sendForCall(cid, {
|
|
@@ -19031,7 +19117,7 @@ class ClientEventReporter {
|
|
|
19031
19117
|
});
|
|
19032
19118
|
this.peerConnectionPairs.delete(key);
|
|
19033
19119
|
};
|
|
19034
|
-
emitPeerConnectionFailure = (cid, role, code, reason
|
|
19120
|
+
emitPeerConnectionFailure = (cid, role, code, reason) => {
|
|
19035
19121
|
const key = pcKey(cid, role);
|
|
19036
19122
|
const pair = this.peerConnectionPairs.get(key);
|
|
19037
19123
|
if (!pair)
|
|
@@ -19049,7 +19135,7 @@ class ClientEventReporter {
|
|
|
19049
19135
|
outcome: 'failure',
|
|
19050
19136
|
retry_count_attempt: 0,
|
|
19051
19137
|
elapsed_time: Date.now() - pair.startedAt,
|
|
19052
|
-
ice_state:
|
|
19138
|
+
ice_state: toReportedIceState(pair.lastIceState),
|
|
19053
19139
|
retry_failure_reason: reason,
|
|
19054
19140
|
retry_failure_code: code,
|
|
19055
19141
|
});
|
|
@@ -19115,6 +19201,17 @@ class ClientEventReporter {
|
|
|
19115
19201
|
return false;
|
|
19116
19202
|
};
|
|
19117
19203
|
}
|
|
19204
|
+
const toReportedIceState = (state) => {
|
|
19205
|
+
switch (state) {
|
|
19206
|
+
case 'connected':
|
|
19207
|
+
case 'completed':
|
|
19208
|
+
return 'CONNECTED';
|
|
19209
|
+
case 'failed':
|
|
19210
|
+
return 'FAILED';
|
|
19211
|
+
default:
|
|
19212
|
+
return 'NOT_CONNECTED';
|
|
19213
|
+
}
|
|
19214
|
+
};
|
|
19118
19215
|
const readPermissionStatus = (permission) => {
|
|
19119
19216
|
const state = getCurrentValue(permission.asStateObservable());
|
|
19120
19217
|
switch (state) {
|