org.mobicents.media.server.impl.rtcp.RtcpHandlerTest.java Source code

Java tutorial

Introduction

Here is the source code for org.mobicents.media.server.impl.rtcp.RtcpHandlerTest.java

Source

/*
 * 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.mobicents.media.server.impl.rtcp;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

import org.apache.commons.net.ntp.TimeStamp;
import org.apache.log4j.Logger;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mobicents.media.server.impl.rtp.CnameGenerator;
import org.mobicents.media.server.impl.rtp.RtpClock;
import org.mobicents.media.server.impl.rtp.SsrcGenerator;
import org.mobicents.media.server.impl.rtp.statistics.RtpMember;
import org.mobicents.media.server.impl.rtp.statistics.RtpStatistics;
import org.mobicents.media.server.io.network.channel.MultiplexedChannel;
import org.mobicents.media.server.io.network.channel.PacketHandlerException;
import org.mobicents.media.server.scheduler.Clock;
import org.mobicents.media.server.scheduler.DefaultClock;

/**
 * 
 * @author Henrique Rosa (henrique.rosa@telestax.com)
 * 
 */
public class RtcpHandlerTest {

    private static final Logger logger = Logger.getLogger(RtcpHandlerTest.class);

    private static final byte[] RTCP_BYE_PACKET = new byte[] { (byte) 0x81, (byte) 0xc8, 0x00, 0x0c, (byte) 0xf1,
            (byte) 0xcf, (byte) 0xb8, (byte) 0xf9, (byte) 0xd7, (byte) 0xc3, 0x17, (byte) 0xd1, (byte) 0xdd,
            (byte) 0xb2, 0x2d, 0x0e, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x02, (byte) 0xee, 0x7c, (byte) 0xb9, 0x07, (byte) 0xac, (byte) 0xbe, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, (byte) 0x83, 0x53, 0x00, 0x00, 0x00, 0x05, (byte) 0x99, 0x4f, 0x54, 0x18, 0x00, 0x02,
            (byte) 0xb6, 0x04, (byte) 0x81, (byte) 0xca, 0x00, 0x06, (byte) 0xf1, (byte) 0xcf, (byte) 0xb8,
            (byte) 0xf9, 0x01, 0x10, 0x6d, 0x4e, 0x56, 0x35, 0x51, 0x31, 0x36, 0x61, 0x6a, 0x52, 0x76, 0x4d, 0x30,
            0x30, 0x77, 0x53, 0x00, 0x00, (byte) 0x81, (byte) 0xcb, 0x00, 0x01, (byte) 0xf1, (byte) 0xcf,
            (byte) 0xb8, (byte) 0xf9 };

    private static final byte[] RTP_PACKET = new byte[] { (byte) 0x80, 0x00, 0x04, 0x5c, 0x00, 0x02, (byte) 0xb9,
            (byte) 0x80, (byte) 0xf1, (byte) 0xcf, (byte) 0xb8, (byte) 0xf9, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };

    // Default messages
    private static final String INTERVAL_RANGE = "The interval (%d) must be in range [%d;%d]";

    private Clock wallClock;
    private RtpClock rtpClock;
    private RtpStatistics statistics;
    private RtcpHandler handler;

    @Before
    public void before() {
        wallClock = new DefaultClock();
        rtpClock = new RtpClock(wallClock);
        statistics = new RtpStatistics(rtpClock);
        handler = new RtcpHandler(statistics);
    }

    @After
    public void after() {
        if (handler.isJoined()) {
            handler.leaveRtpSession();
        }
    }

    @Test
    public void testCanHandle() {
        // given

        // when
        boolean canHandleRtcp = handler.canHandle(RTCP_BYE_PACKET);
        boolean canHandleRtp = handler.canHandle(RTP_PACKET);

        // then
        Assert.assertTrue(canHandleRtcp);
        Assert.assertFalse(canHandleRtp);
    }

    @Test
    public void testHandleReceiverReport() throws PacketHandlerException {
        // given
        InetSocketAddress localPeer = new InetSocketAddress("127.0.0.1", 6100);
        InetSocketAddress remotePeer = new InetSocketAddress("127.0.0.1", 6200);

        long remoteSsr = SsrcGenerator.generateSsrc();
        String remoteCname = CnameGenerator.generateCname();

        RtcpReceiverReport rr = new RtcpReceiverReport(false, remoteSsr);

        RtcpSdes sdes = new RtcpSdes(false);
        RtcpSdesChunk sdesChunk = new RtcpSdesChunk(remoteSsr);
        RtcpSdesItem sdesCname = new RtcpSdesItem(RtcpSdesItem.RTCP_SDES_CNAME, remoteCname);
        sdesChunk.addRtcpSdesItem(sdesCname);
        sdes.addRtcpSdesChunk(sdesChunk);

        RtcpPacket rtcpPacket = new RtcpPacket(rr, sdes);
        byte[] rtcpData = new byte[300];
        rtcpPacket.encode(rtcpData, 0);

        // when
        handler.joinRtpSession();
        byte[] response = handler.handle(rtcpData, localPeer, remotePeer);

        // then
        // There are no responses in RTCP
        Assert.assertNull(response);

        /*
         * When an 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.
         */
        RtpMember newMember = statistics.getMember(remoteSsr);
        Assert.assertEquals(2, statistics.getMembers());
        Assert.assertNotNull(newMember);
        Assert.assertEquals(remoteSsr, newMember.getSsrc());
        Assert.assertEquals(remoteCname, newMember.getCname());

        // The average packet size is updated:
        // avg_rtcp_size = (1/16) * packet_size + (15/16) * avg_rtcp_size
        double expectedSize = (1.0 / 16.0) * rtcpPacket.getSize()
                + (15.0 / 16.0) * RtpStatistics.RTCP_DEFAULT_AVG_SIZE;
        Assert.assertEquals(expectedSize, statistics.getRtcpAvgSize(), 0.0);
    }

    @Test
    public void testHandleSenderReport() throws PacketHandlerException {
        // given
        InetSocketAddress localPeer = new InetSocketAddress("127.0.0.1", 6100);
        InetSocketAddress remotePeer = new InetSocketAddress("127.0.0.1", 6200);

        long localSsrc = statistics.getSsrc();
        long remoteSsrc = SsrcGenerator.generateSsrc();
        String remoteCname = CnameGenerator.generateCname();

        TimeStamp ntp = new TimeStamp(wallClock.getCurrentTime());
        RtcpSenderReport sr = new RtcpSenderReport(false, remoteSsrc, ntp.getSeconds(), ntp.getFraction(), 200, 3,
                3 * 200);
        RtcpReportBlock rrBlock = new RtcpReportBlock(localSsrc, 1, 1, 0, 10, 0, 398416412, 223412);
        sr.addReceiverReport(rrBlock);

        RtcpSdes sdes = new RtcpSdes(false);
        RtcpSdesChunk sdesChunk = new RtcpSdesChunk(remoteSsrc);
        RtcpSdesItem sdesCname = new RtcpSdesItem(RtcpSdesItem.RTCP_SDES_CNAME, remoteCname);
        sdesChunk.addRtcpSdesItem(sdesCname);
        sdes.addRtcpSdesChunk(sdesChunk);

        RtcpPacket rtcpPacket = new RtcpPacket(sr, sdes);
        byte[] rtcpData = new byte[300];
        rtcpPacket.encode(rtcpData, 0);

        // when
        handler.joinRtpSession();
        byte[] response = handler.handle(rtcpData, localPeer, remotePeer);

        // then
        // There are no responses in RTCP
        Assert.assertNull(response);

        /*
         * When an 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.
         */
        RtpMember newMember = statistics.getMember(remoteSsrc);
        Assert.assertEquals(2, statistics.getMembers());
        Assert.assertNotNull(newMember);
        Assert.assertEquals(remoteSsrc, newMember.getSsrc());
        Assert.assertEquals(remoteCname, newMember.getCname());

        Assert.assertEquals(sr.getNtpTs(), newMember.getLastSR());
        Assert.assertEquals(0, newMember.getReceivedSinceSR());

        // The average packet size is updated:
        // avg_rtcp_size = (1/16) * packet_size + (15/16) * avg_rtcp_size
        double expectedSize = (1.0 / 16.0) * rtcpPacket.getSize()
                + (15.0 / 16.0) * RtpStatistics.RTCP_DEFAULT_AVG_SIZE;
        Assert.assertEquals(expectedSize, statistics.getRtcpAvgSize(), 0.0);
    }

    @Test
    public void testHandleBye() throws PacketHandlerException {
        // given
        InetSocketAddress localPeer = new InetSocketAddress("127.0.0.1", 6100);
        InetSocketAddress remotePeer = new InetSocketAddress("127.0.0.1", 6200);

        RtcpPacket rr1 = buildReceiverReportPacket();
        byte[] rr1Data = new byte[300];
        rr1.encode(rr1Data, 0);

        RtcpPacket rr2 = buildReceiverReportPacket();
        byte[] rr2Data = new byte[300];
        rr2.encode(rr2Data, 0);

        RtcpPacket rr3 = buildReceiverReportPacket();
        byte[] rr3Data = new byte[300];
        rr3.encode(rr3Data, 0);

        RtcpPacket bye = buildReceiverReportPacket(rr1.getReceiverReport().ssrc, rr1.getSdes().getCname(), true);
        byte[] byeData = new byte[300];
        bye.encode(byeData, 0);

        // when
        handler.joinRtpSession();
        handler.handle(rr1Data, localPeer, remotePeer);
        double expectedSize = (1.0 / 16.0) * rr1.getSize() + (15.0 / 16.0) * RtpStatistics.RTCP_DEFAULT_AVG_SIZE;

        handler.handle(rr2Data, localPeer, remotePeer);
        expectedSize = (1.0 / 16.0) * rr2.getSize() + (15.0 / 16.0) * expectedSize;

        handler.handle(rr3Data, localPeer, remotePeer);
        expectedSize = (1.0 / 16.0) * rr3.getSize() + (15.0 / 16.0) * expectedSize;

        // then
        long expectedMembers = 4;
        Assert.assertEquals(expectedMembers, statistics.getMembers());

        // when
        handler.handle(byeData, localPeer, remotePeer);
        expectedSize = (1.0 / 16.0) * bye.getSize() + (15.0 / 16.0) * expectedSize;

        // then
        /*
         * 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. 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.
         */
        Assert.assertEquals(expectedMembers - 1, statistics.getMembers());
        Assert.assertEquals(expectedSize, statistics.getRtcpAvgSize(), 0.0);
    }

    private RtcpPacket buildReceiverReportPacket() {
        long ssrc = SsrcGenerator.generateSsrc();
        String cname = CnameGenerator.generateCname();

        RtcpReceiverReport rr = new RtcpReceiverReport(false, ssrc);
        RtcpSdes sdes = new RtcpSdes(false);
        RtcpSdesChunk sdesChunk = new RtcpSdesChunk(ssrc);
        RtcpSdesItem sdesCname = new RtcpSdesItem(RtcpSdesItem.RTCP_SDES_CNAME, cname);
        sdesChunk.addRtcpSdesItem(sdesCname);
        sdes.addRtcpSdesChunk(sdesChunk);
        return new RtcpPacket(rr, sdes);
    }

    private RtcpPacket buildReceiverReportPacket(long ssrc, String cname, boolean bye) {
        RtcpReceiverReport rr = new RtcpReceiverReport(false, ssrc);
        RtcpSdes sdes = new RtcpSdes(false);
        RtcpSdesChunk sdesChunk = new RtcpSdesChunk(ssrc);
        RtcpSdesItem sdesCname = new RtcpSdesItem(RtcpSdesItem.RTCP_SDES_CNAME, cname);
        sdesChunk.addRtcpSdesItem(sdesCname);
        sdes.addRtcpSdesChunk(sdesChunk);

        RtcpBye rtcpBye = null;
        if (bye) {
            rtcpBye = new RtcpBye(false);
            return new RtcpPacket(rr, sdes, rtcpBye);
        }
        return new RtcpPacket(rr, sdes);
    }

    @Test
    public void testJoinAndLeaveRtpSession() throws InterruptedException {
        // given
        // senders > members * 0.25 AND we_sent == false, then C = avg_rtcp_size / rtcp_bw
        double c = statistics.getRtcpAvgSize() / RtpStatistics.RTCP_DEFAULT_BW;
        // senders > members * 0.25 THEN n = members
        int n = statistics.getMembers();
        // initial == true, then Tmin = 2.5 seconds
        double tMin = RtcpIntervalCalculator.INITIAL_MIN_TIME;
        double tD = Math.max(tMin, n * c);
        // T = [Td * 0.5; Td * 1.5]
        // t = T / (e - 3/2)
        double compensation = Math.E - (3.0 / 2.0);
        long lowT = (long) (((tD * 0.5) / compensation) * 1000);
        long highT = (long) (((tD * 1.5) / compensation) * 1000);

        // when
        handler.joinRtpSession();
        long nextReport = handler.getNextScheduledReport();

        // give time to handler supposedly send the initial report
        Thread.sleep(handler.getNextScheduledReport());

        // then
        Assert.assertTrue(handler.isJoined());
        String msg = String.format(INTERVAL_RANGE, nextReport, lowT, highT);
        Assert.assertTrue(msg, nextReport >= lowT);
        Assert.assertTrue(msg, nextReport <= highT);
        Assert.assertEquals(RtcpPacketType.RTCP_REPORT, statistics.getRtcpPacketType());

        // Notice handler will still be in initial stage since it cannot send packets
        // (we have not set a proper data channel)
        Assert.assertTrue(handler.isInitial());

        // when
        /*
         * When the participant decides to leave the system, tp is reset to tc,
         * the current time, members and pmembers are initialized to 1, initial
         * is set to 1, we_sent is set to false, senders is set to 0, and
         * avg_rtcp_size is set to the size of the compound BYE packet.
         * 
         * The calculated interval T is computed. The BYE packet is then
         * scheduled for time tn = tc + T.
         */
        handler.leaveRtpSession();

        // then
        Assert.assertFalse(handler.isJoined());
        Assert.assertEquals(RtcpPacketType.RTCP_BYE, statistics.getRtcpPacketType());
    }

    @Test
    public void testRtcpSend() throws IOException, InterruptedException {
        /* GIVEN */
        SnifferChannel recvChannel = new SnifferChannel();
        recvChannel.open();
        recvChannel.bind(new InetSocketAddress("127.0.0.1", 0));

        DatagramChannel sendChannel = DatagramChannel.open();
        sendChannel.bind(new InetSocketAddress("127.0.0.1", 0));

        recvChannel.connect(sendChannel.getLocalAddress());
        sendChannel.connect(recvChannel.getLocalAddress());

        /* WHEN */
        handler.setChannel(sendChannel);
        handler.joinRtpSession();

        recvChannel.start();
        new Thread(recvChannel).start();
        Thread.sleep(15000);

        handler.leaveRtpSession();
        Thread.sleep(5000);
        recvChannel.stop();

        /* THEN */
        Assert.assertTrue(recvChannel.rxPackets > 0);
        Assert.assertTrue(recvChannel.rxOctets > 0);
        Assert.assertEquals(recvChannel.rxPackets, statistics.getRtcpPacketsSent());
        Assert.assertEquals(recvChannel.rxOctets, statistics.getRtcpOctetsSent());
    }

    private class SnifferChannel extends MultiplexedChannel implements Runnable {

        private volatile boolean running = false;
        private int rxPackets = 0;
        private int rxOctets = 0;

        private final ByteBuffer buffer = ByteBuffer.allocate(300);

        @Override
        public void receive() throws IOException {
            this.buffer.clear();
            int read = super.dataChannel.read(buffer);

            if (read > 0) {
                this.rxPackets++;
                this.rxOctets += read;
            }
        }

        public void start() {
            this.running = true;
        }

        public void stop() {
            this.running = false;
        }

        @Override
        public void run() {
            while (running) {
                try {
                    receive();
                } catch (IOException e) {
                    logger.error(e.getMessage(), e);
                    running = false;
                }
            }
        }

    }

}