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 8.6KB

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