Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

rttmonitor.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. import browser from '../browser';
  2. import { createRttByRegionEvent }
  3. from '../../service/statistics/AnalyticsEvents';
  4. import { getLogger } from 'jitsi-meet-logger';
  5. import RTCUtils from '../RTC/RTCUtils';
  6. import Statistics from '../statistics/statistics';
  7. const logger = getLogger(__filename);
  8. /**
  9. * The options to pass to createOffer (we need to offer to receive *something*
  10. * for the PC to gather candidates.
  11. */
  12. const offerOptions = {
  13. offerToReceiveAudio: 1,
  14. offerToReceiveVideo: 0
  15. };
  16. /**
  17. * The interval at which the webrtc engine sends STUN keep alive requests.
  18. * @type {number}
  19. */
  20. const stunKeepAliveIntervalMs = 10000;
  21. /**
  22. * Wraps a PeerConnection with one specific STUN server and measures the RTT
  23. * to the STUN server.
  24. */
  25. class PCMonitor {
  26. /**
  27. *
  28. * @param {Object} stunServer the STUN server configuration (address and
  29. * region).
  30. * @param {number} getStatsIntervalMs how often to call getStats.
  31. * @param {number} delay the delay after which the PeerConnection will be
  32. * started (that is, createOffer and setLocalDescription will be invoked).
  33. *
  34. */
  35. constructor(stunServer, getStatsIntervalMs, delay) {
  36. this.region = stunServer.region;
  37. this.getStatsIntervalMs = getStatsIntervalMs;
  38. this.getStatsInterval = null;
  39. // What we consider the current RTT. It is Math.min(this.rtts).
  40. this.rtt = Infinity;
  41. // The RTT measurements we've made from the latest getStats() calls.
  42. this.rtts = [];
  43. const iceServers = [ { 'url': `stun:${stunServer.address}` } ];
  44. this.pc = new RTCUtils.RTCPeerConnectionType(
  45. {
  46. 'iceServers': iceServers
  47. });
  48. // Maps a key consisting of the IP address, port and priority of a
  49. // candidate to some state related to it. If we have more than one
  50. // network interface we will might multiple srflx candidates and this
  51. // helps to distinguish between then.
  52. this.candidates = {};
  53. this.stopped = false;
  54. this.start = this.start.bind(this);
  55. this.stop = this.stop.bind(this);
  56. this.startStatsInterval = this.startStatsInterval.bind(this);
  57. this.handleCandidateRtt = this.handleCandidateRtt.bind(this);
  58. window.setTimeout(this.start, delay);
  59. }
  60. /**
  61. * Starts this PCMonitor. That is, invokes createOffer and
  62. * setLocalDescription on the PeerConnection and starts an interval which
  63. * calls getStats.
  64. */
  65. start() {
  66. if (this.stopped) {
  67. return;
  68. }
  69. this.pc.createOffer(offerOptions).then(offer => {
  70. this.pc.setLocalDescription(
  71. offer,
  72. () => {
  73. logger.info(
  74. `setLocalDescription success for ${this.region}`);
  75. this.startStatsInterval();
  76. },
  77. error => {
  78. logger.warn(
  79. `setLocalDescription failed for ${this.region}: ${
  80. error}`);
  81. }
  82. );
  83. });
  84. }
  85. /**
  86. * Starts an interval which invokes getStats on the PeerConnection and
  87. * measures the RTTs for the different candidates.
  88. */
  89. startStatsInterval() {
  90. this.getStatsInterval = window.setInterval(
  91. () => {
  92. // Note that the data that we use to measure the RTT is only
  93. // available in the legacy (callback based) getStats API.
  94. this.pc.getStats(stats => {
  95. const results = stats.result();
  96. for (let i = 0; i < results.length; ++i) {
  97. const res = results[i];
  98. const rttTotal
  99. = Number(res.stat('stunKeepaliveRttTotal'));
  100. // We recognize the results that we care for (local
  101. // candidates of type srflx) by the existance of the
  102. // stunKeepaliveRttTotal stat.
  103. if (rttTotal > 0) {
  104. const candidateKey
  105. = `${res.stat('ipAddress')}_${
  106. res.stat('portNumber')}_${
  107. res.stat('priority')}`;
  108. this.handleCandidateRtt(
  109. candidateKey,
  110. rttTotal,
  111. Number(
  112. res.stat('stunKeepaliveResponsesReceived')),
  113. Number(
  114. res.stat('stunKeepaliveRequestsSent')));
  115. }
  116. }
  117. // After we've measured the RTT for all candidates we,
  118. // update the state of the PC with the shortest one.
  119. let rtt = Infinity;
  120. for (const key in this.candidates) {
  121. if (this.candidates.hasOwnProperty(key)
  122. && this.candidates[key].rtt > 0) {
  123. rtt = Math.min(rtt, this.candidates[key].rtt);
  124. }
  125. }
  126. // We keep the last 6 measured RTTs and choose the shortest
  127. // one to export to analytics. This is because we often see
  128. // failures get a real measurement which end up as Infinity.
  129. this.rtts.push(rtt);
  130. if (this.rtts.length > 6) {
  131. this.rtts = this.rtts.splice(1, 7);
  132. }
  133. this.rtt = Math.min(...this.rtts);
  134. });
  135. },
  136. this.getStatsIntervalMs
  137. );
  138. }
  139. /* eslint-disable max-params */
  140. /**
  141. * Updates the RTT for a candidate identified by "key" based on the values
  142. * from getStats() and the previously saved state (i.e. old values).
  143. *
  144. * @param {String} key the ID for the candidate
  145. * @param {number} rttTotal the value of the 'stunKeepaliveRttTotal' just
  146. * measured.
  147. * @param {number} responsesReceived the value of the
  148. * 'stunKeepaliveResponsesReceived' stat just measured.
  149. * @param {number} requestsSent the value of the 'stunKeepaliveRequestsSent'
  150. * stat just measured.
  151. */
  152. handleCandidateRtt(key, rttTotal, responsesReceived, requestsSent) {
  153. /* eslist-enable max-params */
  154. if (!this.candidates[key]) {
  155. this.candidates[key] = {
  156. rttTotal: 0,
  157. responsesReceived: 0,
  158. requestsSent: 0,
  159. rtt: NaN
  160. };
  161. }
  162. const rttTotalDiff = rttTotal - this.candidates[key].rttTotal;
  163. const responsesReceivedDiff
  164. = responsesReceived - this.candidates[key].responsesReceived;
  165. // We observe that when the difference between the number of requests
  166. // and responses has grown (i.q. when the value below is positive), the
  167. // the RTT measurements are incorrect (too low). For this reason we
  168. // ignore these measurement (setting rtt=NaN), but update our state.
  169. const requestsResponsesDiff
  170. = (requestsSent - responsesReceived)
  171. - (this.candidates[key].requestsSent
  172. - this.candidates[key].responsesReceived);
  173. let rtt = NaN;
  174. if (responsesReceivedDiff > 0 && requestsResponsesDiff === 0) {
  175. rtt = rttTotalDiff / responsesReceivedDiff;
  176. }
  177. this.candidates[key].rttTotal = rttTotal;
  178. this.candidates[key].responsesReceived = responsesReceived;
  179. this.candidates[key].requestsSent = requestsSent;
  180. this.candidates[key].rtt = rtt;
  181. }
  182. /**
  183. * Stops this PCMonitor, clearing its intervals and stopping the
  184. * PeerConnection.
  185. */
  186. stop() {
  187. if (this.getStatsInterval) {
  188. window.clearInterval(this.getStatsInterval);
  189. }
  190. this.pc.close();
  191. this.stopped = true;
  192. }
  193. }
  194. /**
  195. * A class which monitors the round-trip time (RTT) to a set of STUN servers.
  196. * The measured RTTs are sent as analytics events. It uses a separate
  197. * PeerConnection (represented as a PCMonitor) for each STUN server.
  198. */
  199. export default class RttMonitor {
  200. /**
  201. * Initializes a new RttMonitor.
  202. * @param {Object} config the object holding the configuration.
  203. */
  204. constructor(config) {
  205. if (!config || !config.enabled
  206. || !browser.supportsLocalCandidateRttStatistics()) {
  207. return;
  208. }
  209. // Maps a region to the PCMonitor instance for that region.
  210. this.pcMonitors = {};
  211. this.startPCMonitors = this.startPCMonitors.bind(this);
  212. this.sendAnalytics = this.sendAnalytics.bind(this);
  213. this.stop = this.stop.bind(this);
  214. this.analyticsInterval = null;
  215. this.stopped = false;
  216. const initialDelay = config.initialDelay || 60000;
  217. logger.info(
  218. `Starting RTT monitor with an initial delay of ${initialDelay}`);
  219. window.setTimeout(
  220. () => this.startPCMonitors(config),
  221. initialDelay);
  222. }
  223. /**
  224. * Starts the PCMonitors according to the configuration.
  225. */
  226. startPCMonitors(config) {
  227. if (!Array.isArray(config.stunServers)) {
  228. logger.warn('No stun servers configured.');
  229. return;
  230. }
  231. if (this.stopped) {
  232. return;
  233. }
  234. const getStatsIntervalMs
  235. = config.getStatsInterval || stunKeepAliveIntervalMs;
  236. const analyticsIntervalMs
  237. = config.analyticsInterval || getStatsIntervalMs;
  238. const count = config.stunServers.length;
  239. const offset = getStatsIntervalMs / count;
  240. // We delay the initialization of each PC so that they are uniformly
  241. // distributed across the getStatsIntervalMs.
  242. for (let i = 0; i < count; i++) {
  243. const stunServer = config.stunServers[i];
  244. this.pcMonitors[stunServer.region]
  245. = new PCMonitor(stunServer, getStatsIntervalMs, offset * i);
  246. }
  247. window.setTimeout(
  248. () => {
  249. if (!this.stopped) {
  250. this.analyticsInterval
  251. = window.setInterval(
  252. this.sendAnalytics, analyticsIntervalMs);
  253. }
  254. },
  255. 1000);
  256. }
  257. /**
  258. * Sends an analytics event with the measured RTT to each region/STUN
  259. * server.
  260. */
  261. sendAnalytics() {
  262. const rtts = {};
  263. for (const region in this.pcMonitors) {
  264. if (this.pcMonitors.hasOwnProperty(region)) {
  265. const rtt = this.pcMonitors[region].rtt;
  266. if (!isNaN(rtt) && rtt !== Infinity) {
  267. rtts[region.replace('-', '_')] = rtt;
  268. }
  269. }
  270. }
  271. if (rtts) {
  272. Statistics.sendAnalytics(createRttByRegionEvent(rtts));
  273. }
  274. }
  275. /**
  276. * Stops this RttMonitor, clearing all intervals and closing all
  277. * PeerConnections.
  278. */
  279. stop() {
  280. logger.info('Stopping RttMonitor.');
  281. this.stopped = true;
  282. for (const region in this.pcMonitors) {
  283. if (this.pcMonitors.hasOwnProperty(region)) {
  284. this.pcMonitors[region].stop();
  285. }
  286. }
  287. this.pcMonitors = {};
  288. if (this.analyticsInterval) {
  289. window.clearInterval(this.analyticsInterval);
  290. }
  291. }
  292. }