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 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  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. if(this.ssrc2bitrate[ssrc])
  74. {
  75. this.ssrc2bitrate[ssrc].download += bitrate.download;
  76. this.ssrc2bitrate[ssrc].upload += bitrate.upload;
  77. }
  78. else {
  79. this.ssrc2bitrate[ssrc] = bitrate;
  80. }
  81. };
  82. /**
  83. * Sets new audio level(input or output) for given <tt>ssrc</tt> that identifies
  84. * the stream which belongs to the peer represented by this instance.
  85. * @param ssrc RTP stream SSRC for which current audio level value will be
  86. * updated.
  87. * @param audioLevel the new audio level value to be set. Value is truncated to
  88. * fit the range from 0 to 1.
  89. */
  90. PeerStats.prototype.setSsrcAudioLevel = function (ssrc, audioLevel)
  91. {
  92. // Range limit 0 - 1
  93. this.ssrc2AudioLevel[ssrc] = Math.min(Math.max(audioLevel, 0), 1);
  94. };
  95. /**
  96. * Array with the transport information.
  97. * @type {Array}
  98. */
  99. PeerStats.transport = [];
  100. /**
  101. * <tt>StatsCollector</tt> registers for stats updates of given
  102. * <tt>peerconnection</tt> in given <tt>interval</tt>. On each update particular
  103. * stats are extracted and put in {@link PeerStats} objects. Once the processing
  104. * is done <tt>audioLevelsUpdateCallback</tt> is called with <tt>this</tt>
  105. * instance as an event source.
  106. *
  107. * @param peerconnection webRTC peer connection object.
  108. * @param interval stats refresh interval given in ms.
  109. * @param {function(StatsCollector)} audioLevelsUpdateCallback the callback
  110. * called on stats update.
  111. * @constructor
  112. */
  113. function StatsCollector(peerconnection, audioLevelsInterval,
  114. audioLevelsUpdateCallback, statsInterval,
  115. statsUpdateCallback)
  116. {
  117. this.peerconnection = peerconnection;
  118. this.baselineAudioLevelsReport = null;
  119. this.currentAudioLevelsReport = null;
  120. this.currentStatsReport = null;
  121. this.baselineStatsReport = null;
  122. this.audioLevelsIntervalId = null;
  123. // Updates stats interval
  124. this.audioLevelsIntervalMilis = audioLevelsInterval;
  125. this.statsIntervalId = null;
  126. this.statsIntervalMilis = statsInterval;
  127. // Map of jids to PeerStats
  128. this.jid2stats = {};
  129. this.audioLevelsUpdateCallback = audioLevelsUpdateCallback;
  130. this.statsUpdateCallback = statsUpdateCallback;
  131. }
  132. /**
  133. * Stops stats updates.
  134. */
  135. StatsCollector.prototype.stop = function ()
  136. {
  137. if (this.audioLevelsIntervalId)
  138. {
  139. clearInterval(this.audioLevelsIntervalId);
  140. this.audioLevelsIntervalId = null;
  141. clearInterval(this.statsIntervalId);
  142. this.statsIntervalId = null;
  143. }
  144. };
  145. /**
  146. * Callback passed to <tt>getStats</tt> method.
  147. * @param error an error that occurred on <tt>getStats</tt> call.
  148. */
  149. StatsCollector.prototype.errorCallback = function (error)
  150. {
  151. console.error("Get stats error", error);
  152. this.stop();
  153. };
  154. /**
  155. * Starts stats updates.
  156. */
  157. StatsCollector.prototype.start = function ()
  158. {
  159. var self = this;
  160. this.audioLevelsIntervalId = setInterval(
  161. function ()
  162. {
  163. // Interval updates
  164. self.peerconnection.getStats(
  165. function (report)
  166. {
  167. var results = null;
  168. if(!report || !report.result || typeof report.result != 'function')
  169. {
  170. results = report;
  171. }
  172. else
  173. {
  174. results = report.result();
  175. }
  176. //console.error("Got interval report", results);
  177. self.currentAudioLevelsReport = results;
  178. self.processAudioLevelReport();
  179. self.baselineAudioLevelsReport =
  180. self.currentAudioLevelsReport;
  181. },
  182. self.errorCallback
  183. );
  184. },
  185. self.audioLevelsIntervalMilis
  186. );
  187. this.statsIntervalId = setInterval(
  188. function () {
  189. // Interval updates
  190. self.peerconnection.getStats(
  191. function (report)
  192. {
  193. var results = null;
  194. if(!report || !report.result || typeof report.result != 'function')
  195. {
  196. //firefox
  197. results = report;
  198. }
  199. else
  200. {
  201. //chrome
  202. results = report.result();
  203. }
  204. //console.error("Got interval report", results);
  205. self.currentStatsReport = results;
  206. try
  207. {
  208. self.processStatsReport();
  209. }
  210. catch(e)
  211. {
  212. console.error("Unsupported key:" + e);
  213. }
  214. self.baselineStatsReport = self.currentStatsReport;
  215. },
  216. self.errorCallback
  217. );
  218. },
  219. self.statsIntervalMilis
  220. );
  221. };
  222. var keyMap = {
  223. "firefox": {
  224. "ssrc": "ssrc",
  225. "packetsReceived": "packetsReceived",
  226. "packetsLost": "packetsLost",
  227. "packetsSent": "packetsSent",
  228. "bytesReceived": "bytesReceived",
  229. "bytesSent": "bytesSent"
  230. },
  231. "chrome": {
  232. "receiveBandwidth": "googAvailableReceiveBandwidth",
  233. "sendBandwidth": "googAvailableSendBandwidth",
  234. "remoteAddress": "googRemoteAddress",
  235. "transportType": "googTransportType",
  236. "localAddress": "googLocalAddress",
  237. "activeConnection": "googActiveConnection",
  238. "ssrc": "ssrc",
  239. "packetsReceived": "packetsReceived",
  240. "packetsSent": "packetsSent",
  241. "packetsLost": "packetsLost",
  242. "bytesReceived": "bytesReceived",
  243. "bytesSent": "bytesSent",
  244. "googFrameHeightReceived": "googFrameHeightReceived",
  245. "googFrameWidthReceived": "googFrameWidthReceived",
  246. "googFrameHeightSent": "googFrameHeightSent",
  247. "googFrameWidthSent": "googFrameWidthSent",
  248. "audioInputLevel": "audioInputLevel",
  249. "audioOutputLevel": "audioOutputLevel"
  250. }
  251. };
  252. /**
  253. * Stats processing logic.
  254. */
  255. StatsCollector.prototype.processStatsReport = function () {
  256. if (!this.baselineStatsReport) {
  257. return;
  258. }
  259. for (var idx in this.currentStatsReport) {
  260. var now = this.currentStatsReport[idx];
  261. try {
  262. if (getStatValue(now, 'receiveBandwidth') ||
  263. getStatValue(now, 'sendBandwidth')) {
  264. PeerStats.bandwidth = {
  265. "download": Math.round(
  266. (getStatValue(now, 'receiveBandwidth')) / 1000),
  267. "upload": Math.round(
  268. (getStatValue(now, 'sendBandwidth')) / 1000)
  269. };
  270. }
  271. }
  272. catch(e){/*not supported*/}
  273. if(now.type == 'googCandidatePair')
  274. {
  275. var ip, type, localIP, active;
  276. try {
  277. ip = getStatValue(now, 'remoteAddress');
  278. type = getStatValue(now, "transportType");
  279. localIP = getStatValue(now, "localAddress");
  280. active = getStatValue(now, "activeConnection");
  281. }
  282. catch(e){/*not supported*/}
  283. if(!ip || !type || !localIP || active != "true")
  284. continue;
  285. var addressSaved = false;
  286. for(var i = 0; i < PeerStats.transport.length; i++)
  287. {
  288. if(PeerStats.transport[i].ip == ip &&
  289. PeerStats.transport[i].type == type &&
  290. PeerStats.transport[i].localip == localIP)
  291. {
  292. addressSaved = true;
  293. }
  294. }
  295. if(addressSaved)
  296. continue;
  297. PeerStats.transport.push({localip: localIP, ip: ip, type: type});
  298. continue;
  299. }
  300. if(now.type == "candidatepair")
  301. {
  302. if(now.state == "succeeded")
  303. continue;
  304. var local = this.currentStatsReport[now.localCandidateId];
  305. var remote = this.currentStatsReport[now.remoteCandidateId];
  306. PeerStats.transport.push({localip: local.ipAddress + ":" + local.portNumber,
  307. ip: remote.ipAddress + ":" + remote.portNumber, type: local.transport});
  308. }
  309. if (now.type != 'ssrc' && now.type != "outboundrtp" &&
  310. now.type != "inboundrtp") {
  311. continue;
  312. }
  313. var before = this.baselineStatsReport[idx];
  314. if (!before) {
  315. console.warn(getStatValue(now, 'ssrc') + ' not enough data');
  316. continue;
  317. }
  318. var ssrc = getStatValue(now, 'ssrc');
  319. if(!ssrc)
  320. continue;
  321. var jid = ssrc2jid[ssrc];
  322. if (!jid) {
  323. console.warn("No jid for ssrc: " + ssrc);
  324. continue;
  325. }
  326. var jidStats = this.jid2stats[jid];
  327. if (!jidStats) {
  328. jidStats = new PeerStats();
  329. this.jid2stats[jid] = jidStats;
  330. }
  331. var isDownloadStream = true;
  332. var key = 'packetsReceived';
  333. if (!getStatValue(now, key))
  334. {
  335. isDownloadStream = false;
  336. key = 'packetsSent';
  337. if (!getStatValue(now, key))
  338. {
  339. console.warn("No packetsReceived nor packetSent stat found");
  340. continue;
  341. }
  342. }
  343. var packetsNow = getStatValue(now, key);
  344. if(!packetsNow || packetsNow < 0)
  345. packetsNow = 0;
  346. var packetsBefore = getStatValue(before, key);
  347. if(!packetsBefore || packetsBefore < 0)
  348. packetsBefore = 0;
  349. var packetRate = packetsNow - packetsBefore;
  350. if(!packetRate || packetRate < 0)
  351. packetRate = 0;
  352. var currentLoss = getStatValue(now, 'packetsLost');
  353. if(!currentLoss || currentLoss < 0)
  354. currentLoss = 0;
  355. var previousLoss = getStatValue(before, 'packetsLost');
  356. if(!previousLoss || previousLoss < 0)
  357. previousLoss = 0;
  358. var lossRate = currentLoss - previousLoss;
  359. if(!lossRate || lossRate < 0)
  360. lossRate = 0;
  361. var packetsTotal = (packetRate + lossRate);
  362. jidStats.setSsrcLoss(ssrc,
  363. {"packetsTotal": packetsTotal,
  364. "packetsLost": lossRate,
  365. "isDownloadStream": isDownloadStream});
  366. var bytesReceived = 0, bytesSent = 0;
  367. if(getStatValue(now, "bytesReceived"))
  368. {
  369. bytesReceived = getStatValue(now, "bytesReceived") -
  370. getStatValue(before, "bytesReceived");
  371. }
  372. if(getStatValue(now, "bytesSent"))
  373. {
  374. bytesSent = getStatValue(now, "bytesSent") -
  375. getStatValue(before, "bytesSent");
  376. }
  377. var time = Math.round((now.timestamp - before.timestamp) / 1000);
  378. if(bytesReceived <= 0 || time <= 0)
  379. {
  380. bytesReceived = 0;
  381. }
  382. else
  383. {
  384. bytesReceived = Math.round(((bytesReceived * 8) / time) / 1000);
  385. }
  386. if(bytesSent <= 0 || time <= 0)
  387. {
  388. bytesSent = 0;
  389. }
  390. else
  391. {
  392. bytesSent = Math.round(((bytesSent * 8) / time) / 1000);
  393. }
  394. jidStats.setSsrcBitrate(ssrc, {
  395. "download": bytesReceived,
  396. "upload": bytesSent});
  397. var resolution = {height: null, width: null};
  398. try {
  399. if (getStatValue(now, "googFrameHeightReceived") &&
  400. getStatValue(now, "googFrameWidthReceived")) {
  401. resolution.height = getStatValue(now, "googFrameHeightReceived");
  402. resolution.width = getStatValue(now, "googFrameWidthReceived");
  403. }
  404. else if (getStatValue(now, "googFrameHeightSent") &&
  405. getStatValue(now, "googFrameWidthSent")) {
  406. resolution.height = getStatValue(now, "googFrameHeightSent");
  407. resolution.width = getStatValue(now, "googFrameWidthSent");
  408. }
  409. }
  410. catch(e){/*not supported*/}
  411. if(resolution.height && resolution.width)
  412. {
  413. jidStats.setSsrcResolution(ssrc, resolution);
  414. }
  415. else
  416. {
  417. jidStats.setSsrcResolution(ssrc, null);
  418. }
  419. }
  420. var self = this;
  421. // Jid stats
  422. var totalPackets = {download: 0, upload: 0};
  423. var lostPackets = {download: 0, upload: 0};
  424. var bitrateDownload = 0;
  425. var bitrateUpload = 0;
  426. var resolutions = {};
  427. Object.keys(this.jid2stats).forEach(
  428. function (jid)
  429. {
  430. Object.keys(self.jid2stats[jid].ssrc2Loss).forEach(
  431. function (ssrc)
  432. {
  433. var type = "upload";
  434. if(self.jid2stats[jid].ssrc2Loss[ssrc].isDownloadStream)
  435. type = "download";
  436. totalPackets[type] +=
  437. self.jid2stats[jid].ssrc2Loss[ssrc].packetsTotal;
  438. lostPackets[type] +=
  439. self.jid2stats[jid].ssrc2Loss[ssrc].packetsLost;
  440. }
  441. );
  442. Object.keys(self.jid2stats[jid].ssrc2bitrate).forEach(
  443. function (ssrc) {
  444. bitrateDownload +=
  445. self.jid2stats[jid].ssrc2bitrate[ssrc].download;
  446. bitrateUpload +=
  447. self.jid2stats[jid].ssrc2bitrate[ssrc].upload;
  448. delete self.jid2stats[jid].ssrc2bitrate[ssrc];
  449. }
  450. );
  451. resolutions[jid] = self.jid2stats[jid].ssrc2resolution;
  452. }
  453. );
  454. PeerStats.bitrate = {"upload": bitrateUpload, "download": bitrateDownload};
  455. PeerStats.packetLoss = {
  456. total:
  457. calculatePacketLoss(lostPackets.download + lostPackets.upload,
  458. totalPackets.download + totalPackets.upload),
  459. download:
  460. calculatePacketLoss(lostPackets.download, totalPackets.download),
  461. upload:
  462. calculatePacketLoss(lostPackets.upload, totalPackets.upload)
  463. };
  464. this.statsUpdateCallback(
  465. {
  466. "bitrate": PeerStats.bitrate,
  467. "packetLoss": PeerStats.packetLoss,
  468. "bandwidth": PeerStats.bandwidth,
  469. "resolution": resolutions,
  470. "transport": PeerStats.transport
  471. });
  472. PeerStats.transport = [];
  473. };
  474. /**
  475. * Stats processing logic.
  476. */
  477. StatsCollector.prototype.processAudioLevelReport = function ()
  478. {
  479. if (!this.baselineAudioLevelsReport)
  480. {
  481. return;
  482. }
  483. for (var idx in this.currentAudioLevelsReport)
  484. {
  485. var now = this.currentAudioLevelsReport[idx];
  486. if (now.type != 'ssrc')
  487. {
  488. continue;
  489. }
  490. var before = this.baselineAudioLevelsReport[idx];
  491. if (!before)
  492. {
  493. console.warn(getStatValue(now, 'ssrc') + ' not enough data');
  494. continue;
  495. }
  496. var ssrc = getStatValue(now, 'ssrc');
  497. var jid = ssrc2jid[ssrc];
  498. if (!jid)
  499. {
  500. console.warn("No jid for ssrc: " + ssrc);
  501. continue;
  502. }
  503. var jidStats = this.jid2stats[jid];
  504. if (!jidStats)
  505. {
  506. jidStats = new PeerStats();
  507. this.jid2stats[jid] = jidStats;
  508. }
  509. // Audio level
  510. var audioLevel = null;
  511. try {
  512. audioLevel = getStatValue(now, 'audioInputLevel');
  513. if (!audioLevel)
  514. audioLevel = getStatValue(now, 'audioOutputLevel');
  515. }
  516. catch(e) {/*not supported*/
  517. console.warn("Audio Levels are not available in the statistics.");
  518. clearInterval(this.audioLevelsIntervalId);
  519. return;
  520. }
  521. if (audioLevel)
  522. {
  523. // TODO: can't find specs about what this value really is,
  524. // but it seems to vary between 0 and around 32k.
  525. audioLevel = audioLevel / 32767;
  526. jidStats.setSsrcAudioLevel(ssrc, audioLevel);
  527. if(jid != connection.emuc.myroomjid)
  528. this.audioLevelsUpdateCallback(jid, audioLevel);
  529. }
  530. }
  531. };
  532. function getStatValue(item, name) {
  533. if(!keyMap[RTC.browser][name])
  534. throw "The property isn't supported!";
  535. var key = keyMap[RTC.browser][name];
  536. return RTC.browser == "chrome"? item.stat(key) : item[key];
  537. }