Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

statistics.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /* global require */
  2. import analytics from './AnalyticsAdapter';
  3. const CallStats = require('./CallStats');
  4. const EventEmitter = require('events');
  5. import JitsiTrackError from '../../JitsiTrackError';
  6. const logger = require('jitsi-meet-logger').getLogger(__filename);
  7. const LocalStats = require('./LocalStatsCollector.js');
  8. const RTPStats = require('./RTPStatsCollector.js');
  9. const ScriptUtil = require('../util/ScriptUtil');
  10. import * as StatisticsEvents from '../../service/statistics/Events';
  11. /**
  12. * True if callstats API is loaded
  13. */
  14. let isCallstatsLoaded = false;
  15. /**
  16. * Since callstats.io is a third party, we cannot guarantee the quality of their
  17. * service. More specifically, their server may take noticeably long time to
  18. * respond. Consequently, it is in our best interest (in the sense that the
  19. * intergration of callstats.io is pretty important to us but not enough to
  20. * allow it to prevent people from joining a conference) to (1) start
  21. * downloading their API as soon as possible and (2) do the downloading
  22. * asynchronously.
  23. *
  24. * @param customScriptUrl
  25. */
  26. function loadCallStatsAPI(customScriptUrl) {
  27. if (!isCallstatsLoaded) {
  28. ScriptUtil.loadScript(
  29. customScriptUrl ? customScriptUrl
  30. : 'https://api.callstats.io/static/callstats-ws.min.js',
  31. /* async */ true,
  32. /* prepend */ true);
  33. isCallstatsLoaded = true;
  34. }
  35. // FIXME At the time of this writing, we hope that the callstats.io API will
  36. // have loaded by the time we needed it (i.e. CallStats.init is invoked).
  37. }
  38. /**
  39. * callstats strips any additional fields from Error except for "name", "stack",
  40. * "message" and "constraintName". So we need to bundle additional information
  41. * from JitsiTrackError into error passed to callstats to preserve valuable
  42. * information about error.
  43. * @param {JitsiTrackError} error
  44. */
  45. function formatJitsiTrackErrorForCallStats(error) {
  46. const err = new Error();
  47. // Just copy original stack from error
  48. err.stack = error.stack;
  49. // Combine name from error's name plus (possibly) name of original GUM error
  50. err.name = (error.name || 'Unknown error') + (error.gum && error.gum.error
  51. && error.gum.error.name ? ` - ${error.gum.error.name}` : '');
  52. // Put all constraints into this field. For constraint failed errors we will
  53. // still know which exactly constraint failed as it will be a part of
  54. // message.
  55. err.constraintName = error.gum && error.gum.constraints
  56. ? JSON.stringify(error.gum.constraints) : '';
  57. // Just copy error's message.
  58. err.message = error.message;
  59. return err;
  60. }
  61. /**
  62. * Init statistic options
  63. * @param options
  64. */
  65. Statistics.init = function(options) {
  66. Statistics.audioLevelsEnabled = !options.disableAudioLevels;
  67. if (typeof options.audioLevelsInterval === 'number') {
  68. Statistics.audioLevelsInterval = options.audioLevelsInterval;
  69. }
  70. Statistics.disableThirdPartyRequests = options.disableThirdPartyRequests;
  71. };
  72. /**
  73. *
  74. * @param xmpp
  75. * @param options
  76. */
  77. function Statistics(xmpp, options) {
  78. this.rtpStats = null;
  79. this.eventEmitter = new EventEmitter();
  80. this.xmpp = xmpp;
  81. this.options = options || {};
  82. this.callStatsIntegrationEnabled
  83. = this.options.callStatsID && this.options.callStatsSecret
  84. // Even though AppID and AppSecret may be specified, the integration
  85. // of callstats.io may be disabled because of globally-disallowed
  86. // requests to any third parties.
  87. && (Statistics.disableThirdPartyRequests !== true);
  88. if (this.callStatsIntegrationEnabled) {
  89. loadCallStatsAPI(this.options.callStatsCustomScriptUrl);
  90. }
  91. this.callStats = null;
  92. // Flag indicates whether or not the CallStats have been started for this
  93. // Statistics instance
  94. this.callStatsStarted = false;
  95. }
  96. Statistics.audioLevelsEnabled = false;
  97. Statistics.audioLevelsInterval = 200;
  98. Statistics.disableThirdPartyRequests = false;
  99. Statistics.analytics = analytics;
  100. /**
  101. * Array of callstats instances. Used to call Statistics static methods and
  102. * send stats to all cs instances.
  103. */
  104. Statistics.callsStatsInstances = [];
  105. Statistics.prototype.startRemoteStats = function(peerconnection) {
  106. this.stopRemoteStats();
  107. try {
  108. this.rtpStats
  109. = new RTPStats(peerconnection,
  110. Statistics.audioLevelsInterval, 2000, this.eventEmitter);
  111. this.rtpStats.start(Statistics.audioLevelsEnabled);
  112. } catch (e) {
  113. this.rtpStats = null;
  114. logger.error(`Failed to start collecting remote statistics: ${e}`);
  115. }
  116. };
  117. Statistics.localStats = [];
  118. Statistics.startLocalStats = function(stream, callback) {
  119. if (!Statistics.audioLevelsEnabled) {
  120. return;
  121. }
  122. const localStats = new LocalStats(stream, Statistics.audioLevelsInterval,
  123. callback);
  124. this.localStats.push(localStats);
  125. localStats.start();
  126. };
  127. Statistics.prototype.addAudioLevelListener = function(listener) {
  128. if (!Statistics.audioLevelsEnabled) {
  129. return;
  130. }
  131. this.eventEmitter.on(StatisticsEvents.AUDIO_LEVEL, listener);
  132. };
  133. Statistics.prototype.removeAudioLevelListener = function(listener) {
  134. if (!Statistics.audioLevelsEnabled) {
  135. return;
  136. }
  137. this.eventEmitter.removeListener(StatisticsEvents.AUDIO_LEVEL, listener);
  138. };
  139. Statistics.prototype.addBeforeDisposedListener = function(listener) {
  140. this.eventEmitter.on(StatisticsEvents.BEFORE_DISPOSED, listener);
  141. };
  142. Statistics.prototype.removeBeforeDisposedListener = function(listener) {
  143. this.eventEmitter.removeListener(
  144. StatisticsEvents.BEFORE_DISPOSED, listener);
  145. };
  146. Statistics.prototype.addConnectionStatsListener = function(listener) {
  147. this.eventEmitter.on(StatisticsEvents.CONNECTION_STATS, listener);
  148. };
  149. Statistics.prototype.removeConnectionStatsListener = function(listener) {
  150. this.eventEmitter.removeListener(
  151. StatisticsEvents.CONNECTION_STATS,
  152. listener);
  153. };
  154. Statistics.prototype.addByteSentStatsListener = function(listener) {
  155. this.eventEmitter.on(StatisticsEvents.BYTE_SENT_STATS, listener);
  156. };
  157. Statistics.prototype.removeByteSentStatsListener = function(listener) {
  158. this.eventEmitter.removeListener(StatisticsEvents.BYTE_SENT_STATS,
  159. listener);
  160. };
  161. Statistics.prototype.dispose = function() {
  162. if (this.eventEmitter) {
  163. this.eventEmitter.emit(StatisticsEvents.BEFORE_DISPOSED);
  164. }
  165. this.stopCallStats();
  166. this.stopRemoteStats();
  167. if (this.eventEmitter) {
  168. this.eventEmitter.removeAllListeners();
  169. }
  170. };
  171. Statistics.stopLocalStats = function(stream) {
  172. if (!Statistics.audioLevelsEnabled) {
  173. return;
  174. }
  175. for (let i = 0; i < Statistics.localStats.length; i++) {
  176. if (Statistics.localStats[i].stream === stream) {
  177. const localStats = Statistics.localStats.splice(i, 1);
  178. localStats[0].stop();
  179. break;
  180. }
  181. }
  182. };
  183. Statistics.prototype.stopRemoteStats = function() {
  184. if (!this.rtpStats) {
  185. return;
  186. }
  187. this.rtpStats.stop();
  188. this.rtpStats = null;
  189. };
  190. // CALSTATS METHODS
  191. /**
  192. * Initializes the callstats.io API.
  193. * @param peerConnection {JingleSessionPC} the session object
  194. */
  195. Statistics.prototype.startCallStats = function(session) {
  196. if (this.callStatsIntegrationEnabled && !this.callStatsStarted) {
  197. // Here we overwrite the previous instance, but it must be bound to
  198. // the new PeerConnection
  199. this.callstats = new CallStats(session, this.options);
  200. Statistics.callsStatsInstances.push(this.callstats);
  201. this.callStatsStarted = true;
  202. }
  203. };
  204. /**
  205. * Removes the callstats.io instances.
  206. */
  207. Statistics.prototype.stopCallStats = function() {
  208. if (this.callStatsStarted) {
  209. const index = Statistics.callsStatsInstances.indexOf(this.callstats);
  210. if (index > -1) {
  211. Statistics.callsStatsInstances.splice(index, 1);
  212. }
  213. // The next line is commented because we need to be able to send
  214. // feedback even after the conference has been destroyed.
  215. // this.callstats = null;
  216. CallStats.dispose();
  217. this.callStatsStarted = false;
  218. }
  219. };
  220. /**
  221. * Returns true if the callstats integration is enabled, otherwise returns
  222. * false.
  223. *
  224. * @returns true if the callstats integration is enabled, otherwise returns
  225. * false.
  226. */
  227. Statistics.prototype.isCallstatsEnabled = function() {
  228. return this.callStatsIntegrationEnabled;
  229. };
  230. /**
  231. * Notifies CallStats and analytics(if present) for ice connection failed
  232. * @param {RTCPeerConnection} pc connection on which failure occured.
  233. */
  234. Statistics.prototype.sendIceConnectionFailedEvent = function(pc) {
  235. if (this.callstats) {
  236. this.callstats.sendIceConnectionFailedEvent(pc, this.callstats);
  237. }
  238. Statistics.analytics.sendEvent('connection.ice_failed');
  239. };
  240. /**
  241. * Notifies CallStats for mute events
  242. * @param mute {boolean} true for muted and false for not muted
  243. * @param type {String} "audio"/"video"
  244. */
  245. Statistics.prototype.sendMuteEvent = function(muted, type) {
  246. if (this.callstats) {
  247. CallStats.sendMuteEvent(muted, type, this.callstats);
  248. }
  249. };
  250. /**
  251. * Notifies CallStats for screen sharing events
  252. * @param start {boolean} true for starting screen sharing and
  253. * false for not stopping
  254. */
  255. Statistics.prototype.sendScreenSharingEvent = function(start) {
  256. if (this.callstats) {
  257. CallStats.sendScreenSharingEvent(start, this.callstats);
  258. }
  259. };
  260. /**
  261. * Notifies the statistics module that we are now the dominant speaker of the
  262. * conference.
  263. */
  264. Statistics.prototype.sendDominantSpeakerEvent = function() {
  265. if (this.callstats) {
  266. CallStats.sendDominantSpeakerEvent(this.callstats);
  267. }
  268. };
  269. /**
  270. * Notifies about active device.
  271. * @param {{deviceList: {String:String}}} devicesData - list of devices with
  272. * their data
  273. */
  274. Statistics.sendActiveDeviceListEvent = function(devicesData) {
  275. if (Statistics.callsStatsInstances.length) {
  276. Statistics.callsStatsInstances.forEach(cs => {
  277. CallStats.sendActiveDeviceListEvent(devicesData, cs);
  278. });
  279. } else {
  280. CallStats.sendActiveDeviceListEvent(devicesData, null);
  281. }
  282. };
  283. /* eslint-disable max-params */
  284. /**
  285. * Lets the underlying statistics module know where is given SSRC rendered by
  286. * providing renderer tag ID.
  287. * @param ssrc {number} the SSRC of the stream
  288. * @param isLocal {boolean} <tt>true<tt> if this stream is local or
  289. * <tt>false</tt> otherwise.
  290. * @param usageLabel {string} meaningful usage label of this stream like
  291. * 'microphone', 'camera' or 'screen'.
  292. * @param containerId {string} the id of media 'audio' or 'video' tag which
  293. * renders the stream.
  294. */
  295. Statistics.prototype.associateStreamWithVideoTag = function(
  296. ssrc,
  297. isLocal,
  298. usageLabel,
  299. containerId) {
  300. this.callstats && this.callstats.associateStreamWithVideoTag(
  301. ssrc,
  302. isLocal,
  303. usageLabel,
  304. containerId);
  305. };
  306. /* eslint-enable max-params */
  307. /**
  308. * Notifies CallStats that getUserMedia failed.
  309. *
  310. * @param {Error} e error to send
  311. */
  312. Statistics.sendGetUserMediaFailed = function(e) {
  313. if (Statistics.callsStatsInstances.length) {
  314. Statistics.callsStatsInstances.forEach(
  315. cs =>
  316. CallStats.sendGetUserMediaFailed(
  317. e instanceof JitsiTrackError
  318. ? formatJitsiTrackErrorForCallStats(e)
  319. : e,
  320. cs));
  321. } else {
  322. CallStats.sendGetUserMediaFailed(
  323. e instanceof JitsiTrackError
  324. ? formatJitsiTrackErrorForCallStats(e)
  325. : e,
  326. null);
  327. }
  328. };
  329. /**
  330. * Notifies CallStats that peer connection failed to create offer.
  331. *
  332. * @param {Error} e error to send
  333. * @param {RTCPeerConnection} pc connection on which failure occured.
  334. */
  335. Statistics.prototype.sendCreateOfferFailed = function(e, pc) {
  336. if (this.callstats) {
  337. CallStats.sendCreateOfferFailed(e, pc, this.callstats);
  338. }
  339. };
  340. /**
  341. * Notifies CallStats that peer connection failed to create answer.
  342. *
  343. * @param {Error} e error to send
  344. * @param {RTCPeerConnection} pc connection on which failure occured.
  345. */
  346. Statistics.prototype.sendCreateAnswerFailed = function(e, pc) {
  347. if (this.callstats) {
  348. CallStats.sendCreateAnswerFailed(e, pc, this.callstats);
  349. }
  350. };
  351. /**
  352. * Notifies CallStats that peer connection failed to set local description.
  353. *
  354. * @param {Error} e error to send
  355. * @param {RTCPeerConnection} pc connection on which failure occured.
  356. */
  357. Statistics.prototype.sendSetLocalDescFailed = function(e, pc) {
  358. if (this.callstats) {
  359. CallStats.sendSetLocalDescFailed(e, pc, this.callstats);
  360. }
  361. };
  362. /**
  363. * Notifies CallStats that peer connection failed to set remote description.
  364. *
  365. * @param {Error} e error to send
  366. * @param {RTCPeerConnection} pc connection on which failure occured.
  367. */
  368. Statistics.prototype.sendSetRemoteDescFailed = function(e, pc) {
  369. if (this.callstats) {
  370. CallStats.sendSetRemoteDescFailed(e, pc, this.callstats);
  371. }
  372. };
  373. /**
  374. * Notifies CallStats that peer connection failed to add ICE candidate.
  375. *
  376. * @param {Error} e error to send
  377. * @param {RTCPeerConnection} pc connection on which failure occured.
  378. */
  379. Statistics.prototype.sendAddIceCandidateFailed = function(e, pc) {
  380. if (this.callstats) {
  381. CallStats.sendAddIceCandidateFailed(e, pc, this.callstats);
  382. }
  383. };
  384. /**
  385. * Adds to CallStats an application log.
  386. *
  387. * @param {String} a log message to send or an {Error} object to be reported
  388. */
  389. Statistics.sendLog = function(m) {
  390. if (Statistics.callsStatsInstances.length) {
  391. Statistics.callsStatsInstances.forEach(
  392. cs => CallStats.sendApplicationLog(m, cs));
  393. } else {
  394. CallStats.sendApplicationLog(m, null);
  395. }
  396. };
  397. /**
  398. * Sends the given feedback through CallStats.
  399. *
  400. * @param overall an integer between 1 and 5 indicating the user feedback
  401. * @param detailed detailed feedback from the user. Not yet used
  402. */
  403. Statistics.prototype.sendFeedback = function(overall, detailed) {
  404. if (this.callstats) {
  405. this.callstats.sendFeedback(overall, detailed);
  406. }
  407. Statistics.analytics.sendEvent('feedback.rating',
  408. { value: overall,
  409. detailed });
  410. };
  411. Statistics.LOCAL_JID = require('../../service/statistics/constants').LOCAL_JID;
  412. /**
  413. * Reports global error to CallStats.
  414. *
  415. * @param {Error} error
  416. */
  417. Statistics.reportGlobalError = function(error) {
  418. if (error instanceof JitsiTrackError && error.gum) {
  419. Statistics.sendGetUserMediaFailed(error);
  420. } else {
  421. Statistics.sendLog(error);
  422. }
  423. };
  424. /**
  425. * Sends event to analytics and callstats.
  426. * @param {string} eventName the event name.
  427. * @param {Object} data the data to be sent.
  428. */
  429. Statistics.sendEventToAll = function(eventName, data) {
  430. this.analytics.sendEvent(eventName, data);
  431. Statistics.sendLog(JSON.stringify({ name: eventName,
  432. data }));
  433. };
  434. module.exports = Statistics;