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