@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.es.js
CHANGED
|
@@ -3266,6 +3266,7 @@ class ParticipantJoined$Type extends MessageType {
|
|
|
3266
3266
|
super('stream.video.sfu.event.ParticipantJoined', [
|
|
3267
3267
|
{ no: 1, name: 'call_cid', kind: 'scalar', T: 9 /*ScalarType.STRING*/ },
|
|
3268
3268
|
{ no: 2, name: 'participant', kind: 'message', T: () => Participant },
|
|
3269
|
+
{ no: 3, name: 'is_pinned', kind: 'scalar', T: 8 /*ScalarType.BOOL*/ },
|
|
3269
3270
|
]);
|
|
3270
3271
|
}
|
|
3271
3272
|
}
|
|
@@ -6284,7 +6285,7 @@ const getSdkVersion = (sdk) => {
|
|
|
6284
6285
|
return sdk ? `${sdk.major}.${sdk.minor}.${sdk.patch}` : '0.0.0-development';
|
|
6285
6286
|
};
|
|
6286
6287
|
|
|
6287
|
-
const version = "1.
|
|
6288
|
+
const version = "1.46.0";
|
|
6288
6289
|
const [major, minor, patch] = version.split('.');
|
|
6289
6290
|
let sdkInfo = {
|
|
6290
6291
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -9222,6 +9223,7 @@ const watchParticipantJoined = (state) => {
|
|
|
9222
9223
|
// already announced participants.
|
|
9223
9224
|
const orphanedTracks = reconcileOrphanedTracks(state, participant);
|
|
9224
9225
|
state.updateOrAddParticipant(participant.sessionId, Object.assign(participant, orphanedTracks, {
|
|
9226
|
+
...(e.isPinned && { pin: { isLocalPin: false, pinnedAt: Date.now() } }),
|
|
9225
9227
|
viewportVisibilityState: {
|
|
9226
9228
|
videoTrack: VisibilityState.UNKNOWN,
|
|
9227
9229
|
screenShareTrack: VisibilityState.UNKNOWN,
|
|
@@ -9553,11 +9555,14 @@ class AudioBindingsWatchdog {
|
|
|
9553
9555
|
for (const p of this.state.participants) {
|
|
9554
9556
|
if (p.isLocalParticipant)
|
|
9555
9557
|
continue;
|
|
9556
|
-
const { audioStream, screenShareAudioStream, sessionId, userId } = p;
|
|
9557
|
-
if (audioStream &&
|
|
9558
|
+
const { audioStream, screenShareAudioStream, sessionId, userId, publishedTracks, } = p;
|
|
9559
|
+
if (audioStream &&
|
|
9560
|
+
publishedTracks.includes(TrackType.AUDIO) &&
|
|
9561
|
+
!this.bindings.has(toBindingKey(sessionId))) {
|
|
9558
9562
|
danglingUserIds.push(userId);
|
|
9559
9563
|
}
|
|
9560
9564
|
if (screenShareAudioStream &&
|
|
9565
|
+
publishedTracks.includes(TrackType.SCREEN_SHARE_AUDIO) &&
|
|
9561
9566
|
!this.bindings.has(toBindingKey(sessionId, 'screenShareAudioTrack'))) {
|
|
9562
9567
|
danglingUserIds.push(userId);
|
|
9563
9568
|
}
|
|
@@ -9612,6 +9617,31 @@ class DynascaleManager {
|
|
|
9612
9617
|
this.logger = videoLoggerSystem.getLogger('DynascaleManager');
|
|
9613
9618
|
this.useWebAudio = false;
|
|
9614
9619
|
this.pendingSubscriptionsUpdate = null;
|
|
9620
|
+
/**
|
|
9621
|
+
* Audio elements that were blocked by the browser's autoplay policy.
|
|
9622
|
+
* These can be retried by calling `resumeAudio()` from a user gesture.
|
|
9623
|
+
*/
|
|
9624
|
+
this.blockedAudioElementsSubject = new BehaviorSubject(new Set());
|
|
9625
|
+
/**
|
|
9626
|
+
* Whether the browser's autoplay policy is blocking audio playback.
|
|
9627
|
+
* Will be `true` when the browser blocks autoplay (e.g., no prior user interaction).
|
|
9628
|
+
* Use `resumeAudio()` within a user gesture to unblock.
|
|
9629
|
+
*/
|
|
9630
|
+
this.autoplayBlocked$ = this.blockedAudioElementsSubject.pipe(map((elements) => elements.size > 0), distinctUntilChanged());
|
|
9631
|
+
this.addBlockedAudioElement = (audioElement) => {
|
|
9632
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9633
|
+
const next = new Set(elements);
|
|
9634
|
+
next.add(audioElement);
|
|
9635
|
+
return next;
|
|
9636
|
+
});
|
|
9637
|
+
};
|
|
9638
|
+
this.removeBlockedAudioElement = (audioElement) => {
|
|
9639
|
+
setCurrentValue(this.blockedAudioElementsSubject, (elements) => {
|
|
9640
|
+
const nextElements = new Set(elements);
|
|
9641
|
+
nextElements.delete(audioElement);
|
|
9642
|
+
return nextElements;
|
|
9643
|
+
});
|
|
9644
|
+
};
|
|
9615
9645
|
this.videoTrackSubscriptionOverridesSubject = new BehaviorSubject({});
|
|
9616
9646
|
this.videoTrackSubscriptionOverrides$ = this.videoTrackSubscriptionOverridesSubject.asObservable();
|
|
9617
9647
|
this.incomingVideoSettings$ = this.videoTrackSubscriptionOverrides$.pipe(map((overrides) => {
|
|
@@ -9643,6 +9673,7 @@ class DynascaleManager {
|
|
|
9643
9673
|
clearTimeout(this.pendingSubscriptionsUpdate);
|
|
9644
9674
|
}
|
|
9645
9675
|
this.audioBindingsWatchdog?.dispose();
|
|
9676
|
+
setCurrentValue(this.blockedAudioElementsSubject, new Set());
|
|
9646
9677
|
const context = this.audioContext;
|
|
9647
9678
|
if (context && context.state !== 'closed') {
|
|
9648
9679
|
document.removeEventListener('click', this.resumeAudioContext);
|
|
@@ -9940,8 +9971,10 @@ class DynascaleManager {
|
|
|
9940
9971
|
return;
|
|
9941
9972
|
setTimeout(() => {
|
|
9942
9973
|
audioElement.srcObject = source ?? null;
|
|
9943
|
-
if (!source)
|
|
9974
|
+
if (!source) {
|
|
9975
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9944
9976
|
return;
|
|
9977
|
+
}
|
|
9945
9978
|
// Safari has a special quirk that prevents playing audio until the user
|
|
9946
9979
|
// interacts with the page or focuses on the tab where the call happens.
|
|
9947
9980
|
// This is a workaround for the issue where:
|
|
@@ -9965,6 +9998,10 @@ class DynascaleManager {
|
|
|
9965
9998
|
audioElement.muted = false;
|
|
9966
9999
|
audioElement.play().catch((e) => {
|
|
9967
10000
|
this.tracer.trace('audioPlaybackError', e.message);
|
|
10001
|
+
if (e.name === 'NotAllowedError') {
|
|
10002
|
+
this.tracer.trace('audioPlaybackBlocked', null);
|
|
10003
|
+
this.addBlockedAudioElement(audioElement);
|
|
10004
|
+
}
|
|
9968
10005
|
this.logger.warn(`Failed to play audio stream`, e);
|
|
9969
10006
|
});
|
|
9970
10007
|
}
|
|
@@ -9991,6 +10028,7 @@ class DynascaleManager {
|
|
|
9991
10028
|
audioElement.autoplay = true;
|
|
9992
10029
|
return () => {
|
|
9993
10030
|
this.audioBindingsWatchdog?.unregister(sessionId, trackType);
|
|
10031
|
+
this.removeBlockedAudioElement(audioElement);
|
|
9994
10032
|
sinkIdSubscription?.unsubscribe();
|
|
9995
10033
|
volumeSubscription.unsubscribe();
|
|
9996
10034
|
updateMediaStreamSubscription.unsubscribe();
|
|
@@ -9999,6 +10037,28 @@ class DynascaleManager {
|
|
|
9999
10037
|
gainNode?.disconnect();
|
|
10000
10038
|
};
|
|
10001
10039
|
};
|
|
10040
|
+
/**
|
|
10041
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
10042
|
+
* Must be called from within a user gesture (e.g., click handler).
|
|
10043
|
+
*
|
|
10044
|
+
* @returns a promise that resolves when all blocked elements have been retried.
|
|
10045
|
+
*/
|
|
10046
|
+
this.resumeAudio = async () => {
|
|
10047
|
+
this.tracer.trace('resumeAudio', null);
|
|
10048
|
+
const blocked = new Set();
|
|
10049
|
+
await Promise.all(Array.from(getCurrentValue(this.blockedAudioElementsSubject), async (el) => {
|
|
10050
|
+
try {
|
|
10051
|
+
if (el.srcObject) {
|
|
10052
|
+
await el.play();
|
|
10053
|
+
}
|
|
10054
|
+
}
|
|
10055
|
+
catch {
|
|
10056
|
+
this.logger.warn(`Can't resume audio for element: `, el);
|
|
10057
|
+
blocked.add(el);
|
|
10058
|
+
}
|
|
10059
|
+
}));
|
|
10060
|
+
setCurrentValue(this.blockedAudioElementsSubject, blocked);
|
|
10061
|
+
};
|
|
10002
10062
|
this.getOrCreateAudioContext = () => {
|
|
10003
10063
|
if (!this.useWebAudio)
|
|
10004
10064
|
return;
|
|
@@ -11191,6 +11251,7 @@ class DeviceManager {
|
|
|
11191
11251
|
isDeviceReplaced = true;
|
|
11192
11252
|
}
|
|
11193
11253
|
if (isDeviceDisconnected) {
|
|
11254
|
+
this.dispatchDeviceDisconnectedEvent(prevDevice);
|
|
11194
11255
|
await this.disable();
|
|
11195
11256
|
await this.select(undefined);
|
|
11196
11257
|
}
|
|
@@ -11200,7 +11261,7 @@ class DeviceManager {
|
|
|
11200
11261
|
await this.enable();
|
|
11201
11262
|
this.isTrackStoppedDueToTrackEnd = false;
|
|
11202
11263
|
}
|
|
11203
|
-
else {
|
|
11264
|
+
else if (!hasPending(this.statusChangeConcurrencyTag)) {
|
|
11204
11265
|
await this.applySettingsToStream();
|
|
11205
11266
|
}
|
|
11206
11267
|
}
|
|
@@ -11214,6 +11275,20 @@ class DeviceManager {
|
|
|
11214
11275
|
const kind = this.mediaDeviceKind;
|
|
11215
11276
|
return devices.find((d) => d.deviceId === deviceId && d.kind === kind);
|
|
11216
11277
|
}
|
|
11278
|
+
dispatchDeviceDisconnectedEvent(device) {
|
|
11279
|
+
const event = {
|
|
11280
|
+
type: 'device.disconnected',
|
|
11281
|
+
call_cid: this.call.cid,
|
|
11282
|
+
status: this.isTrackStoppedDueToTrackEnd
|
|
11283
|
+
? this.state.prevStatus
|
|
11284
|
+
: this.state.status,
|
|
11285
|
+
deviceId: device.deviceId,
|
|
11286
|
+
label: device.label,
|
|
11287
|
+
kind: device.kind,
|
|
11288
|
+
};
|
|
11289
|
+
this.call.tracer.trace('device.disconnected', event);
|
|
11290
|
+
this.call.streamClient.dispatchEvent(event);
|
|
11291
|
+
}
|
|
11217
11292
|
persistPreference(selectedDevice, status) {
|
|
11218
11293
|
const deviceKind = this.mediaDeviceKind;
|
|
11219
11294
|
const deviceKey = deviceKind === 'audioinput' ? 'microphone' : 'camera';
|
|
@@ -11864,11 +11939,15 @@ class RNSpeechDetector {
|
|
|
11864
11939
|
? this.externalAudioStream
|
|
11865
11940
|
: await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
11866
11941
|
this.audioStream = audioStream;
|
|
11867
|
-
this.pc1.addEventListener('icecandidate',
|
|
11868
|
-
|
|
11942
|
+
this.pc1.addEventListener('icecandidate', (e) => {
|
|
11943
|
+
this.pc2.addIceCandidate(e.candidate).catch(() => {
|
|
11944
|
+
// do nothing
|
|
11945
|
+
});
|
|
11869
11946
|
});
|
|
11870
11947
|
this.pc2.addEventListener('icecandidate', async (e) => {
|
|
11871
|
-
|
|
11948
|
+
this.pc1.addIceCandidate(e.candidate).catch(() => {
|
|
11949
|
+
// do nothing
|
|
11950
|
+
});
|
|
11872
11951
|
});
|
|
11873
11952
|
this.pc2.addEventListener('track', (e) => {
|
|
11874
11953
|
e.streams[0].getTracks().forEach((track) => {
|
|
@@ -12097,6 +12176,7 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12097
12176
|
const deviceId = this.state.selectedDevice;
|
|
12098
12177
|
const devices = await firstValueFrom(this.listDevices());
|
|
12099
12178
|
const label = devices.find((d) => d.deviceId === deviceId)?.label;
|
|
12179
|
+
let lastCapturesAudio;
|
|
12100
12180
|
this.noAudioDetectorCleanup = createNoAudioDetector(mediaStream, {
|
|
12101
12181
|
noAudioThresholdMs: this.silenceThresholdMs,
|
|
12102
12182
|
emitIntervalMs: this.silenceThresholdMs,
|
|
@@ -12108,7 +12188,10 @@ class MicrophoneManager extends AudioDeviceManager {
|
|
|
12108
12188
|
deviceId,
|
|
12109
12189
|
label,
|
|
12110
12190
|
};
|
|
12111
|
-
|
|
12191
|
+
if (capturesAudio !== lastCapturesAudio) {
|
|
12192
|
+
lastCapturesAudio = capturesAudio;
|
|
12193
|
+
this.call.tracer.trace('mic.capture_report', event);
|
|
12194
|
+
}
|
|
12112
12195
|
this.call.streamClient.dispatchEvent(event);
|
|
12113
12196
|
},
|
|
12114
12197
|
});
|
|
@@ -13028,16 +13111,16 @@ class Call {
|
|
|
13028
13111
|
};
|
|
13029
13112
|
const rejectReason = reason ?? 'decline';
|
|
13030
13113
|
const endCallReason = reasonToEndCallReason[rejectReason] ?? 'rejected';
|
|
13031
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13032
13114
|
await this.reject(rejectReason);
|
|
13115
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, endCallReason);
|
|
13033
13116
|
}
|
|
13034
13117
|
else {
|
|
13035
13118
|
// if reject was undefined, we still have to cancel the call automatically
|
|
13036
13119
|
// when I am the creator and everyone else left the call
|
|
13037
13120
|
const hasOtherParticipants = this.state.remoteParticipants.length > 0;
|
|
13038
13121
|
if (this.isCreatedByMe && !hasOtherParticipants) {
|
|
13039
|
-
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13040
13122
|
await this.reject('cancel');
|
|
13123
|
+
globalThis.streamRNVideoSDK?.callingX?.endCall(this, 'canceled');
|
|
13041
13124
|
}
|
|
13042
13125
|
}
|
|
13043
13126
|
}
|
|
@@ -14776,6 +14859,12 @@ class Call {
|
|
|
14776
14859
|
unbind();
|
|
14777
14860
|
};
|
|
14778
14861
|
};
|
|
14862
|
+
/**
|
|
14863
|
+
* Plays all audio elements blocked by the browser's autoplay policy.
|
|
14864
|
+
*/
|
|
14865
|
+
this.resumeAudio = () => {
|
|
14866
|
+
return this.dynascaleManager.resumeAudio();
|
|
14867
|
+
};
|
|
14779
14868
|
/**
|
|
14780
14869
|
* Binds a DOM <img> element to this call's thumbnail (if enabled in settings).
|
|
14781
14870
|
*
|
|
@@ -16017,7 +16106,7 @@ class StreamClient {
|
|
|
16017
16106
|
this.getUserAgent = () => {
|
|
16018
16107
|
if (!this.cachedUserAgent) {
|
|
16019
16108
|
const { clientAppIdentifier = {} } = this.options;
|
|
16020
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
16109
|
+
const { sdkName = 'js', sdkVersion = "1.46.0", ...extras } = clientAppIdentifier;
|
|
16021
16110
|
this.cachedUserAgent = [
|
|
16022
16111
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
16023
16112
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|