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.

rtp_sts.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. /* global ssrc2jid */
  2. /**
  3. * Calculates packet lost percent using the number of lost packets and the
  4. * number of all packet.
  5. * @param lostPackets the number of lost packets
  6. * @param totalPackets the number of all packets.
  7. * @returns {number} packet loss percent
  8. */
  9. function calculatePacketLoss(lostPackets, totalPackets) {
  10. if(!totalPackets || totalPackets <= 0 || !lostPackets || lostPackets <= 0)
  11. return 0;
  12. return Math.round((lostPackets/totalPackets)*100);
  13. }
  14. /**
  15. * Peer statistics data holder.
  16. * @constructor
  17. */
  18. function PeerStats()
  19. {
  20. this.ssrc2Loss = {};
  21. this.ssrc2AudioLevel = {};
  22. this.ssrc2bitrate = {};
  23. this.ssrc2resolution = {};
  24. }
  25. /**
  26. * The bandwidth
  27. * @type {{}}
  28. */
  29. PeerStats.bandwidth = {};
  30. /**
  31. * The bit rate
  32. * @type {{}}
  33. */
  34. PeerStats.bitrate = {};
  35. /**
  36. * The packet loss rate
  37. * @type {{}}
  38. */
  39. PeerStats.packetLoss = null;
  40. /**
  41. * Sets packets loss rate for given <tt>ssrc</tt> that blong to the peer
  42. * represented by this instance.
  43. * @param ssrc audio or video RTP stream SSRC.
  44. * @param lossRate new packet loss rate value to be set.
  45. */
  46. PeerStats.prototype.setSsrcLoss = function (ssrc, lossRate)
  47. {
  48. this.ssrc2Loss[ssrc] = lossRate;
  49. };
  50. /**
  51. * Sets resolution for given <tt>ssrc</tt> that belong to the peer
  52. * represented by this instance.
  53. * @param ssrc audio or video RTP stream SSRC.
  54. * @param resolution new resolution value to be set.
  55. */
  56. PeerStats.prototype.setSsrcResolution = function (ssrc, resolution)
  57. {
  58. if(resolution === null && this.ssrc2resolution[ssrc])
  59. {
  60. delete this.ssrc2resolution[ssrc];
  61. }
  62. else if(resolution !== null)
  63. this.ssrc2resolution[ssrc] = resolution;
  64. };
  65. /**
  66. * Sets the bit rate for given <tt>ssrc</tt> that blong to the peer
  67. * represented by this instance.
  68. * @param ssrc audio or video RTP stream SSRC.
  69. * @param bitrate new bitrate value to be set.
  70. */
  71. PeerStats.prototype.setSsrcBitrate = function (ssrc, bitrate)
  72. {
  73. this.ssrc2bitrate[ssrc] = bitrate;
  74. };
  75. /**
  76. * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
  77. * the stream which belongs to the peer represented by this instance.
  78. * @param ssrc RTP stream SSRC for which current audio level value will be
  79. * updated.
  80. * @param audioLevel the new audio level value to be set. Value is truncated to
  81. * fit the range from 0 to 1.
  82. */
  83. PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
  84. {
  85. // Range limit 0 - 1
  86. this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
  87. };
  88. /**
  89. * Array with the transport information.
  90. * @type {Array}
  91. */
  92. PeerStats.transport = [];
  93. /**
  94. * <tt>StatsCollector</tt> registers for stats updates of given
  95. * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
  96. * stats are extracted and put in {@link PeerStats} objects. Once the processing
  97. * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
  98. * instance as an event source.
  99. *
  100. * @param peerconnection webRTC peer connection object.
  101. * @param interval stats refresh interval given in ms.
  102. * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
  103. * called on stats update.
  104. * @constructor
  105. */
  106. function StatsCollector(peerconnection, audioLevelsInterval,
  107. audioLevelsUpdateCallback, statsInterval,
  108. statsUpdateCallback)
  109. {
  110. this.peerconnection = peerconnection;
  111. this.baselineAudioLevelsReport = null;
  112. this.currentAudioLevelsReport = null;
  113. this.currentStatsReport = null;
  114. this.baselineStatsReport = null;
  115. this.audioLevelsIntervalId = null;
  116. // Updates stats interval
  117. this.audioLevelsIntervalMilis = audioLevelsInterval;
  118. this.statsIntervalId = null;
  119. this.statsIntervalMilis = statsInterval;
  120. // Map of jids to PeerStats
  121. this.jid2stats = {};
  122. this.audioLevelsUpdateCallback = audioLevelsUpdateCallback;
  123. this.statsUpdateCallback = statsUpdateCallback;
  124. }
  125. /**
  126. * Stops stats updates.
  127. */
  128. StatsCollector.prototype.stop = function ()
  129. {
  130. if (this.audioLevelsIntervalId)
  131. {
  132. clearInterval(this.audioLevelsIntervalId);
  133. this.audioLevelsIntervalId = null;
  134. clearInterval(this.statsIntervalId);
  135. this.statsIntervalId = null;
  136. }
  137. };
  138. /**
  139. * Callback passed to <tt>getStats</tt> method.
  140. * @param error an error that occurred on <tt>getStats</tt> call.
  141. */
  142. StatsCollector.prototype.errorCallback = function (error)
  143. {
  144. console.error("Get stats error", error);
  145. this.stop();
  146. };
  147. /**
  148. * Starts stats updates.
  149. */
  150. StatsCollector.prototype.start = function ()
  151. {
  152. var self = this;
  153. this.audioLevelsIntervalId = setInterval(
  154. function ()
  155. {
  156. // Interval updates
  157. self.peerconnection.getStats(
  158. function (report)
  159. {
  160. var results = report.result();
  161. //console.error("Got interval report", results);
  162. self.currentAudioLevelsReport = results;
  163. self.processAudioLevelReport();
  164. self.baselineAudioLevelsReport =
  165. self.currentAudioLevelsReport;
  166. },
  167. self.errorCallback
  168. );
  169. },
  170. self.audioLevelsIntervalMilis
  171. );
  172. this.statsIntervalId = setInterval(
  173. function () {
  174. // Interval updates
  175. self.peerconnection.getStats(
  176. function (report)
  177. {
  178. var results = report.result();
  179. //console.error("Got interval report", results);
  180. self.currentStatsReport = results;
  181. self.processStatsReport();
  182. self.baselineStatsReport = self.currentStatsReport;
  183. },
  184. self.errorCallback
  185. );
  186. },
  187. self.statsIntervalMilis
  188. );
  189. };
  190. /**
  191. * Stats processing logic.
  192. */
  193. StatsCollector.prototype.processStatsReport = function () {
  194. if (!this.baselineStatsReport) {
  195. return;
  196. }
  197. for (var idx in this.currentStatsReport) {
  198. var now = this.currentStatsReport[idx];
  199. if (now.stat('googAvailableReceiveBandwidth') ||
  200. now.stat('googAvailableSendBandwidth'))
  201. {
  202. PeerStats.bandwidth = {
  203. "download": Math.round(
  204. (now.stat('googAvailableReceiveBandwidth')) / 1000),
  205. "upload": Math.round(
  206. (now.stat('googAvailableSendBandwidth')) / 1000)
  207. };
  208. }
  209. if(now.type == 'googCandidatePair')
  210. {
  211. var ip = now.stat('googRemoteAddress');
  212. var type = now.stat("googTransportType");
  213. var localIP = now.stat("googLocalAddress");
  214. var active = now.stat("googActiveConnection");
  215. if(!ip || !type || !localIP || active != "true")
  216. continue;
  217. var addressSaved = false;
  218. for(var i = 0; i < PeerStats.transport.length; i++)
  219. {
  220. if(PeerStats.transport[i].ip == ip &&
  221. PeerStats.transport[i].type == type &&
  222. PeerStats.transport[i].localip == localIP)
  223. {
  224. addressSaved = true;
  225. }
  226. }
  227. if(addressSaved)
  228. continue;
  229. PeerStats.transport.push({localip: localIP, ip: ip, type: type});
  230. continue;
  231. }
  232. if (now.type != 'ssrc') {
  233. continue;
  234. }
  235. var before = this.baselineStatsReport[idx];
  236. if (!before) {
  237. console.warn(now.stat('ssrc') + ' not enough data');
  238. continue;
  239. }
  240. var ssrc = now.stat('ssrc');
  241. var jid = ssrc2jid[ssrc];
  242. if (!jid) {
  243. console.warn("No jid for ssrc: " + ssrc);
  244. continue;
  245. }
  246. var jidStats = this.jid2stats[jid];
  247. if (!jidStats) {
  248. jidStats = new PeerStats();
  249. this.jid2stats[jid] = jidStats;
  250. }
  251. var isDownloadStream = true;
  252. var key = 'packetsReceived';
  253. if (!now.stat(key))
  254. {
  255. isDownloadStream = false;
  256. key = 'packetsSent';
  257. if (!now.stat(key))
  258. {
  259. console.error("No packetsReceived nor packetSent stat found");
  260. this.stop();
  261. return;
  262. }
  263. }
  264. var packetsNow = now.stat(key);
  265. if(!packetsNow || packetsNow < 0)
  266. packetsNow = 0;
  267. var packetsBefore = before.stat(key);
  268. if(!packetsBefore || packetsBefore < 0)
  269. packetsBefore = 0;
  270. var packetRate = packetsNow - packetsBefore;
  271. if(!packetRate || packetRate < 0)
  272. packetRate = 0;
  273. var currentLoss = now.stat('packetsLost');
  274. if(!currentLoss || currentLoss < 0)
  275. currentLoss = 0;
  276. var previousLoss = before.stat('packetsLost');
  277. if(!previousLoss || previousLoss < 0)
  278. previousLoss = 0;
  279. var lossRate = currentLoss - previousLoss;
  280. if(!lossRate || lossRate < 0)
  281. lossRate = 0;
  282. var packetsTotal = (packetRate + lossRate);
  283. jidStats.setSsrcLoss(ssrc,
  284. {"packetsTotal": packetsTotal,
  285. "packetsLost": lossRate,
  286. "isDownloadStream": isDownloadStream});
  287. var bytesReceived = 0, bytesSent = 0;
  288. if(now.stat("bytesReceived"))
  289. {
  290. bytesReceived = now.stat("bytesReceived") -
  291. before.stat("bytesReceived");
  292. }
  293. if(now.stat("bytesSent"))
  294. {
  295. bytesSent = now.stat("bytesSent") - before.stat("bytesSent");
  296. }
  297. var time = Math.round((now.timestamp - before.timestamp) / 1000);
  298. if(bytesReceived <= 0 || time <= 0)
  299. {
  300. bytesReceived = 0;
  301. }
  302. else
  303. {
  304. bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
  305. }
  306. if(bytesSent <= 0 || time <= 0)
  307. {
  308. bytesSent = 0;
  309. }
  310. else
  311. {
  312. bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
  313. }
  314. jidStats.setSsrcBitrate(ssrc, {
  315. "download": bytesReceived,
  316. "upload": bytesSent});
  317. var resolution = {height: null, width: null};
  318. if(now.stat("googFrameHeightReceived") &&
  319. now.stat("googFrameWidthReceived"))
  320. {
  321. resolution.height = now.stat("googFrameHeightReceived");
  322. resolution.width = now.stat("googFrameWidthReceived");
  323. }
  324. else if(now.stat("googFrameHeightSent") &&
  325. now.stat("googFrameWidthSent"))
  326. {
  327. resolution.height = now.stat("googFrameHeightSent");
  328. resolution.width = now.stat("googFrameWidthSent");
  329. }
  330. if(resolution.height && resolution.width)
  331. {
  332. jidStats.setSsrcResolution(ssrc, resolution);
  333. }
  334. else
  335. {
  336. jidStats.setSsrcResolution(ssrc, null);
  337. }
  338. }
  339. var self = this;
  340. // Jid stats
  341. var totalPackets = {download: 0, upload: 0};
  342. var lostPackets = {download: 0, upload: 0};
  343. var bitrateDownload = 0;
  344. var bitrateUpload = 0;
  345. var resolutions = {};
  346. Object.keys(this.jid2stats).forEach(
  347. function (jid)
  348. {
  349. Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
  350. function (ssrc)
  351. {
  352. var type = "upload";
  353. if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream)
  354. type = "download";
  355. totalPackets[type] +=
  356. self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal;
  357. lostPackets[type] +=
  358. self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost;
  359. }
  360. );
  361. Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach(
  362. function (ssrc) {
  363. bitrateDownload +=
  364. self.jid2stats[jid].ssrc2bitrate[ssrc].download;
  365. bitrateUpload +=
  366. self.jid2stats[jid].ssrc2bitrate[ssrc].upload;
  367. }
  368. );
  369. resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
  370. }
  371. );
  372. PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload};
  373. PeerStats.packetLoss = {
  374. total:
  375. calculatePacketLoss(lostPackets.download + lostPackets.upload,
  376. totalPackets.download + totalPackets.upload),
  377. download:
  378. calculatePacketLoss(lostPackets.download, totalPackets.download),
  379. upload:
  380. calculatePacketLoss(lostPackets.upload, totalPackets.upload)
  381. };
  382. this.statsUpdateCallback(
  383. {
  384. "bitrate": PeerStats.bitrate,
  385. "packetLoss": PeerStats.packetLoss,
  386. "bandwidth": PeerStats.bandwidth,
  387. "resolution": resolutions,
  388. "transport": PeerStats.transport
  389. });
  390. PeerStats.transport = [];
  391. };
  392. /**
  393. * Stats processing logic.
  394. */
  395. StatsCollector.prototype.processAudioLevelReport = function ()
  396. {
  397. if (!this.baselineAudioLevelsReport)
  398. {
  399. return;
  400. }
  401. for (var idx in this.currentAudioLevelsReport)
  402. {
  403. var now = this.currentAudioLevelsReport[idx];
  404. if (now.type != 'ssrc')
  405. {
  406. continue;
  407. }
  408. var before = this.baselineAudioLevelsReport[idx];
  409. if (!before)
  410. {
  411. console.warn(now.stat('ssrc') + ' not enough data');
  412. continue;
  413. }
  414. var ssrc = now.stat('ssrc');
  415. var jid = ssrc2jid[ssrc];
  416. if (!jid)
  417. {
  418. console.warn("No jid for ssrc: " + ssrc);
  419. continue;
  420. }
  421. var jidStats = this.jid2stats[jid];
  422. if (!jidStats)
  423. {
  424. jidStats = new PeerStats();
  425. this.jid2stats[jid] = jidStats;
  426. }
  427. // Audio level
  428. var audioLevel = now.stat('audioInputLevel');
  429. if (!audioLevel)
  430. audioLevel = now.stat('audioOutputLevel');
  431. if (audioLevel)
  432. {
  433. // TODO: can't find specs about what this value really is,
  434. // but it seems to vary between 0 and around 32k.
  435. audioLevel = audioLevel / 32767;
  436. jidStats.setSsrcAudioLevel(ssrc, audioLevel);
  437. if(jid != connection.emuc.myroomjid)
  438. this.audioLevelsUpdateCallback(jid, audioLevel);
  439. }
  440. }
  441. };