/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2014, Telestax Inc and individual contributors
* by the @authors tag.
*
* This program is free software: you can redistribute it and/or modify
* under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
package org.restcomm.media.rtp.statistics;
import org.apache.commons.net.ntp.TimeStamp;
import org.apache.log4j.Logger;
import org.restcomm.media.rtcp.RtcpSenderReport;
import org.restcomm.media.rtcp.ntp.NtpUtils;
import org.restcomm.media.rtp.RtpClock;
import org.restcomm.media.rtp.RtpPacket;
import org.restcomm.media.scheduler.Clock;
/**
* Holds statistics for a member of an RTP session.
*
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class RtpMember {
private static final Logger logger = Logger.getLogger(RtpMember.class);
public static final int RTP_SEQ_MOD = 65536;
public static final int MAX_DROPOUT = 100;
public static final int MAX_MISORDER = 100;
public static final int MIN_SEQUENTIAL = 2;
// Core elements
private final RtpClock rtpClock;
private final Clock wallClock;
// Member data
private long ssrc;
private String cname;
// Packet stats
private long receivedPackets;
private long receivedOctets;
private long receivedSinceSR;
private int roundTripDelay;
private long lastPacketReceivedOn;
private int firstSequenceNumber;
private int highestSequence;
private int sequenceCycle;
private int badSequence;
private int probation;
private long receivedPrior;
private long expectedPrior;
// Jitter
/**
* Measures the relative time it takes for an RTP packet to arrive from the
* remote server to MMS.<br>
* Used to calculate network jitter.
*/
private long currentTransit;
private long jitter;
// RTCP
private long lastSrTimestamp;
private long lastSrReceivedOn;
public RtpMember(RtpClock clock, long ssrc, String cname) {
// Core elements
this.rtpClock = clock;
this.wallClock = clock.getWallClock();
// Member data
this.ssrc = ssrc;
this.cname = cname;
// Packet stats
this.receivedPackets = 0;
this.receivedOctets = 0;
this.receivedSinceSR = 0;
this.lastPacketReceivedOn = -1;
this.firstSequenceNumber = -1;
this.highestSequence = 0;
this.badSequence = 0;
this.sequenceCycle = 0;
this.probation = 0;
this.receivedPrior = 0;
this.expectedPrior = 0;
// Jitter
this.currentTransit = 0;
this.jitter = -1;
// RTCP
this.lastSrTimestamp = 0;
this.lastSrReceivedOn = 0;
this.roundTripDelay = 0;
}
public RtpMember(RtpClock clock, long ssrc) {
this(clock, ssrc, "");
}
/**
* Gets the SSRC identifier of the source to which the information in this
* reception report block pertains
*
* @return The SSRC identifier
*/
public long getSsrc() {
return ssrc;
}
/**
* Gets the CNAME of this member
*
* @return The CNAME of the member
*/
public String getCname() {
return cname;
}
/**
* Sets the CNAME of the member
*
* @param cname
* The CNAME of the member
*/
public void setCname(String cname) {
this.cname = cname;
}
/**
* Gets the total number of incoming RTP packets
*
* @return The number of packets received
*/
public long getPacketsReceived() {
return receivedPackets;
}
public long getPacketsExpected() {
return getExtHighSequence() - this.firstSequenceNumber + 1;
}
/**
* Gets the total of incoming RTP octets
*
* @return The total of received octets
*/
public long getOctetsReceived() {
return receivedOctets;
}
/**
* Gets the number of incoming RTP packets since the last SR report was
* sent.
*
* @return The number of incoming RTP packets
*/
public long getReceivedSinceSR() {
return receivedSinceSR;
}
/**
* Gets the fraction of RTP data packets from this source that were lost
* since the previous SR or RR packet was sent, expressed as a fixed point
* number with the binary point at the left edge of the field. (That is
* equivalent to taking the integer part after multiplying the loss fraction
* by 256.)
*
* @return The fraction of lost packets
*/
public long getFractionLost() {
// long expected = this.lastSequenceNumber - this.lastSrSequenceNumber;
// if (expected < 0) {
// expected = RTP_SEQ_MOD + expected;
// }
//
// long fraction = 256 * (expected - this.receivedSinceSR);
// fraction = expected > 0 ? (fraction / expected) : 0;
//
// return fraction;
long expected = getPacketsExpected();
long expectedInterval = expected - this.expectedPrior;
this.expectedPrior = expected;
long receivedInterval = this.receivedPackets - this.receivedPrior;
this.receivedPrior = this.receivedPackets;
long lostInterval = expectedInterval - receivedInterval;
if(expectedInterval == 0 || lostInterval <= 0) {
return 0;
}
return (lostInterval << 8) / expectedInterval;
}
/**
* Gets the total number of RTP data packets from this source that have been
* lost since the beginning of reception.
* <p>
* This number is defined to be the number of packets expected less the
* number of packets actually received, where the number of packets received
* includes any which are late or duplicates. Thus, packets that arrive late
* are not counted as lost, and the loss may be negative if there are
* duplicates.
* </p>
* <p>
* <b>The number of packets expected is defined to be the extended last
* sequence number received, as defined next, less the initial sequence
* number received.</b>
* </p>
* <p>
* Since this signed number is carried in 24 bits, it should be clamped at
* 0x7FFFFF for positive loss or 0x800000 for negative loss rather than
* wrapping around.
* </p>
*
* @return The number of lost packets.<br>
* Loss can be negative, i.e. duplicates have been received.
*/
public long getPacketsLost() {
long lost = getPacketsExpected() - this.receivedPackets;
if (lost > 0x7fffff) {
return 0x7fffff;
}
if (lost < -0x800000) {
return -0x800000;
}
return lost;
}
/**
* Gets the count of sequence number cycles
*
* @return The number of cycles
*/
public int getSequenceCycle() {
return (sequenceCycle >> 16);
}
/**
* Gets an estimate of the statistical variance of the RTP data packet
* interarrival time, measured in timestamp units and expressed as an
* unsigned integer.
*
* @return the estimated jitter for this source
*/
public long getJitter() {
return this.jitter >> 4;
}
/**
* Gets the last time an RTCP Sender Report was received from this source.
*
* @return The middle 32 bits out of 64 in the NTP timestamp received as
* part of the most recent RTCP sender report (SR) packet.<br>
* If no SR has been received yet, returns zero.
*/
public long getLastSR() {
return lastSrTimestamp;
}
/**
* Gets the delay between receiving the last RTCP Sender Report (SR) packet
* from this source and sending this reception report block.
*
* @return The delay between SR reports, expressed in units of 1/65536
* seconds.<br>
* If no SR packet has been received yet, the DLSR field is set to
* zero. seconds
*/
public long getLastSRdelay() {
return getLastSRdelay(this.wallClock.getCurrentTime(), this.lastSrReceivedOn);
}
private long getLastSRdelay(long arrivalTime, long lastSrTime) {
if (this.lastSrReceivedOn == 0) {
return 0;
}
long delay = arrivalTime - lastSrTime;
// convert to units 1/65536 seconds
return (long) (delay * 65.536);
}
/**
* Calculates the extended highest sequence received by adding the last
* sequence number to 65536 times the number of times the sequence counter
* has rolled over.
*
* @return extended highest sequence
*/
public int getExtHighSequence() {
return this.highestSequence + this.sequenceCycle;
}
public int getRTT() {
if(this.roundTripDelay > 0) {
return this.roundTripDelay;
}
return 0;
}
/**
* Calculates interarrival jitter interval.
*
* <p>
* <code>
* int transit = arrival - r->ts;<br>
* int d = transit - s->transit;<br>
* s->transit = transit;<br>
* if (d < 0) d = -d;<br>
* s->jitter += (1./16.) * ((double)d - s->jitter);<br>
* </code>
* </p>
*
* @param packet
* @return
* @see <a
* href="http://tools.ietf.org/html/rfc3550#appendix-A.8">RFC3550</a>
*/
private void estimateJitter(RtpPacket packet) {
long transit = rtpClock.getLocalRtpTime() - packet.getTimestamp();
long d = transit - this.currentTransit;
this.currentTransit = transit;
if(d < 0) {
d = -d;
}
this.jitter += d - ((this.jitter + 8) >> 4);
}
private void initJitter(RtpPacket packet) {
this.currentTransit = rtpClock.getLocalRtpTime() - packet.getTimestamp();
}
public void estimateRtt(long receiptDate, long lastSR, long delaySinceSR) {
TimeStamp receiptNtp = TimeStamp.getNtpTime(receiptDate);
long receiptNtpTime = NtpUtils.calculateLastSrTimestamp(receiptNtp.getSeconds(), receiptNtp.getFraction());
long delay = receiptNtpTime - lastSR - delaySinceSR;
this.roundTripDelay = (delay > 4294967L) ? RTP_SEQ_MOD : (int) ((delay * 1000L) >> 16);
if(logger.isTraceEnabled()) {
logger.trace("rtt=" + receiptNtpTime + " - " + lastSR + " - " + delaySinceSR + " = " + delay + " => " + this.roundTripDelay + "ms");
}
}
private void initSequence(int sequence) {
this.firstSequenceNumber = sequence;
this.highestSequence = sequence;
this.badSequence = RTP_SEQ_MOD + 1; // so seq != bad_seq
this.sequenceCycle = 0;
this.receivedPrior = 0;
this.expectedPrior = 0;
}
private boolean updateSequence(int sequence) {
int delta = Math.abs(sequence - this.highestSequence);
/*
* Source is not valid until MIN_SEQUENTIAL packets with
* sequential sequence numbers have been received.
*/
if(this.probation > 0) {
// packet is in sequence
if(sequence == this.highestSequence + 1) {
this.probation--;
this.highestSequence = sequence;
if(this.probation == 0) {
initSequence(sequence);
return true;
}
} else {
this.probation = MIN_SEQUENTIAL - 1;
this.highestSequence = sequence;
}
return false;
} else if (delta < MAX_DROPOUT) {
// in order, with permissible gap
if(sequence < this.highestSequence) {
// sequence number wrapped - count another 64k cycle
this.sequenceCycle += RTP_SEQ_MOD;
}
this.highestSequence = sequence;
} else if (delta <= RTP_SEQ_MOD - MAX_MISORDER) {
// the sequence number made a very large jump
if(sequence == this.badSequence) {
/*
* Two sequential packets -- assume that the other side
* restarted without telling us so just re-sync (i.e., pretend
* this was the first packet).
*/
initSequence(sequence);
} else {
this.badSequence = (sequence + 1) & (RTP_SEQ_MOD - 1);
return false;
}
} else {
// duplicate or reordered packet
}
return true;
}
public void onReceiveRtp(RtpPacket packet) {
if(validateSequence(packet.getSeqNumber())) {
this.receivedSinceSR++;
this.receivedPackets++;
this.receivedOctets += packet.getPayloadLength();
if(this.lastPacketReceivedOn > 0) {
estimateJitter(packet);
} else {
initJitter(packet);
}
this.lastPacketReceivedOn = rtpClock.getLocalRtpTime();
}
}
private boolean validateSequence(int sequence) {
/*
* When a new source is heard for the first time, that is, its SSRC
* identifier is not in the table (see Section 8.2), and the per-source
* state is allocated for it, s->probation is set to the number of
* sequential packets required before declaring a source valid
* (parameter MIN_SEQUENTIAL) and other variables are initialized
*/
if (this.firstSequenceNumber < 0) {
initSequence(sequence);
this.highestSequence = sequence - 1;
this.probation = MIN_SEQUENTIAL;
return false;
} else {
return updateSequence(sequence);
}
}
public void onReceiveSR(RtcpSenderReport report) {
// Update statistics
this.lastSrTimestamp = report.getNtpTs();
this.lastSrReceivedOn = this.wallClock.getCurrentTime();
this.receivedSinceSR = 0;
}
}