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

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