import { getLogger } from 'jitsi-meet-logger'; import * as ConferenceEvents from '../../JitsiConferenceEvents'; import * as ConnectionQualityEvents from '../../service/connectivity/ConnectionQualityEvents'; import * as MediaType from '../../service/RTC/MediaType'; import { createAudioOutputProblemEvent } from '../../service/statistics/AnalyticsEvents'; import Statistics from './statistics'; const logger = getLogger(__filename); /** * Number of remote samples that will be used for comparison with local ones. */ const NUMBER_OF_REMOTE_SAMPLES = 3; /** * 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._lastReceivedAudioLevel = {}; this._reportedParticipants = []; 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 }) { if (this._reportedParticipants.indexOf(userID) !== -1) { return; } if (!Array.isArray(this._lastReceivedAudioLevel[userID])) { this._lastReceivedAudioLevel[userID] = [ ]; } else if (this._lastReceivedAudioLevel[userID].length >= NUMBER_OF_REMOTE_SAMPLES) { this._lastReceivedAudioLevel[userID].shift(); } this._lastReceivedAudioLevel[userID].push(avgAudioLevels); } /** * 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(this._lastReceivedAudioLevel).forEach(userID => { if (this._reportedParticipants.indexOf(userID) !== -1) { // Do not report the participant again. return; } const remoteAudioLevels = this._lastReceivedAudioLevel[userID]; 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. return; } } if ((!(userID in avgAudioLevels) || avgAudioLevels[userID] === 0) && Array.isArray(remoteAudioLevels) && remoteAudioLevels.length === NUMBER_OF_REMOTE_SAMPLES && remoteAudioLevels.every(audioLevel => audioLevel > 0)) { const remoteAudioLevelsString = JSON.stringify(remoteAudioLevels); Statistics.sendAnalytics( createAudioOutputProblemEvent(userID, avgAudioLevels[userID], remoteAudioLevelsString)); logger.warn(`A potential problem is detected with the audio output for participant ${ userID}, local audio levels: ${avgAudioLevels[userID]}, remote audio levels: ${ remoteAudioLevelsString}`); this._reportedParticipants.push(userID); this._clearUserData(); } }); } /** * Clears the data stored for a participant. * * @param {string} userID - The id of the participant. * @returns {void} */ _clearUserData(userID) { delete this._lastReceivedAudioLevel[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._lastReceivedAudioLevel = undefined; this._reportedParticipants = undefined; this._conference = undefined; } }