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