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

SpeakerStatsCollector.ts 7.0KB

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