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.

statistics.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
  2. import { JitsiTrackEvents } from '../../JitsiTrackEvents';
  3. import { FEEDBACK } from '../../service/statistics/AnalyticsEvents';
  4. import * as StatisticsEvents from '../../service/statistics/Events';
  5. import RTCStats from '../RTCStats/RTCStats';
  6. import browser from '../browser';
  7. import EventEmitter from '../util/EventEmitter';
  8. import WatchRTC from '../watchRTC/WatchRTC';
  9. import analytics from './AnalyticsAdapter';
  10. import LocalStats from './LocalStatsCollector';
  11. import { PerformanceObserverStats } from './PerformanceObserverStats';
  12. import RTPStats from './RTPStatsCollector';
  13. const logger = require('@jitsi/logger').getLogger(__filename);
  14. /**
  15. * Stores all active {@link Statistics} instances.
  16. * @type {Set<Statistics>}
  17. */
  18. let _instances;
  19. /**
  20. * Init statistic options
  21. * @param options
  22. */
  23. Statistics.init = function(options) {
  24. Statistics.audioLevelsEnabled = !options.disableAudioLevels;
  25. if (typeof options.pcStatsInterval === 'number') {
  26. Statistics.pcStatsInterval = options.pcStatsInterval;
  27. }
  28. if (typeof options.audioLevelsInterval === 'number') {
  29. Statistics.audioLevelsInterval = options.audioLevelsInterval;
  30. }
  31. if (typeof options.longTasksStatsInterval === 'number') {
  32. Statistics.longTasksStatsInterval = options.longTasksStatsInterval;
  33. }
  34. Statistics.disableThirdPartyRequests = options.disableThirdPartyRequests;
  35. LocalStats.init();
  36. // WatchRTC is not required to work for react native
  37. browser.isReactNative()
  38. ? logger.warn('Cannot initialize WatchRTC in a react native environment!')
  39. : WatchRTC.init(options);
  40. RTCStats.init(options);
  41. };
  42. /**
  43. * The options to configure Statistics.
  44. * @typedef {Object} StatisticsOptions
  45. * @property {string} userName - The user name to use
  46. * @property {string} roomName - The room name we are currently in.
  47. *
  48. * @param {JitsiConference} conference - The conference instance from which the statistics were initialized.
  49. * @param {StatisticsOptions} options - The options to use creating the
  50. * Statistics.
  51. */
  52. export default function Statistics(conference, options) {
  53. /**
  54. * {@link RTPStats} mapped by {@link TraceablePeerConnection.id} which
  55. * collect RTP statistics for each peerconnection.
  56. * @type {Map<string, RTPStats}
  57. */
  58. this.rtpStatsMap = new Map();
  59. this.eventEmitter = new EventEmitter();
  60. this.conference = conference;
  61. this.xmpp = conference?.xmpp;
  62. this.options = options || {};
  63. Statistics.instances.add(this);
  64. RTCStats.start(this.conference);
  65. // WatchRTC is not required to work for react native
  66. if (!browser.isReactNative()) {
  67. WatchRTC.start(this.options.roomName, this.options.userName);
  68. }
  69. }
  70. Statistics.audioLevelsEnabled = false;
  71. Statistics.audioLevelsInterval = 200;
  72. Statistics.pcStatsInterval = 10000;
  73. Statistics.disableThirdPartyRequests = false;
  74. Statistics.analytics = analytics;
  75. Object.defineProperty(Statistics, 'instances', {
  76. /**
  77. * Returns the Set holding all active {@link Statistics} instances. Lazily
  78. * initializes the Set to allow any Set polyfills to be applied.
  79. * @type {Set<Statistics>}
  80. */
  81. get() {
  82. if (!_instances) {
  83. _instances = new Set();
  84. }
  85. return _instances;
  86. }
  87. });
  88. /**
  89. * Starts collecting RTP stats for given peerconnection.
  90. * @param {TraceablePeerConnection} peerconnection
  91. */
  92. Statistics.prototype.startRemoteStats = function(peerconnection) {
  93. this.stopRemoteStats(peerconnection);
  94. try {
  95. const rtpStats
  96. = new RTPStats(
  97. peerconnection,
  98. Statistics.audioLevelsInterval,
  99. Statistics.pcStatsInterval,
  100. this.eventEmitter);
  101. rtpStats.start(Statistics.audioLevelsEnabled);
  102. this.rtpStatsMap.set(peerconnection.id, rtpStats);
  103. } catch (e) {
  104. logger.error(`Failed to start collecting remote statistics: ${e}`);
  105. }
  106. };
  107. Statistics.localStats = [];
  108. Statistics.startLocalStats = function(track, callback) {
  109. if (browser.isIosBrowser()) {
  110. // On iOS browsers audio is lost if the audio input device is in use by another app
  111. // https://bugs.webkit.org/show_bug.cgi?id=233473
  112. // The culprit was using the AudioContext, so now we close the AudioContext during
  113. // the track being muted, and re-instantiate it afterwards.
  114. track.addEventListener(
  115. JitsiTrackEvents.NO_DATA_FROM_SOURCE,
  116. /**
  117. * Closes AudioContext on no audio data, and enables it on data received again.
  118. *
  119. * @param {boolean} value - Whether we receive audio data or not.
  120. */
  121. async value => {
  122. if (value) {
  123. for (const localStat of Statistics.localStats) {
  124. localStat.stop();
  125. }
  126. await LocalStats.disconnectAudioContext();
  127. } else {
  128. LocalStats.connectAudioContext();
  129. for (const localStat of Statistics.localStats) {
  130. localStat.start();
  131. }
  132. }
  133. });
  134. }
  135. if (!Statistics.audioLevelsEnabled) {
  136. return;
  137. }
  138. track.addEventListener(
  139. JitsiTrackEvents.LOCAL_TRACK_STOPPED,
  140. () => {
  141. Statistics.stopLocalStats(track);
  142. });
  143. const stream = track.getOriginalStream();
  144. const localStats = new LocalStats(stream, Statistics.audioLevelsInterval,
  145. callback);
  146. this.localStats.push(localStats);
  147. localStats.start();
  148. };
  149. Statistics.prototype.addAudioLevelListener = function(listener) {
  150. if (!Statistics.audioLevelsEnabled) {
  151. return;
  152. }
  153. this.eventEmitter.on(StatisticsEvents.AUDIO_LEVEL, listener);
  154. };
  155. Statistics.prototype.removeAudioLevelListener = function(listener) {
  156. if (!Statistics.audioLevelsEnabled) {
  157. return;
  158. }
  159. this.eventEmitter.removeListener(StatisticsEvents.AUDIO_LEVEL, listener);
  160. };
  161. Statistics.prototype.addBeforeDisposedListener = function(listener) {
  162. this.eventEmitter.on(StatisticsEvents.BEFORE_DISPOSED, listener);
  163. };
  164. Statistics.prototype.removeBeforeDisposedListener = function(listener) {
  165. this.eventEmitter.removeListener(
  166. StatisticsEvents.BEFORE_DISPOSED, listener);
  167. };
  168. Statistics.prototype.addConnectionStatsListener = function(listener) {
  169. this.eventEmitter.on(StatisticsEvents.CONNECTION_STATS, listener);
  170. };
  171. Statistics.prototype.removeConnectionStatsListener = function(listener) {
  172. this.eventEmitter.removeListener(
  173. StatisticsEvents.CONNECTION_STATS,
  174. listener);
  175. };
  176. Statistics.prototype.addByteSentStatsListener = function(listener) {
  177. this.eventEmitter.on(StatisticsEvents.BYTE_SENT_STATS, listener);
  178. };
  179. Statistics.prototype.removeByteSentStatsListener = function(listener) {
  180. this.eventEmitter.removeListener(StatisticsEvents.BYTE_SENT_STATS,
  181. listener);
  182. };
  183. /**
  184. * Add a listener that would be notified on a LONG_TASKS_STATS event.
  185. *
  186. * @param {Function} listener a function that would be called when notified.
  187. * @returns {void}
  188. */
  189. Statistics.prototype.addLongTasksStatsListener = function(listener) {
  190. this.eventEmitter.on(StatisticsEvents.LONG_TASKS_STATS, listener);
  191. };
  192. /**
  193. * Creates an instance of {@link PerformanceObserverStats} and starts the
  194. * observer that records the stats periodically.
  195. *
  196. * @returns {void}
  197. */
  198. Statistics.prototype.attachLongTasksStats = function() {
  199. if (!browser.supportsPerformanceObserver()) {
  200. logger.warn('Performance observer for long tasks not supported by browser!');
  201. return;
  202. }
  203. this.performanceObserverStats = new PerformanceObserverStats(
  204. this.eventEmitter,
  205. Statistics.longTasksStatsInterval);
  206. this.conference.on(
  207. JitsiConferenceEvents.CONFERENCE_JOINED,
  208. () => this.performanceObserverStats.startObserver());
  209. this.conference.on(
  210. JitsiConferenceEvents.CONFERENCE_LEFT,
  211. () => this.performanceObserverStats.stopObserver());
  212. };
  213. /**
  214. * Obtains the current value of the LongTasks event statistics.
  215. *
  216. * @returns {Object|null} stats object if the observer has been
  217. * created, null otherwise.
  218. */
  219. Statistics.prototype.getLongTasksStats = function() {
  220. return this.performanceObserverStats
  221. ? this.performanceObserverStats.getLongTasksStats()
  222. : null;
  223. };
  224. /**
  225. * Removes the given listener for the LONG_TASKS_STATS event.
  226. *
  227. * @param {Function} listener the listener we want to remove.
  228. * @returns {void}
  229. */
  230. Statistics.prototype.removeLongTasksStatsListener = function(listener) {
  231. this.eventEmitter.removeListener(StatisticsEvents.LONG_TASKS_STATS, listener);
  232. };
  233. /**
  234. * Updates the list of speakers for which the audio levels are to be calculated. This is needed for the jvb pc only.
  235. *
  236. * @param {Array<string>} speakerList The list of remote endpoint ids.
  237. * @returns {void}
  238. */
  239. Statistics.prototype.setSpeakerList = function(speakerList) {
  240. for (const rtpStats of Array.from(this.rtpStatsMap.values())) {
  241. if (!rtpStats.peerconnection.isP2P) {
  242. rtpStats.setSpeakerList(speakerList);
  243. }
  244. }
  245. };
  246. Statistics.prototype.dispose = function() {
  247. try {
  248. this.eventEmitter.emit(StatisticsEvents.BEFORE_DISPOSED);
  249. for (const tpcId of this.rtpStatsMap.keys()) {
  250. this._stopRemoteStats(tpcId);
  251. }
  252. if (this.eventEmitter) {
  253. this.eventEmitter.removeAllListeners();
  254. }
  255. } finally {
  256. Statistics.instances.delete(this);
  257. }
  258. };
  259. Statistics.stopLocalStats = function(track) {
  260. if (!Statistics.audioLevelsEnabled) {
  261. return;
  262. }
  263. const stream = track.getOriginalStream();
  264. for (let i = 0; i < Statistics.localStats.length; i++) {
  265. if (Statistics.localStats[i].stream === stream) {
  266. const localStats = Statistics.localStats.splice(i, 1);
  267. localStats[0].stop();
  268. break;
  269. }
  270. }
  271. };
  272. /**
  273. * Stops remote RTP stats for given peerconnection ID.
  274. * @param {string} tpcId {@link TraceablePeerConnection.id}
  275. * @private
  276. */
  277. Statistics.prototype._stopRemoteStats = function(tpcId) {
  278. const rtpStats = this.rtpStatsMap.get(tpcId);
  279. if (rtpStats) {
  280. rtpStats.stop();
  281. this.rtpStatsMap.delete(tpcId);
  282. }
  283. };
  284. /**
  285. * Stops collecting RTP stats for given peerconnection
  286. * @param {TraceablePeerConnection} tpc
  287. */
  288. Statistics.prototype.stopRemoteStats = function(tpc) {
  289. this._stopRemoteStats(tpc.id);
  290. };
  291. /**
  292. * Sends the given feedback
  293. *
  294. * @param overall an integer between 1 and 5 indicating the user's rating.
  295. * @param comment the comment from the user.
  296. * @returns {Promise} Resolves immediately.
  297. */
  298. Statistics.prototype.sendFeedback = function(overall, comment) {
  299. // Statistics.analytics.sendEvent is currently fire and forget, without
  300. // confirmation of successful send.
  301. Statistics.analytics.sendEvent(
  302. FEEDBACK,
  303. {
  304. rating: overall,
  305. comment
  306. });
  307. return Promise.resolve();
  308. };
  309. Statistics.LOCAL_JID = require('../../service/statistics/constants').LOCAL_JID;
  310. /**
  311. * Sends event to analytics and logs a message to the logger/console.
  312. *
  313. * @param {string | Object} event the event name, or an object which
  314. * represents the entire event.
  315. * @param {Object} properties properties to attach to the event (if an event
  316. * name as opposed to an event object is provided).
  317. */
  318. Statistics.sendAnalyticsAndLog = function(event, properties = {}) {
  319. if (!event) {
  320. logger.warn('No event or event name given.');
  321. return;
  322. }
  323. let eventToLog;
  324. // Also support an API with a single object as an event.
  325. if (typeof event === 'object') {
  326. eventToLog = event;
  327. } else {
  328. eventToLog = {
  329. name: event,
  330. properties
  331. };
  332. }
  333. logger.log(JSON.stringify(eventToLog));
  334. // We do this last, because it may modify the object which is passed.
  335. this.analytics.sendEvent(event, properties);
  336. };
  337. /**
  338. * Sends event to analytics.
  339. *
  340. * @param {string | Object} eventName the event name, or an object which
  341. * represents the entire event.
  342. * @param {Object} properties properties to attach to the event
  343. */
  344. Statistics.sendAnalytics = function(eventName, properties = {}) {
  345. this.analytics.sendEvent(eventName, properties);
  346. };