您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

AudioOutputProblemDetector.js 6.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { getLogger } from 'jitsi-meet-logger';
  2. import * as ConferenceEvents from '../../JitsiConferenceEvents';
  3. import * as MediaType from '../../service/RTC/MediaType';
  4. import * as ConnectionQualityEvents from '../../service/connectivity/ConnectionQualityEvents';
  5. import { createAudioOutputProblemEvent } from '../../service/statistics/AnalyticsEvents';
  6. import Statistics from './statistics';
  7. const logger = getLogger(__filename);
  8. /**
  9. * Number of local samples that will be used for comparison before and after the remote sample is received.
  10. */
  11. const NUMBER_OF_LOCAL_SAMPLES = 2;
  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._localAudioLevelCache = {};
  25. this._reportedParticipants = [];
  26. this._audioProblemCandidates = {};
  27. this._numberOfRemoteAudioLevelsReceived = {};
  28. this._onLocalAudioLevelsReport = this._onLocalAudioLevelsReport.bind(this);
  29. this._onRemoteAudioLevelReceived = this._onRemoteAudioLevelReceived.bind(this);
  30. this._clearUserData = this._clearUserData.bind(this);
  31. this._conference.on(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
  32. this._conference.statistics.addConnectionStatsListener(this._onLocalAudioLevelsReport);
  33. this._conference.on(ConferenceEvents.USER_LEFT, this._clearUserData);
  34. }
  35. /**
  36. * A listener for audio level data received by a remote participant.
  37. *
  38. * @param {string} userID - The user id of the participant that sent the data.
  39. * @param {number} audioLevel - The average audio level value.
  40. * @returns {void}
  41. */
  42. _onRemoteAudioLevelReceived(userID, { avgAudioLevels }) {
  43. const numberOfReports = (this._numberOfRemoteAudioLevelsReceived[userID] + 1) || 0;
  44. this._numberOfRemoteAudioLevelsReceived[userID] = numberOfReports;
  45. if (this._reportedParticipants.indexOf(userID) !== -1 || (userID in this._audioProblemCandidates)
  46. || avgAudioLevels <= 0 || numberOfReports < 3) {
  47. return;
  48. }
  49. const participant = this._conference.getParticipantById(userID);
  50. if (participant) {
  51. const tracks = participant.getTracksByMediaType(MediaType.AUDIO);
  52. if (tracks.length > 0 && participant.isAudioMuted()) {
  53. // We don't need to report an error if everything seems fine with the participant and its tracks but
  54. // the participant is audio muted. Since those are average audio levels we potentially can receive non
  55. // zero values for muted track.
  56. return;
  57. }
  58. }
  59. const localAudioLevels = this._localAudioLevelCache[userID];
  60. if (!Array.isArray(localAudioLevels) || localAudioLevels.every(audioLevel => audioLevel === 0)) {
  61. this._audioProblemCandidates[userID] = {
  62. remoteAudioLevels: avgAudioLevels,
  63. localAudioLevels: []
  64. };
  65. }
  66. }
  67. /**
  68. * A listener for audio level data retrieved by the local stats.
  69. *
  70. * @param {TraceablePeerConnection} tpc - The <tt>TraceablePeerConnection</tt> instance used to gather the data.
  71. * @param {Object} avgAudioLevels - The average audio levels per participant.
  72. * @returns {void}
  73. */
  74. _onLocalAudioLevelsReport(tpc, { avgAudioLevels }) {
  75. if (tpc !== this._conference.getActivePeerConnection()) {
  76. return;
  77. }
  78. Object.keys(avgAudioLevels).forEach(userID => {
  79. if (this._reportedParticipants.indexOf(userID) !== -1) {
  80. return;
  81. }
  82. const localAudioLevels = this._localAudioLevelCache[userID];
  83. if (!Array.isArray(localAudioLevels)) {
  84. this._localAudioLevelCache[userID] = [ ];
  85. } else if (localAudioLevels.length >= NUMBER_OF_LOCAL_SAMPLES) {
  86. localAudioLevels.shift();
  87. }
  88. this._localAudioLevelCache[userID].push(avgAudioLevels[userID]);
  89. });
  90. Object.keys(this._audioProblemCandidates).forEach(userID => {
  91. const { localAudioLevels, remoteAudioLevels } = this._audioProblemCandidates[userID];
  92. localAudioLevels.push(avgAudioLevels[userID]);
  93. if (localAudioLevels.length === NUMBER_OF_LOCAL_SAMPLES) {
  94. if (localAudioLevels.every(audioLevel => typeof audioLevel === 'undefined' || audioLevel === 0)) {
  95. const localAudioLevelsString = JSON.stringify(localAudioLevels);
  96. Statistics.sendAnalytics(
  97. createAudioOutputProblemEvent(userID, localAudioLevelsString, remoteAudioLevels));
  98. logger.warn(`A potential problem is detected with the audio output for participant ${
  99. userID}, local audio levels: ${localAudioLevelsString}, remote audio levels: ${
  100. remoteAudioLevels}`);
  101. this._reportedParticipants.push(userID);
  102. this._clearUserData(userID);
  103. }
  104. delete this._audioProblemCandidates[userID];
  105. }
  106. });
  107. }
  108. /**
  109. * Clears the data stored for a participant.
  110. *
  111. * @param {string} userID - The id of the participant.
  112. * @returns {void}
  113. */
  114. _clearUserData(userID) {
  115. delete this._localAudioLevelCache[userID];
  116. }
  117. /**
  118. * Disposes the allocated resources.
  119. *
  120. * @returns {void}
  121. */
  122. dispose() {
  123. this._conference.off(ConnectionQualityEvents.REMOTE_STATS_UPDATED, this._onRemoteAudioLevelReceived);
  124. this._conference.off(ConferenceEvents.USER_LEFT, this._clearUserData);
  125. this._conference.statistics.removeConnectionStatsListener(this._onLocalAudioLevelsReport);
  126. this._localAudioLevelCache = undefined;
  127. this._audioProblemCandidates = undefined;
  128. this._reportedParticipants = undefined;
  129. this._numberOfRemoteAudioLevelsReceived = undefined;
  130. this._conference = undefined;
  131. }
  132. }