@stream-io/video-client 1.44.6-beta.0 → 1.46.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 +101 -12
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +101 -12
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +101 -12
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +4 -0
- package/dist/src/coordinator/connection/types.d.ts +22 -1
- package/dist/src/devices/DeviceManager.d.ts +1 -0
- package/dist/src/gen/video/sfu/event/events.d.ts +4 -0
- package/dist/src/helpers/DynascaleManager.d.ts +20 -0
- package/package.json +1 -1
- package/src/Call.ts +9 -2
- package/src/coordinator/connection/types.ts +23 -0
- package/src/devices/DeviceManager.ts +20 -1
- package/src/devices/MicrophoneManager.ts +7 -1
- package/src/devices/__tests__/DeviceManager.test.ts +8 -0
- package/src/devices/__tests__/mocks.ts +4 -0
- package/src/events/__tests__/participant.test.ts +41 -0
- package/src/events/participant.ts +1 -0
- package/src/gen/video/sfu/event/events.ts +5 -0
- package/src/helpers/AudioBindingsWatchdog.ts +14 -2
- package/src/helpers/DynascaleManager.ts +72 -1
- package/src/helpers/RNSpeechDetector.ts +7 -3
- package/src/helpers/__tests__/AudioBindingsWatchdog.test.ts +27 -1
- package/src/helpers/__tests__/DynascaleManager.test.ts +120 -0
package/dist/index.cjs.js
CHANGED
|
@@ -3285,6 +3285,7 @@ class ParticipantJoined$Type extends runtime.MessageType {
|
|
|
3285
3285
|
super('stream.video.sfu.event.ParticipantJoined', [
|
|
3286
3286
|
{ no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
3287
3287
|
{ no: 2, name: 'participant', kind: 'message', T: () => Participant },
|
|
3288
|
+
{ no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
3288
3289
|
]);
|
|
3289
3290
|
}
|
|
3290
3291
|
}
|
|
@@ -6303,7 +6304,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6303
6304
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6304
6305
|
};
|
|
6305
6306
|
|
|
6306
|
-
const version = "1.
|
|
6307
|
+
const version = "1.46.0";
|
|
6307
6308
|
const [major, minor, patch] = version.split('.');
|
|
6308
6309
|
let sdkInfo = {
|
|
6309
6310
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9241,6 +9242,7 @@ const watchParticipantJoined = (state) => {
|
|
|
9241
9242
|
// already announced participants.
|
|
9242
9243
|
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
9243
9244
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9245
|
+
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
9244
9246
|
viewportVisibilityState: {
|
|
9245
9247
|
videoTrack: exports.VisibilityState.UNKNOWN,
|
|
9246
9248
|
screenShareTrack: exports.VisibilityState.UNKNOWN,
|
|
@@ -9572,11 +9574,14 @@ class AudioBindingsWatchdog {
|
|
|
9572
9574
|
for (const p of this.state.participants) {
|
|
9573
9575
|
if (p.isLocalParticipant)
|
|
9574
9576
|
continue;
|
|
9575
|
-
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
9576
|
-
if (audioStream &&
|
|
9577
|
+
const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
|
|
9578
|
+
if (audioStream &&
|
|
9579
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
9580
|
+
!this.bindings.has(toBindingKey(sessionId))) {
|
|
9577
9581
|
danglingUserIds.push(userId);
|
|
9578
9582
|
}
|
|
9579
9583
|
if (screenShareAudioStream &&
|
|
9584
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
9580
9585
|
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
9581
9586
|
danglingUserIds.push(userId);
|
|
9582
9587
|
}
|
|
@@ -9631,6 +9636,31 @@ class DynascaleManager {
|
|
|
9631
9636
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9632
9637
|
this.useWebAudio = false;
|
|
9633
9638
|
this.pendingSubscriptionsUpdate = null;
|
|
9639
|
+
/**
|
|
9640
|
+
* Audio elements that were blocked by the browser's autoplay policy.
|
|
9641
|
+
* These can be retried by calling `resumeAudio()` from a user gesture.
|
|
9642
|
+
*/
|
|
9643
|
+
this.blockedAudioElementsSubject = new rxjs.BehaviorSubject(new Set());
|
|
9644
|
+
/**
|
|
9645
|
+
* Whether the browser's autoplay policy is blocking audio playback.
|
|
9646
|
+
* Will be `true` when the browser blocks autoplay (e.g., no prior user interaction).
|
|
9647
|
+
* Use `resumeAudio()` within a user gesture to unblock.
|
|
9648
|
+
*/
|
|
9649
|
+
this.autoplayBlocked$ = this.blockedAudioElementsSubject.pipe(rxjs.map((elements) => elements.size > 0), rxjs.distinctUntilChanged());
|
|
9650
|
+
this.addBlockedAudioElement = (audioElement) => {
|
|
9651
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9652
|
+
const next = new Set(elements);
|
|
9653
|
+
next.add(audioElement);
|
|
9654
|
+
return next;
|
|
9655
|
+
});
|
|
9656
|
+
};
|
|
9657
|
+
this.removeBlockedAudioElement = (audioElement) => {
|
|
9658
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9659
|
+
const nextElements = new Set(elements);
|
|
9660
|
+
nextElements.delete(audioElement);
|
|
9661
|
+
return nextElements;
|
|
9662
|
+
});
|
|
9663
|
+
};
|
|
9634
9664
|
this.videoTrackSubscriptionOverridesSubject = new rxjs.BehaviorSubject({});
|
|
9635
9665
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
9636
9666
|
this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(rxjs.map((overrides) => {
|
|
@@ -9662,6 +9692,7 @@ class DynascaleManager {
|
|
|
9662
9692
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9663
9693
|
}
|
|
9664
9694
|
this.audioBindingsWatchdog?.dispose();
|
|
9695
|
+
setCurrentValue(this.blockedAudioElementsSubject, new Set());
|
|
9665
9696
|
const context = this.audioContext;
|
|
9666
9697
|
if (context && context.state !== 'closed') {
|
|
9667
9698
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -9959,8 +9990,10 @@ class DynascaleManager {
|
|
|
9959
9990
|
return;
|
|
9960
9991
|
setTimeout(() => {
|
|
9961
9992
|
audioElement.srcObject = source ?? null;
|
|
9962
|
-
if (!source)
|
|
9993
|
+
if (!source) {
|
|
9994
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9963
9995
|
return;
|
|
9996
|
+
}
|
|
9964
9997
|
// Safari has a special quirk that prevents playing audio until the user
|
|
9965
9998
|
// interacts with the page or focuses on the tab where the call happens.
|
|
9966
9999
|
// This is a workaround for the issue where:
|
|
@@ -9984,6 +10017,10 @@ class DynascaleManager {
|
|
|
9984
10017
|
audioElement.muted = false;
|
|
9985
10018
|
audioElement.play().catch((e) => {
|
|
9986
10019
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
10020
|
+
if (e.name === 'NotAllowedError') {
|
|
10021
|
+
this.tracer.trace('audioPlaybackBlocked', null);
|
|
10022
|
+
this.addBlockedAudioElement(audioElement);
|
|
10023
|
+
}
|
|
9987
10024
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
9988
10025
|
});
|
|
9989
10026
|
}
|
|
@@ -10010,6 +10047,7 @@ class DynascaleManager {
|
|
|
10010
10047
|
audioElement.autoplay = true;
|
|
10011
10048
|
return () => {
|
|
10012
10049
|
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
10050
|
+
this.removeBlockedAudioElement(audioElement);
|
|
10013
10051
|
sinkIdSubscription?.unsubscribe();
|
|
10014
10052
|
volumeSubscription.unsubscribe();
|
|
10015
10053
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -10018,6 +10056,28 @@ class DynascaleManager {
|
|
|
10018
10056
|
gainNode?.disconnect();
|
|
10019
10057
|
};
|
|
10020
10058
|
};
|
|
10059
|
+
/**
|
|
10060
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
10061
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
10062
|
+
*
|
|
10063
|
+
* @returns a promise that resolves when all blocked elements have been retried.
|
|
10064
|
+
*/
|
|
10065
|
+
this.resumeAudio = async () => {
|
|
10066
|
+
this.tracer.trace('resumeAudio', null);
|
|
10067
|
+
const blocked = new Set();
|
|
10068
|
+
await Promise.all(Array.from(getCurrentValue(this.blockedAudioElementsSubject), async (el) => {
|
|
10069
|
+
try {
|
|
10070
|
+
if (el.srcObject) {
|
|
10071
|
+
await el.play();
|
|
10072
|
+
}
|
|
10073
|
+
}
|
|
10074
|
+
catch {
|
|
10075
|
+
this.logger.warn(`Can't resume audio for element: `, el);
|
|
10076
|
+
blocked.add(el);
|
|
10077
|
+
}
|
|
10078
|
+
}));
|
|
10079
|
+
setCurrentValue(this.blockedAudioElementsSubject, blocked);
|
|
10080
|
+
};
|
|
10021
10081
|
this.getOrCreateAudioContext = () => {
|
|
10022
10082
|
if (!this.useWebAudio)
|
|
10023
10083
|
return;
|
|
@@ -11210,6 +11270,7 @@ class DeviceManager {
|
|
|
11210
11270
|
isDeviceReplaced = true;
|
|
11211
11271
|
}
|
|
11212
11272
|
if (isDeviceDisconnected) {
|
|
11273
|
+
this.dispatchDeviceDisconnectedEvent(prevDevice);
|
|
11213
11274
|
await this.disable();
|
|
11214
11275
|
await this.select(undefined);
|
|
11215
11276
|
}
|
|
@@ -11219,7 +11280,7 @@ class DeviceManager {
|
|
|
11219
11280
|
await this.enable();
|
|
11220
11281
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
11221
11282
|
}
|
|
11222
|
-
else {
|
|
11283
|
+
else if (!hasPending(this.statusChangeConcurrencyTag)) {
|
|
11223
11284
|
await this.applySettingsToStream();
|
|
11224
11285
|
}
|
|
11225
11286
|
}
|
|
@@ -11233,6 +11294,20 @@ class DeviceManager {
|
|
|
11233
11294
|
const kind = this.mediaDeviceKind;
|
|
11234
11295
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
11235
11296
|
}
|
|
11297
|
+
dispatchDeviceDisconnectedEvent(device) {
|
|
11298
|
+
const event = {
|
|
11299
|
+
type: 'device.disconnected',
|
|
11300
|
+
call_cid: this.call.cid,
|
|
11301
|
+
status: this.isTrackStoppedDueToTrackEnd
|
|
11302
|
+
? this.state.prevStatus
|
|
11303
|
+
: this.state.status,
|
|
11304
|
+
deviceId: device.deviceId,
|
|
11305
|
+
label: device.label,
|
|
11306
|
+
kind: device.kind,
|
|
11307
|
+
};
|
|
11308
|
+
this.call.tracer.trace('device.disconnected', event);
|
|
11309
|
+
this.call.streamClient.dispatchEvent(event);
|
|
11310
|
+
}
|
|
11236
11311
|
persistPreference(selectedDevice, status) {
|
|
11237
11312
|
const deviceKind = this.mediaDeviceKind;
|
|
11238
11313
|
const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
|
|
@@ -11883,11 +11958,15 @@ class RNSpeechDetector {
|
|
|
11883
11958
|
? this.externalAudioStream
|
|
11884
11959
|
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11885
11960
|
this.audioStream = audioStream;
|
|
11886
|
-
this.pc1.addEventListener('icecandidate',
|
|
11887
|
-
|
|
11961
|
+
this.pc1.addEventListener('icecandidate', (e) => {
|
|
11962
|
+
this.pc2.addIceCandidate(e.candidate).catch(() => {
|
|
11963
|
+
// do nothing
|
|
11964
|
+
});
|
|
11888
11965
|
});
|
|
11889
11966
|
this.pc2.addEventListener('icecandidate', async (e) => {
|
|
11890
|
-
|
|
11967
|
+
this.pc1.addIceCandidate(e.candidate).catch(() => {
|
|
11968
|
+
// do nothing
|
|
11969
|
+
});
|
|
11891
11970
|
});
|
|
11892
11971
|
this.pc2.addEventListener('track', (e) => {
|
|
11893
11972
|
e.streams[0].getTracks().forEach((track) => {
|
|
@@ -12116,6 +12195,7 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12116
12195
|
const deviceId = this.state.selectedDevice;
|
|
12117
12196
|
const devices = await rxjs.firstValueFrom(this.listDevices());
|
|
12118
12197
|
const label = devices.find((d) => d.deviceId === deviceId)?.label;
|
|
12198
|
+
let lastCapturesAudio;
|
|
12119
12199
|
this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
|
|
12120
12200
|
noAudioThresholdMs: this.silenceThresholdMs,
|
|
12121
12201
|
emitIntervalMs: this.silenceThresholdMs,
|
|
@@ -12127,7 +12207,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12127
12207
|
deviceId,
|
|
12128
12208
|
label,
|
|
12129
12209
|
};
|
|
12130
|
-
|
|
12210
|
+
if (capturesAudio !== lastCapturesAudio) {
|
|
12211
|
+
lastCapturesAudio = capturesAudio;
|
|
12212
|
+
this.call.tracer.trace('mic.capture_report', event);
|
|
12213
|
+
}
|
|
12131
12214
|
this.call.streamClient.dispatchEvent(event);
|
|
12132
12215
|
},
|
|
12133
12216
|
});
|
|
@@ -13047,16 +13130,16 @@ class Call {
|
|
|
13047
13130
|
};
|
|
13048
13131
|
const rejectReason = reason ?? 'decline';
|
|
13049
13132
|
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
13050
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13051
13133
|
await this.reject(rejectReason);
|
|
13134
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13052
13135
|
}
|
|
13053
13136
|
else {
|
|
13054
13137
|
// if reject was undefined, we still have to cancel the call automatically
|
|
13055
13138
|
// when I am the creator and everyone else left the call
|
|
13056
13139
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
13057
13140
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
13058
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13059
13141
|
await this.reject('cancel');
|
|
13142
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13060
13143
|
}
|
|
13061
13144
|
}
|
|
13062
13145
|
}
|
|
@@ -14795,6 +14878,12 @@ class Call {
|
|
|
14795
14878
|
unbind();
|
|
14796
14879
|
};
|
|
14797
14880
|
};
|
|
14881
|
+
/**
|
|
14882
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
14883
|
+
*/
|
|
14884
|
+
this.resumeAudio = () => {
|
|
14885
|
+
return this.dynascaleManager.resumeAudio();
|
|
14886
|
+
};
|
|
14798
14887
|
/**
|
|
14799
14888
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
14800
14889
|
*
|
|
@@ -16036,7 +16125,7 @@ class StreamClient {
|
|
|
16036
16125
|
this.getUserAgent = () => {
|
|
16037
16126
|
if (!this.cachedUserAgent) {
|
|
16038
16127
|
const { clientAppIdentifier = {} } = this.options;
|
|
16039
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16128
|
+
const { sdkName = 'js', sdkVersion = "1.46.0", ...extras } = clientAppIdentifier;
|
|
16040
16129
|
this.cachedUserAgent = [
|
|
16041
16130
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16042
16131
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|