@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.cjs.js
CHANGED
|
@@ -6303,7 +6303,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6303
6303
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6304
6304
|
};
|
|
6305
6305
|
|
|
6306
|
-
const version = "1.44.
|
|
6306
|
+
const version = "1.44.5";
|
|
6307
6307
|
const [major, minor, patch] = version.split('.');
|
|
6308
6308
|
let sdkInfo = {
|
|
6309
6309
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9511,6 +9511,96 @@ class ViewportTracker {
|
|
|
9511
9511
|
}
|
|
9512
9512
|
}
|
|
9513
9513
|
|
|
9514
|
+
const toBindingKey = (sessionId, trackType = 'audioTrack') => `${sessionId}/${trackType}`;
|
|
9515
|
+
/**
|
|
9516
|
+
* Tracks audio element bindings and periodically warns about
|
|
9517
|
+
* remote participants whose audio streams have no bound element.
|
|
9518
|
+
*/
|
|
9519
|
+
class AudioBindingsWatchdog {
|
|
9520
|
+
constructor(state, tracer) {
|
|
9521
|
+
this.state = state;
|
|
9522
|
+
this.tracer = tracer;
|
|
9523
|
+
this.bindings = new Map();
|
|
9524
|
+
this.enabled = true;
|
|
9525
|
+
this.logger = videoLoggerSystem.getLogger('AudioBindingsWatchdog');
|
|
9526
|
+
/**
|
|
9527
|
+
* Registers an audio element binding for the given session and track type.
|
|
9528
|
+
* Warns if a different element is already bound to the same key.
|
|
9529
|
+
*/
|
|
9530
|
+
this.register = (audioElement, sessionId, trackType) => {
|
|
9531
|
+
const key = toBindingKey(sessionId, trackType);
|
|
9532
|
+
const existing = this.bindings.get(key);
|
|
9533
|
+
if (existing && existing !== audioElement) {
|
|
9534
|
+
this.logger.warn(`Audio element already bound to ${sessionId} and ${trackType}`);
|
|
9535
|
+
this.tracer.trace('audioBinding.alreadyBoundWarning', trackType);
|
|
9536
|
+
}
|
|
9537
|
+
this.bindings.set(key, audioElement);
|
|
9538
|
+
};
|
|
9539
|
+
/**
|
|
9540
|
+
* Removes the audio element binding for the given session and track type.
|
|
9541
|
+
*/
|
|
9542
|
+
this.unregister = (sessionId, trackType) => {
|
|
9543
|
+
this.bindings.delete(toBindingKey(sessionId, trackType));
|
|
9544
|
+
};
|
|
9545
|
+
/**
|
|
9546
|
+
* Enables or disables the watchdog.
|
|
9547
|
+
* When disabled, the periodic check stops but bindings are still tracked.
|
|
9548
|
+
*/
|
|
9549
|
+
this.setEnabled = (enabled) => {
|
|
9550
|
+
this.enabled = enabled;
|
|
9551
|
+
if (enabled) {
|
|
9552
|
+
this.start();
|
|
9553
|
+
}
|
|
9554
|
+
else {
|
|
9555
|
+
this.stop();
|
|
9556
|
+
}
|
|
9557
|
+
};
|
|
9558
|
+
/**
|
|
9559
|
+
* Stops the watchdog and unsubscribes from callingState changes.
|
|
9560
|
+
*/
|
|
9561
|
+
this.dispose = () => {
|
|
9562
|
+
this.stop();
|
|
9563
|
+
this.unsubscribeCallingState();
|
|
9564
|
+
};
|
|
9565
|
+
this.start = () => {
|
|
9566
|
+
clearInterval(this.watchdogInterval);
|
|
9567
|
+
this.watchdogInterval = setInterval(() => {
|
|
9568
|
+
const danglingUserIds = [];
|
|
9569
|
+
for (const p of this.state.participants) {
|
|
9570
|
+
if (p.isLocalParticipant)
|
|
9571
|
+
continue;
|
|
9572
|
+
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
9573
|
+
if (audioStream && !this.bindings.has(toBindingKey(sessionId))) {
|
|
9574
|
+
danglingUserIds.push(userId);
|
|
9575
|
+
}
|
|
9576
|
+
if (screenShareAudioStream &&
|
|
9577
|
+
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
9578
|
+
danglingUserIds.push(userId);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
9581
|
+
if (danglingUserIds.length > 0) {
|
|
9582
|
+
const key = 'audioBinding.danglingWarning';
|
|
9583
|
+
this.tracer.traceOnce(key, key, danglingUserIds);
|
|
9584
|
+
this.logger.warn(`Dangling audio bindings detected. Did you forget to bind the audio element? user_ids: ${danglingUserIds}.`);
|
|
9585
|
+
}
|
|
9586
|
+
}, 3000);
|
|
9587
|
+
};
|
|
9588
|
+
this.stop = () => {
|
|
9589
|
+
clearInterval(this.watchdogInterval);
|
|
9590
|
+
};
|
|
9591
|
+
this.unsubscribeCallingState = createSubscription(state.callingState$, (callingState) => {
|
|
9592
|
+
if (!this.enabled)
|
|
9593
|
+
return;
|
|
9594
|
+
if (callingState !== exports.CallingState.JOINED) {
|
|
9595
|
+
this.stop();
|
|
9596
|
+
}
|
|
9597
|
+
else {
|
|
9598
|
+
this.start();
|
|
9599
|
+
}
|
|
9600
|
+
});
|
|
9601
|
+
}
|
|
9602
|
+
}
|
|
9603
|
+
|
|
9514
9604
|
const DEFAULT_VIEWPORT_VISIBILITY_STATE = {
|
|
9515
9605
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
9516
9606
|
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -9536,7 +9626,7 @@ class DynascaleManager {
|
|
|
9536
9626
|
*/
|
|
9537
9627
|
this.viewportTracker = new ViewportTracker();
|
|
9538
9628
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9539
|
-
this.useWebAudio =
|
|
9629
|
+
this.useWebAudio = false;
|
|
9540
9630
|
this.pendingSubscriptionsUpdate = null;
|
|
9541
9631
|
this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
|
|
9542
9632
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
@@ -9568,7 +9658,8 @@ class DynascaleManager {
|
|
|
9568
9658
|
if (this.pendingSubscriptionsUpdate) {
|
|
9569
9659
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9570
9660
|
}
|
|
9571
|
-
|
|
9661
|
+
this.audioBindingsWatchdog?.dispose();
|
|
9662
|
+
const context = this.audioContext;
|
|
9572
9663
|
if (context && context.state !== 'closed') {
|
|
9573
9664
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
9574
9665
|
await context.close();
|
|
@@ -9767,12 +9858,13 @@ class DynascaleManager {
|
|
|
9767
9858
|
lastDimensions = currentDimensions;
|
|
9768
9859
|
});
|
|
9769
9860
|
resizeObserver?.observe(videoElement);
|
|
9861
|
+
const isVideoTrack = trackType === 'videoTrack';
|
|
9770
9862
|
// element renders and gets bound - track subscription gets
|
|
9771
9863
|
// triggered first other ones get skipped on initial subscriptions
|
|
9772
9864
|
const publishedTracksSubscription = boundParticipant.isLocalParticipant
|
|
9773
9865
|
? null
|
|
9774
9866
|
: participant$
|
|
9775
|
-
.pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) =>
|
|
9867
|
+
.pipe(rxjs.distinctUntilKeyChanged('publishedTracks'), rxjs.map((p) => (isVideoTrack ? hasVideo(p) : hasScreenShare(p))), rxjs.distinctUntilChanged())
|
|
9776
9868
|
.subscribe((isPublishing) => {
|
|
9777
9869
|
if (isPublishing) {
|
|
9778
9870
|
// the participant just started to publish a track
|
|
@@ -9792,10 +9884,11 @@ class DynascaleManager {
|
|
|
9792
9884
|
// without prior user interaction:
|
|
9793
9885
|
// https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide
|
|
9794
9886
|
videoElement.muted = true;
|
|
9887
|
+
const trackKey = isVideoTrack ? 'videoStream' : 'screenShareStream';
|
|
9795
9888
|
const streamSubscription = participant$
|
|
9796
|
-
.pipe(rxjs.distinctUntilKeyChanged(
|
|
9889
|
+
.pipe(rxjs.distinctUntilKeyChanged(trackKey))
|
|
9797
9890
|
.subscribe((p) => {
|
|
9798
|
-
const source =
|
|
9891
|
+
const source = isVideoTrack ? p.videoStream : p.screenShareStream;
|
|
9799
9892
|
if (videoElement.srcObject === source)
|
|
9800
9893
|
return;
|
|
9801
9894
|
videoElement.srcObject = source ?? null;
|
|
@@ -9834,6 +9927,7 @@ class DynascaleManager {
|
|
|
9834
9927
|
const participant = this.callState.findParticipantBySessionId(sessionId);
|
|
9835
9928
|
if (!participant || participant.isLocalParticipant)
|
|
9836
9929
|
return;
|
|
9930
|
+
this.audioBindingsWatchdog?.register(audioElement, sessionId, trackType);
|
|
9837
9931
|
const participant$ = this.callState.participants$.pipe(rxjs.map((ps) => ps.find((p) => p.sessionId === sessionId)), rxjs.takeWhile((p) => !!p), rxjs.distinctUntilChanged(), rxjs.shareReplay({ bufferSize: 1, refCount: true }));
|
|
9838
9932
|
const updateSinkId = (deviceId, audioContext) => {
|
|
9839
9933
|
if (!deviceId)
|
|
@@ -9852,14 +9946,12 @@ class DynascaleManager {
|
|
|
9852
9946
|
};
|
|
9853
9947
|
let sourceNode = undefined;
|
|
9854
9948
|
let gainNode = undefined;
|
|
9949
|
+
const isAudioTrack = trackType === 'audioTrack';
|
|
9950
|
+
const trackKey = isAudioTrack ? 'audioStream' : 'screenShareAudioStream';
|
|
9855
9951
|
const updateMediaStreamSubscription = participant$
|
|
9856
|
-
.pipe(rxjs.distinctUntilKeyChanged(
|
|
9857
|
-
? 'screenShareAudioStream'
|
|
9858
|
-
: 'audioStream'))
|
|
9952
|
+
.pipe(rxjs.distinctUntilKeyChanged(trackKey))
|
|
9859
9953
|
.subscribe((p) => {
|
|
9860
|
-
const source =
|
|
9861
|
-
? p.screenShareAudioStream
|
|
9862
|
-
: p.audioStream;
|
|
9954
|
+
const source = isAudioTrack ? p.audioStream : p.screenShareAudioStream;
|
|
9863
9955
|
if (audioElement.srcObject === source)
|
|
9864
9956
|
return;
|
|
9865
9957
|
setTimeout(() => {
|
|
@@ -9914,6 +10006,7 @@ class DynascaleManager {
|
|
|
9914
10006
|
});
|
|
9915
10007
|
audioElement.autoplay = true;
|
|
9916
10008
|
return () => {
|
|
10009
|
+
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
9917
10010
|
sinkIdSubscription?.unsubscribe();
|
|
9918
10011
|
volumeSubscription.unsubscribe();
|
|
9919
10012
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -9974,6 +10067,9 @@ class DynascaleManager {
|
|
|
9974
10067
|
this.callState = callState;
|
|
9975
10068
|
this.speaker = speaker;
|
|
9976
10069
|
this.tracer = tracer;
|
|
10070
|
+
if (!isReactNative()) {
|
|
10071
|
+
this.audioBindingsWatchdog = new AudioBindingsWatchdog(callState, tracer);
|
|
10072
|
+
}
|
|
9977
10073
|
}
|
|
9978
10074
|
setSfuClient(sfuClient) {
|
|
9979
10075
|
this.sfuClient = sfuClient;
|
|
@@ -10362,9 +10458,6 @@ const getDevices = (permission, kind, tracer) => {
|
|
|
10362
10458
|
const checkIfAudioOutputChangeSupported = () => {
|
|
10363
10459
|
if (typeof document === 'undefined')
|
|
10364
10460
|
return false;
|
|
10365
|
-
// Safari uses WebAudio API for playing audio, so we check the AudioContext prototype
|
|
10366
|
-
if (isSafari())
|
|
10367
|
-
return 'setSinkId' in AudioContext.prototype;
|
|
10368
10461
|
const element = document.createElement('audio');
|
|
10369
10462
|
return 'setSinkId' in element;
|
|
10370
10463
|
};
|
|
@@ -15904,7 +15997,7 @@ class StreamClient {
|
|
|
15904
15997
|
this.getUserAgent = () => {
|
|
15905
15998
|
if (!this.cachedUserAgent) {
|
|
15906
15999
|
const { clientAppIdentifier = {} } = this.options;
|
|
15907
|
-
const { sdkName = 'js', sdkVersion = "1.44.
|
|
16000
|
+
const { sdkName = 'js', sdkVersion = "1.44.5", ...extras } = clientAppIdentifier;
|
|
15908
16001
|
this.cachedUserAgent = [
|
|
15909
16002
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
15910
16003
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|