You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AudioOutputProblemDetector.js 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import { getLogger } from 'jitsi-meet-logger';
  2. import * as ConferenceEvents from '../../JitsiConferenceEvents';
  3. import * as ConnectionQualityEvents from '../../service/connectivity/ConnectionQualityEvents';
  4. import * as MediaType from '../../service/RTC/MediaType';
  5. import { createAudioOutputProblemEvent } from '../../service/statistics/AnalyticsEvents';
  6. import Statistics from './statistics';
  7. const logger = getLogger(__filename);
  8. /**
  9. * Number of remote samples that will be used for comparison with local ones.
  10. */
  11. const NUMBER_OF_REMOTE_SAMPLES = 3;
  12. /**
  13. * Collects the average audio levels per participant from the local stats and the stats received by every remote
  14. * participant and compares them to detect potential audio problem for a participant.
  15. */
  16. export default class AudioOutputProblemDetector {
  17. /**
  18. * Creates new <tt>AudioOutputProblemDetector</tt> instance.
  19. *
  20. * @param {JitsiCofnerence} conference - The conference instance to be monitored.
  21. */
  22. constructor(conference) {
  23. this._conference = conference;
  24. this._lastReceivedAudioLevel = {};
  25. this._reportedParticipants = [];
  26. this._onLocalAudioLevelsReport = this._onLocalAudioLevelsReport.bind(this);
  27. this._onRemoteAudioLevelReceived = this._onRemoteAudioLevelReceived.bind(this);
  28. this._clearUserData = this._clearUserData.bind(this);
  29. this._conference.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
  30. this._conference.statistics.addConnectionStatsListener(this._onLocalAudioLevelsReport);
  31. this._conference.on(ConferenceEvents.USER_LEFT, this._clearUserData);
  32. }
  33. /**
  34. * A listener for audio level data received by a remote participant.
  35. *
  36. * @param {string} userID - The user id of the participant that sent the data.
  37. * @param {number} audioLevel - The average audio level value.
  38. * @returns {void}
  39. */
  40. _onRemoteAudioLevelReceived(userID, { avgAudioLevels }) {
  41. if (this._reportedParticipants.indexOf(userID) !== -1) {
  42. return;
  43. }
  44. if (!Array.isArray(this._lastReceivedAudioLevel[userID])) {
  45. this._lastReceivedAudioLevel[userID] = [ ];
  46. } else if (this._lastReceivedAudioLevel[userID].length >= NUMBER_OF_REMOTE_SAMPLES) {
  47. this._lastReceivedAudioLevel[userID].shift();
  48. }
  49. this._lastReceivedAudioLevel[userID].push(avgAudioLevels);
  50. }
  51. /**
  52. * A listener for audio level data retrieved by the local stats.
  53. *
  54. * @param {TraceablePeerConnection} tpc - The <tt>TraceablePeerConnection</tt> instance used to gather the data.
  55. * @param {Object} avgAudioLevels - The average audio levels per participant.
  56. * @returns {void}
  57. */
  58. _onLocalAudioLevelsReport(tpc, { avgAudioLevels }) {
  59. if (tpc !== this._conference.getActivePeerConnection()) {
  60. return;
  61. }
  62. Object.keys(this._lastReceivedAudioLevel).forEach(userID => {
  63. if (this._reportedParticipants.indexOf(userID) !== -1) {
  64. // Do not report the participant again.
  65. return;
  66. }
  67. const remoteAudioLevels = this._lastReceivedAudioLevel[userID];
  68. const participant = this._conference.getParticipantById(userID);
  69. if (participant) {
  70. const tracks = participant.getTracksByMediaType(MediaType.AUDIO);
  71. if (tracks.length > 0 && participant.isAudioMuted()) {
  72. // We don't need to report an error if everything seems fine with the participant and its tracks but
  73. // the participant is audio muted.
  74. return;
  75. }
  76. }
  77. if ((!(userID in avgAudioLevels) || avgAudioLevels[userID] === 0)
  78. && Array.isArray(remoteAudioLevels)
  79. && remoteAudioLevels.length === NUMBER_OF_REMOTE_SAMPLES
  80. && remoteAudioLevels.every(audioLevel => audioLevel > 0)) {
  81. const remoteAudioLevelsString = JSON.stringify(remoteAudioLevels);
  82. Statistics.sendAnalytics(
  83. createAudioOutputProblemEvent(userID, avgAudioLevels[userID], remoteAudioLevelsString));
  84. logger.warn(`A potential problem is detected with the audio output for participant ${
  85. userID}, local audio levels: ${avgAudioLevels[userID]}, remote audio levels: ${
  86. remoteAudioLevelsString}`);
  87. this._reportedParticipants.push(userID);
  88. this._clearUserData();
  89. }
  90. });
  91. }
  92. /**
  93. * Clears the data stored for a participant.
  94. *
  95. * @param {string} userID - The id of the participant.
  96. * @returns {void}
  97. */
  98. _clearUserData(userID) {
  99. delete this._lastReceivedAudioLevel[userID];
  100. }
  101. /**
  102. * Disposes the allocated resources.
  103. *
  104. * @returns {void}
  105. */
  106. dispose() {
  107. this._conference.off(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
  108. this._conference.off(ConferenceEvents.USER_LEFT, this._clearUserData);
  109. this._conference.statistics.removeConnectionStatsListener(this._onLocalAudioLevelsReport);
  110. this._lastReceivedAudioLevel = undefined;
  111. this._reportedParticipants = undefined;
  112. this._conference = undefined;
  113. }
  114. }