/**
* Copyright 2012 Voxbone SA/NV
*
* 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 com.voxbone.kelpie;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.spi.SelectorProvider;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.Properties;
import javax.sip.DialogState;
import org.apache.log4j.Logger;
import org.jabberstudio.jso.Packet;
import de.javawi.jstun.attribute.MappedAddress;
import de.javawi.jstun.attribute.MessageAttribute;
import de.javawi.jstun.attribute.MessageAttributeException;
import de.javawi.jstun.attribute.MessageAttributeParsingException;
import de.javawi.jstun.attribute.SourceAddress;
import de.javawi.jstun.attribute.UnknownMessageAttributeException;
import de.javawi.jstun.attribute.Username;
import de.javawi.jstun.attribute.MessageAttributeInterface.MessageAttributeType;
import de.javawi.jstun.header.MessageHeader;
import de.javawi.jstun.header.MessageHeaderParsingException;
import de.javawi.jstun.header.MessageHeaderInterface.MessageHeaderType;
import de.javawi.jstun.util.Address;
import de.javawi.jstun.util.UtilityException;
/**
* This is the RTP Media relay thread, can be for video or audio.
* For the xmpp side it also takes care of the STUN signaling
*
*/
public class RtpRelay extends Thread
{
// global variables
private static int RTP_MIN_PORT;
private static int RTP_MAX_PORT;
private static int nextPort = RTP_MIN_PORT;
private static boolean NAT_ENABLE = false;
private static boolean FIR_ENABLE = false;
private static boolean RTP_DEBUG = false;
private static boolean VUP_ENABLE = false;
private static int VUP_TIMER = 5000;
Timer retransTimer = new Timer("Stun Retransmit Thread");
private class StunTransmitter extends TimerTask
{
byte [] message;
SocketAddress dest;
DatagramChannel socket;
String remoteUser;
String localUser;
public StunTransmitter(byte [] message, String remoteUser, String localUser, SocketAddress dest, DatagramChannel socket)
{
this.message = message;
this.dest = dest;
this.socket = socket;
this.remoteUser = remoteUser;
this.localUser = localUser;
}
public void run()
{
if (RTP_DEBUG) {
logger.debug("[[" + cs.internalCallId + "]] Running RtpRelay::StunTransmitter ... : " + dest + " -- " + socket.socket().getLocalPort());
}
try
{
if (socket.isOpen())
{
socket.send(ByteBuffer.wrap(message), dest);
}
}
catch (IOException e)
{
logger.error("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter sending failed ==> " + dest + " -- " + socket.socket().getLocalPort());
}
}
public boolean cancel()
{
logger.debug("[[" + cs.internalCallId + "]] Cancelling RtpRelay::StunTransmitter ... : " + dest + " -- " + socket.socket().getLocalPort());
return super.cancel();
}
}
BlockingQueue<Character> dtmfQueue = new LinkedBlockingQueue<Character>();
private boolean video = false;
private class DtmfGenerator extends Thread
{
@Override
public void run()
{
while (sipSocket.isOpen())
{
char dtmf;
try
{
dtmf = dtmfQueue.take();
if (dtmf == '\0')
{
logger.debug("[[" + cs.internalCallId + "]] End flag detected in dtmf thread");
break;
}
long ts = 0;
synchronized (sipSocket)
{
ts = jabberTimestamp;
}
DtmfEvent de = new DtmfEvent(dtmf, ts, jabberSSRC);
logger.debug("[[" + cs.internalCallId + "]] Preparing to send dtmf " + dtmf);
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.startPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
logger.error("Error sending dtmf start packet!", e);
}
}
for (int i = 0; i < 5; i++)
{
Thread.sleep(20);
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.continuationPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
logger.error("Error sending dtmf continuation packet!", e);
}
}
}
for (int i = 0; i < 3; i++)
{
synchronized (sipSocket)
{
ByteBuffer buffer = ByteBuffer.wrap(de.endPacket());
RtpUtil.setSequenceNumber(buffer.array(), ++jabberSequence);
try
{
sipSocket.send(buffer, sipDest);
}
catch (IOException e)
{
logger.error("Error sending dtmf end packet!", e);
}
}
}
// Ensure at least 40 ms between dtmfs
Thread.sleep(40);
}
catch (InterruptedException e)
{
// do nothing
}
}
logger.debug("[[" + cs.internalCallId + "]] DtmfGenerator shut down");
}
}
public Hashtable<String, StunTransmitter> transmitters = new Hashtable<String, StunTransmitter>();
private class ID
{
byte [] id;
public ID(byte [] id)
{
this.id = id;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(id);
return result;
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
final ID other = (ID) obj;
if (!Arrays.equals(id, other.id))
{
return false;
}
return true;
}
}
private DatagramChannel jabberSocket;
private DatagramChannel sipSocket;
private DatagramChannel jabberSocketRtcp;
private DatagramChannel sipSocketRtcp;
private SocketAddress jabberDest;
private SocketAddress jabberDestRtcp;
private SocketAddress sipDest;
private SocketAddress sipDestRtcp;
byte [] sipSSRC = null;
byte [] jabberSSRC = null;
long jabberTimestamp = 0;
short jabberSequence = 0;
long lastVUpate = 0;
int firSeq = 0;
Logger logger = Logger.getLogger(this.getClass());
private CallSession cs = null;
private DatagramChannel makeDatagramChannel(boolean any) throws IOException
{
DatagramChannel socket = DatagramChannel.open();
while (!socket.socket().isBound())
{
nextPort += 1;
if (nextPort > RTP_MAX_PORT)
{
nextPort = RTP_MIN_PORT;
}
logger.debug("[[" + cs.internalCallId + "]] trying to bind to port: " + nextPort);
try
{
if (!any)
{
socket.socket().bind(new InetSocketAddress(SipService.getLocalIP(), nextPort));
}
else
{
socket.socket().bind(new InetSocketAddress(nextPort));
}
}
catch (SocketException e)
{
logger.error("Unable to make RTP socket!", e);
}
}
return socket;
}
public void sendBind(String user, String me, String destIp, int destPort, boolean rtcp)
{
MessageHeader sendMH = new MessageHeader(MessageHeaderType.BindingRequest);
Username name = new Username(user + me);
try
{
sendMH.generateTransactionID();
}
catch (UtilityException e)
{
logger.error("Unable to make stun transaction id", e);
}
if(name.getUsername().length() > 0)
{
sendMH.addMessageAttribute(name);
}
try
{
byte [] data = sendMH.getBytes();
logger.debug("[[" + cs.internalCallId + "]] Sending: " + Arrays.toString(data));
DatagramChannel socket = null;
if (rtcp)
{
socket = jabberSocketRtcp;
}
else
{
socket = jabberSocket;
}
synchronized (transmitters)
{
if (jabberDest == null)
{
logger.debug("[[" + cs.internalCallId + "]] Sending Bind to: " + destIp + ":" + destPort);
StunTransmitter st = new StunTransmitter(data, user, me, new InetSocketAddress(destIp, destPort), socket);
String key = name.getUsername() + "_" + destIp + ":" + destPort;
if (transmitters.containsKey(key))
{
transmitters.get(key).cancel();
transmitters.remove(key);
}
transmitters.put(key, st);
logger.debug("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter scheduled (fast) [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "] ==> " + st.socket.socket().getLocalPort());
retransTimer.schedule(st, 50, 50);
}
}
}
catch (UtilityException e)
{
logger.error("Error in stun bind!", e);
}
}
public RtpRelay(CallSession cs, boolean video) throws IOException
{
this.video = video;
this.cs = cs;
jabberSocket = makeDatagramChannel(false);
jabberSocketRtcp = makeDatagramChannel(false);
sipSocket = makeDatagramChannel(false);
sipSocketRtcp = makeDatagramChannel(false);
logger.info("[[" + cs.internalCallId + "]] RtpRelay created [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "]");
if (!video)
{
(new DtmfGenerator()).start();
}
start();
}
protected void finalize() throws Throwable
{
logger.info("[[" + cs.internalCallId + "]] RtpRelay destroyed [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "]");
super.finalize(); // not necessary if extending Object.
}
private void processStun(SocketAddress src, byte [] origData, DatagramChannel socket)
{
try
{
MessageHeader receiveMH = MessageHeader.parseHeader(origData);
if (receiveMH.getType() == MessageHeaderType.BindingErrorResponse)
{
return;
}
receiveMH.parseAttributes(origData);
if (receiveMH.getType() == MessageHeaderType.BindingRequest)
{
MessageHeader sendMH = new MessageHeader(MessageHeaderType.BindingResponse);
sendMH.setTransactionID(receiveMH.getTransactionID());
// Mapped address attribute
MappedAddress ma = new MappedAddress();
ma.setAddress(new Address(((InetSocketAddress) src).getAddress().getAddress()));
ma.setPort(((InetSocketAddress) src).getPort());
sendMH.addMessageAttribute(ma);
SourceAddress sa = new SourceAddress();
sa.setAddress(new Address(SipService.getLocalIP()));
sa.setPort(socket.socket().getLocalPort());
sendMH.addMessageAttribute(sa);
MessageAttribute usernameMA = receiveMH.getMessageAttribute(MessageAttributeType.Username);
if (usernameMA != null) {
sendMH.addMessageAttribute(usernameMA);
}
byte [] data = sendMH.getBytes();
socket.send(ByteBuffer.wrap(data), src);
synchronized (transmitters)
{
boolean reflexive = true;
String me = null;
Username user = (Username) receiveMH.getMessageAttribute(MessageAttributeType.Username);
for (String key : transmitters.keySet())
{
if(user == null) break;
StunTransmitter st = transmitters.get(key);
if(user.getUsername().startsWith(st.localUser))
{
me = st.localUser;
if (RTP_DEBUG) {
logger.debug("Local User found " + me);
}
}
if(src.equals(st.dest))
{
reflexive = false;
break;
}
}
if(reflexive && me != null)
{
logger.info("Reflexive detected " + user.getUsername());
String remote = user.getUsername().substring(me.length());
logger.info("Remote = " + remote + " me = " + me);
sendBind(remote, me, ((InetSocketAddress) src).getAddress().getHostAddress(), ((InetSocketAddress) src).getPort(), socket == jabberSocket ? false : true);
}
}
}
else if (receiveMH.getType() == MessageHeaderType.BindingResponse)
{
synchronized (transmitters)
{
if ( (this.jabberDest == null && socket == jabberSocket)
|| (this.jabberDestRtcp == null && socket == jabberSocketRtcp))
{
if (socket == jabberSocket)
{
this.jabberDest = src;
}
else
{
this.jabberDestRtcp = src;
}
Username user = (Username) receiveMH.getMessageAttribute(MessageAttributeType.Username);
StunTransmitter newTimer = null;
String newKey = null;
String desired = user.getUsername() + "_" + ((InetSocketAddress)src).getAddress().getHostAddress() + ":" + ((InetSocketAddress)src).getPort();
for (String key : transmitters.keySet())
{
StunTransmitter st = transmitters.get(key);
if (st.socket == socket)
{
try
{
st.cancel();
}
catch (Exception e)
{
}
logger.debug("[[" + cs.internalCallId + "]] Comparing " + key + " to " + desired);
if (key.equals(desired))
{
newKey = key;
newTimer = new StunTransmitter(st.message, st.remoteUser, st.localUser, st.dest, st.socket);
}
}
}
if (newTimer != null && newKey != null)
{
logger.debug("[[" + cs.internalCallId + "]] ++++++++++++++++ slowing retransmission " + newKey + " ++++++++++++++");
transmitters.put(newKey, newTimer);
logger.debug("[[" + cs.internalCallId + "]] RtpRelay::StunTransmitter scheduled (slow) [" + jabberSocket.socket().getLocalPort() + "][" + sipSocket.socket().getLocalPort() + "] ==> " + newTimer.socket.socket().getLocalPort());
retransTimer.schedule(newTimer, 100, 5000);
}
}
}
}
}
catch (MessageHeaderParsingException e)
{
// ignore (problem occurred in stun code)
}
catch (UnknownMessageAttributeException e)
{
// ignore (problem occurred in stun code)
}
catch (MessageAttributeParsingException e)
{
// ignore (problem occurred in stun code)
}
catch (UtilityException e)
{
logger.error("Error in processStun", e);
}
catch (MessageAttributeException e)
{
logger.error("Error in processStun", e);
}
catch (IOException e)
{
logger.error("Error in processStun", e);
}
catch (ArrayIndexOutOfBoundsException e)
{
// ignore (problem occurred in stun code)
}
catch (Exception e)
{
logger.error("Error in processStun", e);
}
}
/*
* Experimental. I believe that google uses special rtcp packets to send fast video updates - this is an unsuccessful
* attempt at implementing this feature.
*/
public void sendFIR()
{
if (FIR_ENABLE) {
byte [] buffer = new byte[40];
RtpUtil.buildFIR(buffer, firSeq++, sipSSRC, jabberSSRC);
try
{
jabberSocketRtcp.send(ByteBuffer.wrap(buffer), jabberDestRtcp);
}
catch (Exception e)
{
logger.error("Error sending FIR packet!", e);
}
}
}
public void sendSipDTMF(char dtmf)
{
switch (dtmf)
{
case '0' :
case '1' :
case '2' :
case '3' :
case '4' :
case '5' :
case '6' :
case '7' :
case '8' :
case '9' :
case '*' :
case '#' :
case 'A' :
case 'B' :
case 'C' :
case 'D' :
logger.debug("[[" + cs.internalCallId + "]] Logging dtmf " + dtmf + " for generation");
try
{
dtmfQueue.put(dtmf);
}
catch (InterruptedException e)
{
logger.error("Interrupted why queueing a dtmf", e);
}
break;
default :
logger.warn("[[" + cs.internalCallId + "]] Ignoring invalid dtmf " + dtmf);
}
}
public void run()
{
logger.info("[[" + cs.internalCallId + "]] RtpRelay Thread Started");
Selector sel = null;
try
{
sel = SelectorProvider.provider().openSelector();
sipSocket.configureBlocking(false);
sipSocketRtcp.configureBlocking(false);
jabberSocket.configureBlocking(false);
jabberSocketRtcp.configureBlocking(false);
sipSocket.register(sel, SelectionKey.OP_READ);
jabberSocket.register(sel, SelectionKey.OP_READ);
sipSocketRtcp.register(sel, SelectionKey.OP_READ);
jabberSocketRtcp.register(sel, SelectionKey.OP_READ);
ByteBuffer inputBuffer = ByteBuffer.allocate(20000);
ByteBuffer outputBuffer = ByteBuffer.allocate(20000);
byte [] outputBytes = new byte[20000];
while (sipSocket.isOpen())
{
if (sel.select(1000) >= 0)
{
Iterator<SelectionKey> itr = sel.selectedKeys().iterator();
while (itr.hasNext())
{
SelectionKey key = itr.next();
itr.remove();
if (key.isValid() && key.isReadable())
{
DatagramChannel socket = (DatagramChannel) key.channel();
inputBuffer.clear();
if (!socket.isOpen())
{
logger.error("[[" + cs.internalCallId + "]] Socket is not open ... ignoring");
continue;
}
SocketAddress src = socket.receive(inputBuffer);
if (src == null)
{
logger.error("[[" + cs.internalCallId + "]] Src is null ... ignoring");
continue;
}
if ((inputBuffer.get(0) & 0x80) != 0)
{
DatagramChannel destSocket;
SocketAddress destAddr;
inputBuffer.flip();
if (socket == sipSocket)
{
if(NAT_ENABLE && !src.equals(sipDest))
{
logger.debug("Nat detected, updating sip rtp destination from " + sipDest + " to " + src);
sipDest = src;
}
destSocket = jabberSocket;
destAddr = jabberDest;
if (this.sipSSRC == null)
{
this.sipSSRC = RtpUtil.getSSRC(inputBuffer.array());
}
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
}
else if (socket == sipSocketRtcp)
{
destSocket = jabberSocketRtcp;
destAddr = jabberDestRtcp;
if(NAT_ENABLE && !src.equals(sipDestRtcp))
{
logger.debug("Nat detected, updating sip rtcp destination from " + sipDestRtcp + " to " + src);
sipDestRtcp = src;
}
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
}
else if (socket == jabberSocketRtcp)
{
destSocket = sipSocketRtcp;
destAddr = sipDestRtcp;
if (destSocket != null && destAddr != null)
{
destSocket.send(inputBuffer, destAddr);
}
if (video && VUP_ENABLE && System.currentTimeMillis() - lastVUpate > VUP_TIMER
&& (this.cs.sipDialog.getState() != null && this.cs.sipDialog.getState() != DialogState.EARLY) )
{
SipService.sendVideoUpdate(this.cs);
lastVUpate = System.currentTimeMillis();
}
}
else
{
destSocket = sipSocket;
destAddr = sipDest;
if (this.jabberSSRC == null)
{
this.jabberSSRC = RtpUtil.getSSRC(inputBuffer.array());
}
synchronized (destSocket)
{
if (destSocket != null && destAddr != null)
{
if (!video)
{
this.jabberTimestamp = RtpUtil.getTimeStamp(inputBuffer.array());
RtpUtil.setSequenceNumber(inputBuffer.array(), ++this.jabberSequence);
if (destSocket.isOpen())
{
destSocket.send(inputBuffer, destAddr);
}
}
else
{
// TODO: google uses H264 SVC, the rest of the world uses AVC, so convert
// google now supports AVC so we should adapt to that
/*
int length = RtpUtil.filterSVC(inputBuffer.array(), outputBytes, inputBuffer.remaining());
outputBuffer.clear();
outputBuffer.put(outputBytes, 0, length);
outputBuffer.flip();
*/
if (destSocket.isOpen())
{
destSocket.send(inputBuffer, destAddr);
}
}
}
}
}
}
else
{
this.processStun(src, inputBuffer.array(), socket);
}
}
}
}
}
}
catch (IOException e)
{
logger.error("Error in RTP relay thread!", e);
}
catch (Exception e)
{
logger.error("Error in RTP relay thread!", e);
}
finally
{
if (sel != null)
{
try
{
sel.close();
}
catch (IOException e)
{
// ignore (we're dead anyhow)
}
}
}
logger.info("[[" + cs.internalCallId + "]] RtpRelay Thread Stopped");
}
public void setSipDest(String host, int port)
{
this.sipDest = new InetSocketAddress(host, port);
this.sipDestRtcp = new InetSocketAddress(host, port + 1);
}
public int getSipPort()
{
return this.sipSocket.socket().getLocalPort();
}
public int getSipRtcpPort()
{
return this.sipSocketRtcp.socket().getLocalPort();
}
public int getJabberPort()
{
return this.jabberSocket.socket().getLocalPort();
}
public int getJabberRtcpPort()
{
return this.jabberSocketRtcp.socket().getLocalPort();
}
public void shutdown()
{
logger.debug("[[" + cs.internalCallId + "]] Shutdown of rtp thread requested");
synchronized (transmitters)
{
logger.debug("[[" + cs.internalCallId + "]] number of transmitters : " + transmitters.size());
for (String key : transmitters.keySet())
{
logger.debug("[[" + cs.internalCallId + "]] cancelling transmitter : " + key);
transmitters.get(key).cancel();
}
}
try
{
dtmfQueue.put('\0');
}
catch (InterruptedException e)
{
logger.error("unable to queue shutdown signal!", e);
}
try
{
sipSocket.close();
}
catch (IOException e)
{
logger.error("unable to close sip-side rtp socket!", e);
}
try
{
jabberSocket.close();
}
catch (IOException e)
{
logger.error("unable to close xmpp-side rtp socket!", e);
}
try
{
sipSocketRtcp.close();
}
catch (IOException e)
{
logger.error("Error in rtcp shutdown", e);
}
try
{
jabberSocketRtcp.close();
}
catch (IOException e)
{
logger.error("Error in rtcp shutdown", e);
}
}
public static void configure(Properties properties)
{
RTP_MIN_PORT = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.min_port", "8000"));
RTP_MAX_PORT = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.max_port", "10000"));
NAT_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.nat_enable", "false"));
FIR_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.fir_enable", "false"));
RTP_DEBUG = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.debug", "false"));
VUP_ENABLE = Boolean.parseBoolean(properties.getProperty("com.voxbone.kelpie.rtp.vup_enable", "true"));
VUP_TIMER = Integer.parseInt(properties.getProperty("com.voxbone.kelpie.rtp.vup_timer", "5000"));
nextPort = RTP_MIN_PORT;
}
}