| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 | /* global require */
var LocalStats = require("./LocalStatsCollector.js");
var logger = require("jitsi-meet-logger").getLogger(__filename);
var RTPStats = require("./RTPStatsCollector.js");
var EventEmitter = require("events");
var StatisticsEvents = require("../../service/statistics/Events");
var CallStats = require("./CallStats");
var ScriptUtil = require('../util/ScriptUtil');
var JitsiTrackError = require("../../JitsiTrackError");
/**
 * True if callstats API is loaded
 */
 var isCallstatsLoaded = false;
// Since callstats.io is a third party, we cannot guarantee the quality of their
// service. More specifically, their server may take noticeably long time to
// respond. Consequently, it is in our best interest (in the sense that the
// intergration of callstats.io is pretty important to us but not enough to
// allow it to prevent people from joining a conference) to (1) start
// downloading their API as soon as possible and (2) do the downloading
// asynchronously.
function loadCallStatsAPI() {
    if(!isCallstatsLoaded)
        ScriptUtil.loadScript(
                'https://api.callstats.io/static/callstats.min.js',
                /* async */ true,
                /* prepend */ true);
    isCallstatsLoaded = true;
    // FIXME At the time of this writing, we hope that the callstats.io API will
    // have loaded by the time we needed it (i.e. CallStats.init is invoked).
}
/**
 * Log stats via the focus once every this many milliseconds.
 */
var LOG_INTERVAL = 60000;
/**
 * callstats strips any additional fields from Error except for "name", "stack",
 * "message" and "constraintName". So we need to bundle additional information
 * from JitsiTrackError into error passed to callstats to preserve valuable
 * information about error.
 * @param {JitsiTrackError} error
 */
function formatJitsiTrackErrorForCallStats(error) {
    var err = new Error();
    // Just copy original stack from error
    err.stack = error.stack;
    // Combine name from error's name plus (possibly) name of original GUM error
    err.name = (error.name || "Unknown error") + (error.gum && error.gum.error
        && error.gum.error.name ? " - " + error.gum.error.name : "");
    // Put all constraints into this field. For constraint failed errors we will
    // still know which exactly constraint failed as it will be a part of
    // message.
    err.constraintName = error.gum && error.gum.constraints
        ? JSON.stringify(error.gum.constraints) : "";
    // Just copy error's message.
    err.message = error.message;
    return err;
}
function Statistics(xmpp, options) {
    this.rtpStats = null;
    this.eventEmitter = new EventEmitter();
    this.xmpp = xmpp;
    this.options = options || {};
    this.callStatsIntegrationEnabled
        = this.options.callStatsID && this.options.callStatsSecret
            // Even though AppID and AppSecret may be specified, the integration
            // of callstats.io may be disabled because of globally-disallowed
            // requests to any third parties.
            && (this.options.disableThirdPartyRequests !== true);
    if(this.callStatsIntegrationEnabled)
        loadCallStatsAPI();
    this.callStats = null;
    /**
     * Send the stats already saved in rtpStats to be logged via the focus.
     */
    this.logStatsIntervalId = null;
}
Statistics.audioLevelsEnabled = false;
Statistics.audioLevelsInterval = 200;
/**
 * Array of callstats instances. Used to call Statistics static methods and
 * send stats to all cs instances.
 */
Statistics.callsStatsInstances = [];
Statistics.prototype.startRemoteStats = function (peerconnection) {
    if(!Statistics.audioLevelsEnabled)
        return;
    this.stopRemoteStats();
    try {
        this.rtpStats
            = new RTPStats(peerconnection,
                    Statistics.audioLevelsInterval, 2000, this.eventEmitter);
        this.rtpStats.start();
    } catch (e) {
        this.rtpStats = null;
        logger.error('Failed to start collecting remote statistics: ' + e);
    }
    if (this.rtpStats) {
        this.logStatsIntervalId = setInterval(function () {
            var stats = this.rtpStats.getCollectedStats();
            if (this.xmpp.sendLogs(stats)) {
                this.rtpStats.clearCollectedStats();
            }
        }.bind(this), LOG_INTERVAL);
    }
};
Statistics.localStats = [];
Statistics.startLocalStats = function (stream, callback) {
    if(!Statistics.audioLevelsEnabled)
        return;
    var localStats = new LocalStats(stream, Statistics.audioLevelsInterval,
        callback);
    this.localStats.push(localStats);
    localStats.start();
};
Statistics.prototype.addAudioLevelListener = function(listener) {
    if(!Statistics.audioLevelsEnabled)
        return;
    this.eventEmitter.on(StatisticsEvents.AUDIO_LEVEL, listener);
};
Statistics.prototype.removeAudioLevelListener = function(listener) {
    if(!Statistics.audioLevelsEnabled)
        return;
    this.eventEmitter.removeListener(StatisticsEvents.AUDIO_LEVEL, listener);
};
Statistics.prototype.addConnectionStatsListener = function (listener) {
    this.eventEmitter.on(StatisticsEvents.CONNECTION_STATS, listener);
};
/**
 * Adds listener for detected audio problems.
 * @param listener the listener.
 */
Statistics.prototype.addAudioProblemListener = function (listener) {
    this.eventEmitter.on(StatisticsEvents.AUDIO_NOT_WORKING, listener);
};
Statistics.prototype.removeConnectionStatsListener = function (listener) {
    this.eventEmitter.removeListener(StatisticsEvents.CONNECTION_STATS, listener);
};
Statistics.prototype.dispose = function () {
    if(Statistics.audioLevelsEnabled) {
        Statistics.stopAllLocalStats();
        this.stopRemoteStats();
        if(this.eventEmitter)
            this.eventEmitter.removeAllListeners();
    }
};
Statistics.stopAllLocalStats = function () {
    if(!Statistics.audioLevelsEnabled)
        return;
    for(var i = 0; i < this.localStats.length; i++)
        this.localStats[i].stop();
    this.localStats = [];
};
Statistics.stopLocalStats = function (stream) {
    if(!Statistics.audioLevelsEnabled)
        return;
    for(var i = 0; i < Statistics.localStats.length; i++)
        if(Statistics.localStats[i].stream === stream){
            var localStats = Statistics.localStats.splice(i, 1);
            localStats[0].stop();
            break;
        }
};
Statistics.prototype.stopRemoteStats = function () {
    if (!Statistics.audioLevelsEnabled || !this.rtpStats) {
        return;
    }
    this.rtpStats.stop();
    this.rtpStats = null;
    if (this.logStatsIntervalId) {
        clearInterval(this.logStatsIntervalId);
        this.logStatsIntervalId = null;
    }
};
//CALSTATS METHODS
/**
 * Initializes the callstats.io API.
 * @param peerConnection {JingleSessionPC} the session object
 * @param Settings {Settings} the settings instance. Declared in
 * /modules/settings/Settings.js
 */
Statistics.prototype.startCallStats = function (session, settings) {
    if(this.callStatsIntegrationEnabled && !this.callstats) {
        this.callstats = new CallStats(session, settings, this.options);
        Statistics.callsStatsInstances.push(this.callstats);
    }
};
/**
 * Removes the callstats.io instances.
 */
Statistics.prototype.stopCallStats = function () {
    if(this.callStatsIntegrationEnabled && this.callstats) {
        var callstatsList = Statistics.callsStatsInstances;
        for(var i = 0;i < callstatsList.length; i++) {
            if(this.callstats === callstatsList[i]) {
                Statistics.callsStatsInstances.splice(i, 1);
                break;
            }
        }
        this.callstats = null;
        CallStats.dispose();
    }
};
/**
 * Returns true if the callstats integration is enabled, otherwise returns
 * false.
 *
 * @returns true if the callstats integration is enabled, otherwise returns
 * false.
 */
Statistics.prototype.isCallstatsEnabled = function () {
    return this.callStatsIntegrationEnabled;
};
/**
 * Notifies CallStats for ice connection failed
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendIceConnectionFailedEvent = function (pc) {
    if(this.callstats)
        this.callstats.sendIceConnectionFailedEvent(pc, this.callstats);
};
/**
 * Notifies CallStats for mute events
 * @param mute {boolean} true for muted and false for not muted
 * @param type {String} "audio"/"video"
 */
Statistics.prototype.sendMuteEvent = function (muted, type) {
    if(this.callstats)
        CallStats.sendMuteEvent(muted, type, this.callstats);
};
/**
 * Notifies CallStats for screen sharing events
 * @param start {boolean} true for starting screen sharing and
 * false for not stopping
 */
Statistics.prototype.sendScreenSharingEvent = function (start) {
    if(this.callstats)
        CallStats.sendScreenSharingEvent(start, this.callstats);
};
/**
 * Notifies the statistics module that we are now the dominant speaker of the
 * conference.
 */
Statistics.prototype.sendDominantSpeakerEvent = function () {
    if(this.callstats)
        CallStats.sendDominantSpeakerEvent(this.callstats);
};
/**
 * Notifies about active device.
 * @param {{deviceList: {String:String}}} devicesData - list of devices with
 *      their data
 */
Statistics.sendActiveDeviceListEvent = function (devicesData) {
    if (Statistics.callsStatsInstances.length) {
        Statistics.callsStatsInstances.forEach(function (cs) {
            CallStats.sendActiveDeviceListEvent(devicesData, cs);
        });
    } else {
        CallStats.sendActiveDeviceListEvent(devicesData, null);
    }
};
/**
 * Lets the underlying statistics module know where is given SSRC rendered by
 * providing renderer tag ID.
 * @param ssrc {number} the SSRC of the stream
 * @param isLocal {boolean} <tt>true<tt> if this stream is local or
 *        <tt>false</tt> otherwise.
 * @param usageLabel {string} meaningful usage label of this stream like
 *        'microphone', 'camera' or 'screen'.
 * @param containerId {string} the id of media 'audio' or 'video' tag which
 *        renders the stream.
 */
Statistics.prototype.associateStreamWithVideoTag =
function (ssrc, isLocal, usageLabel, containerId) {
    if(this.callstats) {
        this.callstats.associateStreamWithVideoTag(
            ssrc, isLocal, usageLabel, containerId);
    }
};
/**
 * Notifies CallStats that getUserMedia failed.
 *
 * @param {Error} e error to send
 */
Statistics.sendGetUserMediaFailed = function (e) {
    if (Statistics.callsStatsInstances.length) {
        Statistics.callsStatsInstances.forEach(function (cs) {
            CallStats.sendGetUserMediaFailed(
                e instanceof JitsiTrackError
                    ? formatJitsiTrackErrorForCallStats(e)
                    : e,
                cs);
        });
    } else {
        CallStats.sendGetUserMediaFailed(
            e instanceof JitsiTrackError
                ? formatJitsiTrackErrorForCallStats(e)
                : e,
            null);
    }
};
/**
 * Notifies CallStats that peer connection failed to create offer.
 *
 * @param {Error} e error to send
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendCreateOfferFailed = function (e, pc) {
    if(this.callstats)
        CallStats.sendCreateOfferFailed(e, pc, this.callstats);
};
/**
 * Notifies CallStats that peer connection failed to create answer.
 *
 * @param {Error} e error to send
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendCreateAnswerFailed = function (e, pc) {
    if(this.callstats)
        CallStats.sendCreateAnswerFailed(e, pc, this.callstats);
};
/**
 * Notifies CallStats that peer connection failed to set local description.
 *
 * @param {Error} e error to send
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendSetLocalDescFailed = function (e, pc) {
    if(this.callstats)
        CallStats.sendSetLocalDescFailed(e, pc, this.callstats);
};
/**
 * Notifies CallStats that peer connection failed to set remote description.
 *
 * @param {Error} e error to send
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendSetRemoteDescFailed = function (e, pc) {
    if(this.callstats)
        CallStats.sendSetRemoteDescFailed(e, pc, this.callstats);
};
/**
 * Notifies CallStats that peer connection failed to add ICE candidate.
 *
 * @param {Error} e error to send
 * @param {RTCPeerConnection} pc connection on which failure occured.
 */
Statistics.prototype.sendAddIceCandidateFailed = function (e, pc) {
    if(this.callstats)
        CallStats.sendAddIceCandidateFailed(e, pc, this.callstats);
};
/**
 * Notifies CallStats that audio problems are detected.
 *
 * @param {Error} e error to send
 */
Statistics.prototype.sendDetectedAudioProblem = function (e) {
    if(this.callstats)
        this.callstats.sendDetectedAudioProblem(e);
};
/**
 * Adds to CallStats an application log.
 *
 * @param {String} a log message to send or an {Error} object to be reported
 */
Statistics.sendLog = function (m) {
    if (Statistics.callsStatsInstances.length) {
        Statistics.callsStatsInstances.forEach(function (cs) {
            CallStats.sendApplicationLog(m, cs);
        });
    } else {
        CallStats.sendApplicationLog(m, null);
    }
};
/**
 * Sends the given feedback through CallStats.
 *
 * @param overall an integer between 1 and 5 indicating the user feedback
 * @param detailed detailed feedback from the user. Not yet used
 */
Statistics.prototype.sendFeedback = function(overall, detailed) {
    if(this.callstats)
        this.callstats.sendFeedback(overall, detailed);
};
Statistics.LOCAL_JID = require("../../service/statistics/constants").LOCAL_JID;
/**
 * Reports global error to CallStats.
 *
 * @param {Error} error
 */
Statistics.reportGlobalError = function (error) {
    if (error instanceof JitsiTrackError && error.gum) {
        Statistics.sendGetUserMediaFailed(error);
    } else {
        Statistics.sendLog(error);
    }
};
module.exports = Statistics;
 |