import StatsGatherer from "webrtc-stats-gatherer";
export class StatsAggregator {
    constructor(session, sdk) {
        this.session = session;
        this.sdk = sdk;
        this.setBaseline = false;
        this.baselinePacketsReceived = 0;
        this.baselinePacketsLost = 0;
        this.baselineRtt = 0;
        this.baselineRttMeasurements = 0;
        this.totalJitter = 0;
        this.jitterMeasurements = 0;
        if (this.shouldGatherImmediately(session)) {
            this.startGatheringStats();
        }
        const boundSessionStartedHandler = this.onSessionStarted.bind(this);
        const boundPrivateSessionEndedHandler = this.onPrivateSessionEnded.bind(this);
        sdk.on('sessionStarted', boundSessionStartedHandler);
        // STREAM-1590: Use a private event to stop gathering in certain concurrent call scenarios
        sdk.on('_sessionEnded', boundPrivateSessionEndedHandler);
        session.once('terminated', () => {
            this.stopGatheringStats();
            sdk.off('sessionStarted', boundSessionStartedHandler);
            sdk.off('_sessionEnded', boundPrivateSessionEndedHandler);
        });
    }
    shouldGatherImmediately(session) {
        const isEagerPersistentConnection = session.privAnswerMode === 'Auto';
        // The first conversation on an eager persistent connection is fake, so we want to wait
        // for a real conversation before gathering stats.
        return !isEagerPersistentConnection;
    }
    startGatheringStats() {
        if (this.statsGatherer) {
            this.sdk.logger.warn('Cannot create stats gatherer, one already exists');
            return;
        }
        this.statsGatherer = new StatsGatherer(this.session.peerConnection);
        this.boundStatsHandler = this.handleStatsUpdate.bind(this);
        this.statsGatherer.on('stats', this.boundStatsHandler);
    }
    stopGatheringStats() {
        if (!this.statsGatherer) {
            this.sdk.logger.debug('No stats gatherer to remove');
            return;
        }
        this.statsGatherer.off('stats', this.boundStatsHandler);
        this.statsGatherer = undefined;
    }
    onSessionStarted(session) {
        if (session !== this.session) {
            return;
        }
        this.setBaseline = true;
        this.startGatheringStats();
    }
    onPrivateSessionEnded(session) {
        if (session !== this.session) {
            return;
        }
        this.stopGatheringStats();
    }
    isGetStatsEvent(stats) {
        return stats.name === 'getStats';
    }
    handleStatsUpdate(stats) {
        if (!this.isGetStatsEvent(stats)) {
            return;
        }
        if (stats.tracks.length === 0 || stats.remoteTracks.length === 0) {
            return;
        }
        const trackStats = stats.tracks[0];
        const remoteTrackStats = stats.remoteTracks[0];
        const totalPacketsReceived = remoteTrackStats.packetsReceived;
        const totalPacketsLost = remoteTrackStats.packetsLost;
        const totalRtt = trackStats.totalRoundTripTime;
        const totalRttMeasurements = trackStats.roundTripTimeMeasurements;
        if (totalPacketsReceived === undefined || totalPacketsLost === undefined || totalRtt === undefined || totalRttMeasurements === undefined) {
            return;
        }
        if (this.setBaseline) {
            this.setBaseline = false;
            this.baselinePacketsReceived = totalPacketsReceived;
            this.baselinePacketsLost = totalPacketsLost;
            this.baselineRtt = totalRtt;
            this.baselineRttMeasurements = totalRttMeasurements;
            this.totalJitter = 0;
            this.jitterMeasurements = 0;
            return;
        }
        const packetsReceived = totalPacketsReceived - this.baselinePacketsReceived;
        const packetsLost = totalPacketsLost - this.baselinePacketsLost;
        const packetLoss = packetsReceived > 0 ? packetsLost / packetsReceived : 0.0;
        const jitterInSeconds = remoteTrackStats.jitter;
        if (jitterInSeconds === undefined) {
            return;
        }
        const roundTripTimeSum = totalRtt - this.baselineRtt;
        const roundTripTimeMeasurements = totalRttMeasurements - this.baselineRttMeasurements;
        const averageRoundTripTime = roundTripTimeMeasurements > 0 ? roundTripTimeSum / roundTripTimeMeasurements : 0;
        const averageLatency = averageRoundTripTime / 2; // Approximate the one-way latency
        this.totalJitter += jitterInSeconds;
        this.jitterMeasurements += 1;
        const averageJitter = this.totalJitter / this.jitterMeasurements;
        const mos = this.calculateEstimatedMos(averageLatency, averageJitter, packetLoss);
        const rtpStats = {
            packetsReceived,
            averageJitter,
            estimatedAverageMos: mos
        };
        const dateCreated = new Date(remoteTrackStats.timestamp);
        this.sendStats(rtpStats, dateCreated);
    }
    // This follows a code snippet from the backend that I got from Sean Conrad, which follows
    // https://netbeez.net/blog/impact-of-packet-loss-jitter-and-latency-on-voip/
    /**
     * Calculates an estimated MOS (Mean Opinion Score) based on statistics from WebRTC's `getStats`.
     *
     * @param averageLatency The average latency for the call, measured in seconds. e.g. `0.071869`
     * @param averageJitter The average jitter for the call, measured in seconds. e.g. `0.0008539`
     * @param packetLoss The decimal representation of the packet loss over the duration of the call. e.g. 1% packet loss should be passed in as `0.01`
     *
     * @returns An estimated MOS ranging from 1.0 to 5.0.
     */
    calculateEstimatedMos(averageLatency, averageJitter, packetLoss) {
        const maxMosScore = 5.0; // Assume we always negotiate Opus right now
        const maxMosRFactor = 93.2;
        let rFactor;
        const effectiveLatency = (averageLatency * 1000) + (averageJitter * 1000 * 2) + 10;
        if (effectiveLatency < 160) {
            rFactor = maxMosRFactor - (effectiveLatency / 40);
        }
        else {
            rFactor = maxMosRFactor - (effectiveLatency - 120) / 10;
        }
        rFactor = rFactor - (2.5 * 100 * packetLoss);
        if (rFactor < 0) {
            return 1;
        }
        else {
            return (1 + (0.035 * rFactor) + (0.000007 * rFactor * (rFactor - 60) * (100 - rFactor))) * maxMosScore / 4.5;
        }
    }
    sendStats(rtpStats, dateCreated) {
        var _a, _b, _c, _d;
        const pcParticipant = this.session.pcParticipant;
        if (!pcParticipant) {
            return;
        }
        const calls = (_a = pcParticipant['calls']) !== null && _a !== void 0 ? _a : [];
        if (calls.length === 0) {
            this.sdk.logger.warn('Failed to send stats via XMPP - calls.length === 0');
            return;
        }
        if (!this.session.peerID) {
            this.sdk.logger.warn('Failed to send stats via XMPP - peerID');
            return;
        }
        if (!((_c = (_b = this.sdk._streamingConnection) === null || _b === void 0 ? void 0 : _b._webrtcSessions) === null || _c === void 0 ? void 0 : _c.sendIq)) {
            this.sdk.logger.warn('Failed to send stats via XMPP - sendIq');
            return;
        }
        const participant = pcParticipant;
        const communicationId = participant.calls[0].id;
        const conversationId = this.session.conversationId;
        const { originAppId, originAppName, originAppVersion } = this.sdk._config;
        const statsData = {
            sourceType: 'Client',
            client: {
                originAppName,
                originAppId,
                originAppVersion
            },
            rtp: rtpStats,
            dateCreated: dateCreated.toISOString(),
            reconnectAttemptCount: (_d = this.session.reconnectCount) !== null && _d !== void 0 ? _d : 0
        };
        const iq = {
            type: 'set',
            to: this.session.peerID,
            genesysWebrtc: {
                jsonrpc: '2.0',
                id: globalThis.crypto.randomUUID(),
                method: 'report-statistics',
                params: {
                    sessionId: this.session.id,
                    conversationId,
                    communicationId,
                    stats: statsData
                }
            }
        };
        this.sdk.logger.debug('Sending stats over XMPP');
        this.sdk._streamingConnection._webrtcSessions.sendIq(iq)
            .catch((err) => this.sdk.logger.warn('Failed to send stats via XMPP', err));
    }
}
//# sourceMappingURL=stats-aggregator.js.map