File: source/skylink-stats.js

  1. /**
  2. * Function that sends the stats to the API server.
  3. * @method _postStatsToServer
  4. * @private
  5. * @for Skylink
  6. * @since 0.6.31
  7. */
  8. Skylink.prototype._postStats = function (endpoint, params) {
  9. var self = this;
  10. var requestBody = {};
  11. if(self._initOptions.enableStatsGathering){
  12. if(Array.isArray(params)){
  13. requestBody.data = params;
  14. }
  15. else{
  16. requestBody = params;
  17. }
  18. requestBody.client_id = ((self._user && self._user.uid) || 'dummy') + '_' + self._statIdRandom;
  19. requestBody.app_key = self._initOptions.appKey;
  20. requestBody.timestamp = (new Date()).toISOString();
  21.  
  22. // Simply post the data directly to the API server without caring if it is successful or not.
  23. try {
  24. var xhr = new XMLHttpRequest();
  25. xhr.onerror = function () { };
  26. xhr.open('POST', self._initOptions.statsServer + endpoint, true);
  27. xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
  28. xhr.send(JSON.stringify(requestBody));
  29.  
  30. } catch (error) {
  31. log.error([null, 'XMLHttpRequest', "POST", 'Error in posting stats data ->'], error);
  32. }
  33. }
  34. };
  35.  
  36. /**
  37. * Function that handles the posting of client information.
  38. * @method _handleClientStats
  39. * @private
  40. * @for Skylink
  41. * @since 0.6.31
  42. */
  43. Skylink.prototype._handleClientStats = function() {
  44. var self = this;
  45. var statsObject = {
  46. username: (self._user && self._user.uid) || null,
  47. sdk_name: 'web',
  48. sdk_version: self.VERSION,
  49. agent_name: AdapterJS.webrtcDetectedBrowser,
  50. agent_version: AdapterJS.webrtcDetectedVersion,
  51. agent_platform: navigator.platform,
  52. agent_plugin_version: (AdapterJS.WebRTCPlugin.plugin && AdapterJS.WebRTCPlugin.plugin.VERSION) || null
  53. };
  54.  
  55. self._postStats('/rest/stats/client', statsObject);
  56. };
  57.  
  58. /**
  59. * Function that handles the posting of session states.
  60. * @method _handleSessionStats
  61. * @private
  62. * @for Skylink
  63. * @since 0.6.31
  64. */
  65. Skylink.prototype._handleSessionStats = function(message) {
  66. var self = this;
  67. var statsObject = {
  68. room_id: self._room && self._room.id,
  69. user_id: (self._user && self._user.sid) || null,
  70. state: message.type,
  71. contents: message
  72. };
  73.  
  74. self._postStats('/rest/stats/session', statsObject);
  75. };
  76.  
  77. /**
  78. * Function that handles the posting of app key authentication states.
  79. * @method _handleAuthStats
  80. * @private
  81. * @for Skylink
  82. * @since 0.6.31
  83. */
  84. Skylink.prototype._handleAuthStats = function(state, result, status, error) {
  85. var self = this;
  86. var statsObject = {
  87. room_id: (result && result.room_key) || null,
  88. state: state,
  89. http_status: status,
  90. http_error: (typeof error === 'string' ? error : (error && error.message)) || null,
  91. api_url: self._path,
  92. api_result: result
  93. };
  94.  
  95. self._postStats('/rest/stats/auth', statsObject);
  96. };
  97.  
  98. /**
  99. * Function that handles the posting of socket connection states.
  100. * @method _handleSignalingStats
  101. * @private
  102. * @for Skylink
  103. * @since 0.6.31
  104. */
  105. Skylink.prototype._handleSignalingStats = function(state, retries, error) {
  106. var self = this;
  107. var socketSession = clone(self._socketSession);
  108. var statsObject = {
  109. room_id: self._room && self._room.id,
  110. user_id: (self._user && self._user.sid) || null,
  111. state: state,
  112. signaling_url: socketSession.socketServer,
  113. signaling_transport: socketSession.transportType.toLowerCase(),
  114. // Use the retries from the function itself to prevent non-sequential event calls issues.
  115. attempts: retries,
  116. error: (typeof error === 'string' ? error : (error && error.message)) || null
  117. };
  118.  
  119. self._postStats('/rest/stats/client/signaling', statsObject);
  120. };
  121.  
  122. /**
  123. * Function that handles the posting of peer ICE connection states.
  124. * @method _handleIceConnectionStats
  125. * @private
  126. * @for Skylink
  127. * @since 0.6.31
  128. */
  129. Skylink.prototype._handleIceConnectionStats = function(state, peerId) {
  130. var self = this;
  131. var statsObject = {
  132. room_id: self._room && self._room.id,
  133. user_id: self._user && self._user.sid,
  134. peer_id: peerId,
  135. state: state,
  136. local_candidate: {},
  137. remote_candidate: {}
  138. };
  139. // Set a timeout to pause process to ensure the stats retrieval does not run at the same time
  140. // when the state is triggered, so that the selected ICE candidate pair information can be returned.
  141. self._retrieveStats(peerId, function (error, stats) {
  142. if (stats) {
  143. // Parse the selected ICE candidate pair for both local and remote candidate.
  144. ['local', 'remote'].forEach(function (dirType) {
  145. var candidate = stats.selectedCandidate[dirType];
  146.  
  147. if (candidate) {
  148. statsObject[dirType + '_candidate'].ip_address = candidate.ipAddress || null;
  149. statsObject[dirType + '_candidate'].port_number = candidate.portNumber || null;
  150. statsObject[dirType + '_candidate'].candidate_type = candidate.candidateType || null;
  151. statsObject[dirType + '_candidate'].protocol = candidate.transport || null;
  152. statsObject[dirType + '_candidate'].priority = candidate.priority || null;
  153.  
  154. // This is only available for the local ICE candidate.
  155. if (dirType === 'local') {
  156. statsObject.local_candidate.network_type = candidate.networkType || null;
  157. }
  158. }
  159. });
  160. }
  161. self._postStats('/rest/stats/client/iceconnection', statsObject);
  162.  
  163. }, true);
  164. };
  165.  
  166. /**
  167. * Function that handles the posting of peer local/remote ICE candidate processing states.
  168. * @method _handleIceCandidateStats
  169. * @private
  170. * @for Skylink
  171. * @since 0.6.31
  172. */
  173. Skylink.prototype._handleIceCandidateStats = function(state, peerId, candidateId, candidate, error) {
  174. var self = this;
  175. var statsObject = {
  176. room_id: self._room && self._room.id,
  177. user_id: self._user && self._user.sid,
  178. peer_id: peerId,
  179. state: state,
  180. is_remote: !!candidateId,
  181. candidate_id: candidateId || null,
  182. candidate_sdp_mid: candidate.sdpMid,
  183. candidate_sdp_mindex: candidate.sdpMLineIndex,
  184. candidate_candidate: candidate.candidate,
  185. error: (typeof error === 'string' ? error : (error && error.message)) || null,
  186. };
  187. self._manageStatsBuffer('iceCandidate', statsObject, '/rest/stats/client/icecandidate');
  188. };
  189.  
  190. /**
  191. * Function that handles the posting of peer local/remote ICE gathering states.
  192. * @method _handleIceGatheringStats
  193. * @private
  194. * @for Skylink
  195. * @since 0.6.31
  196. */
  197. Skylink.prototype._handleIceGatheringStats = function(state, peerId, isRemote) {
  198. var self = this;
  199. var statsObject = {
  200. room_id: self._room && self._room.id,
  201. user_id: self._user && self._user.sid,
  202. peer_id: peerId,
  203. state: state,
  204. is_remote: isRemote
  205. };
  206. self._manageStatsBuffer('iceGathering', statsObject, '/rest/stats/client/icegathering');
  207. };
  208.  
  209. /**
  210. * Function that handles the posting of peer connection negotiation states.
  211. * @method _handleNegotiationStats
  212. * @private
  213. * @for Skylink
  214. * @since 0.6.31
  215. */
  216. Skylink.prototype._handleNegotiationStats = function(state, peerId, sdpOrMessage, isRemote, error) {
  217. var self = this;
  218. var statsObject = {
  219. room_id: self._room && self._room.id,
  220. user_id: self._user && self._user.sid,
  221. peer_id: peerId,
  222. state: state,
  223. is_remote: isRemote,
  224. // Currently sharing a parameter "sdpOrMessage" that indicates a "welcome" message
  225. // or session description to save parameters length.
  226. weight: sdpOrMessage.weight,
  227. sdp_type: null,
  228. sdp_sdp: null,
  229. error: (typeof error === 'string' ? error : (error && error.message)) || null,
  230. };
  231.  
  232. // Retrieve the weight for states where the "weight" field is not available.
  233. if (['enter', 'welcome', 'restart'].indexOf(state) === -1) {
  234. // Retrieve the peer's weight if it from remote end.
  235. statsObject.weight = self.getPeerInfo(isRemote ? peerId : undefined).config.priorityWeight;
  236. statsObject.sdp_type = (sdpOrMessage && sdpOrMessage.type) || null;
  237. statsObject.sdp_sdp = (sdpOrMessage && sdpOrMessage.sdp) || null;
  238. }
  239. self._manageStatsBuffer('negotiation', statsObject, '/rest/stats/client/negotiation');
  240. };
  241.  
  242. /**
  243. * Function that handles the posting of peer connection bandwidth information.
  244. * @method _handleBandwidthStats
  245. * @private
  246. * @for Skylink
  247. * @since 0.6.31
  248. */
  249. Skylink.prototype._handleBandwidthStats = function (peerId) {
  250. var self = this;
  251. var statsObject = {
  252. room_id: self._room && self._room.id,
  253. user_id: self._user && self._user.sid,
  254. peer_id: peerId,
  255. audio_send: { tracks: [] },
  256. audio_recv: {},
  257. video_send: { tracks: [] },
  258. video_recv: {}
  259. };
  260.  
  261. var useStream = self._streams.screenshare || self._streams.userMedia || null;
  262. var mutedStatus = self.getPeerInfo().mediaStatus;
  263.  
  264. // When stream is available, format the stream tracks information.
  265. // The SDK currently only allows sending of 1 stream at a time that has only 1 audio and video track each.
  266. if (useStream) {
  267. // Parse the audio track if it exists only.
  268. if (useStream.tracks.audio) {
  269. statsObject.audio_send.tracks = [{
  270. stream_id: useStream.id,
  271. id: useStream.tracks.audio.id,
  272. label: useStream.tracks.audio.label,
  273. muted: mutedStatus.audioMuted
  274. }];
  275. }
  276.  
  277. // Parse the video track if it exists only.
  278. if (useStream.tracks.video) {
  279. statsObject.video_send.tracks = [{
  280. stream_id: useStream.id,
  281. id: useStream.tracks.video.id,
  282. label: useStream.tracks.video.label,
  283. height: useStream.tracks.video.height,
  284. width: useStream.tracks.video.width,
  285. muted: mutedStatus.videoMuted
  286. }];
  287. }
  288. }
  289.  
  290. self._retrieveStats(peerId, function (error, stats) {
  291. if (error) {
  292. statsObject.error = error && error.message;
  293. stats = {
  294. audio: { sending: {}, receiving: {} },
  295. video: { sending: {}, receiving: {} }
  296. };
  297. }
  298.  
  299. // Common function to parse and handle any `null`/`undefined` values.
  300. var formatValue = function (mediaType, directionType, itemKey) {
  301. var value = stats[mediaType][directionType === 'send' ? 'sending' : 'receiving'][itemKey];
  302. if (['number', 'string', 'boolean'].indexOf(typeof value) > -1) {
  303. return value;
  304. }
  305. return null;
  306. };
  307.  
  308. // Parse bandwidth information for sending audio packets.
  309. statsObject.audio_send.bytes = formatValue('audio', 'send', 'bytes');
  310. statsObject.audio_send.packets = formatValue('audio', 'send', 'packets');
  311. statsObject.audio_send.round_trip_time = formatValue('audio', 'send', 'rtt');
  312. statsObject.audio_send.nack_count = formatValue('audio', 'send', 'nacks');
  313. statsObject.audio_send.echo_return_loss = formatValue('audio', 'send', 'echoReturnLoss');
  314. statsObject.audio_send.echo_return_loss_enhancement = formatValue('audio', 'send', 'echoReturnLossEnhancement');
  315.  
  316. // Parse bandwidth information for receiving audio packets.
  317. statsObject.audio_recv.bytes = formatValue('audio', 'recv', 'bytes');
  318. statsObject.audio_recv.packets = formatValue('audio', 'recv', 'packets');
  319. statsObject.audio_recv.packets_lost = formatValue('audio', 'recv', 'packetsLost');
  320. statsObject.video_recv.packets_discarded = formatValue('audio', 'recv', 'packetsDiscarded');
  321. statsObject.audio_recv.jitter = formatValue('audio', 'recv', 'jitter');
  322. statsObject.audio_recv.nack_count = formatValue('audio', 'recv', 'nacks');
  323.  
  324. // Parse bandwidth information for sending video packets.
  325. statsObject.video_send.bytes = formatValue('video', 'send', 'bytes');
  326. statsObject.video_send.packets = formatValue('video', 'send', 'packets');
  327. statsObject.video_send.round_trip_time = formatValue('video', 'send', 'rtt');
  328. statsObject.video_send.nack_count = formatValue('video', 'send', 'nacks');
  329. statsObject.video_send.firs_count = formatValue('video', 'send', 'firs');
  330. statsObject.video_send.plis_count = formatValue('video', 'send', 'plis');
  331. statsObject.video_send.frames = formatValue('video', 'send', 'frames');
  332. statsObject.video_send.frames_encoded = formatValue('video', 'send', 'framesEncoded');
  333. statsObject.video_send.frames_dropped = formatValue('video', 'send', 'framesDropped');
  334. statsObject.video_send.frame_width = formatValue('video', 'send', 'frameWidth');
  335. statsObject.video_send.frame_height = formatValue('video', 'send', 'frameHeight');
  336. statsObject.video_send.framerate = formatValue('video', 'send', 'frameRate');
  337. statsObject.video_send.framerate_input = formatValue('video', 'send', 'frameRateInput');
  338. statsObject.video_send.framerate_encoded = formatValue('video', 'send', 'frameRateEncoded');
  339. statsObject.video_send.framerate_mean = formatValue('video', 'send', 'frameRateMean');
  340. statsObject.video_send.framerate_std_dev = formatValue('video', 'send', 'frameRateStdDev');
  341. statsObject.video_send.cpu_limited_resolution = formatValue('video', 'send', 'cpuLimitedResolution');
  342. statsObject.video_send.bandwidth_limited_resolution = formatValue('video', 'send', 'bandwidthLimitedResolution');
  343.  
  344. // Parse bandwidth information for receiving video packets.
  345. statsObject.video_recv.bytes = formatValue('video', 'recv', 'bytes');
  346. statsObject.video_recv.packets = formatValue('video', 'recv', 'packets');
  347. statsObject.video_recv.packets_lost = formatValue('video', 'recv', 'packetsLost');
  348. statsObject.video_recv.packets_discarded = formatValue('video', 'recv', 'packetsDiscarded');
  349. statsObject.video_recv.jitter = formatValue('video', 'recv', 'jitter');
  350. statsObject.video_recv.nack_count = formatValue('video', 'recv', 'nacks');
  351. statsObject.video_recv.firs_count = formatValue('video', 'recv', 'firs');
  352. statsObject.video_recv.plis_count = formatValue('video', 'recv', 'plis');
  353. statsObject.video_recv.frames = formatValue('video', 'recv', 'frames');
  354. statsObject.video_recv.frames_decoded = formatValue('video', 'recv', 'framesDecoded');
  355. statsObject.video_recv.frame_width = formatValue('video', 'recv', 'frameWidth');
  356. statsObject.video_recv.frame_height = formatValue('video', 'recv', 'frameHeight');
  357. statsObject.video_recv.framerate = formatValue('video', 'recv', 'frameRate');
  358. statsObject.video_recv.framerate_output = formatValue('video', 'recv', 'frameRateOutput');
  359. statsObject.video_recv.framerate_decoded = formatValue('video', 'recv', 'frameRateDecoded');
  360. statsObject.video_recv.framerate_mean = formatValue('video', 'recv', 'frameRateMean');
  361. statsObject.video_recv.framerate_std_dev = formatValue('video', 'recv', 'frameRateStdDev');
  362. statsObject.video_recv.qp_sum = formatValue('video', 'recv', 'qpSum');
  363. self._postStats('/rest/stats/client/bandwidth', statsObject);
  364. }, true);
  365. };
  366.  
  367. /**
  368. * Function that handles the posting of recording states.
  369. * @method _handleRecordingStats
  370. * @private
  371. * @for Skylink
  372. * @since 0.6.31
  373. */
  374. Skylink.prototype._handleRecordingStats = function(state, recordingId, recordings, error) {
  375. var self = this;
  376. var statsObject = {
  377. room_id: self._room && self._room.id,
  378. user_id: self._user && self._user.sid,
  379. state: state,
  380. recording_id: recordingId || null,
  381. recordings: recordings,
  382. error: (typeof error === 'string' ? error : (error && error.message)) || null
  383. };
  384.  
  385. self._postStats('/rest/stats/client/recording', statsObject);
  386. };
  387.  
  388. /**
  389. * Function that handles the posting of datachannel states.
  390. * @method _handleDatachannelStats
  391. * @private
  392. * @for Skylink
  393. * @since 0.6.31
  394. */
  395. Skylink.prototype._handleDatachannelStats = function(state, peerId, channel, channelProp, error) {
  396. var self = this;
  397. var statsObject = {
  398. room_id: self._room && self._room.id,
  399. user_id: self._user && self._user.sid,
  400. peer_id: peerId,
  401. state: state,
  402. channel_id: channel && channel.id,
  403. channel_label: channel && channel.label,
  404. channel_type: channelProp === 'main' ? 'persistent' : 'temporal',
  405. channel_binary_type: channel && channel.binaryType,
  406. error: (typeof error === 'string' ? error : (error && error.message)) || null
  407. };
  408.  
  409. if (channel && AdapterJS.webrtcDetectedType === 'plugin') {
  410. statsObject.channel_binary_type = 'int8Array';
  411.  
  412. // For IE 10 and below browsers, binary support is not available.
  413. if (AdapterJS.webrtcDetectedBrowser === 'IE' && AdapterJS.webrtcDetectedVersion < 11) {
  414. statsObject.channel_binary_type = 'none';
  415. }
  416. }
  417.  
  418. self._postStats('/rest/stats/client/datachannel', statsObject);
  419. };
  420.  
  421. Skylink.prototype._stats_buffer = {};
  422. /**
  423. * Function that handles buffer of stats data
  424. * @method _handleDatachannelStats
  425. * @private
  426. * @for Skylink
  427. * @since 0.6.35
  428. */
  429. Skylink.prototype._manageStatsBuffer = function(operation, data, url){
  430. var self = this;
  431. if(self._stats_buffer[operation] === undefined){
  432. self._stats_buffer[operation] = {};
  433. self._stats_buffer[operation].url = url;
  434. self._stats_buffer[operation].data = [];
  435. }
  436. self._stats_buffer[operation].data.push(data);
  437. setInterval(function () {
  438. for (var key in self._stats_buffer) {
  439. if (self._stats_buffer[key]["data"].length > 0) {
  440. self._postStats(self._stats_buffer[key]["url"], self._stats_buffer[key]["data"]);
  441. self._stats_buffer[key]["data"] = [];
  442. }
  443. }
  444. }, 5000);
  445. };
  446.