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.ts 7.0KB

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