This commit is contained in:
148
react/features/rtcstats/RTCStats.ts
Normal file
148
react/features/rtcstats/RTCStats.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import JitsiMeetJS, { RTCStatsEvents } from '../base/lib-jitsi-meet';
|
||||
|
||||
import logger from './logger';
|
||||
import {
|
||||
DominantSpeakerData,
|
||||
E2ERTTData,
|
||||
FaceLandmarksData,
|
||||
VideoTypeData
|
||||
} from './types';
|
||||
|
||||
// TODO(saghul): expose these in libn-jitsi-meet?
|
||||
const PC_CON_STATE_CHANGE = 'connectionstatechange';
|
||||
const PC_STATE_CONNECTED = 'connected';
|
||||
const PC_STATE_FAILED = 'failed';
|
||||
|
||||
/**
|
||||
* Handle lib-jitsi-meet rtcstats events and send jitsi-meet specific statistics.
|
||||
*/
|
||||
class RTCStats {
|
||||
private _connStateEvents: Array<any> = [];
|
||||
private _initialized = false;
|
||||
|
||||
/**
|
||||
* Handles rtcstats events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
this._connStateEvents = [];
|
||||
|
||||
if (!this._initialized) {
|
||||
JitsiMeetJS.rtcstats.on(
|
||||
RTCStatsEvents.RTC_STATS_PC_EVENT,
|
||||
(pcEvent: any) => this.handleRTCStatsEvent(pcEvent));
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send console logs to rtcstats server.
|
||||
*
|
||||
* @param {Array<string|any>} logEntries - The log entries to send to the rtcstats server.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendLogs(logEntries: Array<string | any>) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('logs', logEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send dominant speaker data, the data will be processed by rtcstats-server and saved in the dump file.
|
||||
*
|
||||
* @param {Object} dominantSpeakerData - Dominant speaker data to be saved in the rtcstats dump.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendDominantSpeakerData(dominantSpeakerData: DominantSpeakerData) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('dominantSpeaker', dominantSpeakerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send e2e rtt data, the data will be processed by rtcstats-server and saved in the dump file.
|
||||
*
|
||||
* @param {Object} e2eRttData - The object that holds the e2e data.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendE2ERTTData(e2eRttData: E2ERTTData) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('e2eRtt', e2eRttData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send identity data, the data will be processed by rtcstats-server and saved in the dump file.
|
||||
*
|
||||
* @param {Object} identityData - The object that holds the identity data.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendIdentityData(identityData: Object) {
|
||||
JitsiMeetJS.rtcstats.sendIdentityEntry(identityData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the timestamp of the start of the conference, the data will be processed by the rtcstats-server
|
||||
* and saved in the dump file.
|
||||
*
|
||||
* @param {Object} timestamp - The object which contains the timestamp.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendConferenceTimestamp(timestamp: number) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('conferenceStartTimestamp', timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send videoType data, the data will be processed by rtcstats-server and saved in the dump file.
|
||||
*
|
||||
* @param {Object} videoTypeData - The object that holds the videoType data.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendVideoTypeData(videoTypeData: VideoTypeData) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('setVideoType', videoTypeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send face landmarks data, the data will be processed by rtcstats-server and saved in the dump file.
|
||||
*
|
||||
* @param {Object} faceLandmarksData - Face landmarks data to be saved in the rtcstats dump.
|
||||
* @returns {void}
|
||||
*/
|
||||
sendFaceLandmarksData(faceLandmarksData: FaceLandmarksData) {
|
||||
JitsiMeetJS.rtcstats.sendStatsEntry('faceLandmarks', faceLandmarksData);
|
||||
}
|
||||
|
||||
/**
|
||||
* RTCStats client can notify the APP of any PeerConnection related event that occurs.
|
||||
*
|
||||
* @param {Object} event - The PeerConnection event.
|
||||
* @param {string} event.type - The event type.
|
||||
* @param {Object} event.body - Event body.
|
||||
* @param {string} event.body.isP2P - PeerConnection type.
|
||||
* @param {string} event.body.state - PeerConnection state change which triggered the event.
|
||||
* @returns {void}
|
||||
*/
|
||||
handleRTCStatsEvent(event: any) {
|
||||
switch (event.type) {
|
||||
case PC_CON_STATE_CHANGE: {
|
||||
const { body: { isP2P = null, state = null } } = event;
|
||||
|
||||
this._connStateEvents.push(event.body);
|
||||
|
||||
// We only report PC related connection issues. If the rtcstats websocket is not connected at this point
|
||||
// it usually means that none of our services can be reached i.e. there's problem with the internet
|
||||
// connection and not necessarily with reaching the JVB (due to a firewall or other reasons).
|
||||
if (state === PC_STATE_FAILED) {
|
||||
const connectionType = isP2P ? 'P2P' : 'JVB';
|
||||
const wasConnected = this._connStateEvents.some((connectionEvent: { isP2P: any; state: string; }) =>
|
||||
(connectionEvent.isP2P === isP2P) && (connectionEvent.state === PC_STATE_CONNECTED));
|
||||
|
||||
logger.info(`${connectionType} PeerConnection failed, previously connected: ${wasConnected}`);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyPeerConnectionFailure(isP2P, wasConnected);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new RTCStats();
|
||||
29
react/features/rtcstats/functions.ts
Normal file
29
react/features/rtcstats/functions.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
/**
|
||||
* Checks whether rtcstats is enabled or not.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isRTCStatsEnabled(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { analytics } = state['features/base/config'];
|
||||
|
||||
return analytics?.rtcstatsEnabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the faceLandmarks data can be sent to the rtcstats server.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function canSendFaceLandmarksRTCStatsData(stateful: IStateful): boolean {
|
||||
const state = toState(stateful);
|
||||
const { faceLandmarks } = state['features/base/config'];
|
||||
|
||||
return Boolean(faceLandmarks?.enableRTCStats && isRTCStatsEnabled(state));
|
||||
}
|
||||
3
react/features/rtcstats/logger.ts
Normal file
3
react/features/rtcstats/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../base/logging/functions';
|
||||
|
||||
export default getLogger('features/rtcstats');
|
||||
129
react/features/rtcstats/middleware.ts
Normal file
129
react/features/rtcstats/middleware.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
E2E_RTT_CHANGED
|
||||
} from '../base/conference/actionTypes';
|
||||
import { DOMINANT_SPEAKER_CHANGED } from '../base/participants/actionTypes';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { TRACK_ADDED, TRACK_UPDATED } from '../base/tracks/actionTypes';
|
||||
import { ADD_FACE_LANDMARKS } from '../face-landmarks/actionTypes';
|
||||
import { FaceLandmarks } from '../face-landmarks/types';
|
||||
import { sendGetCustomerIdRequest } from '../jaas/functions';
|
||||
|
||||
import RTCStats from './RTCStats';
|
||||
import {
|
||||
canSendFaceLandmarksRTCStatsData,
|
||||
isRTCStatsEnabled
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Middleware which intercepts lib-jitsi-meet initialization and conference join in order init the
|
||||
* rtcstats-client.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyAction) => {
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
if (isRTCStatsEnabled(state)) {
|
||||
RTCStats.init();
|
||||
|
||||
sendGetCustomerIdRequest(action?.conference, state)
|
||||
.then(customerData => {
|
||||
const { customerId } = customerData ?? {};
|
||||
|
||||
customerId && RTCStats.sendIdentityData({ customerId });
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('Error while getting customer id:', error);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TRACK_ADDED: {
|
||||
if (isRTCStatsEnabled(state)) {
|
||||
const jitsiTrack = action?.track?.jitsiTrack;
|
||||
const { ssrc, videoType } = jitsiTrack || { };
|
||||
|
||||
// Remote tracks store their ssrc in the jitsiTrack object. Local tracks don't. See getSsrcByTrack.
|
||||
if (videoType && ssrc && !jitsiTrack.isLocal() && !jitsiTrack.isAudioTrack()) {
|
||||
RTCStats.sendVideoTypeData({
|
||||
ssrc,
|
||||
videoType
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TRACK_UPDATED: {
|
||||
if (isRTCStatsEnabled(state)) {
|
||||
const { videoType, jitsiTrack, muted } = action?.track || { };
|
||||
const { ssrc, isLocal, videoType: trackVideoType, conference } = jitsiTrack || { };
|
||||
|
||||
if (trackVideoType === 'camera' && conference && isLocal()) {
|
||||
RTCStats.sendFaceLandmarksData({
|
||||
duration: 0,
|
||||
faceLandmarks: muted ? 'camera-off' : 'camera-on',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// if the videoType of the remote track has changed we expect to find it in track.videoType. grep for
|
||||
// trackVideoTypeChanged.
|
||||
if (videoType && ssrc && !jitsiTrack.isLocal() && !jitsiTrack.isAudioTrack()) {
|
||||
|
||||
RTCStats.sendVideoTypeData({
|
||||
ssrc,
|
||||
videoType
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
if (isRTCStatsEnabled(state)) {
|
||||
const { id, previousSpeakers, silence } = action.participant;
|
||||
|
||||
RTCStats.sendDominantSpeakerData({
|
||||
dominantSpeakerEndpoint: silence ? null : id,
|
||||
previousSpeakers
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case E2E_RTT_CHANGED: {
|
||||
if (isRTCStatsEnabled(state)) {
|
||||
const { participant, rtt } = action.e2eRtt;
|
||||
|
||||
RTCStats.sendE2ERTTData({
|
||||
remoteEndpointId: participant.getId(),
|
||||
rtt,
|
||||
remoteRegion: participant.getProperty('region')
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ADD_FACE_LANDMARKS: {
|
||||
if (canSendFaceLandmarksRTCStatsData(state)) {
|
||||
const { duration, faceExpression, timestamp } = action.faceLandmarks as FaceLandmarks;
|
||||
const durationSeconds = Math.round(duration / 1000);
|
||||
|
||||
RTCStats.sendFaceLandmarksData({
|
||||
duration: durationSeconds,
|
||||
faceLandmarks: faceExpression,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
21
react/features/rtcstats/types.ts
Normal file
21
react/features/rtcstats/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user