/******************************************************************************* * Copyright 2013-2016 alladin-IT GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package at.alladin.rmbt.client.v2.task; import java.io.DataOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.HashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import at.alladin.rmbt.client.QualityOfServiceTest; import at.alladin.rmbt.client.v2.task.result.QoSTestResult; import at.alladin.rmbt.client.v2.task.result.QoSTestResultEnum; import at.alladin.rmbt.util.net.rtp.RealtimeTransportProtocol.PayloadType; import at.alladin.rmbt.util.net.rtp.RealtimeTransportProtocol.RtpException; import at.alladin.rmbt.util.net.rtp.RtpPacket; import at.alladin.rmbt.util.net.rtp.RtpUtil; import at.alladin.rmbt.util.net.rtp.RtpUtil.RtpControlData; import at.alladin.rmbt.util.net.rtp.RtpUtil.RtpQoSResult; import at.alladin.rmbt.util.net.udp.StreamSender.UdpStreamCallback; /** * * @author lb * * As of RFC 3550 and RFC 3551 most RTP (VoIP) Codecs have a sampling rate of 8kHz.<br> * The delay between the packets is set to 20ms for most codecs.<br> * The sample size varies from 2 (G726-16) to 16 (L16) bits per sample. Some codecs have a variable sample size.<br> * <br> * * The default VoIP test will be: * <ul> * <li>sampling rate: 8000 Hz</li> * <li>size: 8bit per sample</li> * <li>time/packet: 20ms</li> * </ul> * This is similar to the G722 audio codec (ITU-T Recommendation G.722).<br> * The G722 codec's actual sampling rate is 16kHz but because it was erroneously assigned in RFC 1890 with 8kHz * it needs to have this sampling rate to assure backward compatibility. * */ public class VoipTask extends AbstractQoSTask { private final static Pattern VOIP_RECEIVE_RESPONSE_PATTERN = Pattern.compile("VOIPRESULT (-?[\\d]*) (-?[\\d]*) (-?[\\d]*) (-?[\\d]*) (-?[\\d]*) (-?[\\d]*) (-?[\\d]*) (-?[\\d]*)"); private final static Pattern VOIP_OK_PATTERN = Pattern.compile("OK ([\\d]*)"); private final Integer outgoingPort; private final Integer incomingPort; private final long callDuration; private final long timeout; private final long delay; private final int sampleRate; private final int bitsPerSample; private final PayloadType payloadType; private final static long DEFAULT_TIMEOUT = 3000000000L; //3s private final static long DEFAULT_CALL_DURATION = 1000000000L; //1s private final static long DEFAULT_DELAY = 20000000L; //20ms private final static int DEFAULT_SAMPLE_RATE = 8000; //8kHz private final static int DEFAULT_BITS_PER_SAMPLE = 8; //8 bits per sample private final static PayloadType DEFAULT_PAYLOAD_TYPE = PayloadType.PCMA; public final static String PARAM_BITS_PER_SAMLE = "bits_per_sample"; public final static String PARAM_SAMPLE_RATE = "sample_rate"; public final static String PARAM_DURATION = "call_duration"; //call duration in ns public final static String PARAM_PORT = "in_port"; public final static String PARAM_PORT_OUT = "out_port"; public final static String PARAM_TIMEOUT = "timeout"; public final static String PARAM_DELAY = "delay"; public final static String PARAM_PAYLOAD = "payload"; public final static String RESULT_PAYLOAD = "voip_objective_payload"; public final static String RESULT_IN_PORT = "voip_objective_in_port"; public final static String RESULT_OUT_PORT = "voip_objective_out_port"; public final static String RESULT_CALL_DURATION = "voip_objective_call_duration"; public final static String RESULT_BITS_PER_SAMPLE = "voip_objective_bits_per_sample"; public final static String RESULT_SAMPLE_RATE = "voip_objective_sample_rate"; public final static String RESULT_DELAY = "voip_objective_delay"; public final static String RESULT_TIMEOUT = "voip_objective_timeout"; public final static String RESULT_STATUS = "voip_result_status"; public final static String RESULT_VOIP_PREFIX = "voip_result"; public final static String RESULT_INCOMING_PREFIX = "_in_"; public final static String RESULT_OUTGOING_PREFIX = "_out_"; public final static String RESULT_SHORT_SEQUENTIAL = "short_seq"; public final static String RESULT_LONG_SEQUENTIAL = "long_seq"; public final static String RESULT_MAX_JITTER = "max_jitter"; public final static String RESULT_MEAN_JITTER = "mean_jitter"; public final static String RESULT_MAX_DELTA = "max_delta"; public final static String RESULT_SKEW = "skew"; public final static String RESULT_NUM_PACKETS = "num_packets"; public final static String RESULT_SEQUENCE_ERRORS = "sequence_error"; /** * * @param taskDesc */ public VoipTask(QualityOfServiceTest nnTest, TaskDesc taskDesc, int threadId) { super(nnTest, taskDesc, threadId, threadId); String value = (String) taskDesc.getParams().get(PARAM_DURATION); this.callDuration = value != null ? Long.valueOf(value) : DEFAULT_CALL_DURATION; value = (String) taskDesc.getParams().get(PARAM_PORT); this.incomingPort = value != null ? Integer.valueOf(value) : null; value = (String) taskDesc.getParams().get(PARAM_PORT_OUT); this.outgoingPort = value != null ? Integer.valueOf(value) : null; value = (String) taskDesc.getParams().get(PARAM_TIMEOUT); this.timeout = value != null ? Long.valueOf(value) : DEFAULT_TIMEOUT; value = (String) taskDesc.getParams().get(PARAM_DELAY); this.delay = value != null ? Long.valueOf(value) : DEFAULT_DELAY; value = (String) taskDesc.getParams().get(PARAM_BITS_PER_SAMLE); this.bitsPerSample = value != null ? Integer.valueOf(value) : DEFAULT_BITS_PER_SAMPLE; value = (String) taskDesc.getParams().get(PARAM_SAMPLE_RATE); this.sampleRate = value != null ? Integer.valueOf(value) : DEFAULT_SAMPLE_RATE; value = (String) taskDesc.getParams().get(PARAM_PAYLOAD); this.payloadType = value != null ? PayloadType.getByCodecValue(Integer.valueOf(value), DEFAULT_PAYLOAD_TYPE) : DEFAULT_PAYLOAD_TYPE; } /** * */ public QoSTestResult call() throws Exception { final AtomicInteger ssrc = new AtomicInteger(-1); final QoSTestResult result = initQoSTestResult(QoSTestResultEnum.VOIP); result.getResultMap().put(RESULT_BITS_PER_SAMPLE, bitsPerSample); result.getResultMap().put(RESULT_CALL_DURATION, callDuration); result.getResultMap().put(RESULT_DELAY, delay); result.getResultMap().put(RESULT_IN_PORT, incomingPort); result.getResultMap().put(RESULT_OUT_PORT, outgoingPort); result.getResultMap().put(RESULT_SAMPLE_RATE, sampleRate); result.getResultMap().put(RESULT_PAYLOAD, payloadType.getValue()); result.getResultMap().put(RESULT_STATUS, "OK"); try { onStart(result); final Random r = new Random(); final int initialSequenceNumber = r.nextInt(10000); final CountDownLatch latch = new CountDownLatch(1); final Map<Integer, RtpControlData> rtpControlDataList = new HashMap<Integer, RtpUtil.RtpControlData>(); final ControlConnectionResponseCallback callback = new ControlConnectionResponseCallback() { public void onResponse(String response, String request) { if (response != null && response.startsWith("OK")) { final Matcher m = VOIP_OK_PATTERN.matcher(response); if (m.find()) { DatagramSocket dgsock = null; try { ssrc.set(Integer.parseInt(m.group(1))); dgsock = new DatagramSocket(); final UdpStreamCallback receiveCallback = new UdpStreamCallback() { public boolean onSend(DataOutputStream dataOut, int packetNumber) throws IOException { //nothing to do here return true; } public synchronized void onReceive(DatagramPacket dp) throws IOException { final long receivedNs = System.nanoTime(); final byte[] data = dp.getData(); try { final RtpPacket rtp = new RtpPacket(data); rtpControlDataList.put(rtp.getSequnceNumber(), new RtpControlData(rtp, receivedNs)); } catch (RtpException e) { e.printStackTrace(); } } public void onBind(Integer port) throws IOException { result.getResultMap().put(RESULT_IN_PORT, port); } }; RtpUtil.runVoipStream(null, true, InetAddress.getByName(getTestServerAddr()), outgoingPort, incomingPort, sampleRate, bitsPerSample, payloadType, initialSequenceNumber, ssrc.get(), TimeUnit.MILLISECONDS.convert(callDuration, TimeUnit.NANOSECONDS), TimeUnit.MILLISECONDS.convert(delay, TimeUnit.NANOSECONDS), TimeUnit.MILLISECONDS.convert(timeout, TimeUnit.NANOSECONDS), true, receiveCallback); } catch (InterruptedException e) { result.getResultMap().put(RESULT_STATUS, "TIMEOUT"); e.printStackTrace(); } catch (TimeoutException e) { result.getResultMap().put(RESULT_STATUS, "TIMEOUT"); e.printStackTrace(); } catch (Exception e) { result.getResultMap().put(RESULT_STATUS, "ERROR"); e.printStackTrace(); } finally { if (dgsock != null && !dgsock.isClosed()) { dgsock.close(); } } } } else { result.getResultMap().put(RESULT_STATUS, "ERROR"); } latch.countDown(); } }; /* * syntax: VOIPTEST 0 1 2 3 4 5 6 7 * 0 = outgoing port (server port) * 1 = incoming port (client port) * 2 = sample rate (in Hz) * 3 = bits per sample * 4 = packet delay in ms * 5 = call duration (test duration) in ms * 6 = starting sequence number (see rfc3550, rtp header: sequence number) * 7 = payload type */ sendCommand("VOIPTEST " + outgoingPort + " " + (incomingPort == null ? outgoingPort : incomingPort) + " " + sampleRate + " " + bitsPerSample + " " + TimeUnit.MILLISECONDS.convert(delay, TimeUnit.NANOSECONDS) + " " + TimeUnit.MILLISECONDS.convert(callDuration, TimeUnit.NANOSECONDS) + " " + initialSequenceNumber + " " + payloadType.getValue(), callback); //wait for countdownlatch or timeout: latch.await(timeout, TimeUnit.NANOSECONDS); //if rtpreceivestream did not finish cancel the task /* if (!rtpInTimeoutTask.isDone()) { rtpInTimeoutTask.cancel(true); } */ final CountDownLatch resultLatch = new CountDownLatch(1); final ControlConnectionResponseCallback incomingResultRequestCallback = new ControlConnectionResponseCallback() { public void onResponse(final String response, final String request) { if (response != null && response.startsWith("VOIPRESULT")) { System.out.println(response); Matcher m = VOIP_RECEIVE_RESPONSE_PATTERN.matcher(response); if (m.find()) { final String prefix = RESULT_VOIP_PREFIX + RESULT_OUTGOING_PREFIX; result.getResultMap().put(prefix + RESULT_MAX_JITTER, Long.parseLong(m.group(1))); result.getResultMap().put(prefix + RESULT_MEAN_JITTER, Long.parseLong(m.group(2))); result.getResultMap().put(prefix + RESULT_MAX_DELTA, Long.parseLong(m.group(3))); result.getResultMap().put(prefix + RESULT_SKEW, Long.parseLong(m.group(4))); result.getResultMap().put(prefix + RESULT_NUM_PACKETS, Long.parseLong(m.group(5))); result.getResultMap().put(prefix + RESULT_SEQUENCE_ERRORS, Long.parseLong(m.group(6))); result.getResultMap().put(prefix + RESULT_SHORT_SEQUENTIAL, Long.parseLong(m.group(7))); result.getResultMap().put(prefix + RESULT_LONG_SEQUENTIAL, Long.parseLong(m.group(8))); } resultLatch.countDown(); } } }; //wait a short amount of time until requesting results Thread.sleep(100); //request server results: if (ssrc.get() >= 0) { sendCommand("GET VOIPRESULT " + ssrc.get(), incomingResultRequestCallback); resultLatch.await(CONTROL_CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS); } final RtpQoSResult rtpResults = rtpControlDataList.size() > 0 ? RtpUtil.calculateQoS(rtpControlDataList, initialSequenceNumber, sampleRate) : null; final String prefix = RESULT_VOIP_PREFIX + RESULT_INCOMING_PREFIX; if (rtpResults != null) { result.getResultMap().put(prefix + RESULT_MAX_JITTER, rtpResults.getMaxJitter()); result.getResultMap().put(prefix + RESULT_MEAN_JITTER, rtpResults.getMeanJitter()); result.getResultMap().put(prefix + RESULT_MAX_DELTA, rtpResults.getMaxDelta()); result.getResultMap().put(prefix + RESULT_SKEW, rtpResults.getSkew()); result.getResultMap().put(prefix + RESULT_NUM_PACKETS, rtpResults.getReceivedPackets()); result.getResultMap().put(prefix + RESULT_SEQUENCE_ERRORS, rtpResults.getOutOfOrder()); result.getResultMap().put(prefix + RESULT_SHORT_SEQUENTIAL, rtpResults.getMinSequential()); result.getResultMap().put(prefix + RESULT_LONG_SEQUENTIAL, rtpResults.getMaxSequencial()); } else { result.getResultMap().put(prefix + RESULT_MAX_JITTER, null); result.getResultMap().put(prefix + RESULT_MEAN_JITTER, null); result.getResultMap().put(prefix + RESULT_MAX_DELTA, null); result.getResultMap().put(prefix + RESULT_SKEW, null); result.getResultMap().put(prefix + RESULT_NUM_PACKETS, 0); result.getResultMap().put(prefix + RESULT_SEQUENCE_ERRORS, null); result.getResultMap().put(prefix + RESULT_SHORT_SEQUENTIAL, null); result.getResultMap().put(prefix + RESULT_LONG_SEQUENTIAL, null); } return result; } catch (Exception e) { e.printStackTrace(); throw e; } finally { onEnd(result); } } /* * (non-Javadoc) * @see at.alladin.rmbt.client.v2.task.AbstractRmbtTask#initTask() */ @Override public void initTask() { } /* * (non-Javadoc) * @see at.alladin.rmbt.client.v2.task.QoSTask#getTestType() */ public QoSTestResultEnum getTestType() { return QoSTestResultEnum.VOIP; } /* * (non-Javadoc) * @see at.alladin.rmbt.client.v2.task.QoSTask#needsQoSControlConnection() */ public boolean needsQoSControlConnection() { return true; } }