選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

statistics.js 12KB

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