@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.es.js
CHANGED
|
@@ -4927,7 +4927,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4927
4927
|
}
|
|
4928
4928
|
};
|
|
4929
4929
|
|
|
4930
|
-
const version = "1.
|
|
4930
|
+
const version = "1.56.0";
|
|
4931
4931
|
const [major, minor, patch] = version.split('.');
|
|
4932
4932
|
let sdkInfo = {
|
|
4933
4933
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5256,6 +5256,23 @@ function wrapWithCancellation(tag, cb) {
|
|
|
5256
5256
|
return { cb: wrapped, onContinued };
|
|
5257
5257
|
}
|
|
5258
5258
|
|
|
5259
|
+
/**
|
|
5260
|
+
* Performs shallow comparison of two arrays.
|
|
5261
|
+
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
5262
|
+
*/
|
|
5263
|
+
const isShallowArrayEqual = (a, b) => {
|
|
5264
|
+
if (a.length !== b.length)
|
|
5265
|
+
return false;
|
|
5266
|
+
for (const item of a) {
|
|
5267
|
+
if (!b.includes(item))
|
|
5268
|
+
return false;
|
|
5269
|
+
}
|
|
5270
|
+
for (const item of b) {
|
|
5271
|
+
if (!a.includes(item))
|
|
5272
|
+
return false;
|
|
5273
|
+
}
|
|
5274
|
+
return true;
|
|
5275
|
+
};
|
|
5259
5276
|
/**
|
|
5260
5277
|
* Checks if the provided update is a function patch.
|
|
5261
5278
|
*
|
|
@@ -5368,6 +5385,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
|
|
|
5368
5385
|
createSafeAsyncSubscription: createSafeAsyncSubscription,
|
|
5369
5386
|
createSubscription: createSubscription,
|
|
5370
5387
|
getCurrentValue: getCurrentValue,
|
|
5388
|
+
isShallowArrayEqual: isShallowArrayEqual,
|
|
5371
5389
|
setCurrentValue: setCurrentValue,
|
|
5372
5390
|
setCurrentValueAsync: setCurrentValueAsync,
|
|
5373
5391
|
updateValue: updateValue
|
|
@@ -6108,23 +6126,6 @@ class CallState {
|
|
|
6108
6126
|
this.endedBy$ = this.endedBySubject.asObservable();
|
|
6109
6127
|
this.thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
6110
6128
|
this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
6111
|
-
/**
|
|
6112
|
-
* Performs shallow comparison of two arrays.
|
|
6113
|
-
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
6114
|
-
*/
|
|
6115
|
-
const isShallowEqual = (a, b) => {
|
|
6116
|
-
if (a.length !== b.length)
|
|
6117
|
-
return false;
|
|
6118
|
-
for (const item of a) {
|
|
6119
|
-
if (!b.includes(item))
|
|
6120
|
-
return false;
|
|
6121
|
-
}
|
|
6122
|
-
for (const item of b) {
|
|
6123
|
-
if (!a.includes(item))
|
|
6124
|
-
return false;
|
|
6125
|
-
}
|
|
6126
|
-
return true;
|
|
6127
|
-
};
|
|
6128
6129
|
/**
|
|
6129
6130
|
* Creates an Observable from the given subject by piping to the
|
|
6130
6131
|
* `distinctUntilChanged()` operator.
|
|
@@ -6132,7 +6133,7 @@ class CallState {
|
|
|
6132
6133
|
const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
|
|
6133
6134
|
// primitive values should only emit once the value they hold changes
|
|
6134
6135
|
this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
|
|
6135
|
-
this.blockedUserIds$ = duc(this.blockedUserIdsSubject,
|
|
6136
|
+
this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
|
|
6136
6137
|
this.backstage$ = duc(this.backstageSubject);
|
|
6137
6138
|
this.callingState$ = duc(this.callingStateSubject);
|
|
6138
6139
|
this.ownCapabilities$ = combineLatest([
|
|
@@ -6161,7 +6162,7 @@ class CallState {
|
|
|
6161
6162
|
}
|
|
6162
6163
|
}
|
|
6163
6164
|
return nextCapabilities;
|
|
6164
|
-
}), distinctUntilChanged(
|
|
6165
|
+
}), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
|
|
6165
6166
|
this.participantCount$ = duc(this.participantCountSubject);
|
|
6166
6167
|
this.recording$ = duc(this.recordingSubject);
|
|
6167
6168
|
this.individualRecording$ = duc(this.individualRecordingSubject);
|
|
@@ -8472,6 +8473,7 @@ class BasePeerConnection {
|
|
|
8472
8473
|
try {
|
|
8473
8474
|
this.onPeerConnectionStateChange?.({
|
|
8474
8475
|
peerType: this.peerType,
|
|
8476
|
+
iceConnectionState: this.pc.iceConnectionState,
|
|
8475
8477
|
...event,
|
|
8476
8478
|
});
|
|
8477
8479
|
}
|
|
@@ -8926,6 +8928,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8926
8928
|
clonedTracks = new Set();
|
|
8927
8929
|
publishOptions;
|
|
8928
8930
|
selfSubEnabled;
|
|
8931
|
+
transceiverLockKey = `pub.tx.${this.lock}`;
|
|
8929
8932
|
/**
|
|
8930
8933
|
* Constructs a new `Publisher` instance.
|
|
8931
8934
|
*/
|
|
@@ -8981,32 +8984,34 @@ class Publisher extends BasePeerConnection {
|
|
|
8981
8984
|
* @param options the publish options to use.
|
|
8982
8985
|
*/
|
|
8983
8986
|
publish = async (track, trackType, options = {}) => {
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
if (publishOption.trackType !== trackType)
|
|
8989
|
-
continue;
|
|
8990
|
-
// create a clone of the track as otherwise the same trackId will
|
|
8991
|
-
// appear in the SDP in multiple transceivers
|
|
8992
|
-
const trackToPublish = this.cloneTrack(track);
|
|
8993
|
-
const bundle = this.transceiverCache.get(publishOption);
|
|
8994
|
-
if (!bundle) {
|
|
8995
|
-
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8987
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
8988
|
+
const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
|
|
8989
|
+
if (!publishOptions.length) {
|
|
8990
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
8996
8991
|
}
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
8992
|
+
for (const publishOption of publishOptions) {
|
|
8993
|
+
// create a clone of the track as otherwise the same trackId will
|
|
8994
|
+
// appear in the SDP in multiple transceivers
|
|
8995
|
+
const trackToPublish = this.cloneTrack(track);
|
|
8996
|
+
const bundle = this.transceiverCache.get(publishOption);
|
|
8997
|
+
if (!bundle) {
|
|
8998
|
+
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8999
|
+
}
|
|
9000
|
+
else {
|
|
9001
|
+
const previousTrack = bundle.transceiver.sender.track;
|
|
9002
|
+
await this.updateTransceiver(bundle, trackToPublish, options);
|
|
9003
|
+
if (!isReactNative()) {
|
|
9004
|
+
this.stopTrack(previousTrack);
|
|
9005
|
+
}
|
|
9002
9006
|
}
|
|
9003
9007
|
}
|
|
9004
|
-
}
|
|
9008
|
+
});
|
|
9005
9009
|
};
|
|
9006
9010
|
/**
|
|
9007
9011
|
* Adds a new transceiver carrying the given track to the peer connection.
|
|
9008
9012
|
*/
|
|
9009
9013
|
addTransceiver = async (track, publishOption, options) => {
|
|
9014
|
+
const trackType = publishOption.trackType;
|
|
9010
9015
|
const encodings = isAudioTrackType(publishOption.trackType)
|
|
9011
9016
|
? computeAudioLayers(publishOption, options)
|
|
9012
9017
|
: computeVideoLayers(track, publishOption);
|
|
@@ -9017,15 +9022,17 @@ class Publisher extends BasePeerConnection {
|
|
|
9017
9022
|
direction: 'sendonly',
|
|
9018
9023
|
sendEncodings,
|
|
9019
9024
|
});
|
|
9025
|
+
// claim the cache entry in the same task that created the transceiver:
|
|
9026
|
+
// an await in between would let a concurrent caller add a second one
|
|
9027
|
+
// for the same publish option
|
|
9028
|
+
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9029
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
9030
|
+
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9020
9031
|
const params = transceiver.sender.getParameters();
|
|
9021
9032
|
params.degradationPreference =
|
|
9022
9033
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9023
9034
|
'maintain-framerate';
|
|
9024
9035
|
await transceiver.sender.setParameters(params);
|
|
9025
|
-
const trackType = publishOption.trackType;
|
|
9026
|
-
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9027
|
-
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9028
|
-
this.trackIdToTrackType.set(track.id, trackType);
|
|
9029
9036
|
await this.negotiate();
|
|
9030
9037
|
};
|
|
9031
9038
|
/**
|
|
@@ -9082,33 +9089,35 @@ class Publisher extends BasePeerConnection {
|
|
|
9082
9089
|
* Synchronizes the current Publisher state with the provided publish options.
|
|
9083
9090
|
*/
|
|
9084
9091
|
syncPublishOptions = async () => {
|
|
9085
|
-
|
|
9086
|
-
|
|
9087
|
-
const
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
|
|
9093
|
-
i.
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
const
|
|
9104
|
-
|
|
9105
|
-
option.
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9092
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
9093
|
+
// enable publishing with new options -> [av1, vp9]
|
|
9094
|
+
for (const publishOption of this.publishOptions) {
|
|
9095
|
+
const { trackType } = publishOption;
|
|
9096
|
+
if (!this.isPublishing(trackType))
|
|
9097
|
+
continue;
|
|
9098
|
+
if (this.transceiverCache.has(publishOption))
|
|
9099
|
+
continue;
|
|
9100
|
+
const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
|
|
9101
|
+
i.publishOption.trackType === trackType);
|
|
9102
|
+
if (!item)
|
|
9103
|
+
continue;
|
|
9104
|
+
// take the track from the existing transceiver for the same track type,
|
|
9105
|
+
// clone it and publish it with the new publish options
|
|
9106
|
+
const track = this.cloneTrack(item.transceiver.sender.track);
|
|
9107
|
+
await this.addTransceiver(track, publishOption, item.options);
|
|
9108
|
+
}
|
|
9109
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
9110
|
+
for (const item of this.transceiverCache.items()) {
|
|
9111
|
+
const { publishOption, transceiver } = item;
|
|
9112
|
+
const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
|
|
9113
|
+
option.trackType === publishOption.trackType);
|
|
9114
|
+
if (hasPublishOption)
|
|
9115
|
+
continue;
|
|
9116
|
+
// it is safe to stop the track here, it is a clone
|
|
9117
|
+
this.stopTrack(transceiver.sender.track);
|
|
9118
|
+
await this.updateTransceiver(item, null);
|
|
9119
|
+
}
|
|
9120
|
+
});
|
|
9112
9121
|
};
|
|
9113
9122
|
/**
|
|
9114
9123
|
* Returns true if the given track type is currently being published to the SFU.
|
|
@@ -9276,12 +9285,23 @@ class Publisher extends BasePeerConnection {
|
|
|
9276
9285
|
negotiate = async (options) => {
|
|
9277
9286
|
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
9278
9287
|
const offer = await this.pc.createOffer(options);
|
|
9279
|
-
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9280
|
-
if (!tracks.length)
|
|
9281
|
-
throw new Error(`Can't negotiate without any tracks`);
|
|
9282
9288
|
try {
|
|
9283
9289
|
this.isIceRestarting = options?.iceRestart ?? false;
|
|
9284
9290
|
await this.pc.setLocalDescription(offer);
|
|
9291
|
+
// the announced tracks have to be read after `setLocalDescription`.
|
|
9292
|
+
// `transceiver.mid` is null until the offer is applied, and reading the
|
|
9293
|
+
// tracks earlier makes `extractMid` guess the mid from the offer SDP.
|
|
9294
|
+
// That guess goes stale after a rolled-back negotiation, and a mid that
|
|
9295
|
+
// doesn't match the m-section carrying the track stops the SFU from
|
|
9296
|
+
// correlating the two: it then answers with every offered codec instead
|
|
9297
|
+
// of the announced one, and we publish the first codec of that list.
|
|
9298
|
+
// Per https://www.w3.org/TR/webrtc/#set-description, applying a local
|
|
9299
|
+
// offer associates the transceiver with its m-section and sets
|
|
9300
|
+
// `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
|
|
9301
|
+
// that wasn't associated before and resets both slots to null.
|
|
9302
|
+
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9303
|
+
if (!tracks.length)
|
|
9304
|
+
throw new Error(`Can't negotiate without any tracks`);
|
|
9285
9305
|
const { sdp: baseSdp = '' } = offer;
|
|
9286
9306
|
const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
|
|
9287
9307
|
let sdp = dangerouslyForceCodec
|
|
@@ -10906,34 +10926,67 @@ class AudioBindingsWatchdog {
|
|
|
10906
10926
|
class BlockedAudioTracker {
|
|
10907
10927
|
logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
|
|
10908
10928
|
tracer;
|
|
10909
|
-
blockedElementsSubject = new BehaviorSubject(
|
|
10929
|
+
blockedElementsSubject = new BehaviorSubject([]);
|
|
10910
10930
|
/**
|
|
10911
10931
|
* Whether the browser's autoplay policy is blocking audio playback.
|
|
10912
10932
|
* Will be `true` when at least one audio element is currently blocked.
|
|
10913
10933
|
* Use {@link resumeAudio} within a user gesture to unblock.
|
|
10914
10934
|
*/
|
|
10915
|
-
autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.
|
|
10935
|
+
autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.length > 0), distinctUntilChanged());
|
|
10936
|
+
/**
|
|
10937
|
+
* The list of participant `sessionId`s whose audio element is currently
|
|
10938
|
+
* blocked by the browser's autoplay policy. Only some participants may be
|
|
10939
|
+
* blocked (e.g. one joined while a gesture was active and a later one was
|
|
10940
|
+
* not), so use this to render a per-participant affordance rather than a
|
|
10941
|
+
* call-wide one. Call {@link resumeAudio} within a user gesture to unblock
|
|
10942
|
+
* all of them.
|
|
10943
|
+
*
|
|
10944
|
+
* Session ids are registered together with the audio element.
|
|
10945
|
+
*/
|
|
10946
|
+
blockedSessionIds$ = this.blockedElementsSubject.pipe(map((elements) => {
|
|
10947
|
+
const ids = [];
|
|
10948
|
+
elements.forEach(({ sessionId }) => {
|
|
10949
|
+
if (sessionId && !ids.includes(sessionId))
|
|
10950
|
+
ids.push(sessionId);
|
|
10951
|
+
});
|
|
10952
|
+
return ids;
|
|
10953
|
+
}), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
|
|
10916
10954
|
constructor(tracer) {
|
|
10917
10955
|
this.tracer = tracer;
|
|
10918
10956
|
}
|
|
10919
10957
|
/**
|
|
10920
10958
|
* Registers an audio element as blocked by the browser's autoplay policy.
|
|
10921
10959
|
*/
|
|
10922
|
-
markBlocked = (audioElement, blocked) => {
|
|
10960
|
+
markBlocked = (audioElement, blocked, sessionId) => {
|
|
10923
10961
|
setCurrentValue(this.blockedElementsSubject, (elements) => {
|
|
10924
|
-
if (blocked)
|
|
10925
|
-
elements.
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10962
|
+
if (!blocked) {
|
|
10963
|
+
return elements.filter(({ element }) => element !== audioElement);
|
|
10964
|
+
}
|
|
10965
|
+
const existing = elements.find(({ element }) => element === audioElement);
|
|
10966
|
+
if (existing) {
|
|
10967
|
+
return elements.map((entry) => entry.element === audioElement
|
|
10968
|
+
? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
|
|
10969
|
+
: entry);
|
|
10970
|
+
}
|
|
10971
|
+
return [...elements, { element: audioElement, sessionId }];
|
|
10929
10972
|
});
|
|
10930
10973
|
};
|
|
10974
|
+
/**
|
|
10975
|
+
* Clears all tracked elements. The tracker stays usable afterwards.
|
|
10976
|
+
*/
|
|
10977
|
+
reset = () => {
|
|
10978
|
+
if (this.blockedElementsSubject.getValue().length > 0) {
|
|
10979
|
+
this.blockedElementsSubject.next([]);
|
|
10980
|
+
}
|
|
10981
|
+
};
|
|
10931
10982
|
/**
|
|
10932
10983
|
* Returns whether the given audio element is currently flagged as blocked
|
|
10933
10984
|
* by the browser's autoplay policy.
|
|
10934
10985
|
*/
|
|
10935
10986
|
isBlocked = (audioElement) => {
|
|
10936
|
-
return this.blockedElementsSubject
|
|
10987
|
+
return this.blockedElementsSubject
|
|
10988
|
+
.getValue()
|
|
10989
|
+
.some(({ element }) => element === audioElement);
|
|
10937
10990
|
};
|
|
10938
10991
|
/**
|
|
10939
10992
|
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
@@ -10942,17 +10995,18 @@ class BlockedAudioTracker {
|
|
|
10942
10995
|
resumeAudio = async () => {
|
|
10943
10996
|
this.tracer.trace('resumeAudio', null);
|
|
10944
10997
|
await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
|
|
10945
|
-
|
|
10998
|
+
let next = elements;
|
|
10999
|
+
await Promise.all(elements.map(async ({ element }) => {
|
|
10946
11000
|
try {
|
|
10947
11001
|
if (element.srcObject)
|
|
10948
11002
|
await timeboxed([element.play()], 2000);
|
|
10949
|
-
|
|
11003
|
+
next = next.filter((entry) => entry.element !== element);
|
|
10950
11004
|
}
|
|
10951
11005
|
catch (err) {
|
|
10952
11006
|
this.logger.warn(`Can't resume audio for element`, element, err);
|
|
10953
11007
|
}
|
|
10954
11008
|
}));
|
|
10955
|
-
return
|
|
11009
|
+
return next;
|
|
10956
11010
|
});
|
|
10957
11011
|
};
|
|
10958
11012
|
}
|
|
@@ -11484,6 +11538,12 @@ class DynascaleManager {
|
|
|
11484
11538
|
let sourceNode = undefined;
|
|
11485
11539
|
let gainNode = undefined;
|
|
11486
11540
|
let audioWatchdog = undefined;
|
|
11541
|
+
const clearBlockedAudio = () => {
|
|
11542
|
+
if (!this.blockedAudioTracker.isBlocked(audioElement))
|
|
11543
|
+
return;
|
|
11544
|
+
this.blockedAudioTracker.markBlocked(audioElement, false);
|
|
11545
|
+
};
|
|
11546
|
+
audioElement.addEventListener('playing', clearBlockedAudio);
|
|
11487
11547
|
const isAudioTrack = trackType === 'audioTrack';
|
|
11488
11548
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
11489
11549
|
const updateMediaStreamSubscription = participant$
|
|
@@ -11497,7 +11557,7 @@ class DynascaleManager {
|
|
|
11497
11557
|
audioWatchdog?.dispose();
|
|
11498
11558
|
audioWatchdog = undefined;
|
|
11499
11559
|
if (!source) {
|
|
11500
|
-
|
|
11560
|
+
clearBlockedAudio();
|
|
11501
11561
|
return;
|
|
11502
11562
|
}
|
|
11503
11563
|
// Safari has a special quirk that prevents playing audio until the user
|
|
@@ -11525,7 +11585,7 @@ class DynascaleManager {
|
|
|
11525
11585
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
11526
11586
|
if (e.name === 'NotAllowedError') {
|
|
11527
11587
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
11528
|
-
this.blockedAudioTracker.markBlocked(audioElement, true);
|
|
11588
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
11529
11589
|
}
|
|
11530
11590
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
11531
11591
|
});
|
|
@@ -11558,7 +11618,8 @@ class DynascaleManager {
|
|
|
11558
11618
|
});
|
|
11559
11619
|
audioElement.autoplay = true;
|
|
11560
11620
|
return () => {
|
|
11561
|
-
|
|
11621
|
+
audioElement.removeEventListener('playing', clearBlockedAudio);
|
|
11622
|
+
clearBlockedAudio();
|
|
11562
11623
|
sinkIdSubscription?.unsubscribe();
|
|
11563
11624
|
volumeSubscription.unsubscribe();
|
|
11564
11625
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -14733,6 +14794,7 @@ class Call {
|
|
|
14733
14794
|
this.screenShare = new ScreenShareManager(this);
|
|
14734
14795
|
this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
|
|
14735
14796
|
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
14797
|
+
this.leaveCallHooks.add(this.blockedAudioTracker.reset);
|
|
14736
14798
|
if (typeof document !== 'undefined') {
|
|
14737
14799
|
this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
|
|
14738
14800
|
this.viewportTracker = new ViewportTracker(this.state);
|
|
@@ -18429,11 +18491,11 @@ class StreamClient {
|
|
|
18429
18491
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18430
18492
|
};
|
|
18431
18493
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18432
|
-
"1.
|
|
18494
|
+
"1.56.0";
|
|
18433
18495
|
getUserAgent = () => {
|
|
18434
18496
|
if (!this.cachedUserAgent) {
|
|
18435
18497
|
const { clientAppIdentifier = {} } = this.options;
|
|
18436
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18498
|
+
const { sdkName = 'js', sdkVersion = "1.56.0", ...extras } = clientAppIdentifier;
|
|
18437
18499
|
this.cachedUserAgent = [
|
|
18438
18500
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18439
18501
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18770,8 +18832,8 @@ class ClientEventReporter {
|
|
|
18770
18832
|
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
18771
18833
|
this.failCoordinator(cid);
|
|
18772
18834
|
this.failWs(cid);
|
|
18773
|
-
this.emitPeerConnectionFailure(cid, 'publish', code, reason
|
|
18774
|
-
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason
|
|
18835
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason);
|
|
18836
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
|
|
18775
18837
|
}
|
|
18776
18838
|
catch (err) {
|
|
18777
18839
|
this.logger.warn('Failed to report abort', err);
|
|
@@ -18783,7 +18845,7 @@ class ClientEventReporter {
|
|
|
18783
18845
|
if (this.wsPairs.get(cid))
|
|
18784
18846
|
this.failWs(cid);
|
|
18785
18847
|
for (const role of ['publish', 'subscribe']) {
|
|
18786
|
-
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt'
|
|
18848
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
|
|
18787
18849
|
}
|
|
18788
18850
|
};
|
|
18789
18851
|
emitJoinInitiated = (cid) => {
|
|
@@ -18945,21 +19007,24 @@ class ClientEventReporter {
|
|
|
18945
19007
|
};
|
|
18946
19008
|
onPeerConnectionStateChange = (cid, event) => {
|
|
18947
19009
|
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18948
|
-
|
|
18949
|
-
|
|
19010
|
+
const pair = this.peerConnectionPairs.get(pcKey(cid, role));
|
|
19011
|
+
if (pair)
|
|
19012
|
+
pair.lastIceState = event.iceConnectionState;
|
|
19013
|
+
if (event.stateType === 'ice') {
|
|
19014
|
+
if (event.state === 'failed') {
|
|
19015
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
|
|
19016
|
+
}
|
|
18950
19017
|
return;
|
|
18951
19018
|
}
|
|
18952
|
-
if (event.
|
|
18953
|
-
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed'
|
|
19019
|
+
if (event.state === 'failed') {
|
|
19020
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
|
|
18954
19021
|
return;
|
|
18955
19022
|
}
|
|
18956
|
-
if (event.stateType !== 'peerConnection')
|
|
18957
|
-
return;
|
|
18958
19023
|
switch (event.state) {
|
|
18959
19024
|
case 'connecting':
|
|
18960
|
-
if (
|
|
19025
|
+
if (pair)
|
|
18961
19026
|
return;
|
|
18962
|
-
this.openPeerConnectionPair(cid, role);
|
|
19027
|
+
this.openPeerConnectionPair(cid, role, event.iceConnectionState);
|
|
18963
19028
|
break;
|
|
18964
19029
|
case 'connected':
|
|
18965
19030
|
this.emitPeerConnectionSuccess(cid, role);
|
|
@@ -18967,7 +19032,7 @@ class ClientEventReporter {
|
|
|
18967
19032
|
break;
|
|
18968
19033
|
}
|
|
18969
19034
|
};
|
|
18970
|
-
openPeerConnectionPair = (cid, role) => {
|
|
19035
|
+
openPeerConnectionPair = (cid, role, iceConnectionState) => {
|
|
18971
19036
|
const key = pcKey(cid, role);
|
|
18972
19037
|
const pair = {
|
|
18973
19038
|
sid: generateUUIDv4(),
|
|
@@ -18977,6 +19042,7 @@ class ClientEventReporter {
|
|
|
18977
19042
|
sfuId: this.getSfuId(cid),
|
|
18978
19043
|
userSessionId: this.getUserSessionId(cid),
|
|
18979
19044
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19045
|
+
lastIceState: iceConnectionState,
|
|
18980
19046
|
};
|
|
18981
19047
|
this.peerConnectionPairs.set(key, pair);
|
|
18982
19048
|
this.sendForCall(cid, {
|
|
@@ -19012,7 +19078,7 @@ class ClientEventReporter {
|
|
|
19012
19078
|
});
|
|
19013
19079
|
this.peerConnectionPairs.delete(key);
|
|
19014
19080
|
};
|
|
19015
|
-
emitPeerConnectionFailure = (cid, role, code, reason
|
|
19081
|
+
emitPeerConnectionFailure = (cid, role, code, reason) => {
|
|
19016
19082
|
const key = pcKey(cid, role);
|
|
19017
19083
|
const pair = this.peerConnectionPairs.get(key);
|
|
19018
19084
|
if (!pair)
|
|
@@ -19030,7 +19096,7 @@ class ClientEventReporter {
|
|
|
19030
19096
|
outcome: 'failure',
|
|
19031
19097
|
retry_count_attempt: 0,
|
|
19032
19098
|
elapsed_time: Date.now() - pair.startedAt,
|
|
19033
|
-
ice_state:
|
|
19099
|
+
ice_state: toReportedIceState(pair.lastIceState),
|
|
19034
19100
|
retry_failure_reason: reason,
|
|
19035
19101
|
retry_failure_code: code,
|
|
19036
19102
|
});
|
|
@@ -19096,6 +19162,17 @@ class ClientEventReporter {
|
|
|
19096
19162
|
return false;
|
|
19097
19163
|
};
|
|
19098
19164
|
}
|
|
19165
|
+
const toReportedIceState = (state) => {
|
|
19166
|
+
switch (state) {
|
|
19167
|
+
case 'connected':
|
|
19168
|
+
case 'completed':
|
|
19169
|
+
return 'CONNECTED';
|
|
19170
|
+
case 'failed':
|
|
19171
|
+
return 'FAILED';
|
|
19172
|
+
default:
|
|
19173
|
+
return 'NOT_CONNECTED';
|
|
19174
|
+
}
|
|
19175
|
+
};
|
|
19099
19176
|
const readPermissionStatus = (permission) => {
|
|
19100
19177
|
const state = getCurrentValue(permission.asStateObservable());
|
|
19101
19178
|
switch (state) {
|