123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- import browser from '../browser';
- import { createRttByRegionEvent }
- from '../../service/statistics/AnalyticsEvents';
- import { getLogger } from 'jitsi-meet-logger';
- import RTCUtils from '../RTC/RTCUtils';
- import Statistics from '../statistics/statistics';
-
- const logger = getLogger(__filename);
-
- /**
- * The options to pass to createOffer (we need to offer to receive *something*
- * for the PC to gather candidates.
- */
- const offerOptions = {
- offerToReceiveAudio: 1,
- offerToReceiveVideo: 0
- };
-
-
- /**
- * The interval at which the webrtc engine sends STUN keep alive requests.
- * @type {number}
- */
- const stunKeepAliveIntervalMs = 10000;
-
- /**
- * Wraps a PeerConnection with one specific STUN server and measures the RTT
- * to the STUN server.
- */
- class PCMonitor {
- /* eslint-disable max-params */
- /**
- *
- * @param {String} region - The region of the STUN server.
- * @param {String} address - The address of the STUN server.
- * @param {number} getStatsIntervalMs how often to call getStats.
- * @param {number} delay the delay after which the PeerConnection will be
- * started (that is, createOffer and setLocalDescription will be invoked).
- *
- */
- constructor(region, address, getStatsIntervalMs, delay) {
- /* eslint-disable max-params */
- this.region = region;
- this.getStatsIntervalMs = getStatsIntervalMs;
- this.getStatsInterval = null;
-
- // What we consider the current RTT. It is Math.min(this.rtts).
- this.rtt = Infinity;
-
- // The RTT measurements we've made from the latest getStats() calls.
- this.rtts = [];
-
- const iceServers = [ { 'url': `stun:${address}` } ];
-
- this.pc = new RTCUtils.RTCPeerConnectionType(
- {
- 'iceServers': iceServers
- });
-
- // Maps a key consisting of the IP address, port and priority of a
- // candidate to some state related to it. If we have more than one
- // network interface we will might multiple srflx candidates and this
- // helps to distinguish between then.
- this.candidates = {};
-
- this.stopped = false;
-
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.startStatsInterval = this.startStatsInterval.bind(this);
- this.handleCandidateRtt = this.handleCandidateRtt.bind(this);
-
- window.setTimeout(this.start, delay);
- }
-
- /**
- * Starts this PCMonitor. That is, invokes createOffer and
- * setLocalDescription on the PeerConnection and starts an interval which
- * calls getStats.
- */
- start() {
- if (this.stopped) {
- return;
- }
-
- this.pc.createOffer(offerOptions).then(offer => {
- this.pc.setLocalDescription(
- offer,
- () => {
- logger.info(
- `setLocalDescription success for ${this.region}`);
- this.startStatsInterval();
- },
- error => {
- logger.warn(
- `setLocalDescription failed for ${this.region}: ${
- error}`);
- }
- );
- });
- }
-
- /**
- * Starts an interval which invokes getStats on the PeerConnection and
- * measures the RTTs for the different candidates.
- */
- startStatsInterval() {
- this.getStatsInterval = window.setInterval(
- () => {
- // Note that the data that we use to measure the RTT is only
- // available in the legacy (callback based) getStats API.
- this.pc.getStats(stats => {
- const results = stats.result();
-
- for (let i = 0; i < results.length; ++i) {
- const res = results[i];
- const rttTotal
- = Number(res.stat('stunKeepaliveRttTotal'));
-
- // We recognize the results that we care for (local
- // candidates of type srflx) by the existance of the
- // stunKeepaliveRttTotal stat.
- if (rttTotal > 0) {
- const candidateKey
- = `${res.stat('ipAddress')}_${
- res.stat('portNumber')}_${
- res.stat('priority')}`;
-
- this.handleCandidateRtt(
- candidateKey,
- rttTotal,
- Number(
- res.stat('stunKeepaliveResponsesReceived')),
- Number(
- res.stat('stunKeepaliveRequestsSent')));
- }
- }
-
- // After we've measured the RTT for all candidates we,
- // update the state of the PC with the shortest one.
- let rtt = Infinity;
-
- for (const key in this.candidates) {
- if (this.candidates.hasOwnProperty(key)
- && this.candidates[key].rtt > 0) {
- rtt = Math.min(rtt, this.candidates[key].rtt);
- }
- }
-
- // We keep the last 6 measured RTTs and choose the shortest
- // one to export to analytics. This is because we often see
- // failures get a real measurement which end up as Infinity.
- this.rtts.push(rtt);
- if (this.rtts.length > 6) {
- this.rtts = this.rtts.splice(1, 7);
- }
- this.rtt = Math.min(...this.rtts);
- });
- },
- this.getStatsIntervalMs
- );
- }
-
- /* eslint-disable max-params */
- /**
- * Updates the RTT for a candidate identified by "key" based on the values
- * from getStats() and the previously saved state (i.e. old values).
- *
- * @param {String} key the ID for the candidate
- * @param {number} rttTotal the value of the 'stunKeepaliveRttTotal' just
- * measured.
- * @param {number} responsesReceived the value of the
- * 'stunKeepaliveResponsesReceived' stat just measured.
- * @param {number} requestsSent the value of the 'stunKeepaliveRequestsSent'
- * stat just measured.
- */
- handleCandidateRtt(key, rttTotal, responsesReceived, requestsSent) {
- /* eslist-enable max-params */
- if (!this.candidates[key]) {
- this.candidates[key] = {
- rttTotal: 0,
- responsesReceived: 0,
- requestsSent: 0,
- rtt: NaN
- };
- }
-
- const rttTotalDiff = rttTotal - this.candidates[key].rttTotal;
- const responsesReceivedDiff
- = responsesReceived - this.candidates[key].responsesReceived;
-
- // We observe that when the difference between the number of requests
- // and responses has grown (i.q. when the value below is positive), the
- // the RTT measurements are incorrect (too low). For this reason we
- // ignore these measurement (setting rtt=NaN), but update our state.
- const requestsResponsesDiff
- = (requestsSent - responsesReceived)
- - (this.candidates[key].requestsSent
- - this.candidates[key].responsesReceived);
- let rtt = NaN;
-
- if (responsesReceivedDiff > 0 && requestsResponsesDiff === 0) {
- rtt = rttTotalDiff / responsesReceivedDiff;
- }
-
- this.candidates[key].rttTotal = rttTotal;
- this.candidates[key].responsesReceived = responsesReceived;
- this.candidates[key].requestsSent = requestsSent;
- this.candidates[key].rtt = rtt;
- }
-
-
- /**
- * Stops this PCMonitor, clearing its intervals and stopping the
- * PeerConnection.
- */
- stop() {
- if (this.getStatsInterval) {
- window.clearInterval(this.getStatsInterval);
- }
-
- this.pc.close();
-
- this.stopped = true;
- }
- }
-
- /**
- * A class which monitors the round-trip time (RTT) to a set of STUN servers.
- * The measured RTTs are sent as analytics events. It uses a separate
- * PeerConnection (represented as a PCMonitor) for each STUN server.
- */
- export default class RttMonitor {
- /**
- * Initializes a new RttMonitor.
- * @param {Object} config the object holding the configuration.
- */
- constructor(config) {
- if (!config || !config.enabled
- || !browser.supportsLocalCandidateRttStatistics()) {
- return;
- }
-
- // Maps a region to the PCMonitor instance for that region.
- this.pcMonitors = {};
-
- this.startPCMonitors = this.startPCMonitors.bind(this);
- this.sendAnalytics = this.sendAnalytics.bind(this);
- this.stop = this.stop.bind(this);
-
- this.analyticsInterval = null;
- this.stopped = false;
-
- const initialDelay = config.initialDelay || 60000;
-
-
- logger.info(
- `Starting RTT monitor with an initial delay of ${initialDelay}`);
-
-
- window.setTimeout(
- () => this.startPCMonitors(config),
- initialDelay);
- }
-
- /**
- * Starts the PCMonitors according to the configuration.
- */
- startPCMonitors(config) {
- if (!config.stunServers) {
- logger.warn('No stun servers configured.');
-
- return;
- }
-
- if (this.stopped) {
- return;
- }
-
- const getStatsIntervalMs
- = config.getStatsInterval || stunKeepAliveIntervalMs;
- const analyticsIntervalMs
- = config.analyticsInterval || getStatsIntervalMs;
- const count = Object.keys(config.stunServers).length;
- const offset = getStatsIntervalMs / count;
-
- // We delay the initialization of each PC so that they are uniformly
- // distributed across the getStatsIntervalMs.
- let i = 0;
-
- for (const region in config.stunServers) {
- if (config.stunServers.hasOwnProperty(region)) {
- const address = config.stunServers[region];
-
- this.pcMonitors[region]
- = new PCMonitor(
- region,
- address,
- getStatsIntervalMs,
- offset * i);
- i++;
- }
- }
-
- window.setTimeout(
- () => {
- if (!this.stopped) {
- this.analyticsInterval
- = window.setInterval(
- this.sendAnalytics, analyticsIntervalMs);
- }
- },
- 1000);
- }
-
- /**
- * Sends an analytics event with the measured RTT to each region/STUN
- * server.
- */
- sendAnalytics() {
- const rtts = {};
-
- for (const region in this.pcMonitors) {
- if (this.pcMonitors.hasOwnProperty(region)) {
- const rtt = this.pcMonitors[region].rtt;
-
- if (!isNaN(rtt) && rtt !== Infinity) {
- rtts[region.replace('-', '_')] = rtt;
- }
- }
- }
-
- if (rtts) {
- Statistics.sendAnalytics(createRttByRegionEvent(rtts));
- }
- }
-
- /**
- * Stops this RttMonitor, clearing all intervals and closing all
- * PeerConnections.
- */
- stop() {
- logger.info('Stopping RttMonitor.');
- this.stopped = true;
- for (const region in this.pcMonitors) {
- if (this.pcMonitors.hasOwnProperty(region)) {
- this.pcMonitors[region].stop();
- }
- }
- this.pcMonitors = {};
-
- if (this.analyticsInterval) {
- window.clearInterval(this.analyticsInterval);
- }
- }
- }
|