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.

RTCStats.ts 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. import { getLogger } from '@jitsi/logger';
  2. import rtcstatsInit from '@jitsi/rtcstats/rtcstats';
  3. import traceInit from '@jitsi/rtcstats/trace-ws';
  4. import {
  5. CONFERENCE_CREATED_TIMESTAMP,
  6. CONFERENCE_JOINED,
  7. CONFERENCE_LEFT,
  8. CONFERENCE_UNIQUE_ID_SET
  9. } from '../../JitsiConferenceEvents';
  10. import JitsiConference from '../../JitsiConference';
  11. import { IRTCStatsConfiguration } from './interfaces';
  12. import { RTC_STATS_PC_EVENT, RTC_STATS_WC_DISCONNECTED } from './RTCStatsEvents';
  13. import EventEmitter from '../util/EventEmitter';
  14. import Settings from '../settings/Settings';
  15. const logger = getLogger(__filename);
  16. /**
  17. * RTCStats Singleton that is initialized only once for the lifetime of the app, subsequent calls to init will be ignored.
  18. * Config and conference changes are handled by the start method.
  19. */
  20. class RTCStats {
  21. private _initialized: boolean = false;
  22. private _trace: any = null;
  23. public events: EventEmitter = new EventEmitter();
  24. /**
  25. * RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global objects.
  26. * The proxies will then send data to the rtcstats server via the trace object.
  27. * The initialization procedure must be called once when lib-jitsi-meet is loaded.
  28. *
  29. * @param {IRTCStatsConfiguration} initConfig initial config for rtcstats.
  30. * @returns {void}
  31. */
  32. init(initConfig: IRTCStatsConfiguration) {
  33. const {
  34. analytics: {
  35. rtcstatsUseLegacy: useLegacy = false,
  36. rtcstatsPollInterval: pollInterval= 10000,
  37. rtcstatsSendSdp: sendSdp = false,
  38. rtcstatsEnabled = false
  39. } = {}
  40. } = initConfig;
  41. // If rtcstats is not enabled or already initialized, do nothing.
  42. // Calling rtcsatsInit multiple times will cause the global objects to be rewritten multiple times,
  43. // with unforeseen consequences.
  44. if (!rtcstatsEnabled || this._initialized) return;
  45. rtcstatsInit(
  46. { statsEntry: this.sendStatsEntry.bind(this) },
  47. { pollInterval,
  48. useLegacy,
  49. sendSdp,
  50. eventCallback: (event) => this.events.emit(RTC_STATS_PC_EVENT, event)}
  51. );
  52. this._initialized = true;
  53. }
  54. /**
  55. * When a conference is about to start, we need to reset the trace module, and initialize it with the
  56. * new conference's config. On a normal conference flow this wouldn't be necessary, as the whole page is
  57. * reloaded, but in the case of breakout rooms or react native the js context doesn't reload, hence the
  58. * RTCStats singleton and its config persists between conferences.
  59. *
  60. * @param conference - JitsiConference instance that's about to start.
  61. * @returns {void}
  62. */
  63. start(conference: JitsiConference) {
  64. const {
  65. options: {
  66. config : confConfig = {},
  67. name: confName = ''
  68. } = {},
  69. _statsCurrentId : displayName = ''
  70. } = conference;
  71. const {
  72. analytics: {
  73. rtcstatsEnabled = false,
  74. rtcstatsEndpoint: endpoint = '',
  75. rtcstatsUseLegacy: useLegacy = false
  76. } = {}
  77. } = confConfig;
  78. // The statisticsId, statisticsDisplayName and _statsCurrentId (renamed to displayName) fields
  79. // that are sent through options might be a bit confusing. Depending on the context, they could
  80. // be intermixed inside ljm, for instance _statsCurrentId might refer to the email field which is stored
  81. // in statisticsId or it could have the same value as callStatsUserName.
  82. // The following is the mapping between the fields, and a short explanation of each:
  83. // statisticsId -> email, this is only send by jitsi-meet if enableEmailInStats option is set.
  84. // statisticsDisplayName -> nick, this is only send by jitsi-meet if enableDisplayNameInStats option is set.
  85. // localId, this is the unique id that is used to track users throughout stats.
  86. const localId = Settings?.callStatsUserName ?? '';
  87. // Reset the trace module in case it wasn't during the previous conference.
  88. // Closing the underlying websocket connection and deleting the trace obj.
  89. this.reset();
  90. // The new conference config might have rtcstats disabled, so we need to check again.
  91. if (!rtcstatsEnabled) return;
  92. // If rtcstats proxy module is not initialized, do nothing.
  93. if (!this._initialized) {
  94. logger.error('Calling start before RTCStats proxy module is initialized.');
  95. return;
  96. }
  97. // When the conference is joined, we need to initialize the trace module with the new conference's config.
  98. // The trace module will then connect to the rtcstats server and send the identity data.
  99. conference.once(CONFERENCE_JOINED, () => {
  100. const traceOptions = {
  101. endpoint,
  102. meetingFqn: confName,
  103. onCloseCallback: (event) => this.events.emit(RTC_STATS_WC_DISCONNECTED, event),
  104. useLegacy
  105. };
  106. const isBreakoutRoom = Boolean(conference.getBreakoutRooms()?.isBreakoutRoom());
  107. const endpointId = conference.myUserId();
  108. const meetingUniqueId = conference.getMeetingUniqueId();
  109. this._trace = traceInit(traceOptions);
  110. // Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
  111. // connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
  112. // connected and sent once it is established.
  113. this._trace.connect(isBreakoutRoom);
  114. const identityData = {
  115. ...confConfig,
  116. endpointId,
  117. confName,
  118. displayName,
  119. meetingUniqueId,
  120. isBreakoutRoom,
  121. localId
  122. }
  123. this.sendIdentity(identityData);
  124. });
  125. // Note, this will only be called for normal rooms, not breakout rooms.
  126. conference.once(CONFERENCE_UNIQUE_ID_SET, (meetingUniqueId) => {
  127. this.sendIdentity({meetingUniqueId});
  128. });
  129. conference.once(CONFERENCE_LEFT, () => {
  130. this.reset();
  131. });
  132. conference.once(CONFERENCE_CREATED_TIMESTAMP, (timestamp: number) => {
  133. this.sendStatsEntry('conferenceStartTimestamp', null, timestamp);
  134. })
  135. }
  136. /**
  137. * Sends the identity data to the rtcstats server.
  138. *
  139. * @param identityData - Identity data to send.
  140. * @returns {void}
  141. */
  142. sendIdentity(identityData) {
  143. this._trace?.identity('identity', null, identityData);
  144. }
  145. /**
  146. * Resets the trace module by closing the websocket and deleting the object.
  147. * After reset, the rtcstats proxy module that tries to send data via `sendStatsEntry`, will no longer
  148. * send any data, until the trace module is initialized again. This comes in handy on react-native
  149. * where ljm doesn't get reloaded, so we need to switch the trace module between conferences.
  150. *
  151. * @returns {void}
  152. */
  153. reset() {
  154. this._trace?.close();
  155. this._trace = null;
  156. }
  157. /**
  158. * Sends a stats entry to the rtcstats server. This is called by the rtcstats proxy module,
  159. * or any other app that wants to send custom stats.
  160. *
  161. * @param entry - Stats entry to send.
  162. * @returns {void}
  163. */
  164. sendStatsEntry(statsType, pcId, data) {
  165. this._trace?.statsEntry(statsType, pcId, data);
  166. }
  167. }
  168. export default new RTCStats();