/*
* 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 static org.junit.Assert.assertEquals;
import java.util.Date;
import org.apache.commons.net.ntp.TimeStamp;
import org.junit.Test;
import org.restcomm.media.rtcp.RtcpSenderReport;
import org.restcomm.media.rtcp.ntp.NtpUtils;
import org.restcomm.media.rtp.MockWallClock;
import org.restcomm.media.rtp.RtpClock;
import org.restcomm.media.rtp.RtpPacket;
import org.restcomm.media.rtp.statistics.RtpMember;
/**
* Unit tests for {@link RtpMember}
*
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class RtpMemberTest {
private final MockWallClock wallClock;
private final RtpClock rtpClock;
public RtpMemberTest() {
this.wallClock = new MockWallClock();
this.rtpClock = new RtpClock(this.wallClock);
this.rtpClock.setClockRate(8000);
}
private int sumOctets(RtpPacket ...packets) {
int sum = 0;
for (RtpPacket packet : packets) {
sum += packet.getPayloadLength();
}
return sum;
}
private void receiveRtpPackets(RtpMember member, RtpPacket ...packets) {
for (RtpPacket packet : packets) {
member.onReceiveRtp(packet);
wallClock.tick(20000);
}
}
@Test
public void testOnReceiveRtpPacket() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
RtpPacket p1 = new RtpPacket(172, false);
p1.wrap(false, 8, 1, 160 * 1, 123, new byte[160], 0, 160);
// when
member.onReceiveRtp(p1);
// then
// packet is still in probation so all values are zero
assertEquals(p1.getSyncSource(), member.getSsrc());
assertEquals(0, member.getOctetsReceived());
assertEquals(0, member.getPacketsReceived());
assertEquals(0, member.getReceivedSinceSR());
assertEquals(0, member.getPacketsLost());
assertEquals(0, member.getExtHighSequence());
assertEquals(0, member.getSequenceCycle());
assertEquals(0, member.getLastSR());
assertEquals(0, member.getLastSRdelay());
}
@Test
public void testOnReceiveRtpPackets() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
RtpPacket p1 = new RtpPacket(172, false);
RtpPacket p2 = new RtpPacket(172, false);
RtpPacket p3 = new RtpPacket(172, false);
RtpPacket p4 = new RtpPacket(172, false);
p1.wrap(false, 8, 1, 160 * 1, 123, new byte[160], 0, 160);
p2.wrap(false, 8, 2, 160 * 2, 123, new byte[160], 0, 160);
p3.wrap(false, 8, 3, 160 * 3, 123, new byte[160], 0, 160);
p4.wrap(false, 8, 3, 160 * 4, 123, new byte[160], 0, 160);
// when
receiveRtpPackets(member, p1, p2, p3, p4);
// then
assertEquals(p1.getSyncSource(), member.getSsrc());
assertEquals(sumOctets(p3, p4), member.getOctetsReceived());
assertEquals(2, member.getPacketsReceived());
assertEquals(2, member.getReceivedSinceSR());
assertEquals(-1, member.getPacketsLost()); // expected 1 packet but received 2 (with same seqnum)
assertEquals(p4.getSeqNumber(), member.getExtHighSequence());
assertEquals(0, member.getSequenceCycle());
assertEquals(0, member.getLastSR());
assertEquals(0, member.getLastSRdelay());
}
@Test
public void testSequenceCycleIncrement() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
RtpPacket p1 = new RtpPacket(172, false);
RtpPacket p2 = new RtpPacket(172, false);
RtpPacket p3 = new RtpPacket(172, false);
RtpPacket p4 = new RtpPacket(172, false);
RtpPacket p5 = new RtpPacket(172, false);
p1.wrap(false, 8, 100, 160 * 1, 123, new byte[160], 0, 160);
p2.wrap(false, 8, 101, 160 * 2, 123, new byte[160], 0, 160);
p3.wrap(false, 8, 102, 160 * 3, 123, new byte[160], 0, 160);
p4.wrap(false, 8, 103, 160 * 4, 123, new byte[160], 0, 160);
// The 100+ sequence number gap will cause the sequence cycle to increment
p5.wrap(false, 8, 50, 160 * 5, 123, new byte[160], 0, 160);
// when
receiveRtpPackets(member, p1, p2, p3, p4, p5);
// then
int expectedSeqCycle = 1;
assertEquals(expectedSeqCycle, member.getSequenceCycle());
int expectedHighSequence = (expectedSeqCycle * RtpMember.RTP_SEQ_MOD) + p5.getSeqNumber();
assertEquals(expectedHighSequence, member.getExtHighSequence());
int expected = expectedHighSequence - p1.getSeqNumber() - 1; // discard last packet hence the -1
long expectedLostPackets = expected - member.getPacketsReceived();
assertEquals(expectedLostPackets, member.getPacketsLost());
}
@Test
public void testJitter() {
/*
* Given
*/
RtpMember member = new RtpMember(rtpClock, 123);
// the timestamp for each packet increases by 10ms=160 timestamp units
// for sampling rate 8KHz
RtpPacket p1 = new RtpPacket(172, false);
RtpPacket p2 = new RtpPacket(172, false);
RtpPacket p3 = new RtpPacket(172, false);
RtpPacket p4 = new RtpPacket(172, false);
RtpPacket p5 = new RtpPacket(172, false);
RtpPacket p6 = new RtpPacket(172, false);
RtpPacket p7 = new RtpPacket(172, false);
// packets in probation
p6.wrap(false, 8, 1, 160 * 1, 123, new byte[160], 0, 160);
p7.wrap(false, 8, 3, 160 * 1, 123, new byte[160], 0, 160);
// valid packets
p1.wrap(false, 8, 3, 160 * 1, 123, new byte[160], 0, 160);
p2.wrap(false, 8, 4, 160 * 2, 123, new byte[160], 0, 160);
p3.wrap(false, 8, 5, 160 * 3, 123, new byte[160], 0, 160);
p4.wrap(false, 8, 6, 160 * 4, 123, new byte[160], 0, 160);
p5.wrap(false, 8, 7, 160 * 5, 123, new byte[160], 0, 160);
// 1 sampling units delta for timing and rounding errors , i.e. 1/8ms
long jitterDeltaLimit = 1;
/*
* When/Then
*/
// send two dummy ordered packets to pass probation period
member.onReceiveRtp(p6);
member.onReceiveRtp(p7);
// write first packet, expected jitter = 0
member.onReceiveRtp(p1);
assertEquals(0, member.getJitter(), jitterDeltaLimit);
// move time forward by 20ms and write the second packet
// the transit time should remain approximately the same - near 0ms.
// expected jitter = 0;
wallClock.tick(20000000L);
member.onReceiveRtp(p2);
assertEquals(0, member.getJitter(), jitterDeltaLimit);
// move time forward by 30ms and write the next packet
// the transit time should increase by 10ms,
// as suggested by the difference in the third packet timestamp (160*3)
// and the 20ms delay for the server to receive the second packet
// expected jitter should be close to the 10ms delay in timestamp
// units/16, i.e. 80/16.
wallClock.tick(30000000L);
member.onReceiveRtp(p3);
assertEquals(5, member.getJitter(), jitterDeltaLimit);
// move time forward by 20ms and write the next packet
// the transit time does not change from the previous packet.
// The jitter should stay approximately the same.
wallClock.tick(20000000L);
member.onReceiveRtp(p4);
assertEquals(4, member.getJitter(), jitterDeltaLimit);
// move time forward by 30ms and write the next packet
// packet was delayed 10ms again.
// The estimated jitter should increase significantly, by nearly 5ms
// (80/16)
wallClock.tick(30000000L);
member.onReceiveRtp(p5);
assertEquals(9, member.getJitter(), jitterDeltaLimit);
}
@Test
public void testOnReceiveSR() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
TimeStamp ntp = new TimeStamp(new Date());
RtcpSenderReport sendReport = new RtcpSenderReport(false, 123, ntp.getSeconds(), ntp.getFraction(), 160 * 2, 100, 100 * 130);
long receiveTime = this.wallClock.getCurrentTime();
// when
member.onReceiveSR(sendReport);
this.wallClock.tick(20000000L);
// then
long expectedSrTimestamp = NtpUtils.calculateLastSrTimestamp(ntp.getSeconds(), ntp.getFraction());
assertEquals(expectedSrTimestamp, member.getLastSR());
assertEquals(0, member.getReceivedSinceSR());
long expectedDelay = (long) ((this.wallClock.getCurrentTime() - receiveTime) * 65.536);
assertEquals(expectedDelay, member.getLastSRdelay());
}
@Test
public void testOnReceiveRtpAndSR() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
RtpPacket p1 = new RtpPacket(172, false);
RtpPacket p2 = new RtpPacket(172, false);
RtpPacket p3 = new RtpPacket(172, false);
RtpPacket p4 = new RtpPacket(172, false);
RtpPacket p5 = new RtpPacket(172, false);
p1.wrap(false, 8, 1, 160 * 1, 123, new byte[160], 0, 160);
p2.wrap(false, 8, 2, 160 * 2, 123, new byte[160], 0, 160);
p3.wrap(false, 8, 3, 160 * 3, 123, new byte[160], 0, 160);
p4.wrap(false, 8, 4, 160 * 4, 123, new byte[160], 0, 160);
p5.wrap(false, 8, 5, 160 * 5, 123, new byte[160], 0, 160);
TimeStamp ntp = new TimeStamp(new Date());
RtcpSenderReport sendReport = new RtcpSenderReport(false, 123, ntp.getSeconds(), ntp.getFraction(), 160 * 2, 100, 100 * 130);
// when
member.onReceiveRtp(p1);
wallClock.tick(20000000L);
member.onReceiveRtp(p2);
wallClock.tick(20000000L);
member.onReceiveRtp(p3);
wallClock.tick(20000000L);
member.onReceiveRtp(p4);
wallClock.tick(20000000L);
long receivedSrOn = this.wallClock.getCurrentTime();
member.onReceiveSR(sendReport);
member.onReceiveRtp(p5);
wallClock.tick(20000000L);
// then
assertEquals(0, member.getSequenceCycle());
long expectedHighSeq = (65536 * 0 + p5.getSeqNumber());
assertEquals(expectedHighSeq, member.getExtHighSequence());
assertEquals(3, member.getPacketsReceived());
assertEquals(sumOctets(p3, p4, p5), member.getOctetsReceived());
assertEquals(1, member.getReceivedSinceSR());
assertEquals(0, member.getPacketsLost());
assertEquals(0, member.getFractionLost()); // expected packets = received packets = no loss
long expectedDelay = (long) ((wallClock.getCurrentTime() - receivedSrOn) * 65.536);
assertEquals(expectedDelay, member.getLastSRdelay());
}
@Test
public void testLostPackets() {
// given
RtpMember member = new RtpMember(rtpClock, 123);
RtpPacket p1 = new RtpPacket(172, false);
RtpPacket p2 = new RtpPacket(172, false);
RtpPacket p3 = new RtpPacket(172, false);
RtpPacket p4 = new RtpPacket(172, false);
RtpPacket p5 = new RtpPacket(172, false);
p1.wrap(false, 8, 1, 160 * 1, 123, new byte[160], 0, 160);
p2.wrap(false, 8, 2, 160 * 2, 123, new byte[160], 0, 160);
p3.wrap(false, 8, 3, 160 * 3, 123, new byte[160], 0, 160);
p4.wrap(false, 8, 4, 160 * 4, 123, new byte[160], 0, 160);
p5.wrap(false, 8, 24, 160 * 5, 123, new byte[160], 0, 160);
TimeStamp ntp = new TimeStamp(new Date());
RtcpSenderReport sendReport = new RtcpSenderReport(false, 123, ntp.getSeconds(), ntp.getFraction(), 160 * 2, 100, 100 * 130);
// when
member.onReceiveRtp(p1);
wallClock.tick(20000000L);
member.onReceiveRtp(p2);
wallClock.tick(20000000L);
member.onReceiveRtp(p3);
wallClock.tick(20000000L);
member.onReceiveRtp(p4);
wallClock.tick(20000000L);
member.onReceiveSR(sendReport);
member.onReceiveRtp(p5);
wallClock.tick(20000000L);
// then
int packetsReceived = 3; // 2 first packets were in probation period
int expected = p5.getSeqNumber() - p3.getSeqNumber() + 1; // only considers valid sequence after 2 consecutive packets
long expectedLostPackets = expected - packetsReceived;
long lostInterval = expected - packetsReceived;
long fractionLost = (lostInterval * 256) / expected;
assertEquals(expectedLostPackets, member.getPacketsLost());
assertEquals(fractionLost, member.getFractionLost());
}
}