Skip to content

Commit f46e927

Browse files
oliverlazkhushal87
andauthored
feat: SFU stats reporting (#1297)
Integrates the `SendStats` RPC method. The SDK will now send WebRTC stats on predefined intervals to the currently connected SFU. These stats will then be aggregated, processed, and displayed on our Dashboard to provide better observability to our customers about important call timeline events. Notes: this PR replaces #1276 --------- Co-authored-by: Khushal Agarwal <[email protected]>
1 parent 839b95b commit f46e927

File tree

14 files changed

+467
-27
lines changed

14 files changed

+467
-27
lines changed

packages/client/src/Call.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import {
4848
StartHLSBroadcastingResponse,
4949
StartRecordingRequest,
5050
StartRecordingResponse,
51+
StatsOptions,
5152
StopHLSBroadcastingResponse,
5253
StopLiveResponse,
5354
StopRecordingResponse,
@@ -93,10 +94,7 @@ import {
9394
VideoLayerSetting,
9495
} from './gen/video/sfu/event/events';
9596
import { Timestamp } from './gen/google/protobuf/timestamp';
96-
import {
97-
createStatsReporter,
98-
StatsReporter,
99-
} from './stats/state-store-stats-reporter';
97+
import { createStatsReporter, SfuStatsReporter, StatsReporter } from './stats';
10098
import { DynascaleManager } from './helpers/DynascaleManager';
10199
import { PermissionsContext } from './permissions';
102100
import { CallTypes } from './CallType';
@@ -203,6 +201,7 @@ export class Call {
203201
}>({ type: DebounceType.MEDIUM, data: [] });
204202

205203
private statsReporter?: StatsReporter;
204+
private sfuStatsReporter?: SfuStatsReporter;
206205
private dropTimeout: ReturnType<typeof setTimeout> | undefined;
207206

208207
private readonly clientStore: StreamVideoWriteableStateStore;
@@ -486,6 +485,9 @@ export class Call {
486485
this.statsReporter?.stop();
487486
this.statsReporter = undefined;
488487

488+
this.sfuStatsReporter?.stop();
489+
this.sfuStatsReporter = undefined;
490+
489491
this.subscriber?.close();
490492
this.subscriber = undefined;
491493

@@ -696,12 +698,14 @@ export class Call {
696698
let sfuServer: SFUResponse;
697699
let sfuToken: string;
698700
let connectionConfig: RTCConfiguration | undefined;
701+
let statsOptions: StatsOptions | undefined;
699702
try {
700703
if (this.sfuClient?.isFastReconnecting) {
701704
// use previous SFU configuration and values
702705
connectionConfig = this.publisher?.connectionConfiguration;
703706
sfuServer = this.sfuClient.sfuServer;
704707
sfuToken = this.sfuClient.token;
708+
statsOptions = this.sfuStatsReporter?.options;
705709
} else {
706710
// full join flow - let the Coordinator pick a new SFU for us
707711
const call = await join(this.streamClient, this.type, this.id, data);
@@ -711,6 +715,7 @@ export class Call {
711715
connectionConfig = call.connectionConfig;
712716
sfuServer = call.sfuServer;
713717
sfuToken = call.token;
718+
statsOptions = call.statsOptions;
714719
}
715720

716721
if (this.streamClient._hasConnectionID()) {
@@ -786,6 +791,8 @@ export class Call {
786791
this.publisher = undefined;
787792
this.statsReporter?.stop();
788793
this.statsReporter = undefined;
794+
this.sfuStatsReporter?.stop();
795+
this.sfuStatsReporter = undefined;
789796

790797
// clean up current connection
791798
sfuClient.close(
@@ -972,11 +979,10 @@ export class Call {
972979
});
973980
}
974981

975-
const audioSettings = this.state.settings?.audio;
976-
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
977-
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
978-
979982
if (!this.publisher) {
983+
const audioSettings = this.state.settings?.audio;
984+
const isDtxEnabled = !!audioSettings?.opus_dtx_enabled;
985+
const isRedEnabled = !!audioSettings?.redundant_coding_enabled;
980986
this.publisher = new Publisher({
981987
sfuClient,
982988
dispatcher: this.dispatcher,
@@ -995,6 +1001,17 @@ export class Call {
9951001
});
9961002
}
9971003

1004+
const clientDetails = getClientDetails();
1005+
if (!this.sfuStatsReporter && statsOptions) {
1006+
this.sfuStatsReporter = new SfuStatsReporter(sfuClient, {
1007+
clientDetails,
1008+
options: statsOptions,
1009+
subscriber: this.subscriber,
1010+
publisher: this.publisher,
1011+
});
1012+
this.sfuStatsReporter.start();
1013+
}
1014+
9981015
try {
9991016
// 1. wait for the signal server to be ready before sending "joinRequest"
10001017
sfuClient.signalReady
@@ -1015,7 +1032,7 @@ export class Call {
10151032

10161033
return sfuClient.join({
10171034
subscriberSdp: sdp || '',
1018-
clientDetails: getClientDetails(),
1035+
clientDetails,
10191036
migration,
10201037
fastReconnect: previousSfuClient?.isFastReconnecting ?? false,
10211038
});

packages/client/src/StreamSfuClient.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { JoinRequest, SfuRequest } from './gen/video/sfu/event/events';
1818
import {
1919
ICERestartRequest,
2020
SendAnswerRequest,
21+
SendStatsRequest,
2122
SetPublisherRequest,
2223
TrackSubscriptionDetails,
2324
UpdateMuteStatesRequest,
@@ -301,6 +302,17 @@ export class StreamSfuClient {
301302
);
302303
};
303304

305+
sendStats = async (stats: Omit<SendStatsRequest, 'sessionId'>) => {
306+
return retryable(
307+
() =>
308+
this.rpc.sendStats({
309+
...stats,
310+
sessionId: this.sessionId,
311+
}),
312+
this.logger,
313+
);
314+
};
315+
304316
join = async (data: Omit<JoinRequest, 'sessionId' | 'token'>) => {
305317
const joinRequest = JoinRequest.create({
306318
...data,

packages/client/src/client-details.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { ClientDetails, Device, OS, Sdk } from './gen/video/sfu/models/models';
22
import { isReactNative } from './helpers/platforms';
33
import { UAParser } from 'ua-parser-js';
44

5+
type WebRTCInfoType = {
6+
version: string;
7+
};
8+
59
let sdkInfo: Sdk | undefined;
610
let osInfo: OS | undefined;
711
let deviceInfo: Device | undefined;
12+
let webRtcInfo: WebRTCInfoType | undefined;
813

914
export const setSdkInfo = (info: Sdk) => {
1015
sdkInfo = info;
@@ -30,7 +35,19 @@ export const getDeviceInfo = () => {
3035
return deviceInfo;
3136
};
3237

33-
export const getClientDetails = (): ClientDetails => {
38+
export const getWebRTCInfo = () => {
39+
return webRtcInfo;
40+
};
41+
42+
export const setWebRTCInfo = (info: WebRTCInfoType) => {
43+
webRtcInfo = info;
44+
};
45+
46+
export type LocalClientDetailsType = ClientDetails & {
47+
webRTCInfo?: WebRTCInfoType;
48+
};
49+
50+
export const getClientDetails = (): LocalClientDetailsType => {
3451
if (isReactNative()) {
3552
// Since RN doesn't support web, sharing browser info is not required
3653
return {

packages/client/src/gen/coordinator/index.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2899,6 +2899,12 @@ export interface JoinCallResponse {
28992899
* @memberof JoinCallResponse
29002900
*/
29012901
own_capabilities: Array<OwnCapability>;
2902+
/**
2903+
*
2904+
* @type {StatsOptions}
2905+
* @memberof JoinCallResponse
2906+
*/
2907+
stats_options: StatsOptions;
29022908
}
29032909
/**
29042910
*
@@ -4062,6 +4068,19 @@ export interface StartTranscriptionResponse {
40624068
*/
40634069
duration: string;
40644070
}
4071+
/**
4072+
*
4073+
* @export
4074+
* @interface StatsOptions
4075+
*/
4076+
export interface StatsOptions {
4077+
/**
4078+
*
4079+
* @type {number}
4080+
* @memberof StatsOptions
4081+
*/
4082+
reporting_interval_ms: number;
4083+
}
40654084
/**
40664085
*
40674086
* @export

packages/client/src/gen/video/sfu/signal_rpc/signal.client.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import type {
1515
ICETrickleResponse,
1616
SendAnswerRequest,
1717
SendAnswerResponse,
18+
SendStatsRequest,
19+
SendStatsResponse,
1820
SetPublisherRequest,
1921
SetPublisherResponse,
2022
UpdateMuteStatesRequest,
@@ -80,6 +82,13 @@ export interface ISignalServerClient {
8082
input: ICERestartRequest,
8183
options?: RpcOptions,
8284
): UnaryCall<ICERestartRequest, ICERestartResponse>;
85+
/**
86+
* @generated from protobuf rpc: SendStats(stream.video.sfu.signal.SendStatsRequest) returns (stream.video.sfu.signal.SendStatsResponse);
87+
*/
88+
sendStats(
89+
input: SendStatsRequest,
90+
options?: RpcOptions,
91+
): UnaryCall<SendStatsRequest, SendStatsResponse>;
8392
}
8493
/**
8594
* @generated from protobuf service stream.video.sfu.signal.SignalServer
@@ -197,4 +206,21 @@ export class SignalServerClient implements ISignalServerClient, ServiceInfo {
197206
input,
198207
);
199208
}
209+
/**
210+
* @generated from protobuf rpc: SendStats(stream.video.sfu.signal.SendStatsRequest) returns (stream.video.sfu.signal.SendStatsResponse);
211+
*/
212+
sendStats(
213+
input: SendStatsRequest,
214+
options?: RpcOptions,
215+
): UnaryCall<SendStatsRequest, SendStatsResponse> {
216+
const method = this.methods[6],
217+
opt = this._transport.mergeOptions(options);
218+
return stackIntercept<SendStatsRequest, SendStatsResponse>(
219+
'unary',
220+
this._transport,
221+
method,
222+
opt,
223+
input,
224+
);
225+
}
200226
}

0 commit comments

Comments
 (0)