/*
* 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 java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import org.restcomm.media.rtcp.RtcpIntervalCalculator;
import org.restcomm.media.rtcp.RtcpPacket;
import org.restcomm.media.rtcp.RtcpPacketType;
import org.restcomm.media.rtcp.RtcpReport;
import org.restcomm.media.rtcp.RtcpReportBlock;
import org.restcomm.media.rtcp.RtcpSdes;
import org.restcomm.media.rtcp.RtcpSenderReport;
import org.restcomm.media.rtp.CnameGenerator;
import org.restcomm.media.rtp.RtpClock;
import org.restcomm.media.rtp.RtpPacket;
import org.restcomm.media.rtp.SsrcGenerator;
import org.restcomm.media.scheduler.Clock;
/**
* Encapsulates statistics of an RTP/RTCP channel
*
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class RtpStatistics {
private static final Logger logger = Logger.getLogger(RtpStatistics.class);
/** Default session bandwidth (in octets per second). Matches g.711: 64kbps */
public static final int RTP_DEFAULT_BW = 8000;
/** Fraction of the session bandwidth added for RTCP */
public static final double RTCP_BW_FRACTION = 0.05;
/** Default value for the RTCP bandwidth */
public static final double RTCP_DEFAULT_BW = RTP_DEFAULT_BW * RTCP_BW_FRACTION;
/** Fraction of the RTCP bandwidth to be shared among active senders. */
public static final double RTCP_SENDER_BW_FRACTION = 0.25;
/** Fraction of the RTCP bandwidth to be shared among receivers. */
public static final double RTCP_RECEIVER_BW_FRACTION = 1.0 - RTCP_SENDER_BW_FRACTION;
/** Default value for the average RTCP packet size */
public static final double RTCP_DEFAULT_AVG_SIZE = 200.0;
/* Core */
private final RtpClock rtpClock;
private final Clock wallClock;
/* SSRC Data */
private long ssrc;
private String cname;
/* Global RTP statistics */
private long rtpLastHeartbeat;
private volatile long rtpRxPackets;
private volatile long rtpRxOctets;
private volatile long rtpTxPackets;
private volatile long rtpTxOctets;
private volatile long rtpReceivedOn;
private volatile long rtpSentOn;
private volatile long rtpTimestamp;
/* Global RTCP statistics */
private RtcpPacketType rtcpNextPacketType;
private double rtcpBw;
private double rtcpAvgSize;
private boolean weSent;
private volatile long rtcpTxPackets;
private volatile long rtcpTxOctets;
/**
* Calculation of the RTCP packet interval depends upon an estimate of the
* number of sites participating in the session. New sites are added to the
* count when they are heard, and an entry for each SHOULD be created in a
* table indexed by the SSRC or CSRC identifier to keep track of them.
*
* Entries MAY be deleted from the table when an RTCP BYE packet with the
* corresponding SSRC identifier is received, except that some straggler
* data packets might arrive after the BYE and cause the entry to be
* recreated. Instead, the entry SHOULD be marked as having received a BYE
* and then deleted after an appropriate delay.
*
* A participant MAY mark another site inactive, or delete it if not yet
* valid, if no RTP or RTCP packet has been received for a small number of
* RTCP report intervals (5 is RECOMMENDED). This provides some robustness
* against packet loss.
*/
private final Map<Long, RtpMember> membersMap;
private int pmembers;
private int members;
private final List<Long> sendersList;
private int senders;
public RtpStatistics(final RtpClock clock, final long ssrc, final String cname) {
// Common
this.rtpClock = clock;
this.wallClock = clock.getWallClock();
this.ssrc = ssrc;
this.cname = cname;
// RTP statistics
this.rtpLastHeartbeat = 0;
this.rtpRxPackets = 0;
this.rtpRxOctets = 0;
this.rtpTxPackets = 0;
this.rtpTxOctets = 0;
this.rtpReceivedOn = 0;
this.rtpSentOn = 0;
this.rtpTimestamp = -1;
// RTCP statistics
this.senders = 0;
this.sendersList = new ArrayList<Long>();
this.pmembers = 1;
this.members = 1;
this.membersMap = new HashMap<Long, RtpMember>();
this.membersMap.put(Long.valueOf(this.ssrc), new RtpMember(this.rtpClock, this.ssrc));
this.rtcpBw = RTP_DEFAULT_BW * RTCP_BW_FRACTION;
this.rtcpAvgSize = RTCP_DEFAULT_AVG_SIZE;
this.rtcpNextPacketType = RtcpPacketType.RTCP_REPORT;
this.weSent = false;
this.rtcpTxPackets = 0;
this.rtcpTxOctets = 0;
}
public RtpStatistics(final RtpClock clock, final long ssrc) {
this(clock, ssrc, "");
}
public RtpStatistics(final RtpClock clock) {
this(clock, SsrcGenerator.generateSsrc(), CnameGenerator.generateCname());
}
public void setSsrc(long ssrc) {
this.ssrc = ssrc;
}
public void setCname(String cname) {
this.cname = cname;
}
/**
* Gets the relative time since an RTP packet or Heartbeat was received.
*
* @return The last heartbeat timestamp, in nanoseconds
*/
public long getLastHeartbeat() {
return rtpLastHeartbeat;
}
/**
* Sets the relative time for the last received Heartbeat on a RTP Channel.<br>
* Used for RTP timeout control, not RTCP statistics.
*
* @param rtpKeepAlive
* The heartbeat timestamp, in nanoseconds.
*/
public void setLastHeartbeat(long rtpKeepAlive) {
this.rtpLastHeartbeat = rtpKeepAlive;
}
/**
* Gets the RTP time stamp equivalent to the current time of the Wall Clock.
*
* @return The current time stamp in RTP format.
*/
public long getTime() {
return this.wallClock.getTime();
}
/**
* Gets the current time of the Wall Clock.<br>
*
* @return The current time of the wall clock, in milliseconds.
*/
public long getCurrentTime() {
return this.wallClock.getCurrentTime();
}
public long getRtpTime(long time) {
return this.rtpClock.convertToRtpTime(time);
}
/**
* Gets the SSRC of the RTP Channel
*
* @return The SSRC identifier of the channel
*/
public long getSsrc() {
return ssrc;
}
/**
* Gets the CNAME that identifies this source
*
* @return The CNAME of the source
*/
public String getCname() {
return cname;
}
/*
* RTP Statistics
*/
/**
* Gets the total number of RTP packets that were received during the RTP
* session.
*
* @return The number of RTP packets
*/
public long getRtpPacketsReceived() {
return rtpRxPackets;
}
/**
*
* @return
*/
public long getRtpOctetsReceived() {
return rtpRxOctets;
}
public long getRtpPacketsSent() {
return rtpTxPackets;
}
public long getRtpOctetsSent() {
return rtpTxOctets;
}
/**
* Gets the relative timestamp of the last received RTP packet.
*
* @return The elapsed time, in nanoseconds.
*/
public long getRtpReceivedOn() {
return rtpReceivedOn;
}
/**
* Gets the relative timestamp of the last transmitted RTP packet.
*
* @return The elapsed time, in nanoseconds.
*/
public long getRtpSentOn() {
return rtpSentOn;
}
/**
* Gets ths time stamp of the last RTP packet sent.
*
* @return The time stamp of the packet.
*/
public long getRtpTimestamp() {
return rtpTimestamp;
}
/*
* RTCP Statistics
*/
/**
* Checks whether the application has sent data since the 2nd previous RTCP
* report was sent.
*
* @return Whether data has been sent recently
*/
public boolean hasSent() {
return this.weSent;
}
/**
* Gets the total RTCP bandwidth of this session.
*
* @return The bandwidth, in octets per second
*/
public double getRtcpBw() {
return rtcpBw;
}
/**
* Gets the type of RTCP packet that is scheduled to be transmitted next.
*
* @return The type of the packet
*/
public RtcpPacketType getRtcpPacketType() {
return rtcpNextPacketType;
}
/**
* Sets the type of RTCP packet that is scheduled to be transmitted next.
*
* @param packetType
* The type of the packet
*/
public void setRtcpPacketType(RtcpPacketType packetType) {
this.rtcpNextPacketType = packetType;
}
/**
* Gets the most current estimate for the number of senders in the session
*
* @return The estimate number of senders
*/
public int getSenders() {
return this.senders;
}
public boolean isSender(long ssrc) {
synchronized (this.sendersList) {
return this.sendersList.contains(Long.valueOf(ssrc));
}
}
private void addSender(long ssrc) {
synchronized (this.sendersList) {
if (!this.sendersList.contains(Long.valueOf(ssrc))) {
this.sendersList.add(Long.valueOf(ssrc));
this.senders++;
if (this.ssrc == ssrc) {
this.weSent = true;
}
}
}
}
private void removeSender(long ssrc) {
synchronized (this.sendersList) {
if (this.sendersList.remove(Long.valueOf(ssrc))) {
this.senders--;
if (this.ssrc == ssrc) {
this.weSent = false;
}
}
}
}
public void clearSenders() {
synchronized (this.sendersList) {
this.sendersList.clear();
this.senders = 0;
this.weSent = false;
}
}
/**
* Gets the estimated number of session members at the time <code>tn</code>
* was last recomputed.
*
* @return The number of members
*/
public int getPmembers() {
return pmembers;
}
/**
* Gets the most current estimate for the number of session members.
*
* @return The number of members
*/
public int getMembers() {
return members;
}
public RtpMember getMember(long ssrc) {
synchronized (this.membersMap) {
return this.membersMap.get(Long.valueOf(ssrc));
}
}
public List<Long> getMembersList() {
List<Long> copy;
synchronized (this.membersMap) {
copy = new ArrayList<Long>(this.membersMap.keySet());
}
return copy;
}
public boolean isMember(long ssrc) {
synchronized (this.membersMap) {
return this.membersMap.containsKey(Long.valueOf(ssrc));
}
}
private RtpMember addMember(long ssrc) {
return addMember(ssrc, "");
}
private RtpMember addMember(long ssrc, String cname) {
RtpMember member = getMember(ssrc);
if (member == null) {
synchronized (this.membersMap) {
member = new RtpMember(this.rtpClock, ssrc, cname);
this.membersMap.put(Long.valueOf(ssrc), member);
this.members++;
}
}
return member;
}
private void removeMember(long ssrc) {
synchronized (this.membersMap) {
if (this.membersMap.remove(Long.valueOf(ssrc)) != null) {
this.members--;
}
}
}
/**
* Sets the estimate number of members (pmembers) equal to the number of
* currently registered members.
*/
public void confirmMembers() {
this.pmembers = this.members;
}
public void resetMembers() {
synchronized (this.membersMap) {
this.membersMap.clear();
this.membersMap.put(Long.valueOf(this.ssrc), new RtpMember(this.rtpClock, this.ssrc));
this.members = 1;
this.pmembers = 1;
}
}
/**
* Gets the average compound RTCP packet size.
*
* @return The average packet size, in octets
*/
public double getRtcpAvgSize() {
return rtcpAvgSize;
}
public void setRtcpAvgSize(double avgSize) {
this.rtcpAvgSize = avgSize;
}
private double calculateAvgRtcpSize(double packetSize) {
this.rtcpAvgSize = (1.0 / 16.0) * packetSize + (15.0 / 16.0) * this.rtcpAvgSize;
return this.rtcpAvgSize;
}
public long getRtcpPacketsSent() {
return rtcpTxPackets;
}
public long getRtcpOctetsSent() {
return rtcpTxOctets;
}
/**
* Calculates a random interval to transmit the next RTCP report, according
* to <a
* href="http://tools.ietf.org/html/rfc3550#section-6.3.1">RFC3550</a>.
*
* @param initial
* Whether an RTCP packet was already sent or not. Usually the
* minimum interval for the first packet is lower than the rest.
*
* @return the new transmission interval, in milliseconds
*/
public long rtcpInterval(boolean initial) {
return RtcpIntervalCalculator.calculateInterval(initial, weSent,
senders, members, rtcpAvgSize, rtcpBw, RTCP_BW_FRACTION,
RTCP_SENDER_BW_FRACTION, RTCP_RECEIVER_BW_FRACTION);
}
/**
* Calculates the RTCP interval for a receiver, that is without the
* randomization factor (we_sent=false), according to <a
* href="http://tools.ietf.org/html/rfc3550#section-6.3.1">RFC3550</a>.
*
* @param initial
* Whether an RTCP packet was already sent or not. Usually the
* minimum interval for the first packet is lower than the rest.
* @return the new transmission interval, in milliseconds
*/
public long rtcpReceiverInterval(boolean initial) {
return RtcpIntervalCalculator.calculateInterval(initial, false,
senders, members, rtcpAvgSize, rtcpBw, RTCP_BW_FRACTION,
RTCP_SENDER_BW_FRACTION, RTCP_RECEIVER_BW_FRACTION);
}
/**
* Checks whether this SSRC is still a sender.
*
* If an RTP packet has not been transmitted since time tc - 2T, the
* participant removes itself from the sender table, decrements the sender
* count, and sets we_sent to false.
*
* @return whether this SSRC is still considered a sender
*/
public boolean isSenderTimeout() {
long t = rtcpReceiverInterval(false);
long minTime = getCurrentTime() - (2 * t);
if (this.rtpSentOn < minTime) {
removeSender(this.ssrc);
}
return this.weSent;
}
public void reset() {
// Common
this.ssrc = SsrcGenerator.generateSsrc();
this.cname = CnameGenerator.generateCname();
// RTP statistics
this.rtpLastHeartbeat = 0;
this.rtpRxPackets = 0;
this.rtpTxPackets = 0;
this.rtpReceivedOn = 0;
this.rtpSentOn = 0;
this.rtpTimestamp = -1;
// RTCP statistics
this.senders = 0;
this.sendersList.clear();
this.pmembers = 1;
this.members = 1;
this.membersMap.clear();
this.membersMap.put(Long.valueOf(this.ssrc), new RtpMember(this.rtpClock, this.ssrc));
this.rtcpBw = RTP_DEFAULT_BW * RTCP_BW_FRACTION;
this.rtcpAvgSize = RTCP_DEFAULT_AVG_SIZE;
this.rtcpNextPacketType = RtcpPacketType.RTCP_REPORT;
this.weSent = false;
}
/*
* EVENTS
*/
public void onRtpSent(RtpPacket packet) {
this.rtpTxPackets++;
this.rtpTxOctets += packet.getPayloadLength();
this.rtpSentOn = this.wallClock.getCurrentTime();
this.rtpTimestamp = packet.getTimestamp();
/*
* If the participant sends an RTP packet when we_sent is false, it adds
* itself to the sender table and sets we_sent to true.
*/
if (!this.weSent) {
addSender(Long.valueOf(this.ssrc));
}
}
public void onRtpReceive(RtpPacket packet) {
// Increment global statistics
this.rtpRxPackets++;
this.rtpRxOctets += packet.getPayloadLength();
this.rtpReceivedOn = this.wallClock.getTime();
// Note that there is no point in registering new members if RTCP handler has scheduled a BYE
if(RtcpPacketType.RTCP_REPORT.equals(this.rtcpNextPacketType)) {
long syncSource = packet.getSyncSource();
/*
* When an RTP packet is received from a participant whose SSRC is
* not in the sender table, the SSRC is added to the table, and the
* value for senders is updated.
*/
RtpMember member = getMember(syncSource);
if (member == null) {
member = addMember(syncSource);
}
if (!isSender(syncSource)) {
addSender(syncSource);
}
// Update member statistics
member.onReceiveRtp(packet);
}
}
public void onRtcpSent(RtcpPacket packet) {
calculateAvgRtcpSize(packet.getSize());
this.rtcpTxPackets++;
this.rtcpTxOctets += packet.getSize();
}
public void onRtcpReceive(RtcpPacket rtcpPacket) {
/*
* All RTCP packets MUST be sent in a compound packet of at least two
* individual packets. The first RTCP packet in the compound packet MUST
* always be a report packet to facilitate header validation
*/
RtcpReport report = rtcpPacket.getReport();
long ssrc = report.getSsrc();
/*
* What we do depends on whether we have left the group, and are waiting
* to send a BYE or an RTCP report.
*/
switch (rtcpPacket.getPacketType()) {
case RTCP_REPORT:
/*
* When an RTP or (non-bye) RTCP packet is received from a
* participant whose SSRC is not in the member table, the SSRC is
* added to the table, and the value for members is updated once the
* participant has been validated.
*
* Don't bother registering members if an RTCP BYE is scheduled!
*/
RtpMember member = getMember(ssrc);
if (member == null && RtcpPacketType.RTCP_REPORT.equals(this.rtcpNextPacketType)) {
RtcpSdes sdes = rtcpPacket.getSdes();
String cname = sdes == null ? "" : sdes.getCname();
member = addMember(ssrc, cname);
}
if(rtcpPacket.isSender() && member != null) {
// Receiving an SR has impact on the statistics of the member
member.onReceiveSR((RtcpSenderReport) report);
// estimate round trip delay
RtcpReportBlock reportBlock = report.getReportBlock(this.ssrc);
if(reportBlock!= null) {
member.estimateRtt(this.wallClock.getCurrentTime(), reportBlock.getLsr(), reportBlock.getDlsr());
}
}
break;
case RTCP_BYE:
switch (this.rtcpNextPacketType) {
case RTCP_REPORT:
/*
* If the received packet is an RTCP BYE packet, the SSRC is
* checked against the member table. If present, the entry is
* removed from the table, and the value for members is updated.
*/
if (isMember(ssrc)) {
removeMember(ssrc);
}
/*
* The SSRC is then checked against the sender table. If
* present, the entry is removed from the table, and the value
* for senders is updated.
*/
if (isSender(ssrc)) {
removeSender(ssrc);
}
break;
case RTCP_BYE:
/*
* Every time a BYE packet from another participant is received,
* members is incremented by 1 regardless of whether that
* participant exists in the member table or not, and when SSRC
* sampling is in use, regardless of whether or not the BYE SSRC
* would be included in the sample.
*
* members is NOT incremented when other RTCP packets or RTP
* packets are received, but only for BYE packets. Similarly,
* avg_rtcp_size is updated only for received BYE packets.
* senders is NOT updated when RTP packets arrive; it remains 0.
*/
this.members++;
break;
default:
logger.warn("Unknown type of scheduled event: " + this.rtcpNextPacketType.name());
break;
}
break;
default:
logger.warn("Unkown RTCP packet type: " + rtcpPacket.getPacketType().name() + ". Dropping packet.");
break;
}
// For each RTCP packet received, the value of avg_rtcp_size is updated.
calculateAvgRtcpSize(rtcpPacket.getSize());
}
}