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.

SpeakerStatsCollector.js 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  2. import { XMPPEvents } from '../../service/xmpp/XMPPEvents';
  3. import SpeakerStats from './SpeakerStats';
  4. /**
  5. * The value to use for the "type" field for messages sent
  6. * over the data channel that contain a face landmark.
  7. */
  8. const FACE_LANDMARK_MESSAGE_TYPE = 'face-landmarks';
  9. /**
  10. * A collection for tracking speaker stats. Attaches listeners
  11. * to the conference to automatically update on tracked events.
  12. */
  13. export default class SpeakerStatsCollector {
  14. /**
  15. * Initializes a new SpeakerStatsCollector instance.
  16. *
  17. * @constructor
  18. * @param {JitsiConference} conference - The conference to track.
  19. * @returns {void}
  20. */
  21. constructor(conference) {
  22. this.stats = {
  23. users: {
  24. // userId: SpeakerStats
  25. },
  26. dominantSpeakerId: null
  27. };
  28. const userId = conference.myUserId();
  29. this.stats.users[userId] = new SpeakerStats(userId, null, true);
  30. this.conference = conference;
  31. conference.addEventListener(
  32. JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
  33. this._onDominantSpeaker.bind(this));
  34. conference.addEventListener(
  35. JitsiConferenceEvents.USER_JOINED,
  36. this._onUserJoin.bind(this));
  37. conference.addEventListener(
  38. JitsiConferenceEvents.USER_LEFT,
  39. this._onUserLeave.bind(this));
  40. conference.addEventListener(
  41. JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
  42. this._onDisplayNameChange.bind(this));
  43. conference.on(
  44. JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
  45. (participant, { type, faceLandmarks }) => {
  46. if (type === FACE_LANDMARK_MESSAGE_TYPE) {
  47. this._onFaceLandmarkAdd(participant.getId(), faceLandmarks);
  48. }
  49. });
  50. if (conference.xmpp) {
  51. conference.xmpp.addListener(
  52. XMPPEvents.SPEAKER_STATS_RECEIVED,
  53. this._updateStats.bind(this));
  54. }
  55. }
  56. /**
  57. * Reacts to dominant speaker change events by changing its speaker stats
  58. * models to reflect the current dominant speaker.
  59. *
  60. * @param {string} dominantSpeakerId - The user id of the new dominant speaker.
  61. * @param {Array[string]} previous - The array with previous speakers.
  62. * @param {boolean} silence - Indecates whether the dominant speaker is silent or not.
  63. * @returns {void}
  64. * @private
  65. */
  66. _onDominantSpeaker(dominantSpeakerId, previous, silence) {
  67. const oldDominantSpeaker
  68. = this.stats.users[this.stats.dominantSpeakerId];
  69. const newDominantSpeaker = this.stats.users[dominantSpeakerId];
  70. oldDominantSpeaker && oldDominantSpeaker.setDominantSpeaker(false);
  71. newDominantSpeaker && newDominantSpeaker.setDominantSpeaker(true, silence);
  72. this.stats.dominantSpeakerId = dominantSpeakerId;
  73. }
  74. /**
  75. * Reacts to user join events by creating a new SpeakerStats model.
  76. *
  77. * @param {string} userId - The user id of the new user.
  78. * @param {JitsiParticipant} - The JitsiParticipant model for the new user.
  79. * @returns {void}
  80. * @private
  81. */
  82. _onUserJoin(userId, participant) {
  83. if (participant.isHidden()) {
  84. return;
  85. }
  86. if (!this.stats.users[userId]) {
  87. this.stats.users[userId] = new SpeakerStats(userId, participant.getDisplayName());
  88. }
  89. }
  90. /**
  91. * Reacts to user leave events by updating the associated user's
  92. * SpeakerStats model.
  93. *
  94. * @param {string} userId - The user id of the user that left.
  95. * @returns {void}
  96. * @private
  97. */
  98. _onUserLeave(userId) {
  99. const savedUser = this.stats.users[userId];
  100. if (savedUser) {
  101. savedUser.markAsHasLeft();
  102. }
  103. }
  104. /**
  105. * Reacts to user name change events by updating the last known name
  106. * tracked in the associated SpeakerStats model.
  107. *
  108. * @param {string} userId - The user id of the user that left.
  109. * @returns {void}
  110. * @private
  111. */
  112. _onDisplayNameChange(userId, newName) {
  113. const savedUser = this.stats.users[userId];
  114. if (savedUser) {
  115. savedUser.setDisplayName(newName);
  116. }
  117. }
  118. /**
  119. * Processes a new face landmark object of a remote user.
  120. *
  121. * @param {string} userId - The user id of the user that left.
  122. * @param {Object} data - The face landmark object.
  123. * @returns {void}
  124. * @private
  125. */
  126. _onFaceLandmarkAdd(userId, data) {
  127. const savedUser = this.stats.users[userId];
  128. if (savedUser && data) {
  129. savedUser.addFaceLandmarks(data);
  130. }
  131. }
  132. /**
  133. * Return a copy of the tracked SpeakerStats models.
  134. *
  135. * @returns {Object} The keys are the user ids and the values are the
  136. * associated user's SpeakerStats model.
  137. */
  138. getStats() {
  139. return this.stats.users;
  140. }
  141. /**
  142. * Updates of the current stats is requested, passing the new values.
  143. *
  144. * @param {Object} newStats - The new values used to update current one.
  145. * @private
  146. */
  147. _updateStats(newStats) {
  148. for (const userId in newStats) { // eslint-disable-line guard-for-in
  149. let speakerStatsToUpdate;
  150. const newParticipant = this.conference.getParticipantById(userId);
  151. // we want to ignore hidden participants
  152. if (!newParticipant || !newParticipant.isHidden()) {
  153. if (this.stats.users[userId]) {
  154. speakerStatsToUpdate = this.stats.users[userId];
  155. if (!speakerStatsToUpdate.getDisplayName()) {
  156. speakerStatsToUpdate
  157. .setDisplayName(newStats[userId].displayName);
  158. }
  159. } else {
  160. speakerStatsToUpdate = new SpeakerStats(
  161. userId, newStats[userId].displayName);
  162. this.stats.users[userId] = speakerStatsToUpdate;
  163. speakerStatsToUpdate.markAsHasLeft();
  164. }
  165. speakerStatsToUpdate.totalDominantSpeakerTime
  166. = newStats[userId].totalDominantSpeakerTime;
  167. if (Array.isArray(newStats[userId].faceLandmarks)) {
  168. speakerStatsToUpdate.setFaceLandmarks(newStats[userId].faceLandmarks);
  169. }
  170. }
  171. }
  172. }
  173. }