@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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.57.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.56.0...@stream-io/video-client-1.57.0) (2026-08-07)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **react-native:** Expo SDK 57 / RN 0.86.2 upgrade and iOS 27 compatibility fixes ([#2355](https://github.com/GetStream/stream-video-js/issues/2355)) ([ea3c29b](https://github.com/GetStream/stream-video-js/commit/ea3c29b227f7a2a9ddb0eb046325e4783b558c94))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
- **client:** require stable playback before resetting watchdog retries ([#2361](https://github.com/GetStream/stream-video-js/issues/2361)) ([3e6b675](https://github.com/GetStream/stream-video-js/commit/3e6b67527702111cd0ea80e3bee1c9e6995406aa))
|
|
14
|
+
|
|
15
|
+
## [1.56.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.55.2...@stream-io/video-client-1.56.0) (2026-07-30)
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **react-sdk:** show blocked audio overlay per participant ([#2341](https://github.com/GetStream/stream-video-js/issues/2341)) ([4fe5c40](https://github.com/GetStream/stream-video-js/commit/4fe5c40aaed1829125bff49deec39a008c7abec5))
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
- **client:** fix stale mid in SetPublisher after PeerConnection rollback ([#2344](https://github.com/GetStream/stream-video-js/issues/2344)) ([9c3e201](https://github.com/GetStream/stream-video-js/commit/9c3e201c9a1cf129e182da961b1d73652508b9f2))
|
|
24
|
+
- **client:** never announce two tracks for the same publish option ([#2352](https://github.com/GetStream/stream-video-js/issues/2352)) ([b9a0c6e](https://github.com/GetStream/stream-video-js/commit/b9a0c6e709805e8658e74a68dc6e84459353db37))
|
|
25
|
+
- **client:** preserve ICE state in peer connection failure reports ([#2340](https://github.com/GetStream/stream-video-js/issues/2340)) ([2ce42a2](https://github.com/GetStream/stream-video-js/commit/2ce42a229eecfc60ae8f6783672c517b94b6ab42))
|
|
26
|
+
|
|
5
27
|
## [1.55.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.55.1...@stream-io/video-client-1.55.2) (2026-07-17)
|
|
6
28
|
|
|
7
29
|
### Bug Fixes
|
package/dist/index.browser.es.js
CHANGED
|
@@ -4926,7 +4926,7 @@ const toIceCandidate = (iceTrickle) => {
|
|
|
4926
4926
|
}
|
|
4927
4927
|
};
|
|
4928
4928
|
|
|
4929
|
-
const version = "1.
|
|
4929
|
+
const version = "1.57.0";
|
|
4930
4930
|
const [major, minor, patch] = version.split('.');
|
|
4931
4931
|
let sdkInfo = {
|
|
4932
4932
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5255,6 +5255,23 @@ function wrapWithCancellation(tag, cb) {
|
|
|
5255
5255
|
return { cb: wrapped, onContinued };
|
|
5256
5256
|
}
|
|
5257
5257
|
|
|
5258
|
+
/**
|
|
5259
|
+
* Performs shallow comparison of two arrays.
|
|
5260
|
+
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
5261
|
+
*/
|
|
5262
|
+
const isShallowArrayEqual = (a, b) => {
|
|
5263
|
+
if (a.length !== b.length)
|
|
5264
|
+
return false;
|
|
5265
|
+
for (const item of a) {
|
|
5266
|
+
if (!b.includes(item))
|
|
5267
|
+
return false;
|
|
5268
|
+
}
|
|
5269
|
+
for (const item of b) {
|
|
5270
|
+
if (!a.includes(item))
|
|
5271
|
+
return false;
|
|
5272
|
+
}
|
|
5273
|
+
return true;
|
|
5274
|
+
};
|
|
5258
5275
|
/**
|
|
5259
5276
|
* Checks if the provided update is a function patch.
|
|
5260
5277
|
*
|
|
@@ -5367,6 +5384,7 @@ var rxUtils = /*#__PURE__*/Object.freeze({
|
|
|
5367
5384
|
createSafeAsyncSubscription: createSafeAsyncSubscription,
|
|
5368
5385
|
createSubscription: createSubscription,
|
|
5369
5386
|
getCurrentValue: getCurrentValue,
|
|
5387
|
+
isShallowArrayEqual: isShallowArrayEqual,
|
|
5370
5388
|
setCurrentValue: setCurrentValue,
|
|
5371
5389
|
setCurrentValueAsync: setCurrentValueAsync,
|
|
5372
5390
|
updateValue: updateValue
|
|
@@ -6107,23 +6125,6 @@ class CallState {
|
|
|
6107
6125
|
this.endedBy$ = this.endedBySubject.asObservable();
|
|
6108
6126
|
this.thumbnails$ = this.thumbnailsSubject.asObservable();
|
|
6109
6127
|
this.closedCaptions$ = this.closedCaptionsSubject.asObservable();
|
|
6110
|
-
/**
|
|
6111
|
-
* Performs shallow comparison of two arrays.
|
|
6112
|
-
* Expects primitive values: [1, 2, 3] is equal to [2, 1, 3].
|
|
6113
|
-
*/
|
|
6114
|
-
const isShallowEqual = (a, b) => {
|
|
6115
|
-
if (a.length !== b.length)
|
|
6116
|
-
return false;
|
|
6117
|
-
for (const item of a) {
|
|
6118
|
-
if (!b.includes(item))
|
|
6119
|
-
return false;
|
|
6120
|
-
}
|
|
6121
|
-
for (const item of b) {
|
|
6122
|
-
if (!a.includes(item))
|
|
6123
|
-
return false;
|
|
6124
|
-
}
|
|
6125
|
-
return true;
|
|
6126
|
-
};
|
|
6127
6128
|
/**
|
|
6128
6129
|
* Creates an Observable from the given subject by piping to the
|
|
6129
6130
|
* `distinctUntilChanged()` operator.
|
|
@@ -6131,7 +6132,7 @@ class CallState {
|
|
|
6131
6132
|
const duc = (subject, comparator) => subject.pipe(distinctUntilChanged(comparator));
|
|
6132
6133
|
// primitive values should only emit once the value they hold changes
|
|
6133
6134
|
this.anonymousParticipantCount$ = duc(this.anonymousParticipantCountSubject);
|
|
6134
|
-
this.blockedUserIds$ = duc(this.blockedUserIdsSubject,
|
|
6135
|
+
this.blockedUserIds$ = duc(this.blockedUserIdsSubject, isShallowArrayEqual);
|
|
6135
6136
|
this.backstage$ = duc(this.backstageSubject);
|
|
6136
6137
|
this.callingState$ = duc(this.callingStateSubject);
|
|
6137
6138
|
this.ownCapabilities$ = combineLatest([
|
|
@@ -6160,7 +6161,7 @@ class CallState {
|
|
|
6160
6161
|
}
|
|
6161
6162
|
}
|
|
6162
6163
|
return nextCapabilities;
|
|
6163
|
-
}), distinctUntilChanged(
|
|
6164
|
+
}), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
|
|
6164
6165
|
this.participantCount$ = duc(this.participantCountSubject);
|
|
6165
6166
|
this.recording$ = duc(this.recordingSubject);
|
|
6166
6167
|
this.individualRecording$ = duc(this.individualRecordingSubject);
|
|
@@ -8471,6 +8472,7 @@ class BasePeerConnection {
|
|
|
8471
8472
|
try {
|
|
8472
8473
|
this.onPeerConnectionStateChange?.({
|
|
8473
8474
|
peerType: this.peerType,
|
|
8475
|
+
iceConnectionState: this.pc.iceConnectionState,
|
|
8474
8476
|
...event,
|
|
8475
8477
|
});
|
|
8476
8478
|
}
|
|
@@ -8925,6 +8927,7 @@ class Publisher extends BasePeerConnection {
|
|
|
8925
8927
|
clonedTracks = new Set();
|
|
8926
8928
|
publishOptions;
|
|
8927
8929
|
selfSubEnabled;
|
|
8930
|
+
transceiverLockKey = `pub.tx.${this.lock}`;
|
|
8928
8931
|
/**
|
|
8929
8932
|
* Constructs a new `Publisher` instance.
|
|
8930
8933
|
*/
|
|
@@ -8980,32 +8983,34 @@ class Publisher extends BasePeerConnection {
|
|
|
8980
8983
|
* @param options the publish options to use.
|
|
8981
8984
|
*/
|
|
8982
8985
|
publish = async (track, trackType, options = {}) => {
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
if (publishOption.trackType !== trackType)
|
|
8988
|
-
continue;
|
|
8989
|
-
// create a clone of the track as otherwise the same trackId will
|
|
8990
|
-
// appear in the SDP in multiple transceivers
|
|
8991
|
-
const trackToPublish = this.cloneTrack(track);
|
|
8992
|
-
const bundle = this.transceiverCache.get(publishOption);
|
|
8993
|
-
if (!bundle) {
|
|
8994
|
-
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8986
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
8987
|
+
const publishOptions = this.publishOptions.filter((o) => o.trackType === trackType);
|
|
8988
|
+
if (!publishOptions.length) {
|
|
8989
|
+
throw new Error(`No publish options found for ${TrackType[trackType]}`);
|
|
8995
8990
|
}
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
8991
|
+
for (const publishOption of publishOptions) {
|
|
8992
|
+
// create a clone of the track as otherwise the same trackId will
|
|
8993
|
+
// appear in the SDP in multiple transceivers
|
|
8994
|
+
const trackToPublish = this.cloneTrack(track);
|
|
8995
|
+
const bundle = this.transceiverCache.get(publishOption);
|
|
8996
|
+
if (!bundle) {
|
|
8997
|
+
await this.addTransceiver(trackToPublish, publishOption, options);
|
|
8998
|
+
}
|
|
8999
|
+
else {
|
|
9000
|
+
const previousTrack = bundle.transceiver.sender.track;
|
|
9001
|
+
await this.updateTransceiver(bundle, trackToPublish, options);
|
|
9002
|
+
if (!isReactNative()) {
|
|
9003
|
+
this.stopTrack(previousTrack);
|
|
9004
|
+
}
|
|
9001
9005
|
}
|
|
9002
9006
|
}
|
|
9003
|
-
}
|
|
9007
|
+
});
|
|
9004
9008
|
};
|
|
9005
9009
|
/**
|
|
9006
9010
|
* Adds a new transceiver carrying the given track to the peer connection.
|
|
9007
9011
|
*/
|
|
9008
9012
|
addTransceiver = async (track, publishOption, options) => {
|
|
9013
|
+
const trackType = publishOption.trackType;
|
|
9009
9014
|
const encodings = isAudioTrackType(publishOption.trackType)
|
|
9010
9015
|
? computeAudioLayers(publishOption, options)
|
|
9011
9016
|
: computeVideoLayers(track, publishOption);
|
|
@@ -9016,15 +9021,17 @@ class Publisher extends BasePeerConnection {
|
|
|
9016
9021
|
direction: 'sendonly',
|
|
9017
9022
|
sendEncodings,
|
|
9018
9023
|
});
|
|
9024
|
+
// claim the cache entry in the same task that created the transceiver:
|
|
9025
|
+
// an await in between would let a concurrent caller add a second one
|
|
9026
|
+
// for the same publish option
|
|
9027
|
+
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9028
|
+
this.trackIdToTrackType.set(track.id, trackType);
|
|
9029
|
+
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9019
9030
|
const params = transceiver.sender.getParameters();
|
|
9020
9031
|
params.degradationPreference =
|
|
9021
9032
|
toRTCDegradationPreference(publishOption.degradationPreference) ??
|
|
9022
9033
|
'maintain-framerate';
|
|
9023
9034
|
await transceiver.sender.setParameters(params);
|
|
9024
|
-
const trackType = publishOption.trackType;
|
|
9025
|
-
this.logger.debug(`Added ${TrackType[trackType]} transceiver`);
|
|
9026
|
-
this.transceiverCache.add({ publishOption, transceiver, options });
|
|
9027
|
-
this.trackIdToTrackType.set(track.id, trackType);
|
|
9028
9035
|
await this.negotiate();
|
|
9029
9036
|
};
|
|
9030
9037
|
/**
|
|
@@ -9081,33 +9088,35 @@ class Publisher extends BasePeerConnection {
|
|
|
9081
9088
|
* Synchronizes the current Publisher state with the provided publish options.
|
|
9082
9089
|
*/
|
|
9083
9090
|
syncPublishOptions = async () => {
|
|
9084
|
-
|
|
9085
|
-
|
|
9086
|
-
const
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
9090
|
-
|
|
9091
|
-
|
|
9092
|
-
i.
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
const
|
|
9103
|
-
|
|
9104
|
-
option.
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9091
|
+
return withoutConcurrency(this.transceiverLockKey, async () => {
|
|
9092
|
+
// enable publishing with new options -> [av1, vp9]
|
|
9093
|
+
for (const publishOption of this.publishOptions) {
|
|
9094
|
+
const { trackType } = publishOption;
|
|
9095
|
+
if (!this.isPublishing(trackType))
|
|
9096
|
+
continue;
|
|
9097
|
+
if (this.transceiverCache.has(publishOption))
|
|
9098
|
+
continue;
|
|
9099
|
+
const item = this.transceiverCache.find((i) => !!i.transceiver.sender.track &&
|
|
9100
|
+
i.publishOption.trackType === trackType);
|
|
9101
|
+
if (!item)
|
|
9102
|
+
continue;
|
|
9103
|
+
// take the track from the existing transceiver for the same track type,
|
|
9104
|
+
// clone it and publish it with the new publish options
|
|
9105
|
+
const track = this.cloneTrack(item.transceiver.sender.track);
|
|
9106
|
+
await this.addTransceiver(track, publishOption, item.options);
|
|
9107
|
+
}
|
|
9108
|
+
// stop publishing with options not required anymore -> [vp9]
|
|
9109
|
+
for (const item of this.transceiverCache.items()) {
|
|
9110
|
+
const { publishOption, transceiver } = item;
|
|
9111
|
+
const hasPublishOption = this.publishOptions.some((option) => option.id === publishOption.id &&
|
|
9112
|
+
option.trackType === publishOption.trackType);
|
|
9113
|
+
if (hasPublishOption)
|
|
9114
|
+
continue;
|
|
9115
|
+
// it is safe to stop the track here, it is a clone
|
|
9116
|
+
this.stopTrack(transceiver.sender.track);
|
|
9117
|
+
await this.updateTransceiver(item, null);
|
|
9118
|
+
}
|
|
9119
|
+
});
|
|
9111
9120
|
};
|
|
9112
9121
|
/**
|
|
9113
9122
|
* Returns true if the given track type is currently being published to the SFU.
|
|
@@ -9275,12 +9284,23 @@ class Publisher extends BasePeerConnection {
|
|
|
9275
9284
|
negotiate = async (options) => {
|
|
9276
9285
|
return withoutConcurrency(`publisher.negotiate.${this.lock}`, async () => {
|
|
9277
9286
|
const offer = await this.pc.createOffer(options);
|
|
9278
|
-
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9279
|
-
if (!tracks.length)
|
|
9280
|
-
throw new Error(`Can't negotiate without any tracks`);
|
|
9281
9287
|
try {
|
|
9282
9288
|
this.isIceRestarting = options?.iceRestart ?? false;
|
|
9283
9289
|
await this.pc.setLocalDescription(offer);
|
|
9290
|
+
// the announced tracks have to be read after `setLocalDescription`.
|
|
9291
|
+
// `transceiver.mid` is null until the offer is applied, and reading the
|
|
9292
|
+
// tracks earlier makes `extractMid` guess the mid from the offer SDP.
|
|
9293
|
+
// That guess goes stale after a rolled-back negotiation, and a mid that
|
|
9294
|
+
// doesn't match the m-section carrying the track stops the SFU from
|
|
9295
|
+
// correlating the two: it then answers with every offered codec instead
|
|
9296
|
+
// of the announced one, and we publish the first codec of that list.
|
|
9297
|
+
// Per https://www.w3.org/TR/webrtc/#set-description, applying a local
|
|
9298
|
+
// offer associates the transceiver with its m-section and sets
|
|
9299
|
+
// `[[Mid]]` to `[[JsepMid]]`; a rollback disassociates a transceiver
|
|
9300
|
+
// that wasn't associated before and resets both slots to null.
|
|
9301
|
+
const tracks = this.getAnnouncedTracks(offer.sdp);
|
|
9302
|
+
if (!tracks.length)
|
|
9303
|
+
throw new Error(`Can't negotiate without any tracks`);
|
|
9284
9304
|
const { sdp: baseSdp = '' } = offer;
|
|
9285
9305
|
const { dangerouslyForceCodec, dangerouslySetStartBitrateFactor, fmtpLine, } = this.clientPublishOptions || {};
|
|
9286
9306
|
let sdp = dangerouslyForceCodec
|
|
@@ -10905,34 +10925,67 @@ class AudioBindingsWatchdog {
|
|
|
10905
10925
|
class BlockedAudioTracker {
|
|
10906
10926
|
logger = videoLoggerSystem.getLogger('BlockedAudioTracker');
|
|
10907
10927
|
tracer;
|
|
10908
|
-
blockedElementsSubject = new BehaviorSubject(
|
|
10928
|
+
blockedElementsSubject = new BehaviorSubject([]);
|
|
10909
10929
|
/**
|
|
10910
10930
|
* Whether the browser's autoplay policy is blocking audio playback.
|
|
10911
10931
|
* Will be `true` when at least one audio element is currently blocked.
|
|
10912
10932
|
* Use {@link resumeAudio} within a user gesture to unblock.
|
|
10913
10933
|
*/
|
|
10914
|
-
autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.
|
|
10934
|
+
autoplayBlocked$ = this.blockedElementsSubject.pipe(map((elements) => elements.length > 0), distinctUntilChanged());
|
|
10935
|
+
/**
|
|
10936
|
+
* The list of participant `sessionId`s whose audio element is currently
|
|
10937
|
+
* blocked by the browser's autoplay policy. Only some participants may be
|
|
10938
|
+
* blocked (e.g. one joined while a gesture was active and a later one was
|
|
10939
|
+
* not), so use this to render a per-participant affordance rather than a
|
|
10940
|
+
* call-wide one. Call {@link resumeAudio} within a user gesture to unblock
|
|
10941
|
+
* all of them.
|
|
10942
|
+
*
|
|
10943
|
+
* Session ids are registered together with the audio element.
|
|
10944
|
+
*/
|
|
10945
|
+
blockedSessionIds$ = this.blockedElementsSubject.pipe(map((elements) => {
|
|
10946
|
+
const ids = [];
|
|
10947
|
+
elements.forEach(({ sessionId }) => {
|
|
10948
|
+
if (sessionId && !ids.includes(sessionId))
|
|
10949
|
+
ids.push(sessionId);
|
|
10950
|
+
});
|
|
10951
|
+
return ids;
|
|
10952
|
+
}), distinctUntilChanged(isShallowArrayEqual), shareReplay({ bufferSize: 1, refCount: true }));
|
|
10915
10953
|
constructor(tracer) {
|
|
10916
10954
|
this.tracer = tracer;
|
|
10917
10955
|
}
|
|
10918
10956
|
/**
|
|
10919
10957
|
* Registers an audio element as blocked by the browser's autoplay policy.
|
|
10920
10958
|
*/
|
|
10921
|
-
markBlocked = (audioElement, blocked) => {
|
|
10959
|
+
markBlocked = (audioElement, blocked, sessionId) => {
|
|
10922
10960
|
setCurrentValue(this.blockedElementsSubject, (elements) => {
|
|
10923
|
-
if (blocked)
|
|
10924
|
-
elements.
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10961
|
+
if (!blocked) {
|
|
10962
|
+
return elements.filter(({ element }) => element !== audioElement);
|
|
10963
|
+
}
|
|
10964
|
+
const existing = elements.find(({ element }) => element === audioElement);
|
|
10965
|
+
if (existing) {
|
|
10966
|
+
return elements.map((entry) => entry.element === audioElement
|
|
10967
|
+
? { element: audioElement, sessionId: sessionId ?? entry.sessionId }
|
|
10968
|
+
: entry);
|
|
10969
|
+
}
|
|
10970
|
+
return [...elements, { element: audioElement, sessionId }];
|
|
10928
10971
|
});
|
|
10929
10972
|
};
|
|
10973
|
+
/**
|
|
10974
|
+
* Clears all tracked elements. The tracker stays usable afterwards.
|
|
10975
|
+
*/
|
|
10976
|
+
reset = () => {
|
|
10977
|
+
if (this.blockedElementsSubject.getValue().length > 0) {
|
|
10978
|
+
this.blockedElementsSubject.next([]);
|
|
10979
|
+
}
|
|
10980
|
+
};
|
|
10930
10981
|
/**
|
|
10931
10982
|
* Returns whether the given audio element is currently flagged as blocked
|
|
10932
10983
|
* by the browser's autoplay policy.
|
|
10933
10984
|
*/
|
|
10934
10985
|
isBlocked = (audioElement) => {
|
|
10935
|
-
return this.blockedElementsSubject
|
|
10986
|
+
return this.blockedElementsSubject
|
|
10987
|
+
.getValue()
|
|
10988
|
+
.some(({ element }) => element === audioElement);
|
|
10936
10989
|
};
|
|
10937
10990
|
/**
|
|
10938
10991
|
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
@@ -10941,17 +10994,18 @@ class BlockedAudioTracker {
|
|
|
10941
10994
|
resumeAudio = async () => {
|
|
10942
10995
|
this.tracer.trace('resumeAudio', null);
|
|
10943
10996
|
await setCurrentValueAsync(this.blockedElementsSubject, async (elements) => {
|
|
10944
|
-
|
|
10997
|
+
let next = elements;
|
|
10998
|
+
await Promise.all(elements.map(async ({ element }) => {
|
|
10945
10999
|
try {
|
|
10946
11000
|
if (element.srcObject)
|
|
10947
11001
|
await timeboxed([element.play()], 2000);
|
|
10948
|
-
|
|
11002
|
+
next = next.filter((entry) => entry.element !== element);
|
|
10949
11003
|
}
|
|
10950
11004
|
catch (err) {
|
|
10951
11005
|
this.logger.warn(`Can't resume audio for element`, element, err);
|
|
10952
11006
|
}
|
|
10953
11007
|
}));
|
|
10954
|
-
return
|
|
11008
|
+
return next;
|
|
10955
11009
|
});
|
|
10956
11010
|
};
|
|
10957
11011
|
}
|
|
@@ -11142,8 +11196,10 @@ class MediaPlaybackWatchdog {
|
|
|
11142
11196
|
tracer;
|
|
11143
11197
|
controller = new AbortController();
|
|
11144
11198
|
pendingTimer;
|
|
11199
|
+
settleTimer;
|
|
11145
11200
|
attempt = 0;
|
|
11146
11201
|
disposed = false;
|
|
11202
|
+
stopped = false;
|
|
11147
11203
|
constructor(opts) {
|
|
11148
11204
|
this.element = opts.element;
|
|
11149
11205
|
this.kind = opts.kind;
|
|
@@ -11164,33 +11220,49 @@ class MediaPlaybackWatchdog {
|
|
|
11164
11220
|
return;
|
|
11165
11221
|
this.disposed = true;
|
|
11166
11222
|
this.controller.abort();
|
|
11223
|
+
this.clearTimers();
|
|
11224
|
+
};
|
|
11225
|
+
clearTimers = () => {
|
|
11167
11226
|
if (this.pendingTimer)
|
|
11168
11227
|
clearTimeout(this.pendingTimer);
|
|
11169
11228
|
this.pendingTimer = undefined;
|
|
11229
|
+
if (this.settleTimer)
|
|
11230
|
+
clearTimeout(this.settleTimer);
|
|
11231
|
+
this.settleTimer = undefined;
|
|
11170
11232
|
};
|
|
11171
11233
|
onPlaying = () => {
|
|
11172
|
-
if (this.attempt > 0) {
|
|
11173
|
-
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11174
|
-
kind: this.kind,
|
|
11175
|
-
attempts: this.attempt,
|
|
11176
|
-
});
|
|
11177
|
-
}
|
|
11178
|
-
this.attempt = 0;
|
|
11179
11234
|
if (this.pendingTimer)
|
|
11180
11235
|
clearTimeout(this.pendingTimer);
|
|
11181
11236
|
this.pendingTimer = undefined;
|
|
11237
|
+
if (this.attempt === 0 || this.settleTimer)
|
|
11238
|
+
return;
|
|
11239
|
+
this.settleTimer ??= setTimeout(this.onSettled, 1000);
|
|
11240
|
+
};
|
|
11241
|
+
onSettled = () => {
|
|
11242
|
+
this.settleTimer = undefined;
|
|
11243
|
+
if (this.element.paused)
|
|
11244
|
+
return;
|
|
11245
|
+
this.tracer.trace('mediaPlayback.recover.success', {
|
|
11246
|
+
kind: this.kind,
|
|
11247
|
+
attempts: this.attempt,
|
|
11248
|
+
});
|
|
11249
|
+
this.attempt = 0;
|
|
11250
|
+
this.stopped = false;
|
|
11182
11251
|
};
|
|
11183
11252
|
onPauseOrSuspend = (event) => {
|
|
11184
|
-
if (this.disposed)
|
|
11253
|
+
if (this.disposed || this.stopped)
|
|
11185
11254
|
return;
|
|
11186
11255
|
this.tracer.trace('mediaPlayback.paused', {
|
|
11187
11256
|
kind: this.kind,
|
|
11188
11257
|
reason: event.type,
|
|
11189
11258
|
});
|
|
11259
|
+
if (this.settleTimer)
|
|
11260
|
+
clearTimeout(this.settleTimer);
|
|
11261
|
+
this.settleTimer = undefined;
|
|
11190
11262
|
this.scheduleRecovery();
|
|
11191
11263
|
};
|
|
11192
11264
|
scheduleRecovery = () => {
|
|
11193
|
-
if (this.disposed || this.pendingTimer)
|
|
11265
|
+
if (this.disposed || this.stopped || this.pendingTimer)
|
|
11194
11266
|
return;
|
|
11195
11267
|
const skipReason = this.computeSkipReason();
|
|
11196
11268
|
if (skipReason) {
|
|
@@ -11200,6 +11272,15 @@ class MediaPlaybackWatchdog {
|
|
|
11200
11272
|
});
|
|
11201
11273
|
return;
|
|
11202
11274
|
}
|
|
11275
|
+
if (this.attempt >= 10) {
|
|
11276
|
+
this.stopped = true;
|
|
11277
|
+
this.clearTimers();
|
|
11278
|
+
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11279
|
+
kind: this.kind,
|
|
11280
|
+
attempts: this.attempt,
|
|
11281
|
+
});
|
|
11282
|
+
return;
|
|
11283
|
+
}
|
|
11203
11284
|
const delay = this.attempt === 0 ? 0 : retryInterval(this.attempt);
|
|
11204
11285
|
this.pendingTimer = setTimeout(this.attemptPlay, delay);
|
|
11205
11286
|
};
|
|
@@ -11235,13 +11316,6 @@ class MediaPlaybackWatchdog {
|
|
|
11235
11316
|
if (this.disposed)
|
|
11236
11317
|
return;
|
|
11237
11318
|
this.logger.warn(`Failed to recover ${this.kind} playback`, err);
|
|
11238
|
-
if (this.attempt >= 10) {
|
|
11239
|
-
this.tracer.trace('mediaPlayback.recover.giveUp', {
|
|
11240
|
-
kind: this.kind,
|
|
11241
|
-
attempts: this.attempt,
|
|
11242
|
-
});
|
|
11243
|
-
return;
|
|
11244
|
-
}
|
|
11245
11319
|
this.scheduleRecovery();
|
|
11246
11320
|
}
|
|
11247
11321
|
};
|
|
@@ -11483,6 +11557,12 @@ class DynascaleManager {
|
|
|
11483
11557
|
let sourceNode = undefined;
|
|
11484
11558
|
let gainNode = undefined;
|
|
11485
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);
|
|
11486
11566
|
const isAudioTrack = trackType === 'audioTrack';
|
|
11487
11567
|
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
11488
11568
|
const updateMediaStreamSubscription = participant$
|
|
@@ -11496,7 +11576,7 @@ class DynascaleManager {
|
|
|
11496
11576
|
audioWatchdog?.dispose();
|
|
11497
11577
|
audioWatchdog = undefined;
|
|
11498
11578
|
if (!source) {
|
|
11499
|
-
|
|
11579
|
+
clearBlockedAudio();
|
|
11500
11580
|
return;
|
|
11501
11581
|
}
|
|
11502
11582
|
// Safari has a special quirk that prevents playing audio until the user
|
|
@@ -11524,7 +11604,7 @@ class DynascaleManager {
|
|
|
11524
11604
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
11525
11605
|
if (e.name === 'NotAllowedError') {
|
|
11526
11606
|
this.tracer.trace('audioPlaybackBlocked', null);
|
|
11527
|
-
this.blockedAudioTracker.markBlocked(audioElement, true);
|
|
11607
|
+
this.blockedAudioTracker.markBlocked(audioElement, true, sessionId);
|
|
11528
11608
|
}
|
|
11529
11609
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
11530
11610
|
});
|
|
@@ -11557,7 +11637,8 @@ class DynascaleManager {
|
|
|
11557
11637
|
});
|
|
11558
11638
|
audioElement.autoplay = true;
|
|
11559
11639
|
return () => {
|
|
11560
|
-
|
|
11640
|
+
audioElement.removeEventListener('playing', clearBlockedAudio);
|
|
11641
|
+
clearBlockedAudio();
|
|
11561
11642
|
sinkIdSubscription?.unsubscribe();
|
|
11562
11643
|
volumeSubscription.unsubscribe();
|
|
11563
11644
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -14732,6 +14813,7 @@ class Call {
|
|
|
14732
14813
|
this.screenShare = new ScreenShareManager(this);
|
|
14733
14814
|
this.trackSubscriptionManager = new TrackSubscriptionManager(this.state, this.tracer);
|
|
14734
14815
|
this.blockedAudioTracker = new BlockedAudioTracker(this.tracer);
|
|
14816
|
+
this.leaveCallHooks.add(this.blockedAudioTracker.reset);
|
|
14735
14817
|
if (typeof document !== 'undefined') {
|
|
14736
14818
|
this.audioBindingsWatchdog = new AudioBindingsWatchdog(this.state, this.tracer);
|
|
14737
14819
|
this.viewportTracker = new ViewportTracker(this.state);
|
|
@@ -18430,11 +18512,11 @@ class StreamClient {
|
|
|
18430
18512
|
return await this.wsConnection.connect(this.defaultWSTimeout);
|
|
18431
18513
|
};
|
|
18432
18514
|
getSdkVersion = () => this.options.clientAppIdentifier?.sdkVersion ||
|
|
18433
|
-
"1.
|
|
18515
|
+
"1.57.0";
|
|
18434
18516
|
getUserAgent = () => {
|
|
18435
18517
|
if (!this.cachedUserAgent) {
|
|
18436
18518
|
const { clientAppIdentifier = {} } = this.options;
|
|
18437
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
18519
|
+
const { sdkName = 'js', sdkVersion = "1.57.0", ...extras } = clientAppIdentifier;
|
|
18438
18520
|
this.cachedUserAgent = [
|
|
18439
18521
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
18440
18522
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|
|
@@ -18771,8 +18853,8 @@ class ClientEventReporter {
|
|
|
18771
18853
|
applyErrorIfAbsent(this.wsPairs.get(cid), stageError);
|
|
18772
18854
|
this.failCoordinator(cid);
|
|
18773
18855
|
this.failWs(cid);
|
|
18774
|
-
this.emitPeerConnectionFailure(cid, 'publish', code, reason
|
|
18775
|
-
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason
|
|
18856
|
+
this.emitPeerConnectionFailure(cid, 'publish', code, reason);
|
|
18857
|
+
this.emitPeerConnectionFailure(cid, 'subscribe', code, reason);
|
|
18776
18858
|
}
|
|
18777
18859
|
catch (err) {
|
|
18778
18860
|
this.logger.warn('Failed to report abort', err);
|
|
@@ -18784,7 +18866,7 @@ class ClientEventReporter {
|
|
|
18784
18866
|
if (this.wsPairs.get(cid))
|
|
18785
18867
|
this.failWs(cid);
|
|
18786
18868
|
for (const role of ['publish', 'subscribe']) {
|
|
18787
|
-
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt'
|
|
18869
|
+
this.emitPeerConnectionFailure(cid, role, 'CLIENT_ABORTED', 'superseded by a new join attempt');
|
|
18788
18870
|
}
|
|
18789
18871
|
};
|
|
18790
18872
|
emitJoinInitiated = (cid) => {
|
|
@@ -18946,21 +19028,24 @@ class ClientEventReporter {
|
|
|
18946
19028
|
};
|
|
18947
19029
|
onPeerConnectionStateChange = (cid, event) => {
|
|
18948
19030
|
const role = event.peerType === PeerType.SUBSCRIBER ? 'subscribe' : 'publish';
|
|
18949
|
-
|
|
18950
|
-
|
|
19031
|
+
const pair = this.peerConnectionPairs.get(pcKey(cid, role));
|
|
19032
|
+
if (pair)
|
|
19033
|
+
pair.lastIceState = event.iceConnectionState;
|
|
19034
|
+
if (event.stateType === 'ice') {
|
|
19035
|
+
if (event.state === 'failed') {
|
|
19036
|
+
this.emitPeerConnectionFailure(cid, role, 'ICE_CONNECTIVITY_FAILED', 'ICE connectivity checks failed');
|
|
19037
|
+
}
|
|
18951
19038
|
return;
|
|
18952
19039
|
}
|
|
18953
|
-
if (event.
|
|
18954
|
-
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed'
|
|
19040
|
+
if (event.state === 'failed') {
|
|
19041
|
+
this.emitPeerConnectionFailure(cid, role, 'DTLS_CONNECTIVITY_FAILED', 'DTLS connectivity checks failed');
|
|
18955
19042
|
return;
|
|
18956
19043
|
}
|
|
18957
|
-
if (event.stateType !== 'peerConnection')
|
|
18958
|
-
return;
|
|
18959
19044
|
switch (event.state) {
|
|
18960
19045
|
case 'connecting':
|
|
18961
|
-
if (
|
|
19046
|
+
if (pair)
|
|
18962
19047
|
return;
|
|
18963
|
-
this.openPeerConnectionPair(cid, role);
|
|
19048
|
+
this.openPeerConnectionPair(cid, role, event.iceConnectionState);
|
|
18964
19049
|
break;
|
|
18965
19050
|
case 'connected':
|
|
18966
19051
|
this.emitPeerConnectionSuccess(cid, role);
|
|
@@ -18968,7 +19053,7 @@ class ClientEventReporter {
|
|
|
18968
19053
|
break;
|
|
18969
19054
|
}
|
|
18970
19055
|
};
|
|
18971
|
-
openPeerConnectionPair = (cid, role) => {
|
|
19056
|
+
openPeerConnectionPair = (cid, role, iceConnectionState) => {
|
|
18972
19057
|
const key = pcKey(cid, role);
|
|
18973
19058
|
const pair = {
|
|
18974
19059
|
sid: generateUUIDv4(),
|
|
@@ -18978,6 +19063,7 @@ class ClientEventReporter {
|
|
|
18978
19063
|
sfuId: this.getSfuId(cid),
|
|
18979
19064
|
userSessionId: this.getUserSessionId(cid),
|
|
18980
19065
|
wasPreviouslyConnected: this.pcEverConnected.get(key) === true,
|
|
19066
|
+
lastIceState: iceConnectionState,
|
|
18981
19067
|
};
|
|
18982
19068
|
this.peerConnectionPairs.set(key, pair);
|
|
18983
19069
|
this.sendForCall(cid, {
|
|
@@ -19013,7 +19099,7 @@ class ClientEventReporter {
|
|
|
19013
19099
|
});
|
|
19014
19100
|
this.peerConnectionPairs.delete(key);
|
|
19015
19101
|
};
|
|
19016
|
-
emitPeerConnectionFailure = (cid, role, code, reason
|
|
19102
|
+
emitPeerConnectionFailure = (cid, role, code, reason) => {
|
|
19017
19103
|
const key = pcKey(cid, role);
|
|
19018
19104
|
const pair = this.peerConnectionPairs.get(key);
|
|
19019
19105
|
if (!pair)
|
|
@@ -19031,7 +19117,7 @@ class ClientEventReporter {
|
|
|
19031
19117
|
outcome: 'failure',
|
|
19032
19118
|
retry_count_attempt: 0,
|
|
19033
19119
|
elapsed_time: Date.now() - pair.startedAt,
|
|
19034
|
-
ice_state:
|
|
19120
|
+
ice_state: toReportedIceState(pair.lastIceState),
|
|
19035
19121
|
retry_failure_reason: reason,
|
|
19036
19122
|
retry_failure_code: code,
|
|
19037
19123
|
});
|
|
@@ -19097,6 +19183,17 @@ class ClientEventReporter {
|
|
|
19097
19183
|
return false;
|
|
19098
19184
|
};
|
|
19099
19185
|
}
|
|
19186
|
+
const toReportedIceState = (state) => {
|
|
19187
|
+
switch (state) {
|
|
19188
|
+
case 'connected':
|
|
19189
|
+
case 'completed':
|
|
19190
|
+
return 'CONNECTED';
|
|
19191
|
+
case 'failed':
|
|
19192
|
+
return 'FAILED';
|
|
19193
|
+
default:
|
|
19194
|
+
return 'NOT_CONNECTED';
|
|
19195
|
+
}
|
|
19196
|
+
};
|
|
19100
19197
|
const readPermissionStatus = (permission) => {
|
|
19101
19198
|
const state = getCurrentValue(permission.asStateObservable());
|
|
19102
19199
|
switch (state) {
|