@stream-io/video-client 1.19.3 → 1.20.1
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 +15 -0
- package/dist/index.browser.es.js +145 -116
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +145 -116
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +145 -116
- package/dist/index.es.js.map +1 -1
- package/dist/src/Call.d.ts +13 -1
- package/dist/src/devices/BrowserPermission.d.ts +1 -0
- package/dist/src/devices/InputMediaDeviceManagerState.d.ts +7 -2
- package/dist/src/gen/coordinator/index.d.ts +274 -836
- package/dist/src/gen/shims.d.ts +177 -0
- package/package.json +1 -1
- package/src/Call.ts +18 -1
- package/src/__tests__/StreamVideoClient.api.test.ts +1 -1
- package/src/devices/BrowserPermission.ts +4 -0
- package/src/devices/InputMediaDeviceManager.ts +135 -123
- package/src/devices/InputMediaDeviceManagerState.ts +12 -2
- package/src/devices/__tests__/mocks.ts +1 -0
- package/src/gen/coordinator/index.ts +275 -833
- package/src/gen/shims.ts +192 -0
- package/src/stats/SfuStatsReporter.ts +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
|
|
4
4
|
|
|
5
|
+
## [1.20.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.20.0...@stream-io/video-client-1.20.1) (2025-04-29)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- dispose media stream if it cannot be published ([#1771](https://github.com/GetStream/stream-video-js/issues/1771)) ([83fbfd7](https://github.com/GetStream/stream-video-js/commit/83fbfd7bb77bd9a06d6955e6b48bb8238e573f57))
|
|
10
|
+
- use more granular permission state for stats reporter ([#1774](https://github.com/GetStream/stream-video-js/issues/1774)) ([55afdfc](https://github.com/GetStream/stream-video-js/commit/55afdfcdac55fad25ba32978caf55a2f25f7580b))
|
|
11
|
+
|
|
12
|
+
## [1.20.0](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.19.3...@stream-io/video-client-1.20.0) (2025-04-24)
|
|
13
|
+
|
|
14
|
+
- bump test timeout ([7d922ed](https://github.com/GetStream/stream-video-js/commit/7d922ed34c46851a257fb36ee644f1ff5e4cb917))
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
- add getCallReport method ([#1767](https://github.com/GetStream/stream-video-js/issues/1767)) ([12e064f](https://github.com/GetStream/stream-video-js/commit/12e064f34a08731ded289651125bbe20e2bbf4f4))
|
|
19
|
+
|
|
5
20
|
## [1.19.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.19.2...@stream-io/video-client-1.19.3) (2025-04-15)
|
|
6
21
|
|
|
7
22
|
### Bug Fixes
|
package/dist/index.browser.es.js
CHANGED
|
@@ -5643,7 +5643,7 @@ const aggregate = (stats) => {
|
|
|
5643
5643
|
return report;
|
|
5644
5644
|
};
|
|
5645
5645
|
|
|
5646
|
-
const version = "1.
|
|
5646
|
+
const version = "1.20.1";
|
|
5647
5647
|
const [major, minor, patch] = version.split('.');
|
|
5648
5648
|
let sdkInfo = {
|
|
5649
5649
|
type: SdkType.PLAIN_JAVASCRIPT,
|
|
@@ -5784,9 +5784,9 @@ class SfuStatsReporter {
|
|
|
5784
5784
|
this.logger = getLogger(['SfuStatsReporter']);
|
|
5785
5785
|
this.inputDevices = new Map();
|
|
5786
5786
|
this.observeDevice = (device, kind) => {
|
|
5787
|
-
const {
|
|
5787
|
+
const { browserPermissionState$ } = device.state;
|
|
5788
5788
|
this.unsubscribeDevicePermissionsSubscription?.();
|
|
5789
|
-
this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([
|
|
5789
|
+
this.unsubscribeDevicePermissionsSubscription = createSubscription(combineLatest([browserPermissionState$, this.state.ownCapabilities$]), ([browserPermissionState, ownCapabilities]) => {
|
|
5790
5790
|
// cleanup the previous listDevices() subscription in case
|
|
5791
5791
|
// permissions or capabilities have changed.
|
|
5792
5792
|
// we will subscribe again if everything is in order.
|
|
@@ -5794,7 +5794,7 @@ class SfuStatsReporter {
|
|
|
5794
5794
|
const hasCapability = kind === 'mic'
|
|
5795
5795
|
? ownCapabilities.includes(OwnCapability.SEND_AUDIO)
|
|
5796
5796
|
: ownCapabilities.includes(OwnCapability.SEND_VIDEO);
|
|
5797
|
-
if (
|
|
5797
|
+
if (browserPermissionState !== 'granted' || !hasCapability) {
|
|
5798
5798
|
this.inputDevices.set(kind, {
|
|
5799
5799
|
currentDevice: '',
|
|
5800
5800
|
availableDevices: [],
|
|
@@ -8728,6 +8728,9 @@ class BrowserPermission {
|
|
|
8728
8728
|
// Instead of checking if a permission is granted, we check if it isn't denied
|
|
8729
8729
|
map((state) => state !== 'denied'));
|
|
8730
8730
|
}
|
|
8731
|
+
asStateObservable() {
|
|
8732
|
+
return this.getStateObservable();
|
|
8733
|
+
}
|
|
8731
8734
|
getIsPromptingObservable() {
|
|
8732
8735
|
return this.getStateObservable().pipe(map((state) => state === 'prompting'));
|
|
8733
8736
|
}
|
|
@@ -9254,122 +9257,130 @@ class InputMediaDeviceManager {
|
|
|
9254
9257
|
this.logger('debug', 'Starting stream');
|
|
9255
9258
|
let stream;
|
|
9256
9259
|
let rootStream;
|
|
9257
|
-
|
|
9258
|
-
this.
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
parentTrack.
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9260
|
+
try {
|
|
9261
|
+
if (this.state.mediaStream &&
|
|
9262
|
+
this.getTracks().every((t) => t.readyState === 'live')) {
|
|
9263
|
+
stream = this.state.mediaStream;
|
|
9264
|
+
this.enableTracks();
|
|
9265
|
+
}
|
|
9266
|
+
else {
|
|
9267
|
+
const defaultConstraints = this.state.defaultConstraints;
|
|
9268
|
+
const constraints = {
|
|
9269
|
+
...defaultConstraints,
|
|
9270
|
+
deviceId: this.state.selectedDevice
|
|
9271
|
+
? { exact: this.state.selectedDevice }
|
|
9272
|
+
: undefined,
|
|
9273
|
+
};
|
|
9274
|
+
/**
|
|
9275
|
+
* Chains two media streams together.
|
|
9276
|
+
*
|
|
9277
|
+
* In our case, filters MediaStreams are derived from their parent MediaStream.
|
|
9278
|
+
* However, once a child filter's track is stopped,
|
|
9279
|
+
* the tracks of the parent MediaStream aren't automatically stopped.
|
|
9280
|
+
* This leads to a situation where the camera indicator light is still on
|
|
9281
|
+
* even though the user stopped publishing video.
|
|
9282
|
+
*
|
|
9283
|
+
* This function works around this issue by stopping the parent MediaStream's tracks
|
|
9284
|
+
* as well once the child filter's tracks are stopped.
|
|
9285
|
+
*
|
|
9286
|
+
* It works by patching the stop() method of the child filter's tracks to also stop
|
|
9287
|
+
* the parent MediaStream's tracks of the same type. Here we assume that
|
|
9288
|
+
* the parent MediaStream has only one track of each type.
|
|
9289
|
+
*
|
|
9290
|
+
* @param parentStream the parent MediaStream. Omit for the root stream.
|
|
9291
|
+
*/
|
|
9292
|
+
const chainWith = (parentStream) => async (filterStream) => {
|
|
9293
|
+
if (!parentStream)
|
|
9294
|
+
return filterStream;
|
|
9295
|
+
// TODO OL: take care of track.enabled property as well
|
|
9296
|
+
const parent = await parentStream;
|
|
9297
|
+
filterStream.getTracks().forEach((track) => {
|
|
9298
|
+
const originalStop = track.stop;
|
|
9299
|
+
track.stop = function stop() {
|
|
9300
|
+
originalStop.call(track);
|
|
9301
|
+
parent.getTracks().forEach((parentTrack) => {
|
|
9302
|
+
if (parentTrack.kind === track.kind) {
|
|
9303
|
+
parentTrack.stop();
|
|
9304
|
+
}
|
|
9305
|
+
});
|
|
9306
|
+
};
|
|
9307
|
+
});
|
|
9308
|
+
parent.getTracks().forEach((parentTrack) => {
|
|
9309
|
+
// When the parent stream abruptly ends, we propagate the event
|
|
9310
|
+
// to the filter stream.
|
|
9311
|
+
// This usually happens when the camera/microphone permissions
|
|
9312
|
+
// are revoked or when the device is disconnected.
|
|
9313
|
+
const handleParentTrackEnded = () => {
|
|
9314
|
+
filterStream.getTracks().forEach((track) => {
|
|
9315
|
+
if (parentTrack.kind !== track.kind)
|
|
9316
|
+
return;
|
|
9317
|
+
track.stop();
|
|
9318
|
+
track.dispatchEvent(new Event('ended')); // propagate the event
|
|
9319
|
+
});
|
|
9320
|
+
};
|
|
9321
|
+
parentTrack.addEventListener('ended', handleParentTrackEnded);
|
|
9322
|
+
this.subscriptions.push(() => {
|
|
9323
|
+
parentTrack.removeEventListener('ended', handleParentTrackEnded);
|
|
9315
9324
|
});
|
|
9316
|
-
};
|
|
9317
|
-
|
|
9325
|
+
});
|
|
9326
|
+
return filterStream;
|
|
9327
|
+
};
|
|
9328
|
+
// the rootStream represents the stream coming from the actual device
|
|
9329
|
+
// e.g. camera or microphone stream
|
|
9330
|
+
rootStream = this.getStream(constraints);
|
|
9331
|
+
// we publish the last MediaStream of the chain
|
|
9332
|
+
stream = await this.filters.reduce((parent, entry) => parent
|
|
9333
|
+
.then((inputStream) => {
|
|
9334
|
+
const { stop, output } = entry.start(inputStream);
|
|
9335
|
+
entry.stop = stop;
|
|
9336
|
+
return output;
|
|
9337
|
+
})
|
|
9338
|
+
.then(chainWith(parent), (error) => {
|
|
9339
|
+
this.logger('warn', 'Filter failed to start and will be ignored', error);
|
|
9340
|
+
return parent;
|
|
9341
|
+
}), rootStream);
|
|
9342
|
+
}
|
|
9343
|
+
if (this.call.state.callingState === CallingState.JOINED) {
|
|
9344
|
+
await this.publishStream(stream);
|
|
9345
|
+
}
|
|
9346
|
+
if (this.state.mediaStream !== stream) {
|
|
9347
|
+
this.state.setMediaStream(stream, await rootStream);
|
|
9348
|
+
const handleTrackEnded = async () => {
|
|
9349
|
+
await this.statusChangeSettled();
|
|
9350
|
+
if (this.enabled) {
|
|
9351
|
+
this.isTrackStoppedDueToTrackEnd = true;
|
|
9352
|
+
setTimeout(() => {
|
|
9353
|
+
this.isTrackStoppedDueToTrackEnd = false;
|
|
9354
|
+
}, 2000);
|
|
9355
|
+
await this.disable();
|
|
9356
|
+
}
|
|
9357
|
+
};
|
|
9358
|
+
const createTrackMuteHandler = (muted) => () => {
|
|
9359
|
+
if (!isMobile() || this.trackType !== TrackType.VIDEO)
|
|
9360
|
+
return;
|
|
9361
|
+
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
9362
|
+
this.logger('warn', 'Error while notifying track mute state', err);
|
|
9363
|
+
});
|
|
9364
|
+
};
|
|
9365
|
+
stream.getTracks().forEach((track) => {
|
|
9366
|
+
const muteHandler = createTrackMuteHandler(true);
|
|
9367
|
+
const unmuteHandler = createTrackMuteHandler(false);
|
|
9368
|
+
track.addEventListener('mute', muteHandler);
|
|
9369
|
+
track.addEventListener('unmute', unmuteHandler);
|
|
9370
|
+
track.addEventListener('ended', handleTrackEnded);
|
|
9318
9371
|
this.subscriptions.push(() => {
|
|
9319
|
-
|
|
9372
|
+
track.removeEventListener('mute', muteHandler);
|
|
9373
|
+
track.removeEventListener('unmute', unmuteHandler);
|
|
9374
|
+
track.removeEventListener('ended', handleTrackEnded);
|
|
9320
9375
|
});
|
|
9321
9376
|
});
|
|
9322
|
-
|
|
9323
|
-
};
|
|
9324
|
-
// the rootStream represents the stream coming from the actual device
|
|
9325
|
-
// e.g. camera or microphone stream
|
|
9326
|
-
rootStream = this.getStream(constraints);
|
|
9327
|
-
// we publish the last MediaStream of the chain
|
|
9328
|
-
stream = await this.filters.reduce((parent, entry) => parent
|
|
9329
|
-
.then((inputStream) => {
|
|
9330
|
-
const { stop, output } = entry.start(inputStream);
|
|
9331
|
-
entry.stop = stop;
|
|
9332
|
-
return output;
|
|
9333
|
-
})
|
|
9334
|
-
.then(chainWith(parent), (error) => {
|
|
9335
|
-
this.logger('warn', 'Filter failed to start and will be ignored', error);
|
|
9336
|
-
return parent;
|
|
9337
|
-
}), rootStream);
|
|
9338
|
-
}
|
|
9339
|
-
if (this.call.state.callingState === CallingState.JOINED) {
|
|
9340
|
-
await this.publishStream(stream);
|
|
9377
|
+
}
|
|
9341
9378
|
}
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
this.isTrackStoppedDueToTrackEnd = true;
|
|
9348
|
-
setTimeout(() => {
|
|
9349
|
-
this.isTrackStoppedDueToTrackEnd = false;
|
|
9350
|
-
}, 2000);
|
|
9351
|
-
await this.disable();
|
|
9352
|
-
}
|
|
9353
|
-
};
|
|
9354
|
-
const createTrackMuteHandler = (muted) => () => {
|
|
9355
|
-
if (!isMobile() || this.trackType !== TrackType.VIDEO)
|
|
9356
|
-
return;
|
|
9357
|
-
this.call.notifyTrackMuteState(muted, this.trackType).catch((err) => {
|
|
9358
|
-
this.logger('warn', 'Error while notifying track mute state', err);
|
|
9359
|
-
});
|
|
9360
|
-
};
|
|
9361
|
-
stream.getTracks().forEach((track) => {
|
|
9362
|
-
const muteHandler = createTrackMuteHandler(true);
|
|
9363
|
-
const unmuteHandler = createTrackMuteHandler(false);
|
|
9364
|
-
track.addEventListener('mute', muteHandler);
|
|
9365
|
-
track.addEventListener('unmute', unmuteHandler);
|
|
9366
|
-
track.addEventListener('ended', handleTrackEnded);
|
|
9367
|
-
this.subscriptions.push(() => {
|
|
9368
|
-
track.removeEventListener('mute', muteHandler);
|
|
9369
|
-
track.removeEventListener('unmute', unmuteHandler);
|
|
9370
|
-
track.removeEventListener('ended', handleTrackEnded);
|
|
9371
|
-
});
|
|
9372
|
-
});
|
|
9379
|
+
catch (err) {
|
|
9380
|
+
if (rootStream) {
|
|
9381
|
+
disposeOfMediaStream(await rootStream);
|
|
9382
|
+
}
|
|
9383
|
+
throw err;
|
|
9373
9384
|
}
|
|
9374
9385
|
}
|
|
9375
9386
|
get mediaDeviceKind() {
|
|
@@ -9491,6 +9502,9 @@ class InputMediaDeviceManagerState {
|
|
|
9491
9502
|
this.hasBrowserPermission$ = permission
|
|
9492
9503
|
? permission.asObservable().pipe(shareReplay(1))
|
|
9493
9504
|
: of(true);
|
|
9505
|
+
this.browserPermissionState$ = permission
|
|
9506
|
+
? permission.asStateObservable().pipe(shareReplay(1))
|
|
9507
|
+
: of('prompt');
|
|
9494
9508
|
this.isPromptingPermission$ = permission
|
|
9495
9509
|
? permission.getIsPromptingObservable().pipe(shareReplay(1))
|
|
9496
9510
|
: of(false);
|
|
@@ -12124,11 +12138,26 @@ class Call {
|
|
|
12124
12138
|
*
|
|
12125
12139
|
* @param callSessionID the call session ID to retrieve statistics for.
|
|
12126
12140
|
* @returns The call stats.
|
|
12141
|
+
* @deprecated use `call.getCallReport` instead.
|
|
12142
|
+
* @internal
|
|
12127
12143
|
*/
|
|
12128
12144
|
this.getCallStats = async (callSessionID) => {
|
|
12129
12145
|
const endpoint = `${this.streamClientBasePath}/stats/${callSessionID}`;
|
|
12130
12146
|
return this.streamClient.get(endpoint);
|
|
12131
12147
|
};
|
|
12148
|
+
/**
|
|
12149
|
+
* Retrieve call report. If the `callSessionID` is not specified, then the
|
|
12150
|
+
* report for the latest call session is retrieved. If it is specified, then
|
|
12151
|
+
* the report for that particular session is retrieved if it exists.
|
|
12152
|
+
*
|
|
12153
|
+
* @param callSessionID the optional call session ID to retrieve statistics for
|
|
12154
|
+
* @returns the call report
|
|
12155
|
+
*/
|
|
12156
|
+
this.getCallReport = async (callSessionID = '') => {
|
|
12157
|
+
const endpoint = `${this.streamClientBasePath}/report`;
|
|
12158
|
+
const params = callSessionID !== '' ? { session_id: callSessionID } : {};
|
|
12159
|
+
return this.streamClient.get(endpoint, params);
|
|
12160
|
+
};
|
|
12132
12161
|
/**
|
|
12133
12162
|
* Submit user feedback for the call
|
|
12134
12163
|
*
|
|
@@ -13443,7 +13472,7 @@ class StreamClient {
|
|
|
13443
13472
|
this.getUserAgent = () => {
|
|
13444
13473
|
if (!this.cachedUserAgent) {
|
|
13445
13474
|
const { clientAppIdentifier = {} } = this.options;
|
|
13446
|
-
const { sdkName = 'js', sdkVersion = "1.
|
|
13475
|
+
const { sdkName = 'js', sdkVersion = "1.20.1", ...extras } = clientAppIdentifier;
|
|
13447
13476
|
this.cachedUserAgent = [
|
|
13448
13477
|
`stream-video-${sdkName}-v${sdkVersion}`,
|
|
13449
13478
|
...Object.entries(extras).map(([key, value]) => `${key}=${value}`),
|