@stream-io/video-client 1.55.2 → 1.56.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 +12 -0
- package/dist/index.browser.es.js +180 -103
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +180 -103
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +180 -103
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/BlockedAudioTracker.d.ts +16 -1
- 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 +2 -2
- package/src/Call.ts +1 -0
- package/src/helpers/BlockedAudioTracker.ts +71 -13
- package/src/helpers/DynascaleManager.ts +14 -3
- package/src/helpers/__tests__/BlockedAudioTracker.test.ts +54 -6
- package/src/helpers/__tests__/DynascaleManager.test.ts +45 -0
- package/src/reporting/ClientEventReporter.ts +37 -31
- 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 +13 -11
- package/src/store/CallState.ts +5 -17
- package/src/store/rxUtils.ts +15 -0
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.56.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
|
}
|
|
@@ -11503,6 +11557,12 @@ class DynascaleManager {
|
|
|
11503
11557
|
let sourceNode = undefined;
|
|
11504
11558
|
let gainNode = undefined;
|
|
11505
11559
|
let audioWatchdog = undefined;
|
|
11560
|
+
const clearBlockedAudio = () => {
|
|
11561
|
+
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
11562
|
+
return;
|
|
11563
|
+
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
11564
|
+
};
|
|
11565
|
+
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
11506
11566
|
const isAudioTrack = trackType === 'audioTrack';
|
|
11507
11567
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
11508
11568
|
const updateMediaStreamSubscription = participant$
|
|
@@ -11516,7 +11576,7 @@ class DynascaleManager {
|
|
|
11516
11576
|
audioWatchdog?.dispose();
|
|
11517
11577
|
audioWatchdog = undefined;
|
|
11518
11578
|
if (!source) {
|
|
11519
|
-
|
|
11579
|
+
clearBlockedAudio();
|
|
11520
11580
|
return;
|
|
11521
11581
|
}
|
|
11522
11582
|
// Safari has a special quirk that prevents playing audio until the user
|
|
@@ -11544,7 +11604,7 @@ class DynascaleManager {
|
|
|
11544
11604
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
11545
11605
|
if (e.name === 'NotAllowedError') {
|
|
11546
11606
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
11547
|
-
this.blockedAudioTracker.markBlocked(audioElement, true);
|
|
11607
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
11548
11608
|
}
|
|
11549
11609
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
11550
11610
|
});
|
|
@@ -11577,7 +11637,8 @@ class DynascaleManager {
|
|
|
11577
11637
|
});
|
|
11578
11638
|
audioElement.autoplay = true;
|
|
11579
11639
|
return () => {
|
|
11580
|
-
|
|
11640
|
+
audioElement.removeEventListener('playing', clearBlockedAudio);
|
|
11641
|
+
clearBlockedAudio();
|
|
11581
11642
|
sinkIdSubscription?.unsubscribe();
|
|
11582
11643
|
volumeSubscription.unsubscribe();
|
|
11583
11644
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -14752,6 +14813,7 @@ class Call {
|
|
|
14752
14813
|
this.screenShare = new ScreenShareManager(this);
|
|
14753
14814
|
this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
|
|
14754
14815
|
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
14816
|
+
this.leaveCallHooks.add(this.blockedAudioTracker.reset);
|
|
14755
14817
|
if (typeof document !== 'undefined') {
|
|
14756
14818
|
this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
|
|
14757
14819
|
this.viewportTracker = new ViewportTracker(this.state);
|
|
@@ -18448,11 +18510,11 @@ class StreamClient {
|
|
|
18448
18510
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18449
18511
|
};
|
|
18450
18512
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18451
|
-
"1.
|
|
18513
|
+
"1.56.0";
|
|
18452
18514
|
getUserAgent = () => {
|
|
18453
18515
|
if (!this.cachedUserAgent) {
|
|
18454
18516
|
const { clientAppIdentifier = {} } = this.options;
|
|
18455
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18517
|
+
const { sdkName = 'js', sdkVersion = "1.56.0", ...extras } = clientAppIdentifier;
|
|
18456
18518
|
this.cachedUserAgent = [
|
|
18457
18519
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18458
18520
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18789,8 +18851,8 @@ class ClientEventReporter {
|
|
|
18789
18851
|
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
18790
18852
|
this.failCoordinator(cid);
|
|
18791
18853
|
this.failWs(cid);
|
|
18792
|
-
this.emitPeerConnectionFailure(cid, 'publish', code, reason
|
|
18793
|
-
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason
|
|
18854
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason);
|
|
18855
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
|
|
18794
18856
|
}
|
|
18795
18857
|
catch (err) {
|
|
18796
18858
|
this.logger.warn('Failed to report abort', err);
|
|
@@ -18802,7 +18864,7 @@ class ClientEventReporter {
|
|
|
18802
18864
|
if (this.wsPairs.get(cid))
|
|
18803
18865
|
this.failWs(cid);
|
|
18804
18866
|
for (const role of ['publish', 'subscribe']) {
|
|
18805
|
-
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt'
|
|
18867
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
|
|
18806
18868
|
}
|
|
18807
18869
|
};
|
|
18808
18870
|
emitJoinInitiated = (cid) => {
|
|
@@ -18964,21 +19026,24 @@ class ClientEventReporter {
|
|
|
18964
19026
|
};
|
|
18965
19027
|
onPeerConnectionStateChange = (cid, event) => {
|
|
18966
19028
|
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18967
|
-
|
|
18968
|
-
|
|
19029
|
+
const pair = this.peerConnectionPairs.get(pcKey(cid, role));
|
|
19030
|
+
if (pair)
|
|
19031
|
+
pair.lastIceState = event.iceConnectionState;
|
|
19032
|
+
if (event.stateType === 'ice') {
|
|
19033
|
+
if (event.state === 'failed') {
|
|
19034
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
|
|
19035
|
+
}
|
|
18969
19036
|
return;
|
|
18970
19037
|
}
|
|
18971
|
-
if (event.
|
|
18972
|
-
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed'
|
|
19038
|
+
if (event.state === 'failed') {
|
|
19039
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
|
|
18973
19040
|
return;
|
|
18974
19041
|
}
|
|
18975
|
-
if (event.stateType !== 'peerConnection')
|
|
18976
|
-
return;
|
|
18977
19042
|
switch (event.state) {
|
|
18978
19043
|
case 'connecting':
|
|
18979
|
-
if (
|
|
19044
|
+
if (pair)
|
|
18980
19045
|
return;
|
|
18981
|
-
this.openPeerConnectionPair(cid, role);
|
|
19046
|
+
this.openPeerConnectionPair(cid, role, event.iceConnectionState);
|
|
18982
19047
|
break;
|
|
18983
19048
|
case 'connected':
|
|
18984
19049
|
this.emitPeerConnectionSuccess(cid, role);
|
|
@@ -18986,7 +19051,7 @@ class ClientEventReporter {
|
|
|
18986
19051
|
break;
|
|
18987
19052
|
}
|
|
18988
19053
|
};
|
|
18989
|
-
openPeerConnectionPair = (cid, role) => {
|
|
19054
|
+
openPeerConnectionPair = (cid, role, iceConnectionState) => {
|
|
18990
19055
|
const key = pcKey(cid, role);
|
|
18991
19056
|
const pair = {
|
|
18992
19057
|
sid: generateUUIDv4(),
|
|
@@ -18996,6 +19061,7 @@ class ClientEventReporter {
|
|
|
18996
19061
|
sfuId: this.getSfuId(cid),
|
|
18997
19062
|
userSessionId: this.getUserSessionId(cid),
|
|
18998
19063
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19064
|
+
lastIceState: iceConnectionState,
|
|
18999
19065
|
};
|
|
19000
19066
|
this.peerConnectionPairs.set(key, pair);
|
|
19001
19067
|
this.sendForCall(cid, {
|
|
@@ -19031,7 +19097,7 @@ class ClientEventReporter {
|
|
|
19031
19097
|
});
|
|
19032
19098
|
this.peerConnectionPairs.delete(key);
|
|
19033
19099
|
};
|
|
19034
|
-
emitPeerConnectionFailure = (cid, role, code, reason
|
|
19100
|
+
emitPeerConnectionFailure = (cid, role, code, reason) => {
|
|
19035
19101
|
const key = pcKey(cid, role);
|
|
19036
19102
|
const pair = this.peerConnectionPairs.get(key);
|
|
19037
19103
|
if (!pair)
|
|
@@ -19049,7 +19115,7 @@ class ClientEventReporter {
|
|
|
19049
19115
|
outcome: 'failure',
|
|
19050
19116
|
retry_count_attempt: 0,
|
|
19051
19117
|
elapsed_time: Date.now() - pair.startedAt,
|
|
19052
|
-
ice_state:
|
|
19118
|
+
ice_state: toReportedIceState(pair.lastIceState),
|
|
19053
19119
|
retry_failure_reason: reason,
|
|
19054
19120
|
retry_failure_code: code,
|
|
19055
19121
|
});
|
|
@@ -19115,6 +19181,17 @@ class ClientEventReporter {
|
|
|
19115
19181
|
return false;
|
|
19116
19182
|
};
|
|
19117
19183
|
}
|
|
19184
|
+
const toReportedIceState = (state) => {
|
|
19185
|
+
switch (state) {
|
|
19186
|
+
case 'connected':
|
|
19187
|
+
case 'completed':
|
|
19188
|
+
return 'CONNECTED';
|
|
19189
|
+
case 'failed':
|
|
19190
|
+
return 'FAILED';
|
|
19191
|
+
default:
|
|
19192
|
+
return 'NOT_CONNECTED';
|
|
19193
|
+
}
|
|
19194
|
+
};
|
|
19118
19195
|
const readPermissionStatus = (permission) => {
|
|
19119
19196
|
const state = getCurrentValue(permission.asStateObservable());
|
|
19120
19197
|
switch (state) {
|