@stream-io/video-client 1.44.4 → 1.44.5
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 +6 -0
- package/dist/index.browser.es.js +109 -16
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +109 -16
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +109 -16
- package/dist/index.es.js.map +1 -1
- package/dist/src/helpers/AudioBindingsWatchdog.d.ts +37 -0
- package/dist/src/helpers/DynascaleManager.d.ts +3 -1
- package/package.json +1 -1
- package/src/devices/devices.ts +1 -3
- package/src/helpers/AudioBindingsWatchdog.ts +118 -0
- package/src/helpers/DynascaleManager.ts +22 -24
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +325 -0
- package/src/helpers/__tests__/DynascaleManager.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.44.5](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.44.4...@stream-io/video-client-1.44.5) (2026-03-27)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- make WebAudio opt-in, add AudioBindingsWatchdog ([#2171](https://github.com/GetStream/stream-video-js/issues/2171)) ([8d00f48](https://github.com/GetStream/stream-video-js/commit/8d00f485a37fec23dca340d32738a3cb1f7f325a))
|
|
10
|
+
|
|
5
11
|
## [1.44.4](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.44.3...@stream-io/video-client-1.44.4) (2026-03-20)
|
|
6
12
|
|
|
7
13
|
- trace device permission state transitions ([#2168](https://github.com/GetStream/stream-video-js/issues/2168)) ([e4203a3](https://github.com/GetStream/stream-video-js/commit/e4203a34cad1c90d1bc5612fc379dd1f0f0ebe5d))
|
package/dist/index.browser.es.js
CHANGED
|
@@ -6283,7 +6283,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6283
6283
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6284
6284
|
};
|
|
6285
6285
|
|
|
6286
|
-
const version = "1.44.
|
|
6286
|
+
const version = "1.44.5";
|
|
6287
6287
|
const [major, minor, patch] = version.split('.');
|
|
6288
6288
|
let sdkInfo = {
|
|
6289
6289
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9491,6 +9491,96 @@ class ViewportTracker {
|
|
|
9491
9491
|
}
|
|
9492
9492
|
}
|
|
9493
9493
|
|
|
9494
|
+
const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
|
|
9495
|
+
/**
|
|
9496
|
+
* Tracks audio element bindings and periodically warns about
|
|
9497
|
+
* remote participants whose audio streams have no bound element.
|
|
9498
|
+
*/
|
|
9499
|
+
class AudioBindingsWatchdog {
|
|
9500
|
+
constructor(state, tracer) {
|
|
9501
|
+
this.state = state;
|
|
9502
|
+
this.tracer = tracer;
|
|
9503
|
+
this.bindings = new Map();
|
|
9504
|
+
this.enabled = true;
|
|
9505
|
+
this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
|
|
9506
|
+
/**
|
|
9507
|
+
* Registers an audio element binding for the given session and track type.
|
|
9508
|
+
* Warns if a different element is already bound to the same key.
|
|
9509
|
+
*/
|
|
9510
|
+
this.register = (audioElement, sessionId, trackType) => {
|
|
9511
|
+
const key = toBindingKey(sessionId, trackType);
|
|
9512
|
+
const existing = this.bindings.get(key);
|
|
9513
|
+
if (existing && existing !== audioElement) {
|
|
9514
|
+
this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
|
|
9515
|
+
this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
|
|
9516
|
+
}
|
|
9517
|
+
this.bindings.set(key, audioElement);
|
|
9518
|
+
};
|
|
9519
|
+
/**
|
|
9520
|
+
* Removes the audio element binding for the given session and track type.
|
|
9521
|
+
*/
|
|
9522
|
+
this.unregister = (sessionId, trackType) => {
|
|
9523
|
+
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
9524
|
+
};
|
|
9525
|
+
/**
|
|
9526
|
+
* Enables or disables the watchdog.
|
|
9527
|
+
* When disabled, the periodic check stops but bindings are still tracked.
|
|
9528
|
+
*/
|
|
9529
|
+
this.setEnabled = (enabled) => {
|
|
9530
|
+
this.enabled = enabled;
|
|
9531
|
+
if (enabled) {
|
|
9532
|
+
this.start();
|
|
9533
|
+
}
|
|
9534
|
+
else {
|
|
9535
|
+
this.stop();
|
|
9536
|
+
}
|
|
9537
|
+
};
|
|
9538
|
+
/**
|
|
9539
|
+
* Stops the watchdog and unsubscribes from callingState changes.
|
|
9540
|
+
*/
|
|
9541
|
+
this.dispose = () => {
|
|
9542
|
+
this.stop();
|
|
9543
|
+
this.unsubscribeCallingState();
|
|
9544
|
+
};
|
|
9545
|
+
this.start = () => {
|
|
9546
|
+
clearInterval(this.watchdogInterval);
|
|
9547
|
+
this.watchdogInterval = setInterval(() => {
|
|
9548
|
+
const danglingUserIds = [];
|
|
9549
|
+
for (const p of this.state.participants) {
|
|
9550
|
+
if (p.isLocalParticipant)
|
|
9551
|
+
continue;
|
|
9552
|
+
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
9553
|
+
if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
|
|
9554
|
+
danglingUserIds.push(userId);
|
|
9555
|
+
}
|
|
9556
|
+
if (screenShareAudioStream &&
|
|
9557
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
9558
|
+
danglingUserIds.push(userId);
|
|
9559
|
+
}
|
|
9560
|
+
}
|
|
9561
|
+
if (danglingUserIds.length > 0) {
|
|
9562
|
+
const key = 'audioBinding.danglingWarning';
|
|
9563
|
+
this.tracer.traceOnce(key, key, danglingUserIds);
|
|
9564
|
+
this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
|
|
9565
|
+
}
|
|
9566
|
+
}, 3000);
|
|
9567
|
+
};
|
|
9568
|
+
this.stop = () => {
|
|
9569
|
+
clearInterval(this.watchdogInterval);
|
|
9570
|
+
};
|
|
9571
|
+
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
9572
|
+
if (!this.enabled)
|
|
9573
|
+
return;
|
|
9574
|
+
if (callingState !== CallingState.JOINED) {
|
|
9575
|
+
this.stop();
|
|
9576
|
+
}
|
|
9577
|
+
else {
|
|
9578
|
+
this.start();
|
|
9579
|
+
}
|
|
9580
|
+
});
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
|
|
9494
9584
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
9495
9585
|
videoTrack: VisibilityState.UNKNOWN,
|
|
9496
9586
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
@@ -9516,7 +9606,7 @@ class DynascaleManager {
|
|
|
9516
9606
|
*/
|
|
9517
9607
|
this.viewportTracker = new ViewportTracker();
|
|
9518
9608
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9519
|
-
this.useWebAudio =
|
|
9609
|
+
this.useWebAudio = false;
|
|
9520
9610
|
this.pendingSubscriptionsUpdate = null;
|
|
9521
9611
|
this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
|
|
9522
9612
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
@@ -9548,7 +9638,8 @@ class DynascaleManager {
|
|
|
9548
9638
|
if (this.pendingSubscriptionsUpdate) {
|
|
9549
9639
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9550
9640
|
}
|
|
9551
|
-
|
|
9641
|
+
this.audioBindingsWatchdog?.dispose();
|
|
9642
|
+
const context = this.audioContext;
|
|
9552
9643
|
if (context && context.state !== 'closed') {
|
|
9553
9644
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
9554
9645
|
await context.close();
|
|
@@ -9747,12 +9838,13 @@ class DynascaleManager {
|
|
|
9747
9838
|
lastDimensions = currentDimensions;
|
|
9748
9839
|
});
|
|
9749
9840
|
resizeObserver?.observe(videoElement);
|
|
9841
|
+
const isVideoTrack = trackType === 'videoTrack';
|
|
9750
9842
|
// element renders and gets bound - track subscription gets
|
|
9751
9843
|
// triggered first other ones get skipped on initial subscriptions
|
|
9752
9844
|
const publishedTracksSubscription = boundParticipant.isLocalParticipant
|
|
9753
9845
|
? null
|
|
9754
9846
|
: participant$
|
|
9755
|
-
.pipe(distinctUntilKeyChanged('publishedTracks'), map((p) =>
|
|
9847
|
+
.pipe(distinctUntilKeyChanged('publishedTracks'), map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), distinctUntilChanged())
|
|
9756
9848
|
.subscribe((isPublishing) => {
|
|
9757
9849
|
if (isPublishing) {
|
|
9758
9850
|
// the participant just started to publish a track
|
|
@@ -9772,10 +9864,11 @@ class DynascaleManager {
|
|
|
9772
9864
|
// without prior user interaction:
|
|
9773
9865
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
9774
9866
|
videoElement.muted = true;
|
|
9867
|
+
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
9775
9868
|
const streamSubscription = participant$
|
|
9776
|
-
.pipe(distinctUntilKeyChanged(
|
|
9869
|
+
.pipe(distinctUntilKeyChanged(trackKey))
|
|
9777
9870
|
.subscribe((p) => {
|
|
9778
|
-
const source =
|
|
9871
|
+
const source = isVideoTrack ? p.videoStream : p.screenShareStream;
|
|
9779
9872
|
if (videoElement.srcObject === source)
|
|
9780
9873
|
return;
|
|
9781
9874
|
videoElement.srcObject = source ?? null;
|
|
@@ -9814,6 +9907,7 @@ class DynascaleManager {
|
|
|
9814
9907
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
9815
9908
|
if (!participant || participant.isLocalParticipant)
|
|
9816
9909
|
return;
|
|
9910
|
+
this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
|
|
9817
9911
|
const participant$ = this.callState.participants$.pipe(map((ps) => ps.find((p) => p.sessionId === sessionId)), takeWhile((p) => !!p), distinctUntilChanged(), shareReplay({ bufferSize: 1, refCount: true }));
|
|
9818
9912
|
const updateSinkId = (deviceId, audioContext) => {
|
|
9819
9913
|
if (!deviceId)
|
|
@@ -9832,14 +9926,12 @@ class DynascaleManager {
|
|
|
9832
9926
|
};
|
|
9833
9927
|
let sourceNode = undefined;
|
|
9834
9928
|
let gainNode = undefined;
|
|
9929
|
+
const isAudioTrack = trackType === 'audioTrack';
|
|
9930
|
+
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
9835
9931
|
const updateMediaStreamSubscription = participant$
|
|
9836
|
-
.pipe(distinctUntilKeyChanged(
|
|
9837
|
-
? 'screenShareAudioStream'
|
|
9838
|
-
: 'audioStream'))
|
|
9932
|
+
.pipe(distinctUntilKeyChanged(trackKey))
|
|
9839
9933
|
.subscribe((p) => {
|
|
9840
|
-
const source =
|
|
9841
|
-
? p.screenShareAudioStream
|
|
9842
|
-
: p.audioStream;
|
|
9934
|
+
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
9843
9935
|
if (audioElement.srcObject === source)
|
|
9844
9936
|
return;
|
|
9845
9937
|
setTimeout(() => {
|
|
@@ -9894,6 +9986,7 @@ class DynascaleManager {
|
|
|
9894
9986
|
});
|
|
9895
9987
|
audioElement.autoplay = true;
|
|
9896
9988
|
return () => {
|
|
9989
|
+
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
9897
9990
|
sinkIdSubscription?.unsubscribe();
|
|
9898
9991
|
volumeSubscription.unsubscribe();
|
|
9899
9992
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -9954,6 +10047,9 @@ class DynascaleManager {
|
|
|
9954
10047
|
this.callState = callState;
|
|
9955
10048
|
this.speaker = speaker;
|
|
9956
10049
|
this.tracer = tracer;
|
|
10050
|
+
if (!isReactNative()) {
|
|
10051
|
+
this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
|
|
10052
|
+
}
|
|
9957
10053
|
}
|
|
9958
10054
|
setSfuClient(sfuClient) {
|
|
9959
10055
|
this.sfuClient = sfuClient;
|
|
@@ -10342,9 +10438,6 @@ const getDevices = (permission, kind, tracer) => {
|
|
|
10342
10438
|
const checkIfAudioOutputChangeSupported = () => {
|
|
10343
10439
|
if (typeof document === 'undefined')
|
|
10344
10440
|
return false;
|
|
10345
|
-
// Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
|
|
10346
|
-
if (isSafari())
|
|
10347
|
-
return 'setSinkId' in AudioContext.prototype;
|
|
10348
10441
|
const element = document.createElement('audio');
|
|
10349
10442
|
return 'setSinkId' in element;
|
|
10350
10443
|
};
|
|
@@ -15886,7 +15979,7 @@ class StreamClient {
|
|
|
15886
15979
|
this.getUserAgent = () => {
|
|
15887
15980
|
if (!this.cachedUserAgent) {
|
|
15888
15981
|
const { clientAppIdentifier = {} } = this.options;
|
|
15889
|
-
const { sdkName = 'js', sdkVersion = "1.44.
|
|
15982
|
+
const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
|
|
15890
15983
|
this.cachedUserAgent = [
|
|
15891
15984
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
15892
15985
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|