@stream-io/video-client 1.41.1 → 1.41.3
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 +11 -0
- package/dist/index.browser.es.js +55 -30
- package/dist/index.browser.es.js.map +1 -1
- package/dist/index.cjs.js +55 -30
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.es.js +55 -30
- package/dist/index.es.js.map +1 -1
- package/dist/src/stats/SfuStatsReporter.d.ts +2 -1
- package/dist/src/stats/types.d.ts +10 -0
- package/package.json +1 -1
- package/src/Call.ts +5 -9
- package/src/devices/SpeakerManager.ts +11 -15
- package/src/stats/CallStateStatsReporter.ts +22 -0
- package/src/stats/SfuStatsReporter.ts +22 -14
- package/src/stats/types.ts +11 -0
|
@@ -29,6 +29,7 @@ export declare class SfuStatsReporter {
|
|
|
29
29
|
private readonly unifiedSessionId;
|
|
30
30
|
private intervalId;
|
|
31
31
|
private timeoutId;
|
|
32
|
+
private reportCount;
|
|
32
33
|
private unsubscribeDevicePermissionsSubscription?;
|
|
33
34
|
private unsubscribeListDevicesSubscription?;
|
|
34
35
|
private readonly sdkName;
|
|
@@ -41,8 +42,8 @@ export declare class SfuStatsReporter {
|
|
|
41
42
|
sendReconnectionTime: (strategy: WebsocketReconnectStrategy, timeSeconds: number) => void;
|
|
42
43
|
private sendTelemetryData;
|
|
43
44
|
private run;
|
|
45
|
+
private scheduleNextReport;
|
|
44
46
|
start: () => void;
|
|
45
47
|
stop: () => void;
|
|
46
48
|
flush: () => void;
|
|
47
|
-
scheduleOne: (timeout: number) => void;
|
|
48
49
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
2
|
+
export type CameraStats = {
|
|
3
|
+
frameHeight?: number;
|
|
4
|
+
frameWidth?: number;
|
|
5
|
+
frameRate?: number;
|
|
6
|
+
};
|
|
2
7
|
export type BaseStats = {
|
|
3
8
|
audioLevel?: number;
|
|
4
9
|
bytesSent?: number;
|
|
@@ -19,6 +24,7 @@ export type BaseStats = {
|
|
|
19
24
|
concealmentEvents?: number;
|
|
20
25
|
packetsReceived?: number;
|
|
21
26
|
packetsLost?: number;
|
|
27
|
+
camera?: CameraStats;
|
|
22
28
|
};
|
|
23
29
|
export type StatsReport = {
|
|
24
30
|
rawStats?: RTCStatsReport;
|
|
@@ -47,6 +53,7 @@ export type AggregatedStatsReport = {
|
|
|
47
53
|
highestFrameWidth: number;
|
|
48
54
|
highestFrameHeight: number;
|
|
49
55
|
highestFramesPerSecond: number;
|
|
56
|
+
camera?: CameraStats;
|
|
50
57
|
codec: string;
|
|
51
58
|
codecPerTrackType: Partial<Record<TrackType, string>>;
|
|
52
59
|
timestamp: number;
|
|
@@ -108,6 +115,9 @@ export interface RTCMediaSourceStats {
|
|
|
108
115
|
kind: string;
|
|
109
116
|
trackIdentifier: string;
|
|
110
117
|
audioLevel?: number;
|
|
118
|
+
framesPerSecond?: number;
|
|
119
|
+
width?: number;
|
|
120
|
+
height?: number;
|
|
111
121
|
}
|
|
112
122
|
export type RTCCodecStats = {
|
|
113
123
|
id: string;
|
package/package.json
CHANGED
package/src/Call.ts
CHANGED
|
@@ -638,6 +638,8 @@ export class Call {
|
|
|
638
638
|
this.statsReporter?.stop();
|
|
639
639
|
this.statsReporter = undefined;
|
|
640
640
|
|
|
641
|
+
const leaveReason = message ?? reason ?? 'user is leaving the call';
|
|
642
|
+
this.tracer.trace('call.leaveReason', leaveReason);
|
|
641
643
|
this.sfuStatsReporter?.flush();
|
|
642
644
|
this.sfuStatsReporter?.stop();
|
|
643
645
|
this.sfuStatsReporter = undefined;
|
|
@@ -648,9 +650,7 @@ export class Call {
|
|
|
648
650
|
this.publisher?.dispose();
|
|
649
651
|
this.publisher = undefined;
|
|
650
652
|
|
|
651
|
-
await this.sfuClient?.leaveAndClose(
|
|
652
|
-
message ?? reason ?? 'user is leaving the call',
|
|
653
|
-
);
|
|
653
|
+
await this.sfuClient?.leaveAndClose(leaveReason);
|
|
654
654
|
this.sfuClient = undefined;
|
|
655
655
|
this.dynascaleManager.setSfuClient(undefined);
|
|
656
656
|
await this.dynascaleManager.dispose();
|
|
@@ -854,6 +854,7 @@ export class Call {
|
|
|
854
854
|
* Unless you are implementing a custom "ringing" flow, you should not use this method.
|
|
855
855
|
*/
|
|
856
856
|
accept = async () => {
|
|
857
|
+
this.tracer.trace('call.accept', '');
|
|
857
858
|
return this.streamClient.post<AcceptCallResponse>(
|
|
858
859
|
`${this.streamClientBasePath}/accept`,
|
|
859
860
|
);
|
|
@@ -871,6 +872,7 @@ export class Call {
|
|
|
871
872
|
reject = async (
|
|
872
873
|
reason: RejectReason = 'decline',
|
|
873
874
|
): Promise<RejectCallResponse> => {
|
|
875
|
+
this.tracer.trace('call.reject', reason);
|
|
874
876
|
return this.streamClient.post<RejectCallResponse, RejectCallRequest>(
|
|
875
877
|
`${this.streamClientBasePath}/reject`,
|
|
876
878
|
{ reason: reason },
|
|
@@ -1834,12 +1836,6 @@ export class Call {
|
|
|
1834
1836
|
}
|
|
1835
1837
|
}
|
|
1836
1838
|
|
|
1837
|
-
if (track.kind === 'video') {
|
|
1838
|
-
// schedules calibration report - the SFU will use the performance stats
|
|
1839
|
-
// to adjust the quality thresholds as early as possible
|
|
1840
|
-
this.sfuStatsReporter?.scheduleOne(3000);
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
1839
|
await this.updateLocalStreamState(mediaStream, ...trackTypes);
|
|
1844
1840
|
};
|
|
1845
1841
|
|
|
@@ -91,11 +91,7 @@ export class SpeakerManager {
|
|
|
91
91
|
* @returns an Observable that will be updated if a device is connected or disconnected
|
|
92
92
|
*/
|
|
93
93
|
listDevices() {
|
|
94
|
-
|
|
95
|
-
throw new Error(
|
|
96
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
97
|
-
);
|
|
98
|
-
}
|
|
94
|
+
assertUnsupportedInReactNative();
|
|
99
95
|
return getAudioOutputDevices(this.call.tracer);
|
|
100
96
|
}
|
|
101
97
|
|
|
@@ -107,11 +103,7 @@ export class SpeakerManager {
|
|
|
107
103
|
* @param deviceId empty string means the system default
|
|
108
104
|
*/
|
|
109
105
|
select(deviceId: string) {
|
|
110
|
-
|
|
111
|
-
throw new Error(
|
|
112
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
113
|
-
);
|
|
114
|
-
}
|
|
106
|
+
assertUnsupportedInReactNative();
|
|
115
107
|
this.state.setDevice(deviceId);
|
|
116
108
|
}
|
|
117
109
|
|
|
@@ -133,11 +125,7 @@ export class SpeakerManager {
|
|
|
133
125
|
* Note: This method is not supported in React Native
|
|
134
126
|
*/
|
|
135
127
|
setVolume(volume: number) {
|
|
136
|
-
|
|
137
|
-
throw new Error(
|
|
138
|
-
'This feature is not supported in React Native. Please visit https://getstream.io/video/docs/reactnative/core/camera-and-microphone/#speaker-management for more details',
|
|
139
|
-
);
|
|
140
|
-
}
|
|
128
|
+
assertUnsupportedInReactNative();
|
|
141
129
|
if (volume && (volume < 0 || volume > 1)) {
|
|
142
130
|
throw new Error('Volume must be between 0 and 1');
|
|
143
131
|
}
|
|
@@ -165,3 +153,11 @@ export class SpeakerManager {
|
|
|
165
153
|
});
|
|
166
154
|
}
|
|
167
155
|
}
|
|
156
|
+
|
|
157
|
+
const assertUnsupportedInReactNative = () => {
|
|
158
|
+
if (isReactNative()) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
'Unsupported in React Native. See: https://getstream.io/video/docs/react-native/guides/camera-and-microphone/#speaker-management',
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
AggregatedStatsReport,
|
|
3
3
|
AudioAggregatedStats,
|
|
4
4
|
BaseStats,
|
|
5
|
+
CameraStats,
|
|
5
6
|
ParticipantsStatsReport,
|
|
6
7
|
RTCCodecStats,
|
|
7
8
|
RTCMediaSourceStats,
|
|
@@ -237,6 +238,17 @@ export type StatsTransformOpts = {
|
|
|
237
238
|
publisher: Publisher | undefined;
|
|
238
239
|
};
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Extracts camera statistics from a media source.
|
|
243
|
+
*
|
|
244
|
+
* @param mediaSource the media source stats to extract camera info from.
|
|
245
|
+
*/
|
|
246
|
+
const getCameraStats = (mediaSource: RTCMediaSourceStats): CameraStats => ({
|
|
247
|
+
frameRate: mediaSource.framesPerSecond,
|
|
248
|
+
frameWidth: mediaSource.width,
|
|
249
|
+
frameHeight: mediaSource.height,
|
|
250
|
+
});
|
|
251
|
+
|
|
240
252
|
/**
|
|
241
253
|
* Transforms raw RTC stats into a slimmer and uniform across browsers format.
|
|
242
254
|
*
|
|
@@ -280,6 +292,7 @@ const transform = (
|
|
|
280
292
|
|
|
281
293
|
let trackType: TrackType | undefined;
|
|
282
294
|
let audioLevel: number | undefined;
|
|
295
|
+
let camera: CameraStats | undefined;
|
|
283
296
|
let concealedSamples: number | undefined;
|
|
284
297
|
let concealmentEvents: number | undefined;
|
|
285
298
|
let packetsReceived: number | undefined;
|
|
@@ -300,6 +313,9 @@ const transform = (
|
|
|
300
313
|
) {
|
|
301
314
|
audioLevel = mediaSource.audioLevel;
|
|
302
315
|
}
|
|
316
|
+
if (trackKind === 'video') {
|
|
317
|
+
camera = getCameraStats(mediaSource);
|
|
318
|
+
}
|
|
303
319
|
}
|
|
304
320
|
} else if (kind === 'subscriber' && trackKind === 'audio') {
|
|
305
321
|
const inboundStats = rtcStreamStats as RTCInboundRtpStreamStats;
|
|
@@ -333,6 +349,7 @@ const transform = (
|
|
|
333
349
|
concealmentEvents,
|
|
334
350
|
packetsReceived,
|
|
335
351
|
packetsLost,
|
|
352
|
+
camera,
|
|
336
353
|
};
|
|
337
354
|
});
|
|
338
355
|
|
|
@@ -354,6 +371,7 @@ const getEmptyVideoStats = (stats?: StatsReport): AggregatedStatsReport => {
|
|
|
354
371
|
highestFrameWidth: 0,
|
|
355
372
|
highestFrameHeight: 0,
|
|
356
373
|
highestFramesPerSecond: 0,
|
|
374
|
+
camera: {},
|
|
357
375
|
codec: '',
|
|
358
376
|
codecPerTrackType: {},
|
|
359
377
|
timestamp: Date.now(),
|
|
@@ -404,6 +422,10 @@ const aggregate = (stats: StatsReport): AggregatedStatsReport => {
|
|
|
404
422
|
maxArea = streamArea;
|
|
405
423
|
}
|
|
406
424
|
|
|
425
|
+
if (stream.trackType === TrackType.VIDEO) {
|
|
426
|
+
acc.camera = stream.camera;
|
|
427
|
+
}
|
|
428
|
+
|
|
407
429
|
qualityLimitationReasons.add(stream.qualityLimitationReason || '');
|
|
408
430
|
return acc;
|
|
409
431
|
}, aggregatedStats);
|
|
@@ -44,6 +44,7 @@ export class SfuStatsReporter {
|
|
|
44
44
|
|
|
45
45
|
private intervalId: NodeJS.Timeout | undefined;
|
|
46
46
|
private timeoutId: NodeJS.Timeout | undefined;
|
|
47
|
+
private reportCount: number = 0;
|
|
47
48
|
private unsubscribeDevicePermissionsSubscription?: () => void;
|
|
48
49
|
private unsubscribeListDevicesSubscription?: () => void;
|
|
49
50
|
private readonly sdkName: string;
|
|
@@ -208,18 +209,33 @@ export class SfuStatsReporter {
|
|
|
208
209
|
}
|
|
209
210
|
};
|
|
210
211
|
|
|
212
|
+
private scheduleNextReport = () => {
|
|
213
|
+
const intervals = [1500, 3000, 3000, 5000];
|
|
214
|
+
if (this.reportCount < intervals.length) {
|
|
215
|
+
this.timeoutId = setTimeout(() => {
|
|
216
|
+
this.flush();
|
|
217
|
+
this.reportCount++;
|
|
218
|
+
this.scheduleNextReport();
|
|
219
|
+
}, intervals[this.reportCount]);
|
|
220
|
+
} else {
|
|
221
|
+
clearInterval(this.intervalId);
|
|
222
|
+
this.intervalId = setInterval(() => {
|
|
223
|
+
this.flush();
|
|
224
|
+
}, this.options.reporting_interval_ms);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
211
228
|
start = () => {
|
|
212
229
|
if (this.options.reporting_interval_ms <= 0) return;
|
|
213
230
|
|
|
214
231
|
this.observeDevice(this.microphone, 'mic');
|
|
215
232
|
this.observeDevice(this.camera, 'camera');
|
|
216
233
|
|
|
234
|
+
this.reportCount = 0;
|
|
217
235
|
clearInterval(this.intervalId);
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
});
|
|
222
|
-
}, this.options.reporting_interval_ms);
|
|
236
|
+
clearTimeout(this.timeoutId);
|
|
237
|
+
|
|
238
|
+
this.scheduleNextReport();
|
|
223
239
|
};
|
|
224
240
|
|
|
225
241
|
stop = () => {
|
|
@@ -233,6 +249,7 @@ export class SfuStatsReporter {
|
|
|
233
249
|
this.intervalId = undefined;
|
|
234
250
|
clearTimeout(this.timeoutId);
|
|
235
251
|
this.timeoutId = undefined;
|
|
252
|
+
this.reportCount = 0;
|
|
236
253
|
};
|
|
237
254
|
|
|
238
255
|
flush = () => {
|
|
@@ -240,13 +257,4 @@ export class SfuStatsReporter {
|
|
|
240
257
|
this.logger.warn('Failed to flush report stats', err);
|
|
241
258
|
});
|
|
242
259
|
};
|
|
243
|
-
|
|
244
|
-
scheduleOne = (timeout: number) => {
|
|
245
|
-
clearTimeout(this.timeoutId);
|
|
246
|
-
this.timeoutId = setTimeout(() => {
|
|
247
|
-
this.run().catch((err) => {
|
|
248
|
-
this.logger.warn('Failed to report stats', err);
|
|
249
|
-
});
|
|
250
|
-
}, timeout);
|
|
251
|
-
};
|
|
252
260
|
}
|
package/src/stats/types.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { TrackType } from '../gen/video/sfu/models/models';
|
|
2
2
|
|
|
3
|
+
export type CameraStats = {
|
|
4
|
+
frameHeight?: number;
|
|
5
|
+
frameWidth?: number;
|
|
6
|
+
frameRate?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
3
9
|
export type BaseStats = {
|
|
4
10
|
audioLevel?: number;
|
|
5
11
|
bytesSent?: number;
|
|
@@ -20,6 +26,7 @@ export type BaseStats = {
|
|
|
20
26
|
concealmentEvents?: number;
|
|
21
27
|
packetsReceived?: number;
|
|
22
28
|
packetsLost?: number;
|
|
29
|
+
camera?: CameraStats;
|
|
23
30
|
};
|
|
24
31
|
|
|
25
32
|
export type StatsReport = {
|
|
@@ -51,6 +58,7 @@ export type AggregatedStatsReport = {
|
|
|
51
58
|
highestFrameWidth: number;
|
|
52
59
|
highestFrameHeight: number;
|
|
53
60
|
highestFramesPerSecond: number;
|
|
61
|
+
camera?: CameraStats;
|
|
54
62
|
codec: string;
|
|
55
63
|
codecPerTrackType: Partial<Record<TrackType, string>>;
|
|
56
64
|
timestamp: number;
|
|
@@ -118,6 +126,9 @@ export interface RTCMediaSourceStats {
|
|
|
118
126
|
kind: string;
|
|
119
127
|
trackIdentifier: string;
|
|
120
128
|
audioLevel?: number;
|
|
129
|
+
framesPerSecond?: number;
|
|
130
|
+
width?: number;
|
|
131
|
+
height?: number;
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
// shim for RTCCodecStats, not yet available in the standard types
|