/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2017, Telestax Inc and individual contributors
* by the @authors tag.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.restcomm.media.rtp.rfc2833;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URL;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.restcomm.media.codec.g711.alaw.Decoder;
import org.restcomm.media.codec.g711.ulaw.Encoder;
import org.restcomm.media.component.audio.AudioComponent;
import org.restcomm.media.component.audio.AudioMixer;
import org.restcomm.media.component.dsp.DspFactoryImpl;
import org.restcomm.media.component.oob.OOBComponent;
import org.restcomm.media.component.oob.OOBMixer;
import org.restcomm.media.network.deprecated.PortManager;
import org.restcomm.media.network.deprecated.RtpPortManager;
import org.restcomm.media.network.deprecated.UdpManager;
import org.restcomm.media.network.netty.NettyNetworkManager;
import org.restcomm.media.network.netty.channel.NettyNetworkChannelGlobalContext;
import org.restcomm.media.pcap.AsyncPcapChannel;
import org.restcomm.media.pcap.AsyncPcapChannelHandler;
import org.restcomm.media.pcap.PcapPacketEncoder;
import org.restcomm.media.pcap.PcapPlayer;
import org.restcomm.media.resource.dtmf.DetectorImpl;
import org.restcomm.media.rtp.RtpChannel;
import org.restcomm.media.rtp.RtpClock;
import org.restcomm.media.rtp.crypto.DtlsSrtpServerProvider;
import org.restcomm.media.rtp.statistics.RtpStatistics;
import org.restcomm.media.scheduler.Clock;
import org.restcomm.media.scheduler.PriorityQueueScheduler;
import org.restcomm.media.scheduler.Scheduler;
import org.restcomm.media.scheduler.ServiceScheduler;
import org.restcomm.media.scheduler.WallClock;
import org.restcomm.media.sdp.format.AVProfile;
import org.restcomm.media.spi.ConnectionMode;
import org.restcomm.media.spi.dtmf.DtmfDetectorListener;
import org.restcomm.media.spi.dtmf.DtmfEvent;
import org.restcomm.media.spi.format.AudioFormat;
import org.restcomm.media.spi.format.FormatFactory;
import org.restcomm.media.spi.format.Formats;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioDatagramChannel;
/**
* @author Henrique Rosa (henrique.rosa@telestax.com)
*
*/
public class DtmfRfc2833Test {
// Netty IO Stack
private ScheduledThreadPoolExecutor executor;
private ListeningScheduledExecutorService scheduler;
private NioEventLoopGroup eventGroup;
private Bootstrap bootstrap;
private NettyNetworkManager networkManager;
// PCAP stack
private AsyncPcapChannel channel;
private PcapPlayer player;
// Legacy IO Stack
private Scheduler ioScheduler;
private UdpManager udpManager;
private PortManager portManager;
// Media Stack
private Clock clock;
private PriorityQueueScheduler mediaScheduler;
private AudioMixer mixer;
private OOBMixer oobMixer;
private AudioComponent inbandDetectorComponent;
private OOBComponent oobDetectorComponent;
private DspFactoryImpl dspFactory;
// DTMF Detector
private DetectorImpl dtmfDetector;
// RTP stack
private RtpClock rtpClock;
private RtpClock oobClock;
private RtpStatistics rtpStatistics;
private RtpChannel rtpChannel;
@Before
public void before() throws Exception {
// Media Formats
AudioFormat pcma = FormatFactory.createAudioFormat("pcma", 8000, 8, 1);
Formats fmts = new Formats();
fmts.add(pcma);
Formats dstFormats = new Formats();
dstFormats.add(FormatFactory.createAudioFormat("LINEAR", 8000, 16, 1));
// Netty IO Stack
this.executor = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(5);
this.executor.prestartAllCoreThreads();
this.executor.setRemoveOnCancelPolicy(true);
this.scheduler = MoreExecutors.listeningDecorator(executor);
this.eventGroup = new NioEventLoopGroup(Runtime.getRuntime().availableProcessors(), scheduler);
this.bootstrap = new Bootstrap().channel(NioDatagramChannel.class).group(eventGroup);
this.networkManager = new NettyNetworkManager(bootstrap);
// Legacy IO Stack
this.ioScheduler = new ServiceScheduler(this.clock);
this.ioScheduler.start();
this.portManager = new RtpPortManager(6000, 65000);
this.udpManager = new UdpManager(this.ioScheduler, this.portManager, this.portManager);
this.udpManager.setBindAddress("127.0.0.1");
this.udpManager.setLocalBindAddress("127.0.0.1");
this.udpManager.start();
// Media Stack
this.clock = new WallClock();
this.mediaScheduler = new PriorityQueueScheduler(this.clock);
this.mediaScheduler.start();
this.mixer = new AudioMixer(this.mediaScheduler);
this.oobMixer = new OOBMixer(this.mediaScheduler);
this.dspFactory = new DspFactoryImpl();
this.dspFactory.addCodec(Encoder.class.getName());
this.dspFactory.addCodec(Decoder.class.getName());
this.dspFactory.addCodec(org.restcomm.media.codec.g711.alaw.Encoder.class.getName());
this.dspFactory.addCodec(org.restcomm.media.codec.g711.alaw.Decoder.class.getName());
// RTP Stack
this.rtpClock = new RtpClock(this.clock);
this.oobClock = new RtpClock(this.clock);
this.rtpStatistics = new RtpStatistics(this.rtpClock);
this.rtpChannel = new RtpChannel(1, 50, this.rtpStatistics, this.rtpClock, this.oobClock, mediaScheduler, udpManager,
mock(DtlsSrtpServerProvider.class));
this.rtpChannel.setInputDsp(this.dspFactory.newProcessor());
this.mixer.addComponent(this.rtpChannel.getAudioComponent());
this.oobMixer.addComponent(this.rtpChannel.getOobComponent());
}
@SuppressWarnings("unchecked")
@After
public void after() {
// DTMF Detector
if (this.dtmfDetector != null) {
this.dtmfDetector.deactivate();
this.dtmfDetector = null;
}
// RTP stack
if (this.rtpChannel != null) {
this.rtpChannel.close();
this.rtpChannel = null;
}
if (this.rtpStatistics != null) {
this.rtpStatistics.reset();
this.rtpStatistics = null;
}
this.rtpClock = null;
// Media Stack
this.oobMixer.stop();
this.mixer.stop();
this.mediaScheduler.stop();
// Legacy IO
this.udpManager.stop();
this.ioScheduler.stop();
// PCAP Stack
if (player != null) {
if (player.isPlaying()) {
player.stop();
}
player = null;
}
if (channel != null) {
if (channel.isOpen()) {
channel.close(mock(FutureCallback.class));
}
channel = null;
}
if (dtmfDetector != null) {
this.dtmfDetector.deactivate();
this.dtmfDetector = null;
}
this.networkManager = null;
this.bootstrap = null;
if (this.eventGroup != null) {
if (!this.eventGroup.isShutdown()) {
this.eventGroup.shutdownGracefully(0L, 0L, TimeUnit.NANOSECONDS);
}
this.eventGroup = null;
}
if (this.scheduler != null) {
if (!this.scheduler.isShutdown()) {
scheduler.shutdown();
}
scheduler = null;
}
if (this.executor != null) {
if (!this.executor.isShutdown()) {
this.executor.shutdown();
}
this.executor = null;
}
}
@SuppressWarnings("unchecked")
@Test
public void testDetectHashFromPcap() throws Exception {
// given
int rtpEventPacketCount = 35;
int rtpEventPacketLength = 24;
int rtpPacketCount = 39;
int rtpPacketLength = 180;
long rtpStreamDuration = 900;
int totalOctets = (rtpEventPacketLength * rtpEventPacketCount) + (rtpPacketCount * rtpPacketLength);
int totalPackets = rtpEventPacketCount + rtpPacketCount;
InetSocketAddress localAddress = new InetSocketAddress("127.0.0.1", 64000);
String filepath = "dtmf-oob-one-hash.cap.gz";
PcapPacketEncoder packetEncoder = new PcapPacketEncoder();
AsyncPcapChannelHandler channelInitializer = new AsyncPcapChannelHandler(packetEncoder);
bootstrap.handler(channelInitializer);
NettyNetworkChannelGlobalContext context = new NettyNetworkChannelGlobalContext(networkManager);
channel = new AsyncPcapChannel(context);
player = new PcapPlayer(channel, scheduler);
FutureCallback<Void> openCallback = mock(FutureCallback.class);
FutureCallback<Void> bindCallback = mock(FutureCallback.class);
FutureCallback<Void> connectCallback = mock(FutureCallback.class);
rtpChannel.bind(false, false);
rtpChannel.connect(localAddress);
SocketAddress remoteAddress = rtpChannel.getLocalAddress();
rtpChannel.setFormatMap(AVProfile.audio);
rtpChannel.updateMode(ConnectionMode.RECV_ONLY);
channel.open(openCallback);
verify(openCallback, timeout(100)).onSuccess(null);
channel.bind(localAddress, bindCallback);
verify(bindCallback, timeout(100)).onSuccess(null);
channel.connect(remoteAddress, connectCallback);
verify(connectCallback, timeout(100)).onSuccess(null);
this.dtmfDetector = new DetectorImpl("dtmf-detector", 0, 40, 0, this.mediaScheduler);
this.inbandDetectorComponent = new AudioComponent(8);
this.inbandDetectorComponent.addOutput(this.dtmfDetector.getAudioOutput());
this.inbandDetectorComponent.updateMode(true, true);
this.mixer.addComponent(this.inbandDetectorComponent);
this.oobDetectorComponent = new OOBComponent(9);
this.oobDetectorComponent.addOutput(this.dtmfDetector.getOOBOutput());
this.oobDetectorComponent.updateMode(true, true);
this.oobMixer.addComponent(this.oobDetectorComponent);
final DtmfListener dtmfListener = new DtmfListener();
this.dtmfDetector.addListener(dtmfListener);
// when
this.mixer.start();
this.oobMixer.start();
this.dtmfDetector.activate();
URL pcap = DtmfRfc2833Test.class.getResource(filepath);
player.play(pcap);
// then
Assert.assertTrue(player.isPlaying());
Thread.sleep(rtpStreamDuration);
Assert.assertFalse(player.isPlaying());
Assert.assertEquals(totalPackets, player.countPacketsSent());
Assert.assertEquals(totalOctets, player.countOctetsSent());
this.dtmfDetector.flushBuffer();
Assert.assertEquals(1, dtmfListener.countTones());
Assert.assertEquals("#", dtmfListener.pollTone().getTone());
}
@SuppressWarnings("unchecked")
@Test
public void testDetectHashThenHashFromPcap() throws Exception {
// given
int rtpEventPacketCount = 72;
int rtpEventPacketLength = 24;
int rtpPacketCount = 429;
int rtpPacketLength = 180;
long rtpStreamDuration = 9000;
int totalOctets = (rtpEventPacketLength * rtpEventPacketCount) + (rtpPacketCount * rtpPacketLength);
int totalPackets = rtpEventPacketCount + rtpPacketCount;
InetSocketAddress localAddress = new InetSocketAddress("127.0.0.1", 64000);
String filepath = "dtmf-oob-two-hash.cap.gz";
PcapPacketEncoder packetEncoder = new PcapPacketEncoder();
AsyncPcapChannelHandler channelInitializer = new AsyncPcapChannelHandler(packetEncoder);
bootstrap.handler(channelInitializer);
NettyNetworkChannelGlobalContext context = new NettyNetworkChannelGlobalContext(networkManager);
channel = new AsyncPcapChannel(context);
player = new PcapPlayer(channel, scheduler);
FutureCallback<Void> openCallback = mock(FutureCallback.class);
FutureCallback<Void> bindCallback = mock(FutureCallback.class);
FutureCallback<Void> connectCallback = mock(FutureCallback.class);
rtpChannel.bind(false, false);
rtpChannel.connect(localAddress);
SocketAddress remoteAddress = rtpChannel.getLocalAddress();
rtpChannel.setFormatMap(AVProfile.audio);
rtpChannel.updateMode(ConnectionMode.RECV_ONLY);
channel.open(openCallback);
verify(openCallback, timeout(100)).onSuccess(null);
channel.bind(localAddress, bindCallback);
verify(bindCallback, timeout(100)).onSuccess(null);
channel.connect(remoteAddress, connectCallback);
verify(connectCallback, timeout(100)).onSuccess(null);
this.dtmfDetector = new DetectorImpl("dtmf-detector", 0, 40, 0, this.mediaScheduler);
this.inbandDetectorComponent = new AudioComponent(8);
this.inbandDetectorComponent.addOutput(this.dtmfDetector.getAudioOutput());
this.inbandDetectorComponent.updateMode(true, true);
this.mixer.addComponent(this.inbandDetectorComponent);
this.oobDetectorComponent = new OOBComponent(9);
this.oobDetectorComponent.addOutput(this.dtmfDetector.getOOBOutput());
this.oobDetectorComponent.updateMode(true, true);
this.oobMixer.addComponent(this.oobDetectorComponent);
final DtmfListener dtmfListener = new DtmfListener();
this.dtmfDetector.addListener(dtmfListener);
// when
this.mixer.start();
this.oobMixer.start();
this.dtmfDetector.activate();
URL pcap = DtmfRfc2833Test.class.getResource(filepath);
player.play(pcap);
// then
Assert.assertTrue(player.isPlaying());
Thread.sleep(rtpStreamDuration);
Assert.assertFalse(player.isPlaying());
Assert.assertEquals(totalPackets, player.countPacketsSent());
Assert.assertEquals(totalOctets, player.countOctetsSent());
this.dtmfDetector.flushBuffer();
Assert.assertEquals(2, dtmfListener.countTones());
Assert.assertEquals("#", dtmfListener.pollTone().getTone());
Assert.assertEquals("#", dtmfListener.pollTone().getTone());
}
@SuppressWarnings("unchecked")
@Test
public void testGigasetN510IpProRfc2833() throws Exception {
// given
int rtpEventPacketCount = 70;
int rtpEventPacketLength = 24;
int rtpPacketCount = 2494;
int rtpPacketLength = 180;
long rtpStreamDuration = 50000;
int totalOctets = (rtpEventPacketLength * rtpEventPacketCount) + (rtpPacketCount * rtpPacketLength);
int totalPackets = rtpEventPacketCount + rtpPacketCount;
InetSocketAddress localAddress = new InetSocketAddress("127.0.0.1", 64000);
String filepath = "gigaset-n510-ip-pro-rfc2833.pcap";
PcapPacketEncoder packetEncoder = new PcapPacketEncoder();
AsyncPcapChannelHandler channelInitializer = new AsyncPcapChannelHandler(packetEncoder);
bootstrap.handler(channelInitializer);
NettyNetworkChannelGlobalContext context = new NettyNetworkChannelGlobalContext(networkManager);
channel = new AsyncPcapChannel(context);
player = new PcapPlayer(channel, scheduler);
FutureCallback<Void> openCallback = mock(FutureCallback.class);
FutureCallback<Void> bindCallback = mock(FutureCallback.class);
FutureCallback<Void> connectCallback = mock(FutureCallback.class);
rtpChannel.bind(false, false);
rtpChannel.connect(localAddress);
SocketAddress remoteAddress = rtpChannel.getLocalAddress();
rtpChannel.setFormatMap(AVProfile.audio);
rtpChannel.updateMode(ConnectionMode.RECV_ONLY);
channel.open(openCallback);
verify(openCallback, timeout(100)).onSuccess(null);
channel.bind(localAddress, bindCallback);
verify(bindCallback, timeout(100)).onSuccess(null);
channel.connect(remoteAddress, connectCallback);
verify(connectCallback, timeout(100)).onSuccess(null);
this.dtmfDetector = new DetectorImpl("dtmf-detector", 0, 100, 100, this.mediaScheduler);
this.inbandDetectorComponent = new AudioComponent(8);
this.inbandDetectorComponent.addOutput(this.dtmfDetector.getAudioOutput());
this.inbandDetectorComponent.updateMode(true, true);
this.mixer.addComponent(this.inbandDetectorComponent);
this.oobDetectorComponent = new OOBComponent(9);
this.oobDetectorComponent.addOutput(this.dtmfDetector.getOOBOutput());
this.oobDetectorComponent.updateMode(true, true);
this.oobMixer.addComponent(this.oobDetectorComponent);
final DtmfListener dtmfListener = new DtmfListener();
this.dtmfDetector.addListener(dtmfListener);
// when
this.mixer.start();
this.oobMixer.start();
this.dtmfDetector.activate();
URL pcap = DtmfRfc2833Test.class.getResource(filepath);
player.play(pcap);
// then
Assert.assertTrue(player.isPlaying());
Thread.sleep(rtpStreamDuration);
Assert.assertFalse(player.isPlaying());
Assert.assertEquals(totalPackets, player.countPacketsSent());
Assert.assertEquals(totalOctets, player.countOctetsSent());
this.dtmfDetector.flushBuffer();
Assert.assertEquals(10, dtmfListener.countTones());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("2", dtmfListener.pollTone().getTone());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("#", dtmfListener.pollTone().getTone());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("2", dtmfListener.pollTone().getTone());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("1", dtmfListener.pollTone().getTone());
Assert.assertEquals("#", dtmfListener.pollTone().getTone());
}
private class DtmfListener implements DtmfDetectorListener {
private final Queue<DtmfEvent> tones;
public DtmfListener() {
this.tones = new ConcurrentLinkedQueue<>();
}
public int countTones() {
return this.tones.size();
}
public DtmfEvent pollTone() {
return tones.poll();
}
@Override
public void process(DtmfEvent event) {
this.tones.offer(event);
}
}
}