Skip to content
Snippets Groups Projects
MediaStreamStatsImpl.java 28.1 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
     *
     * Distributable under LGPL license.
     * See terms of license at gnu.org.
     */
    package org.jitsi.impl.neomedia;
    
    import java.awt.*;
    import java.net.*;
    
    import javax.media.format.*;
    import javax.media.rtp.*;
    
    import net.sf.fmj.media.rtp.*;
    
    
    import org.jitsi.impl.neomedia.device.*;
    import org.jitsi.service.neomedia.*;
    
    import org.jitsi.service.neomedia.control.*;
    
    import org.jitsi.util.*;
    
    /**
     * Class used to compute stats concerning a MediaStream.
     *
     * @author Vincent Lucas
    
     */
    public class MediaStreamStatsImpl
        implements MediaStreamStats
    {
    
        /**
         * The <tt>Logger</tt> used by the <tt>MediaStreamImpl</tt> class and its
         * instances for logging output.
         */
        private static final Logger logger
            = Logger.getLogger(MediaStreamStatsImpl.class);
    
    
        /**
         * Enumeation of the direction (DOWNLOAD or UPLOAD) used for the stats.
         */ 
        private enum StreamDirection
        {
            DOWNLOAD,
            UPLOAD
        }
    
        /**
    
         * The source data stream to analyze in order to compute the stats.
    
         */
        private MediaStreamImpl mediaStreamImpl;
    
        /**
         * The last time these stats have been updated.
         */
        private long updateTimeMs;
    
        /**
         * The last number of received/sent packets.
         */
        private long[] nbPackets = {0, 0};
    
        /**
         * The last number of sent packets when the last feedback has been received.
         * This counter is used to compute the upload loss rate.
         */
        private long uploadFeedbackNbPackets = 0;
    
        /**
         * The last number of download/upload lost packets.
         */
        private long[] nbLost = {0, 0};
    
    
        /**
         * The total number of discarded packets
         */
        private long nbDiscarded = 0;
    
    
        /**
         * The number of packets for which FEC data was decoded. This is only
         */
        private long nbFec = 0;
    
    
        /**
         * The last number of received/sent Bytes.
         */
        private long[] nbByte = {0, 0};
    
        /**
         * The last download/upload loss rate computed (in %).
         */
        private double[] percentLoss = {0, 0};
    
    
        /**
         * The last percent of discarded packets
         */
        private double percentDiscarded = 0;
    
    
        /**
         * The last used bandwidth computed in download/upload (in Kbit/s).
         */
        private double[] rateKiloBitPerSec = {0, 0};
    
        /**
         * The last jitter received/sent in a RTCP feedback (in RTP timestamp
         * units).
         */
        private double[] jitterRTPTimestampUnits = {0, 0};
    
    
        /**
         * The RTT computed with the RTCP feedback (cf. RFC3550, section 6.4.1,
         * subsection "delay since last SR (DLSR): 32 bits").
         * -1 if the RTT has not been computed yet. Otherwise the RTT in ms.
         */
        private long rttMs = -1;
    
    
        /**
         * Creates a new instance of stats concerning a MediaStream.
         *
         * @param mediaStreamImpl The MediaStreamImpl used to compute the stats.
         */
        public MediaStreamStatsImpl(MediaStreamImpl mediaStreamImpl)
        {
            this.updateTimeMs = System.currentTimeMillis();
            this.mediaStreamImpl = mediaStreamImpl;
        }
    
        /**
         * Computes and updates information for a specific stream.
         */
        public void updateStats()
        {
            // Gets the current time.
            long currentTimeMs = System.currentTimeMillis();
    
            // UPdates stats for the download stream.
            this.updateStreamDirectionStats(
                    StreamDirection.DOWNLOAD,
                    currentTimeMs);
            // UPdates stats for the upload stream.
            this.updateStreamDirectionStats(
                    StreamDirection.UPLOAD,
                    currentTimeMs);
    
            // Saves the last update values.
            this.updateTimeMs = currentTimeMs;
        }
    
        /**
         * Computes and updates information for a specific stream.
         *
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function updates the stats.
    
         * @param currentTimeMs The current time in ms.
    
         */
        private void updateStreamDirectionStats(
                StreamDirection streamDirection,
                long currentTimeMs)
        {
            int streamDirectionIndex = streamDirection.ordinal();
    
            // Gets the current number of packets correctly received since the
            // beginning of this stream.
            long newNbRecv = this.getNbPDU(streamDirection);
            // Gets the number of byte received/sent since the beginning of this
            // stream.
            long newNbByte = this.getNbBytes(streamDirection);
    
            // Computes the number of update steps which has not been done since
            // last update.
            long nbSteps = newNbRecv - this.nbPackets[streamDirectionIndex];
            // Even if the remote peer does not send any packets (i.e. is
            // microphone is muted), Jitsi must updates it stats. Thus, Jitsi
            // computes a number of steps equivalent as if Jitsi receives a packet
            // each 20ms (default value).
            if(nbSteps == 0)
            {
                nbSteps = (currentTimeMs - this.updateTimeMs) / 20;
            }
    
            // The upload percentLoss is only computed when a new RTCP feedback is
            // received. This is not the case for the download percentLoss which is
            // updated for each new RTP packet received.
            // Computes the loss rate for this stream.
            if(streamDirection == StreamDirection.DOWNLOAD)
            {
                // Gets the current number of losses in download since the beginning
                // of this stream.
                long newNbLost =
                    this.getDownloadNbPDULost() - this.nbLost[streamDirectionIndex];
    
                updateNbLoss(streamDirection, newNbLost, nbSteps + newNbLost);
    
    
                long newNbDiscarded = this.getNbDiscarded() - this.nbDiscarded;
                updateNbDiscarded(newNbDiscarded, nbSteps + newNbDiscarded);
    
            }
    
            // Computes the bandwidth used by this stream.
            double newRateKiloBitPerSec =
                MediaStreamStatsImpl.computeRateKiloBitPerSec(
                        newNbByte - this.nbByte[streamDirectionIndex],
                        currentTimeMs - this.updateTimeMs);
            this.rateKiloBitPerSec[streamDirectionIndex] =
                MediaStreamStatsImpl.computeEWMA(
                        nbSteps,
                        this.rateKiloBitPerSec[streamDirectionIndex],
                        newRateKiloBitPerSec);
    
            // Saves the last update values.
            this.nbPackets[streamDirectionIndex] = newNbRecv;
            this.nbByte[streamDirectionIndex] = newNbByte;
    
        }
    
        /**
         * Returns the local IP address of the MediaStream.
         *
         * @return the local IP address of the stream.
         */
        public String getLocalIPAddress()
        {
    
            InetSocketAddress mediaStreamLocalDataAddress
                = mediaStreamImpl.getLocalDataAddress();
    
            return
                (mediaStreamLocalDataAddress == null)
                    ? null
                    : mediaStreamLocalDataAddress.getAddress().getHostAddress();
    
        }
    
        /**
         * Returns the local port of the MediaStream.
         *
         * @return the local port of the stream.
         */
        public int getLocalPort()
        {
    
            InetSocketAddress mediaStreamLocalDataAddress
                = mediaStreamImpl.getLocalDataAddress();
    
            return
                (mediaStreamLocalDataAddress == null)
                    ? -1
                    : mediaStreamLocalDataAddress.getPort();
    
        }
    
        /**
         * Returns the remote IP address of the MediaStream.
         *
         * @return the remote IP address of the stream.
         */
        public String getRemoteIPAddress()
        {
            MediaStreamTarget mediaStreamTarget = mediaStreamImpl.getTarget();
            // Stops if the endpoint is disconnected.
            if(mediaStreamTarget == null)
            {
                return null;
            }
            // Gets this stream IP address endpoint.
            return mediaStreamTarget.getDataAddress().getAddress().getHostAddress();
        }
    
        /**
         * Returns the remote port of the MediaStream.
         *
         * @return the remote port of the stream.
         */
        public int getRemotePort()
        {
            MediaStreamTarget mediaStreamTarget = mediaStreamImpl.getTarget();
            // Stops if the endpoint is disconnected.
            if(mediaStreamTarget == null)
            {
                return -1;
            }
            // Gets this stream port endpoint.
            return mediaStreamTarget.getDataAddress().getPort();
        }
    
        /**
         * Returns the MediaStream enconding.
         *
         * @return the encoding used by the stream.
         */
        public String getEncoding()
        {
    
            MediaFormat format = mediaStreamImpl.getFormat();
    
            return (format == null) ? null : format.getEncoding();
    
        }
    
        /**
         * Returns the MediaStream enconding rate (in Hz)..
         *
         * @return the encoding rate used by the stream.
         */
        public String getEncodingClockRate()
        {
    
            MediaFormat format = mediaStreamImpl.getFormat();
    
            return (format == null) ? null : format.getRealUsedClockRateString();
    
        }
    
        /**
         * Returns the upload video format if this stream uploads a video, or null
         * if not.
         *
         * @return the upload video format if this stream uploads a video, or null
         * if not.
         */
        private VideoFormat getUploadVideoFormat()
        {
    
            MediaDeviceSession mediaDeviceSession
                = mediaStreamImpl.getDeviceSession();
    
            return
                (mediaDeviceSession instanceof VideoMediaDeviceSession)
                    ? ((VideoMediaDeviceSession) mediaDeviceSession)
                        .getSentVideoFormat()
                    : null;
    
        }
    
        /**
         * Returns the download video format if this stream downloads a video, or
         * null if not.
         *
         * @return the download video format if this stream downloads a video, or
         * null if not.
         */
        private VideoFormat getDownloadVideoFormat()
        {
    
            MediaDeviceSession mediaDeviceSession
                = mediaStreamImpl.getDeviceSession();
    
            return
                (mediaDeviceSession instanceof VideoMediaDeviceSession)
                    ? ((VideoMediaDeviceSession) mediaDeviceSession)
                        .getReceivedVideoFormat()
                    : null;
    
        }
    
        /**
         * Returns the upload video size if this stream uploads a video, or null if
         * not.
         *
         * @return the upload video size if this stream uploads a video, or null if
         * not.
         */
        public Dimension getUploadVideoSize()
        {
            Dimension videoSize = null;
            VideoFormat format = this.getUploadVideoFormat();
            if(format != null)
            {
                videoSize = format.getSize();
            }
            return videoSize;
        }
    
        /**
         * Returns the download video size if this stream downloads a video, or
         * null if not.
         *
         * @return the download video size if this stream downloads a video, or null
         * if not.
         */
        public Dimension getDownloadVideoSize()
        {
            Dimension videoSize = null;
            VideoFormat format = this.getDownloadVideoFormat();
            if(format != null)
            {
                videoSize = format.getSize();
            }
            return videoSize;
        }
    
        /**
         * Returns the percent loss of the download stream.
         *
         * @return the last loss rate computed (in %).
         */
        public double getDownloadPercentLoss()
        {
            return this.percentLoss[StreamDirection.DOWNLOAD.ordinal()];
        }
    
    
        /**
         * Returns the percent of discarded packets
         *
         * @return the percent of discarded packets
         */
        public double getPercentDiscarded()
        {
            return percentDiscarded;
        }
    
    
        /**
         * Returns the percent loss of the upload stream.
         *
         * @return the last loss rate computed (in %).
         */
        public double getUploadPercentLoss()
        {
            return this.percentLoss[StreamDirection.UPLOAD.ordinal()];
        }
    
        /**
         * Returns the bandwidth used by this download stream.
         *
         * @return the last used download bandwidth computed (in Kbit/s).
         */
        public double getDownloadRateKiloBitPerSec()
        {
            return this.rateKiloBitPerSec[StreamDirection.DOWNLOAD.ordinal()];
        }
    
        /**
         * Returns the bandwidth used by this download stream.
         *
         * @return the last used upload bandwidth computed (in Kbit/s).
         */
        public double getUploadRateKiloBitPerSec()
        {
            return this.rateKiloBitPerSec[StreamDirection.UPLOAD.ordinal()];
        }
    
        /**
         * Returns the jitter average of this download stream.
         *
         * @return the last jitter average computed (in ms).
         */
        public double getDownloadJitterMs()
        {
            return this.getJitterMs(StreamDirection.DOWNLOAD);
        }
    
        /**
         * Returns the jitter average of this upload stream.
         *
         * @return the last jitter average computed (in ms).
         */
        public double getUploadJitterMs()
        {
            return this.getJitterMs(StreamDirection.UPLOAD);
        }
    
        /**
         * Returns the jitter average of this upload/download stream.
         *
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function retrieve the jitter.
         *
         * @return the last jitter average computed (in ms).
         */
        private double getJitterMs(StreamDirection streamDirection)
        {
    
            MediaFormat format = mediaStreamImpl.getFormat();
            double clockRate;
    
            if (format == null)
            {
                MediaType mediaType = mediaStreamImpl.getMediaType();
    
                if (MediaType.VIDEO.equals(mediaType))
                    clockRate = 90000;
                else
                    clockRate = -1;
            }
            else
                clockRate = format.getClockRate();
    
            if (clockRate <= 0)
                return -1;
    
    
            // RFC3550 says that concerning the RTP timestamp unit (cf. section 5.1
            // RTP Fixed Header Fields, subsection timestamp: 32 bits):
            // As an example, for fixed-rate audio the timestamp clock would likely
            // increment by one for each sampling period.
            //
            // Thus we take the jitter (in RTP timestamp units), converts it to
            // seconds (deivision by the codec clock rate) and finally converts it
            // in Ms (* 1000).
    
            return
                (jitterRTPTimestampUnits[streamDirection.ordinal()] / clockRate)
                    * 1000.0;
    
        }
    
        /**
         * Updates the jitter stream stats with the new feedback sent.
         *
         * @param feedback The last RTCP feedback sent by the MediaStream.
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function retrieve the jitter.
         */
        private void updateJitterRTPTimestampUnits(
                RTCPFeedback feedback,
                StreamDirection streamDirection)
        {
            // Updates the download jitter in RTP timestamp units.
            // There is no need to compute a jitter average, since (cf. RFC3550,
            // section 6.4.1 SR: Sender Report RTCP Packet, subsection interarrival
            // jitter: 32 bits) the value contained in the RTCP sender report packet
            // contains a mean deviation of the jitter.
            this.jitterRTPTimestampUnits[streamDirection.ordinal()] =
                feedback.getJitter();
        }
    
        /**
         * Updates this stream stats with the new feedback sent.
         *
         * @param feedback The last RTCP feedback sent by the MediaStream.
         */
        public void updateNewSentFeedback(RTCPFeedback feedback)
        {
            updateJitterRTPTimestampUnits(feedback, StreamDirection.DOWNLOAD);
    
    
            // No need to update the download loss as we have a more accurate value
    
            // in the global reception stats, which are updated for each new packet
            // received.
        }
    
        /**
         * Updates this stream stats with the new feedback received.
         *
         * @param feedback The last RTCP feedback received by the MediaStream.
         */
        public void updateNewReceivedFeedback(RTCPFeedback feedback)
        {
            StreamDirection streamDirection = StreamDirection.UPLOAD;
    
            updateJitterRTPTimestampUnits(feedback, streamDirection);
    
            // Updates the loss rate with the RTCP sender report feedback, since
            // this is the only information source available for the upalod stream.
            long uploadNewNbRecv = feedback.getXtndSeqNum();
            long newNbLost =
                feedback.getNumLost() - this.nbLost[streamDirection.ordinal()];
            long nbSteps = uploadNewNbRecv - this.uploadFeedbackNbPackets;
    
            updateNbLoss(streamDirection, newNbLost, nbSteps);
    
            // Updates the upload loss counters.
            this.uploadFeedbackNbPackets = uploadNewNbRecv;
    
    
            // Computes RTT.
            this.rttMs = computeRTTInMs(feedback);
    
        }
    
        /**
         * Updates the number of loss for a given stream.
         *
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function updates the stats.
         * @param newNbLost The last update of the number of lost.
    
         * @param nbSteps The number of elapsed steps since the last number of loss
    
         * update.
         */
        private void updateNbLoss(
                StreamDirection streamDirection,
                long newNbLost,
                long nbSteps)
        {
            int streamDirectionIndex = streamDirection.ordinal();
    
            double newPercentLoss = MediaStreamStatsImpl.computePercentLoss(
                    nbSteps,
                    newNbLost);
            this.percentLoss[streamDirectionIndex] =
                MediaStreamStatsImpl.computeEWMA(
                        nbSteps,
                        this.percentLoss[streamDirectionIndex],
                        newPercentLoss);
    
            // Saves the last update number download lost value.
            this.nbLost[streamDirectionIndex] += newNbLost;
        }
    
        /**
         * Computes the loss rate.
         *
         * @param nbLostAndRecv The number of lost and received packets.
         * @param nbLost The number of lost packets.
         *
         * @return The loss rate in percent.
         */
        private static double computePercentLoss(long nbLostAndRecv, long nbLost)
        {
            if(nbLostAndRecv == 0)
            {
                return 0;
            }
            return ((double) 100 * nbLost) / ((double)(nbLostAndRecv));
        }
    
        /**
    
         * Computes the bandwidth usage in Kilo bits per seconds.
    
         *
         * @param nbByteRecv The number of Byte received.
         * @param callNbTimeMsSpent The time spent since the mediaStreamImpl is
         * connected to the endpoint.
         *
    
         * @return the bandwidth rate computed in Kilo bits per seconds.
    
         */
        private static double computeRateKiloBitPerSec(
                long nbByteRecv,
                long callNbTimeMsSpent)
        {
            if(nbByteRecv == 0)
            {
                return 0;
            }
            return (nbByteRecv * 8.0 / 1000.0) / (callNbTimeMsSpent / 1000.0);
        }
    
        /**
         * Computes an Exponentially Weighted Moving Average (EWMA). Thus, the most
         * recent history has a more preponderant importance in the average
         * computed.
         *
         * @param nbStepSinceLastUpdate The number of step which has not been
         * computed since last update. In our case the number of packets received
         * since the last computation.
         * @param lastValue The value computed during the last update.
         * @param newValue The value newly computed.
         *
         * @return The EWMA average computed.
         */
        private static double computeEWMA(
                long nbStepSinceLastUpdate,
                double lastValue,
                double newValue)
        {
            // For each new packet received the EWMA moves by a 0.1 coefficient.
            double EWMACoeff = 0.01 * nbStepSinceLastUpdate;
            // EWMA must be <= 1.
            if(EWMACoeff > 1)
            {
                EWMACoeff = 1.0;
            }
            return lastValue * (1.0 - EWMACoeff) + newValue * EWMACoeff;
    
        }
    
        /**
         * Returns the number of Protocol Data Units (PDU) sent/received since the
         * beginning of the session.
         *
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function retrieve the number of sent/received
         * packets.
         *
         * @return the number of packets sent/received for this stream.
         */
        private long getNbPDU(StreamDirection streamDirection)
        {
            long nbPDU = 0;
            StreamRTPManager rtpManager = this.mediaStreamImpl.getRTPManager();
    
            if(rtpManager != null)
            {
                switch(streamDirection)
                {
                    case UPLOAD:
                        nbPDU =
                            rtpManager.getGlobalTransmissionStats().getRTPSent();
                        break;
                    case DOWNLOAD:
                        GlobalReceptionStats globalReceptionStats =
                            rtpManager.getGlobalReceptionStats();
                        nbPDU =
                            globalReceptionStats.getPacketsRecd()
                            - globalReceptionStats.getRTCPRecd();
                        break;
                }
            }
            return nbPDU;
        }
    
        /**
         * Returns the number of Protocol Data Units (PDU) lost in download since
         * the beginning of the session.
         *
         * @return the number of packets lost for this stream.
         */
        private long getDownloadNbPDULost()
        {
            int nbLost = 0;
            java.util.List<ReceiveStream> listReceiveStream =
                this.mediaStreamImpl.getDeviceSession().getReceiveStreams();
    
    
            for(ReceiveStream receiveStream : listReceiveStream)
    
                nbLost += receiveStream.getSourceReceptionStats().getPDUlost();
    
            return nbLost;
        }
    
    
        /**
         * Returns the number of Protocol Data Units (PDU) discarded by the
         * FMJ packet queue since the beginning of the session. It's the sum over
         * all <tt>ReceiveStream</tt>s of the <tt>MediaStream</tt>
         *
         * @return the number of discarded packets.
         */
        public long getNbDiscarded()
        {
            int nbDiscarded = 0;
    
            if(this.mediaStreamImpl != null && this.mediaStreamImpl.isStarted())
            {
                java.util.List<ReceiveStream> listReceiveStream =
                        this.mediaStreamImpl.getDeviceSession().getReceiveStreams();
    
                for(ReceiveStream receiveStream : listReceiveStream)
                    nbDiscarded
                        += receiveStream.getSourceReceptionStats().getPDUDrop();
            }
    
        /**
         * Returns the number of sent/received bytes since the beginning of the
         * session.
         *
         * @param streamDirection The stream direction (DOWNLOAD or UPLOAD) of the
         * stream from which this function retrieve the number of sent/received
         * bytes.
         *
         * @return the number of sent/received bytes for this stream.
         */
        private long getNbBytes(StreamDirection streamDirection)
        {
            long nbBytes = 0;
            StreamRTPManager rtpManager = this.mediaStreamImpl.getRTPManager();
    
            if(rtpManager != null)
            {
                switch(streamDirection)
                {
                    case DOWNLOAD:
                        nbBytes =
                            rtpManager.getGlobalReceptionStats().getBytesRecd();
                        break;
                    case UPLOAD:
                        nbBytes =
                            rtpManager.getGlobalTransmissionStats().getBytesSent();
                        break;
                }
            }
            return nbBytes;
        }
    
    
        /**
         * Computes the RTT with the data (LSR and DLSR) contained in the last
         * RTCP Sender Report (RTCP feedback). This RTT computation is based on
         * RFC3550, section 6.4.1, subsection "delay since last SR (DLSR): 32
         * bits".
         *
         * @param feedback The last RTCP feedback received by the MediaStream.
         *
         * @return The RTT in milliseconds, or -1 if the RTT is not computable.
         */
        private long computeRTTInMs(RTCPFeedback feedback)
        {
            // Computes RTT.
            long currentTime = System.currentTimeMillis();
            long DLSR = feedback.getDLSR();
            long LSR = feedback.getLSR();
    
            // If the peer sending us the sender report has at least received on
            // sender report from our side, then computes the RTT.
            if(DLSR != 0 && LSR != 0)
            {
                long LSRs = LSR >> 16;
                long LSRms = ((LSR & 0xffff) * 1000) / 0xffff;
                long DLSRs = DLSR / 0xffff;
                long DLSRms = ((DLSR & 0xffff) *1000) / 0xffff;
                long currentTimeS = (currentTime / 1000) & 0x0000ffff;
                long currentTimeMs = (currentTime % 1000);
    
                long rttS = currentTimeS - DLSRs - LSRs;
                long rttMs = currentTimeMs - DLSRms - LSRms;
    
    
                long computedRTTms = (rttS * 1000) + rttMs;
    
                // If the RTT is greater than a minute there might be a bug. Thus we
                // log the info to see the source of this error.
                if(computedRTTms > 60000 && logger.isInfoEnabled())
                {
                    logger.info("RTT computation seems to be wrong ("
                            + computedRTTms + "> 60 seconds):"
    
                            + "\n\tcurrentTime: " + currentTime
                            + " (" + Long.toHexString(currentTime) + ")"
                            + "\n\tDLSR: " + DLSR
                            + " (" + Long.toHexString(DLSR) + ")"
                            + "\n\tLSR: " + LSR
                            + " (" + Long.toHexString(LSR) + ")"
    
                            + "\n\n\tcurrentTimeS: " + currentTimeS
                            + " (" + Long.toHexString(currentTimeS) + ")"
                            + "\n\tDLSRs: " + DLSRs
                            + " (" + Long.toHexString(DLSRs) + ")"
                            + "\n\tLSRs: " + LSRs
                            + " (" + Long.toHexString(LSRs) + ")"
                            + "\n\trttS: " + rttS
                            + " (" + Long.toHexString(rttS) + ")"
    
                            + "\n\n\tcurrentTimeMs: " + currentTimeMs
                            + " (" + Long.toHexString(currentTimeMs) + ")"
                            + "\n\tDLSRms: " + DLSRms
                            + " (" + Long.toHexString(DLSRms) + ")"
                            + "\n\tLSRms: " + LSRms
                            + " (" + Long.toHexString(LSRms) + ")"
                            + "\n\trttMs: " + rttMs
                            + " (" + Long.toHexString(rttMs) + ")"
                            );
                }
    
                return computedRTTms;
    
            }
            // Else the RTT can not be computed yet.
            return -1;
        }
    
        /**
         * Returns the RTT computed with the RTCP feedback (cf. RFC3550, section
         * 6.4.1, subsection "delay since last SR (DLSR): 32 bits").
         *
         * @return The RTT computed with the RTCP feedback. Returns -1 if the RTT
         * has not been computed yet. Otherwise the RTT in ms.
         */
        public long getRttMs()
        {
            return this.rttMs;
        }
    
    
        /**
         * Returns the number of packets for which FEC data was decoded. Currently
         * this is cumulative over all <tt>ReceiveStream</tt>s.
         *
         * @return the number of packets for which FEC data was decoded. Currently
         * this is cumulative over all <tt>ReceiveStream</tt>s.
         *
         * @see org.jitsi.impl.neomedia.MediaStreamStatsImpl#updateNbFec()
         */
        public long getNbFec()
        {
            return nbFec;
        }
    
        /**
         * Updates the <tt>nbFec</tt> field with the sum of FEC-decoded packets
         * over the different <tt>ReceiveStream</tt>s
         */
        private void updateNbFec()
        {
            int nbFec = 0;
            FECDecoderControl fecDecoderControl;
            for(ReceiveStream receiveStream :
                    this.mediaStreamImpl.getDeviceSession().getReceiveStreams())
            {
                fecDecoderControl =
                        mediaStreamImpl.getFecDecoderControl(receiveStream);
                if(fecDecoderControl != null)
                    nbFec += fecDecoderControl.fecPacketsDecoded();
            }
            this.nbFec = nbFec;
        }
    
        /**
         * Updates the number of discarded packets.
         *
         * @param newNbDiscarded The last update of the number of lost.
         * @param nbSteps The number of elapsed steps since the last number of loss
         * update.
         */
        private void updateNbDiscarded(
                long newNbDiscarded,
                long nbSteps)
        {
    
            double newPercentDiscarded = MediaStreamStatsImpl.computePercentLoss(
                    nbSteps,
                    newNbDiscarded);
            this.percentDiscarded =
                    MediaStreamStatsImpl.computeEWMA(
                            nbSteps,
                            this.percentDiscarded,
                            newPercentDiscarded);
    
            // Saves the last update number download lost value.
            this.nbDiscarded += newNbDiscarded;
        }