import { getLogger } from '@jitsi/logger';
import * as ConferenceEvents from '../../JitsiConferenceEvents';
import * as MediaType from '../../service/RTC/MediaType';
import * as ConnectionQualityEvents from '../../service/connectivity/ConnectionQualityEvents';
import { createAudioOutputProblemEvent } from '../../service/statistics/AnalyticsEvents';
import Statistics from './statistics';
const logger = getLogger(__filename);
/**
* Number of local samples that will be used for comparison before and after the remote sample is received.
*/
const NUMBER_OF_LOCAL_SAMPLES = 2;
/**
* Collects the average audio levels per participant from the local stats and the stats received by every remote
* participant and compares them to detect potential audio problem for a participant.
*/
export default class AudioOutputProblemDetector {
/**
* Creates new AudioOutputProblemDetector instance.
*
* @param {JitsiCofnerence} conference - The conference instance to be monitored.
*/
constructor(conference) {
this._conference = conference;
this._localAudioLevelCache = {};
this._reportedParticipants = [];
this._audioProblemCandidates = {};
this._numberOfRemoteAudioLevelsReceived = {};
this._onLocalAudioLevelsReport = this._onLocalAudioLevelsReport.bind(this);
this._onRemoteAudioLevelReceived = this._onRemoteAudioLevelReceived.bind(this);
this._clearUserData = this._clearUserData.bind(this);
this._conference.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
this._conference.statistics.addConnectionStatsListener(this._onLocalAudioLevelsReport);
this._conference.on(ConferenceEvents.USER_LEFT, this._clearUserData);
}
/**
* A listener for audio level data received by a remote participant.
*
* @param {string} userID - The user id of the participant that sent the data.
* @param {number} audioLevel - The average audio level value.
* @returns {void}
*/
_onRemoteAudioLevelReceived(userID, { avgAudioLevels }) {
const numberOfReports = (this._numberOfRemoteAudioLevelsReceived[userID] + 1) || 0;
this._numberOfRemoteAudioLevelsReceived[userID] = numberOfReports;
if (this._reportedParticipants.indexOf(userID) !== -1 || (userID in this._audioProblemCandidates)
|| avgAudioLevels <= 0 || numberOfReports < 3) {
return;
}
const participant = this._conference.getParticipantById(userID);
if (participant) {
const tracks = participant.getTracksByMediaType(MediaType.AUDIO);
if (tracks.length > 0 && participant.isAudioMuted()) {
// We don't need to report an error if everything seems fine with the participant and its tracks but
// the participant is audio muted. Since those are average audio levels we potentially can receive non
// zero values for muted track.
return;
}
}
const localAudioLevels = this._localAudioLevelCache[userID];
if (!Array.isArray(localAudioLevels) || localAudioLevels.every(audioLevel => audioLevel === 0)) {
this._audioProblemCandidates[userID] = {
remoteAudioLevels: avgAudioLevels,
localAudioLevels: []
};
}
}
/**
* A listener for audio level data retrieved by the local stats.
*
* @param {TraceablePeerConnection} tpc - The TraceablePeerConnection instance used to gather the data.
* @param {Object} avgAudioLevels - The average audio levels per participant.
* @returns {void}
*/
_onLocalAudioLevelsReport(tpc, { avgAudioLevels }) {
if (tpc !== this._conference.getActivePeerConnection()) {
return;
}
Object.keys(avgAudioLevels).forEach(userID => {
if (this._reportedParticipants.indexOf(userID) !== -1) {
return;
}
const localAudioLevels = this._localAudioLevelCache[userID];
if (!Array.isArray(localAudioLevels)) {
this._localAudioLevelCache[userID] = [ ];
} else if (localAudioLevels.length >= NUMBER_OF_LOCAL_SAMPLES) {
localAudioLevels.shift();
}
this._localAudioLevelCache[userID].push(avgAudioLevels[userID]);
});
Object.keys(this._audioProblemCandidates).forEach(userID => {
const { localAudioLevels, remoteAudioLevels } = this._audioProblemCandidates[userID];
localAudioLevels.push(avgAudioLevels[userID]);
if (localAudioLevels.length === NUMBER_OF_LOCAL_SAMPLES) {
if (localAudioLevels.every(audioLevel => typeof audioLevel === 'undefined' || audioLevel === 0)) {
const localAudioLevelsString = JSON.stringify(localAudioLevels);
Statistics.sendAnalytics(
createAudioOutputProblemEvent(userID, localAudioLevelsString, remoteAudioLevels));
logger.warn(`A potential problem is detected with the audio output for participant ${
userID}, local audio levels: ${localAudioLevelsString}, remote audio levels: ${
remoteAudioLevels}`);
this._reportedParticipants.push(userID);
this._clearUserData(userID);
}
delete this._audioProblemCandidates[userID];
}
});
}
/**
* Clears the data stored for a participant.
*
* @param {string} userID - The id of the participant.
* @returns {void}
*/
_clearUserData(userID) {
delete this._localAudioLevelCache[userID];
}
/**
* Disposes the allocated resources.
*
* @returns {void}
*/
dispose() {
this._conference.off(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
this._conference.off(ConferenceEvents.USER_LEFT, this._clearUserData);
this._conference.statistics.removeConnectionStatsListener(this._onLocalAudioLevelsReport);
this._localAudioLevelCache = undefined;
this._audioProblemCandidates = undefined;
this._reportedParticipants = undefined;
this._numberOfRemoteAudioLevelsReceived = undefined;
this._conference = undefined;
}
}