feat(rtc-stats): support for react native and breakout rooms (#11835)

* feat(rtc-stats): mobile flow

* fix(rtc-stats): create websocket connection event

* feat(rtc-stats): separate middlewares and filter callstats pcs

* fix: linting problems

* fix: linting problems 2

* fix(rtc-stats): middlewares

* ref(rtc-stats): rewrite in typescript

* remove blank line and flow tag

* fix: linting problems

* remove redundant comment

* remove index file

* fix: sort interface keys

* feat(rtc-stats): support for breakout rooms

* ref(rtc-stats): send isBreakoutRoom flag when connecting to rtc stats

* code review

* fix(rtc-stats): rebase issues

* remove empty lines

* chore(rtc-stats): update rtc-stats to latest version

* fix: linting issues
This commit is contained in:
Gabriel Borlea 2022-09-06 10:42:59 +03:00 committed by GitHub
parent 06842c724a
commit 9323b86e3c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 257 additions and 133 deletions

14
package-lock.json generated
View File

@ -30,7 +30,7 @@
"@jitsi/js-utils": "2.0.3",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.2.0",
"@jitsi/rtcstats": "9.3.0",
"@material-ui/core": "4.11.3",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",
@ -3481,9 +3481,9 @@
"integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
},
"node_modules/@jitsi/rtcstats": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.2.0.tgz",
"integrity": "sha512-bGQRLFio25++bi3s0em0xKD+WIqhDwg8OQ71K4BXZNOVL3eVX3R/bxbSEok6UYjFQxwoVjfdM4J8cHUu7RwrQw==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.3.0.tgz",
"integrity": "sha512-aipr1Tt/vfouMmgISCSu64Np3pD1u51y/2SztYNDt5bd6f79Qrieceu0JFqZWxC9KQRsamoJL7Mb9qxo2KkULg==",
"dependencies": {
"@jitsi/js-utils": "^2.0.0",
"sdp": "^3.0.3",
@ -22421,9 +22421,9 @@
"integrity": "sha512-JujivPbOUvdRYa2xqByHYKfKGNGa7ZPyNLaNuh8hEp9XsiNfjaJAHdboq6M1VY9TP+765nyxC0LjpAw1VkikOQ=="
},
"@jitsi/rtcstats": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.2.0.tgz",
"integrity": "sha512-bGQRLFio25++bi3s0em0xKD+WIqhDwg8OQ71K4BXZNOVL3eVX3R/bxbSEok6UYjFQxwoVjfdM4J8cHUu7RwrQw==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.3.0.tgz",
"integrity": "sha512-aipr1Tt/vfouMmgISCSu64Np3pD1u51y/2SztYNDt5bd6f79Qrieceu0JFqZWxC9KQRsamoJL7Mb9qxo2KkULg==",
"requires": {
"@jitsi/js-utils": "^2.0.0",
"sdp": "^3.0.3",

View File

@ -35,7 +35,7 @@
"@jitsi/js-utils": "2.0.3",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.2.0",
"@jitsi/rtcstats": "9.3.0",
"@material-ui/core": "4.11.3",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@microsoft/microsoft-graph-client": "3.0.1",

View File

@ -110,6 +110,9 @@ function _addConferenceListeners(conference, dispatch, state) {
conference.on(
JitsiConferenceEvents.CONFERENCE_JOINED,
(...args) => dispatch(conferenceJoined(conference, ...args)));
conference.on(
JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET,
(...args) => dispatch(conferenceUniqueIdSet(conference, ...args)));
conference.on(
JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS,
(...args) => dispatch(conferenceJoinInProgress(conference, ...args)));

View File

@ -100,6 +100,8 @@ export interface IConfig {
rtcstatsEnabled?: boolean;
rtcstatsEndpoint?: string;
rtcstatsPollInterval?: number;
rtcstatsSendSdp?: boolean;
rtcstatsUseLegacy?: boolean;
scriptURLs?: Array<string>;
};
apiLogLevels?: Array<'warn' | 'log' | 'error' | 'info' | 'debug'>;

View File

@ -1,12 +1,21 @@
/* eslint-disable import/order */
// @ts-ignore
import rtcstatsInit from '@jitsi/rtcstats/rtcstats';
// @ts-ignore
import traceInit from '@jitsi/rtcstats/trace-ws';
import {
createRTCStatsTraceCloseEvent,
sendAnalytics
} from '../analytics';
// @ts-ignore
import { createRTCStatsTraceCloseEvent, sendAnalytics } from '../analytics';
import logger from './logger';
import {
DominantSpeakerData,
E2ERTTData,
FaceLandmarksData,
InitOptions,
VideoTypeData
} from './types';
/**
* Filter out RTCPeerConnection that are created by callstats.io.
@ -14,10 +23,10 @@ import logger from './logger';
* @param {*} config - Config object sent to the PC c'tor.
* @returns {boolean}
*/
function connectionFilter(config) {
function connectionFilter(config: any) {
if (config && config.iceServers[0] && config.iceServers[0].urls) {
for (const iceUrl of config.iceServers[0].urls) {
if (iceUrl.indexOf('taas.callstats.io') >= 0) {
if (iceUrl.indexOf('callstats.io') >= 0) {
return true;
}
}
@ -29,6 +38,9 @@ function connectionFilter(config) {
* initialized once.
*/
class RTCStats {
trace: any;
initialized = false;
/**
* Initialize the rtcstats components. First off we initialize the trace, which is a wrapped websocket
* that does the actual communication with the server. Secondly, the rtcstats component is initialized,
@ -38,18 +50,20 @@ class RTCStats {
*
* @param {Object} options -.
* @param {string} options.endpoint - The Amplitude app key required.
* @param {string} options.useLegacy - Switch to legacy chrome webrtc statistics. Parameter will only have
* @param {string} options.meetingFqn - The meeting fqn.
* @param {boolean} options.useLegacy - Switch to legacy chrome webrtc statistics. Parameter will only have
* an effect on chrome based applications.
* @param {number} options.pollInterval - The getstats poll interval in ms.
* @param {boolean} options.sendSdp - Determines if the client sends SDP to the rtcstats server.
* @returns {void}
*/
init(options) {
init(options: InitOptions) {
const { endpoint, useLegacy, pollInterval, sendSdp } = options;
const { endpoint, meetingFqn, useLegacy, pollInterval, sendSdp } = options;
const traceOptions = {
endpoint,
meetingFqn,
onCloseCallback: this.handleTraceWSClose.bind(this),
useLegacy
};
@ -80,10 +94,10 @@ class RTCStats {
* It can be generally used to send additional metadata that might be relevant such as amplitude user data
* or deployment specific information.
*
* @param {Object} identityData - Metadata object to send as identity.
* @param {any} identityData - Metadata object to send as identity.
* @returns {void}
*/
sendIdentityData(identityData) {
sendIdentityData(identityData: any) {
this.trace && this.trace.identity('identity', null, identityData);
}
@ -93,7 +107,7 @@ class RTCStats {
* @param {Array<string|any>} logEntries - The log entries to send to the rtcstats server.
* @returns {void}
*/
sendLogs(logEntries) {
sendLogs(logEntries: Array<string|any>) {
this.trace && this.trace.statsEntry('logs', null, logEntries);
}
@ -103,7 +117,7 @@ class RTCStats {
* @param {Object} dominantSpeakerData - Dominant speaker data to be saved in the rtcstats dump.
* @returns {void}
*/
sendDominantSpeakerData(dominantSpeakerData) {
sendDominantSpeakerData(dominantSpeakerData: DominantSpeakerData) {
this.trace && this.trace.statsEntry('dominantSpeaker', null, dominantSpeakerData);
}
@ -113,7 +127,7 @@ class RTCStats {
* @param {Object} e2eRttData - The object that holds the e2e data.
* @returns {void}
*/
sendE2eRttData(e2eRttData) {
sendE2eRttData(e2eRttData: E2ERTTData) {
this.trace && this.trace.statsEntry('e2eRtt', null, e2eRttData);
}
@ -124,7 +138,7 @@ class RTCStats {
* @param {Object} timestamp - The object which contains the timestamp.
* @returns {void}
*/
sendConferenceTimestamp(timestamp) {
sendConferenceTimestamp(timestamp: number) {
this.trace && this.trace.statsEntry('conferenceStartTimestamp', null, timestamp);
}
@ -134,18 +148,18 @@ class RTCStats {
* @param {Object} videoTypeData - The object that holds the videoType data.
* @returns {void}
*/
sendVideoTypeData(videoTypeData) {
sendVideoTypeData(videoTypeData: VideoTypeData) {
this.trace && this.trace.statsEntry('setVideoType', null, videoTypeData);
}
/**
* Send face expression data, the data will be processed by rtcstats-server and saved in the dump file.
* Send face landmarks data, the data will be processed by rtcstats-server and saved in the dump file.
*
* @param {Object} faceExpressionData - Face expression data to be saved in the rtcstats dump.
* @param {Object} faceLandmarksData - Face landmarks data to be saved in the rtcstats dump.
* @returns {void}
*/
sendFaceExpressionData(faceExpressionData) {
this.trace && this.trace.statsEntry('faceLandmarks', null, faceExpressionData);
sendFaceLandmarksData(faceLandmarksData: FaceLandmarksData) {
this.trace && this.trace.statsEntry('faceLandmarks', null, faceLandmarksData);
}
/**
@ -153,10 +167,11 @@ class RTCStats {
* connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
* connected and sent once it is established.
*
* @param {boolean} isBreakoutRoom - Flag indicating if the user is in a breakout room.
* @returns {void}
*/
connect() {
this.trace && this.trace.connect();
connect(isBreakoutRoom: boolean) {
this.trace && this.trace.connect(isBreakoutRoom);
}
/**
@ -180,7 +195,7 @@ class RTCStats {
* @param {Object} closeEvent - Event sent by ws onclose.
* @returns {void}
*/
handleTraceWSClose(closeEvent) {
handleTraceWSClose(closeEvent: any) {
logger.info('RTCStats trace ws closed', closeEvent);
sendAnalytics(createRTCStatsTraceCloseEvent(closeEvent));

View File

@ -1,34 +0,0 @@
// @flow
import { toState } from '../base/redux';
import RTCStats from './RTCStats';
/**
* Checks whether rtcstats is enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function isRtcstatsEnabled(stateful: Function | Object) {
// TODO: Remove when rtcstats is fully cimpatible with mobile.
if (navigator.product === 'ReactNative') {
return false;
}
const state = toState(stateful);
const config = state['features/base/config'];
return config?.analytics?.rtcstatsEnabled ?? false;
}
/**
* Can the rtcstats service send data.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function canSendRtcstatsData(stateful: Function | Object) {
return isRtcstatsEnabled(stateful) && RTCStats.isInitialized();
}

View File

@ -0,0 +1,101 @@
/* eslint-disable import/order */
// @ts-ignore
import { jitsiLocalStorage } from '@jitsi/js-utils';
// @ts-ignore
import { getAmplitudeIdentity } from '../analytics';
import {
getConferenceOptions,
getAnalyticsRoomName
// @ts-ignore
} from '../base/conference';
// @ts-ignore
import { getLocalParticipant } from '../base/participants';
// @ts-ignore
import { toState } from '../base/redux';
import RTCStats from './RTCStats';
import logger from './logger';
/**
* Checks whether rtcstats is enabled or not.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function isRtcstatsEnabled(stateful: Function | Object) {
const state = toState(stateful);
const config = state['features/base/config'];
return config?.analytics?.rtcstatsEnabled ?? false;
}
/**
* Can the rtcstats service send data.
*
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @returns {boolean}
*/
export function canSendRtcstatsData(stateful: Function | Object) {
return isRtcstatsEnabled(stateful) && RTCStats.isInitialized();
}
type Identity = {
isBreakoutRoom: boolean;
// Unique identifier for a conference session, not to be confused with meeting name
// i.e. If all participants leave a meeting it will have a different value on the next join.
meetingUniqueId?: string;
roomId?: string;
}
/**
* Connects to the rtcstats service and sends the identity data.
*
* @param {Function} dispatch - The redux dispatch function.
* @param {Function|Object} stateful - The redux store or {@code getState} function.
* @param {Identity} identity - Identity data for the client.
* @returns {void}
*/
export function connectAndSendIdentity(dispatch: Function, stateful: Function | Object, identity: Identity) {
const state = toState(stateful);
if (canSendRtcstatsData(state)) {
// Once the conference started connect to the rtcstats server and send data.
try {
RTCStats.connect(identity.isBreakoutRoom);
const localParticipant = getLocalParticipant(state);
const options = getConferenceOptions(state);
// The current implementation of rtcstats-server is configured to send data to amplitude, thus
// we add identity specific information so we can correlate on the amplitude side. If amplitude is
// not configured an empty object will be sent.
// The current configuration of the conference is also sent as metadata to rtcstats server.
// This is done in order to facilitate queries based on different conference configurations.
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
// conference with a specific version.
// XXX(george): we also want to be able to correlate between rtcstats and callstats, so we're
// appending the callstats user name (if it exists) to the display name.
const displayName = options.statisticsId
|| options.statisticsDisplayName
|| jitsiLocalStorage.getItem('callStatsUserName');
RTCStats.sendIdentityData({
...getAmplitudeIdentity(),
...options,
endpointId: localParticipant?.id,
confName: getAnalyticsRoomName(state, dispatch),
displayName,
...identity
});
} catch (error) {
// If the connection failed do not impact jitsi-meet just silently fail.
logger.error('RTCStats connect failed with: ', error);
}
}
}

View File

@ -1 +0,0 @@
import './middleware';

View File

@ -1,5 +1,4 @@
// @flow
// @ts-ignore
import { getLogger } from '../base/logging/functions';
export default getLogger('features/rtcstats');

View File

@ -1,23 +1,34 @@
// @flow
import { jitsiLocalStorage } from '@jitsi/js-utils';
import { getAmplitudeIdentity } from '../analytics';
/* eslint-disable import/order */
import { IStore } from '../app/types';
import {
CONFERENCE_UNIQUE_ID_SET,
E2E_RTT_CHANGED,
CONFERENCE_JOINED,
CONFERENCE_TIMESTAMP_CHANGED,
getConferenceOptions,
getAnalyticsRoomName
CONFERENCE_UNIQUE_ID_SET,
CONFERENCE_WILL_LEAVE
// @ts-ignore
} from '../base/conference';
import { LIB_WILL_INIT } from '../base/lib-jitsi-meet/actionTypes';
import { DOMINANT_SPEAKER_CHANGED, getLocalParticipant } from '../base/participants';
// @ts-ignore
import { DOMINANT_SPEAKER_CHANGED } from '../base/participants';
// @ts-ignore
import { MiddlewareRegistry } from '../base/redux';
// @ts-ignore
import { TRACK_ADDED, TRACK_UPDATED } from '../base/tracks';
// @ts-ignore
import { isInBreakoutRoom, getCurrentRoomId } from '../breakout-rooms/functions';
// @ts-ignore
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import { ADD_FACE_EXPRESSION } from '../face-landmarks/actionTypes';
import RTCStats from './RTCStats';
import { canSendRtcstatsData, isRtcstatsEnabled } from './functions';
import { canSendRtcstatsData, connectAndSendIdentity, isRtcstatsEnabled } from './functions';
import logger from './logger';
/**
@ -27,30 +38,30 @@ import logger from './logger';
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const state = store.getState();
const { dispatch } = store;
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
const { analytics, faceLandmarks } = config;
switch (action.type) {
case LIB_WILL_INIT: {
if (isRtcstatsEnabled(state)) {
if (isRtcstatsEnabled(state) && !RTCStats.isInitialized()) {
// RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global
// window functions. Because lib-jitsi-meet uses references to those functions that are taken on
// init, we need to add these proxies before it initializes, otherwise lib-jitsi-meet will use the
// original non proxy versions of these functions.
try {
// Default poll interval is 10000ms and standard stats will be used, if not provided in the config.
const pollInterval = analytics.rtcstatsPollInterval || 10000;
const useLegacy = analytics.rtcstatsUseLegacy || false;
const sendSdp = analytics.rtcstatsSendSdp || false;
const pollInterval = analytics?.rtcstatsPollInterval || 10000;
const useLegacy = analytics?.rtcstatsUseLegacy || false;
const sendSdp = analytics?.rtcstatsSendSdp || false;
// Initialize but don't connect to the rtcstats server wss, as it will start sending data for all
// media calls made even before the conference started.
RTCStats.init({
endpoint: analytics.rtcstatsEndpoint,
endpoint: analytics?.rtcstatsEndpoint,
meetingFqn: extractFqnFromPath(state),
useLegacy,
pollInterval,
sendSdp
@ -61,6 +72,43 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
// Used for connecting to rtcstats server when joining a breakout room.
// Breakout rooms do not have a meetingUniqueId.
case CONFERENCE_JOINED: {
if (isInBreakoutRoom(getState())) {
connectAndSendIdentity(
dispatch,
state,
{
isBreakoutRoom: true,
roomId: getCurrentRoomId(getState())
}
);
}
break;
}
// Used for connecting to rtcstats server when joining the main room.
// Using this event to be sure the meetingUniqueId can be retrieved.
case CONFERENCE_UNIQUE_ID_SET: {
if (!isInBreakoutRoom(getState())) {
// Unique identifier for a conference session, not to be confused with meeting name
// i.e. If all participants leave a meeting it will have a different value on the next join.
const { conference } = action;
const meetingUniqueId = conference && conference.getMeetingUniqueId();
connectAndSendIdentity(
dispatch,
state,
{
isBreakoutRoom: false,
meetingUniqueId
}
);
}
break;
}
case TRACK_ADDED: {
if (canSendRtcstatsData(state)) {
const jitsiTrack = action?.track?.jitsiTrack;
@ -93,50 +141,6 @@ MiddlewareRegistry.register(store => next => action => {
}
break;
}
case CONFERENCE_UNIQUE_ID_SET: {
if (canSendRtcstatsData(state)) {
// Once the conference started connect to the rtcstats server and send data.
try {
RTCStats.connect();
const localParticipant = getLocalParticipant(state);
const options = getConferenceOptions(state);
// Unique identifier for a conference session, not to be confused with meeting name
// i.e. If all participants leave a meeting it will have a different value on the next join.
const { conference } = action;
const meetingUniqueId = conference && conference.getMeetingUniqueId();
// The current implementation of rtcstats-server is configured to send data to amplitude, thus
// we add identity specific information so we can correlate on the amplitude side. If amplitude is
// not configured an empty object will be sent.
// The current configuration of the conference is also sent as metadata to rtcstats server.
// This is done in order to facilitate queries based on different conference configurations.
// e.g. Find all RTCPeerConnections that connect to a specific shard or were created in a
// conference with a specific version.
// XXX(george): we also want to be able to correlate between rtcstats and callstats, so we're
// appending the callstats user name (if it exists) to the display name.
const displayName = options.statisticsId
|| options.statisticsDisplayName
|| jitsiLocalStorage.getItem('callStatsUserName');
RTCStats.sendIdentityData({
...getAmplitudeIdentity(),
...options,
endpointId: localParticipant?.id,
confName: getAnalyticsRoomName(state, dispatch),
displayName,
meetingUniqueId
});
} catch (error) {
// If the connection failed do not impact jitsi-meet just silently fail.
logger.error('RTCStats connect failed with: ', error);
}
}
break;
}
case DOMINANT_SPEAKER_CHANGED: {
if (canSendRtcstatsData(state)) {
const { id, previousSpeakers } = action.participant;
@ -162,7 +166,7 @@ MiddlewareRegistry.register(store => next => action => {
if (canSendRtcstatsData(state) && faceLandmarks && faceLandmarks.enableRTCStats) {
const { duration, faceExpression, timestamp } = action;
RTCStats.sendFaceExpressionData({
RTCStats.sendFaceLandmarksData({
duration,
faceLandmarks: faceExpression,
timestamp
@ -172,12 +176,18 @@ MiddlewareRegistry.register(store => next => action => {
}
case CONFERENCE_TIMESTAMP_CHANGED: {
if (canSendRtcstatsData(state)) {
const conferenceTimestamp = action.conferenceTimestamp;
const { conferenceTimestamp } = action;
RTCStats.sendConferenceTimestamp(conferenceTimestamp);
}
break;
}
case CONFERENCE_WILL_LEAVE: {
if (canSendRtcstatsData(state)) {
RTCStats.close();
}
break;
}
}
return next(action);

View File

@ -0,0 +1,29 @@
export type InitOptions = {
endpoint?: string;
meetingFqn: string;
pollInterval: number;
sendSdp: boolean;
useLegacy: boolean;
}
export type VideoTypeData = {
ssrc: number;
videoType: string;
}
export type DominantSpeakerData = {
dominantSpeakerEndpoint: string;
previousSpeakers: string[];
}
export type E2ERTTData = {
remoteEndpointId: string;
remoteRegion: string;
rtt: number;
}
export type FaceLandmarksData = {
duration: number;
faceLandmarks: string;
timestamp: number;
}