Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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