Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

statistics.js 15KB

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