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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 JitsiConnection from '../../JitsiConnection';
  6. import {
  7. BEFORE_STATISTICS_DISPOSED,
  8. CONFERENCE_CREATED_TIMESTAMP,
  9. CONFERENCE_JOINED,
  10. CONFERENCE_LEFT,
  11. CONFERENCE_UNIQUE_ID_SET
  12. } from '../../JitsiConferenceEvents';
  13. import Settings from '../settings/Settings';
  14. import EventEmitter from '../util/EventEmitter';
  15. import DefaultLogStorage from './DefaulLogStorage';
  16. import { RTC_STATS_PC_EVENT, RTC_STATS_WC_DISCONNECTED } from './RTCStatsEvents';
  17. import { IRTCStatsConfiguration, ITraceOptions } from './interfaces';
  18. const logger = getLogger('modules/RTCStats/RTCStats');
  19. /**
  20. * RTCStats Singleton that is initialized only once for the lifetime of the app, subsequent calls to init will be
  21. * ignored. Config and conference changes are handled by the start method.
  22. */
  23. class RTCStats {
  24. public events: EventEmitter = new EventEmitter();
  25. public _startedWithNewConnection: boolean = true;
  26. private _defaultLogCollector: any = null;
  27. private _initialized: boolean = false;
  28. private _trace: any = null;
  29. /**
  30. * RTCStats "proxies" WebRTC functions such as GUM and RTCPeerConnection by rewriting the global objects.
  31. * The proxies will then send data to the rtcstats server via the trace object.
  32. * The initialization procedure must be called once when lib-jitsi-meet is loaded.
  33. *
  34. * @param {IRTCStatsConfiguration} initConfig initial config for rtcstats.
  35. * @returns {void}
  36. */
  37. init(initConfig: IRTCStatsConfiguration) {
  38. const {
  39. analytics: {
  40. rtcstatsPollInterval: pollInterval = 10000,
  41. rtcstatsSendSdp: sendSdp = false,
  42. rtcstatsEnabled = false
  43. } = {}
  44. } = initConfig;
  45. // If rtcstats is not enabled or already initialized, do nothing.
  46. // Calling rtcsatsInit multiple times will cause the global objects to be rewritten multiple times,
  47. // with unforeseen consequences.
  48. if (!rtcstatsEnabled || this._initialized) {
  49. return;
  50. }
  51. rtcstatsInit(
  52. { statsEntry: this.sendStatsEntry.bind(this) },
  53. { pollInterval,
  54. useLegacy: false,
  55. sendSdp,
  56. eventCallback: event => this.events.emit(RTC_STATS_PC_EVENT, event) }
  57. );
  58. this._initialized = true;
  59. }
  60. isTraceAvailable() {
  61. return this._trace !== null;
  62. }
  63. /**
  64. * A JitsiConnection instance is created before the conference is joined, so even though
  65. * we don't have any conference specific data yet, we can initialize the trace module and
  66. * send any logs that might of otherwise be missed in case an error occurs between the connection
  67. * and conference initialization.
  68. *
  69. * @param connection - The JitsiConnection instance.
  70. * @returns {void}
  71. */
  72. startWithConnection(connection: JitsiConnection) {
  73. const { options } = connection;
  74. const name = options?.name ?? '';
  75. const {
  76. analytics: {
  77. rtcstatsEndpoint: endpoint = '',
  78. rtcstatsEnabled = false
  79. } = {},
  80. } = options;
  81. // Even though we have options being passed to init we need to recheck it as some client (react-native)
  82. // don't always re-initialize the module and could create multiple connections with different options.
  83. if (!rtcstatsEnabled) return;
  84. // If rtcstats proxy module is not initialized, do nothing (should never happen).
  85. if (!this._initialized) {
  86. logger.error('Calling startWithConnection before RTCStats proxy module is initialized.');
  87. return;
  88. }
  89. const traceOptions: ITraceOptions = {
  90. endpoint,
  91. meetingFqn: name,
  92. isBreakoutRoom: false
  93. };
  94. // Can't be a breakout room.
  95. this._connectTrace(traceOptions);
  96. this._defaultLogCollector?.flush();
  97. this.sendIdentity({
  98. confName: name,
  99. ...options
  100. });
  101. // This module is tightly tied with the ljm JitsiConnection and JitsiConference flows, technically
  102. // the connection isn't associated with a conference, but we still need to have some association for
  103. // data that is logged before the conference is joined.
  104. // In short the flow is as follows:
  105. // 1. Connection is created.
  106. // 2. The trace module is initialized and connected to the rtcstats server, so data starts being sent.
  107. // 3. Conference is created.
  108. // 4. If the trace wasn't already initialized from the connection creation, it will be initialized again.
  109. // this will take care of the cases where the connection is created and then multiple conferences are
  110. // sequentially joined and left, such as breakout rooms.
  111. this._startedWithNewConnection = true;
  112. }
  113. /**
  114. * When a conference is about to start, we need to reset the trace module, and initialize it with the
  115. * new conference's config. On a normal conference flow this wouldn't be necessary, as the whole page is
  116. * reloaded, but in the case of breakout rooms or react native the js context doesn't reload, hence the
  117. * RTCStats singleton and its config persists between conferences.
  118. *
  119. * @param conference - JitsiConference instance that's about to start.
  120. * @returns {void}
  121. */
  122. attachToConference(conference: JitsiConference) {
  123. const {
  124. options: {
  125. config: confConfig = {},
  126. name: confName = ''
  127. } = {},
  128. _statsCurrentId: displayName = ''
  129. } = conference;
  130. const {
  131. analytics: {
  132. rtcstatsEnabled = false,
  133. rtcstatsEndpoint: endpoint = ''
  134. } = {}
  135. } = confConfig;
  136. // The statisticsId, statisticsDisplayName and _statsCurrentId (renamed to displayName) fields
  137. // that are sent through options might be a bit confusing. Depending on the context, they could
  138. // be intermixed inside ljm, for instance _statsCurrentId might refer to the email field which is stored
  139. // in statisticsId or it could have the same value as callStatsUserName.
  140. // The following is the mapping between the fields, and a short explanation of each:
  141. // statisticsId -> email, this is only send by jitsi-meet if enableEmailInStats option is set.
  142. // statisticsDisplayName -> nick, this is only send by jitsi-meet if enableDisplayNameInStats option is set.
  143. // localId, this is the unique id that is used to track users throughout stats.
  144. const localId = Settings?.callStatsUserName ?? '';
  145. // The new conference config might have rtcstats disabled, so we need to check again.
  146. if (!rtcstatsEnabled) {
  147. return;
  148. }
  149. // If rtcstats proxy module is not initialized, do nothing.
  150. if (!this._initialized) {
  151. logger.error('Calling attachToConference before RTCStats proxy module is initialized.');
  152. return;
  153. }
  154. // When the conference is joined, we need to initialize the trace module with the new conference's config.
  155. // The trace module will then connect to the rtcstats server and send the identity data.
  156. conference.once(CONFERENCE_JOINED, () => {
  157. const isBreakoutRoom = Boolean(conference.getBreakoutRooms()?.isBreakoutRoom());
  158. const endpointId = conference.myUserId();
  159. const meetingUniqueId = conference.getMeetingUniqueId();
  160. // Connect to the rtcstats server instance. Stats (data obtained from getstats) won't be send until the
  161. // connect successfully initializes, however calls to GUM are recorded in an internal buffer even if not
  162. // connected and sent once it is established.
  163. if (!this._startedWithNewConnection) {
  164. const traceOptions = {
  165. endpoint,
  166. meetingFqn: confName,
  167. isBreakoutRoom
  168. };
  169. this._connectTrace(traceOptions);
  170. // In cases where the conference was left but the connection was not closed,
  171. // logs could get cached, so we flush them as soon as we get a chance after the
  172. // conference is joined.
  173. this._defaultLogCollector?.flush();
  174. }
  175. const identityData = {
  176. ...confConfig,
  177. endpointId,
  178. confName,
  179. displayName,
  180. meetingUniqueId,
  181. isBreakoutRoom,
  182. localId
  183. };
  184. this.sendIdentity(identityData);
  185. // Reset the flag, so that the next conference that is joined will have the trace module initialized, such as a breakout room.
  186. this._startedWithNewConnection = false;
  187. });
  188. // Note, this will only be called for normal rooms, not breakout rooms.
  189. conference.once(CONFERENCE_UNIQUE_ID_SET, meetingUniqueId => {
  190. this.sendIdentity({ meetingUniqueId });
  191. });
  192. conference.once(CONFERENCE_LEFT, () => {
  193. this.reset();
  194. });
  195. conference.once(CONFERENCE_CREATED_TIMESTAMP, (timestamp: number) => {
  196. this.sendStatsEntry('conferenceStartTimestamp', null, timestamp);
  197. });
  198. conference.once(
  199. BEFORE_STATISTICS_DISPOSED,
  200. () => this._defaultLogCollector?.flush()
  201. );
  202. }
  203. /**
  204. * Reset and connects the trace module to the s server.
  205. *
  206. * @param traceOptions - Options for the trace module.
  207. * @returns {void}
  208. */
  209. _connectTrace(traceOptions: ITraceOptions) {
  210. const traceOptionsComplete = {
  211. ...traceOptions,
  212. useLegacy: false,
  213. onCloseCallback: event => this.events.emit(RTC_STATS_WC_DISCONNECTED, event)
  214. };
  215. const { isBreakoutRoom } = traceOptionsComplete;
  216. this.reset();
  217. this._trace = traceInit(traceOptionsComplete);
  218. this._trace.connect(isBreakoutRoom);
  219. }
  220. /**
  221. * Sends the identity data to the rtcstats server.
  222. *
  223. * @param identityData - Identity data to send.
  224. * @returns {void}
  225. */
  226. sendIdentity(identityData) {
  227. this._trace?.identity('identity', null, identityData);
  228. }
  229. /**
  230. * Resets the trace module by closing the websocket and deleting the object.
  231. * After reset, the rtcstats proxy module that tries to send data via `sendStatsEntry`, will no longer
  232. * send any data, until the trace module is initialized again. This comes in handy on react-native
  233. * where ljm doesn't get reloaded, so we need to switch the trace module between conferences.
  234. *
  235. * @returns {void}
  236. */
  237. reset() {
  238. // If a trace is connected, flush the remaining logs before closing the connection,
  239. // if the trace is not present and we flush the logs will be lost,
  240. this._trace && this._defaultLogCollector?.flush();
  241. this._trace?.close();
  242. this._trace = null;
  243. }
  244. /**
  245. * Sends a stats entry to the rtcstats server. This is called by the rtcstats proxy module,
  246. * or any other app that wants to send custom stats.
  247. *
  248. * @param entry - Stats entry to send.
  249. * @returns {void}
  250. */
  251. sendStatsEntry(statsType, pcId, data) {
  252. this._trace?.statsEntry(statsType, pcId, data);
  253. }
  254. /**
  255. * Creates a new log collector with the default log storage.
  256. */
  257. getDefaultLogCollector(maxEntryLength) {
  258. if (!this._defaultLogCollector) {
  259. // If undefined is passed as maxEntryLength LogCollector will default to 10000 bytes
  260. this._defaultLogCollector = new Logger.LogCollector(new DefaultLogStorage(this), { maxEntryLength });
  261. this._defaultLogCollector.start();
  262. }
  263. return this._defaultLogCollector;
  264. }
  265. }
  266. export default new RTCStats();