package javaforce.voip; import java.net.*; import java.util.*; import javaforce.*; /** * Handles sending/receiving RTP packets. */ public class RTP implements STUN.Listener { private static int nextlocalrtpport = 32768; private static int rtpmin = 32768; private static int rtpmax = 65536; protected DatagramSocket sock1, sock2; private Worker worker1, worker2; //inbound Workers private int localrtpport; public volatile boolean active = false; protected RTPInterface iface; private int mtu = 1500; //max size of packet protected boolean rawMode; public Vector<RTPChannel> channels = new Vector<RTPChannel>(); public static long now = 0; //this is copied into each RTPChannel as it receives packets private static boolean hasBouncyCastle; //TURN related data protected static boolean useTURN = false; protected static String turnHost, turnIP, turnUser, turnPass; protected STUN stun1, stun2; protected byte turnToken[]; protected long turnAllocExpires; public Object userobj; //free to use public static boolean debug = false; //set to true and recompile to get a lot of output public final static Codec CODEC_UNKNOWN = new Codec("?", -1); public final static Codec CODEC_G711u = new Codec("PCMU", 0); //patent expired public final static Codec CODEC_G711a = new Codec("PCMA", 8); //patent expired public final static Codec CODEC_G722 = new Codec("G722", 9); //patent expired public final static Codec CODEC_G729a = new Codec("G729", 18); //patent encumbered public final static Codec CODEC_JPEG = new Codec("JPEG", 26); //public domain public final static Codec CODEC_H263 = new Codec("H263", 34); //patent expired //dynamic ids (96-127) public final static Codec CODEC_VP8 = new Codec("VP8", 96); //open source (Google!) public final static Codec CODEC_H264 = new Codec("H264", 97); //patent encumbered public final static Codec CODEC_H263_1998 = new Codec("H263-1998", 98); //patent expired public final static Codec CODEC_H263_2000 = new Codec("H263-2000", 99); //patent expired public final static Codec CODEC_RFC2833 = new Codec("telephone-event", 100); static { try { Class.forName("org.bouncycastle.crypto.tls.TlsServer"); hasBouncyCastle = true; } catch (Exception e) { // JFLog.log(e); JFLog.log("Warning:BouncyCastle not found, SRTP/DTLS not available"); } } public static void enableTURN(String host, String user, String pass) { useTURN = true; turnHost = host; turnUser = user; turnPass = pass; } public static void disableTURN() { useTURN = false; } public static synchronized int getnextlocalrtpport() { int ret = nextlocalrtpport; nextlocalrtpport += 2; if (nextlocalrtpport + 1 > rtpmax) { nextlocalrtpport = rtpmin; } return ret; } public int getlocalrtpport() { if (useTURN) { return stun1.getPort(); } else { return localrtpport; } } private boolean turnSuccess; private boolean turnFailed; protected volatile RTPChannel bindingChannel; protected final Object bindLock = new Object(); //public interface STUN.Listener public void stunPublicIP(STUN stun, String ip, int port) {} public void turnAlloc(STUN stun, String ip, int port, byte token[], int lifetime) { turnToken = token; turnSuccess = true; turnIP = ip; //static - should be the same for all allocations //lifetime = 600 (typically) turnAllocExpires = System.currentTimeMillis() + (lifetime * 1000); } public void turnBind(STUN stun) { turnSuccess = true; //not sure why Xlite does rebind early (RFC says 10mins) bindingChannel.turnBindExpires = System.currentTimeMillis() + (300 * 1000); } public void turnRefresh(STUN stun, int lifetime) { turnAllocExpires = System.currentTimeMillis() + (lifetime * 1000); } public void turnFailed(STUN stun) { turnFailed = true; } public void turnData(STUN stun, byte data[], int offset, int length, short turnChannel) { RTPChannel channel = findChannel(turnChannel); if (channel == null) return; if (stun == stun1) { channel.processRTP(data, offset, length); } else if (stun == stun2) { channel.processRTCP(data, offset, length); } } protected void wait4reset() { turnSuccess = false; turnFailed = false; } protected void wait4reply() throws Exception { for(int a=0;a<100;a++) { JF.sleep(10); if (turnFailed) throw new Exception("Turn failed"); if (turnSuccess) return; } throw new Exception("Turn timeout"); } /** For testing only */ public boolean init(RTPInterface iface, int port) { this.iface = iface; try { localrtpport = port; if (useTURN) { //alloc TURN relay wait4reset(); stun1 = new STUN(); if (!stun1.start(localrtpport, turnHost, turnUser, turnPass, this)) throw new Exception("STUN init failed"); stun1.requestAlloc(true, null); wait4reply(); if (turnToken == null) throw new Exception("Turn token missing"); JFLog.log("RTP:TURN:host=" + stun1.getIP()); JFLog.log("RTP:TURN:port=" + stun1.getPort()); wait4reset(); stun2 = new STUN(); if (!stun2.start(localrtpport + 1, turnHost, turnUser, turnPass, this)) throw new Exception("STUN init failed"); stun2.requestAlloc(false, turnToken); wait4reply(); JFLog.log("RTP:TURN:host=" + stun2.getIP()); JFLog.log("RTP:TURN:port=" + stun2.getPort()); } else { sock1 = new DatagramSocket(localrtpport); sock2 = new DatagramSocket(localrtpport + 1); } JFLog.log("RTP:localport=" + localrtpport); } catch (Exception e2) { JFLog.log(e2); return false; } return true; } public boolean init(RTPInterface iface) { this.iface = iface; for(int a=0;a<5;a++) { try { localrtpport = getnextlocalrtpport(); if (useTURN) { //alloc TURN relay wait4reset(); stun1 = new STUN(); if (!stun1.start(localrtpport, turnHost, turnUser, turnPass, this)) throw new Exception("STUN init failed"); stun1.requestAlloc(true, null); wait4reply(); if (turnToken == null) throw new Exception("Turn token missing"); JFLog.log("RTP:TURN:host=" + stun1.getIP()); JFLog.log("RTP:TURN:port=" + stun1.getPort()); wait4reset(); stun2 = new STUN(); if (!stun2.start(localrtpport + 1, turnHost, turnUser, turnPass, this)) throw new Exception("STUN init failed"); stun2.requestAlloc(false, turnToken); wait4reply(); JFLog.log("RTP:TURN:host=" + stun2.getIP()); JFLog.log("RTP:TURN:port=" + stun2.getPort()); } else { sock1 = new DatagramSocket(localrtpport); sock2 = new DatagramSocket(localrtpport + 1); } JFLog.log("RTP:localport=" + localrtpport); } catch (Exception e2) { JFLog.log(e2); continue; } return true; } return false; } /** * Sets maximum packet size. Must call before start(). (default = 1500) */ public void setMTU(int mtu) { if (active) { return; } this.mtu = mtu; } Random r = new Random(); protected short getNextTURNChannel() { return (short)(r.nextInt(0x7ffe - 0x4000) + 0x4000); } /** * Starts RTP session. */ public boolean start() { now = System.currentTimeMillis(); if (active) { return true; } active = true; if (!useTURN) { worker1 = new Worker(sock1, false); worker1.start(); worker2 = new Worker(sock2, true); worker2.start(); } return true; } /** * Stops RTP session and frees resources. */ public void stop() { if (!active) { return; } active = false; closeSockets(); freeWorkers(); if (useTURN) { if (stun1 != null) { stun1.requestRefresh(0); stun1.close(); stun1 = null; } if (stun2 != null) { stun2.requestRefresh(0); stun2.close(); stun2 = null; } } } private void freeWorkers() { if (worker1 != null) { try { worker1.join(); } catch (Exception e1) { } worker1 = null; } if (worker2 != null) { try { worker2.join(); } catch (Exception e2) { } worker2 = null; } } /** * Frees resources. */ public void uninit() { stop(); closeSockets(); freeWorkers(); } private void closeSockets() { if (sock1 != null) { try { sock1.close(); } catch (Exception e) { } sock1 = null; } if (sock2 != null) { try { sock2.close(); } catch (Exception e) { } sock2 = null; } } /** * Create a new RTP channel with a random ssrc id. */ public RTPChannel createChannel(SDP.Stream stream) { return createChannel(-1, stream); } /** * Create a new RTP channel with a specified ssrc id. */ public RTPChannel createChannel(int ssrc, SDP.Stream stream) { JFLog.log("RTP.createChannel()" + stream.getIP() + ":" + stream.port); RTPChannel channel = null; switch (stream.profile) { case AVP: case AVPF: channel = new RTPChannel(this, ssrc, stream); break; case SAVP: case SAVPF: if (!hasBouncyCastle) { JFLog.log("RTP:Couldn't create SRTPChannel"); return null; } channel = new SRTPChannel(this, ssrc, stream); break; case UNKNOWN: JFLog.log("RTP:Can not create unknown profile"); return null; } channels.add(channel); return channel; } /** Remote RTPChannel */ public void removeChannel(RTPChannel channel) { channels.remove(channel); } /** * Returns default RTP channel. */ public RTPChannel getDefaultChannel() { if (channels.size() == 0) return null; return channels.get(0); } public RTPChannel findChannel(short turnChannel) { for(int a=0;a<channels.size();a++) { RTPChannel channel = channels.get(a); if (!channel.active) continue; if (channel.turn1ch == turnChannel) return channel; if (channel.turn2ch == turnChannel) return channel; } return null; } public RTPChannel findChannel(String ip, int port) { for(int a=0;a<channels.size();a++) { RTPChannel channel = channels.get(a); if (!channel.active) continue; if (channel.stream.getIP().equals(ip) && (channel.stream.port == -1 || channel.stream.port == port)) { return channel; } } return null; } /** * Sets global RTP port range to use (should be set before calling init()). */ public static void setPortRange(int min, int max) { rtpmin = min; rtpmax = max; nextlocalrtpport = min; } /** * Reads inbound packets for RTP session. */ private class Worker extends Thread { private DatagramSocket sock; private boolean rtcp; public Worker(DatagramSocket sock, boolean rtcp) { this.sock = sock; this.rtcp = rtcp; } public void run() { byte data[] = new byte[mtu]; while (active) { try { DatagramPacket pack = new DatagramPacket(data, mtu); sock.receive(pack); int len = pack.getLength(); if (len < 12) { continue; } String remoteip = pack.getAddress().getHostAddress(); int remoteport = pack.getPort(); // JFLog.log("RTP:receive:" + remoteip + ":" + remoteport); //test if (rtcp) { RTPChannel channel = findChannel(remoteip, remoteport-1); if (channel == null) { JFLog.log("RTP:No channel found:" + remoteip + ":" + remoteport); continue; } channel.processRTCP(data, 0, len); } else { RTPChannel channel = findChannel(remoteip, remoteport); if (channel == null) { JFLog.log("RTP:No channel found:" + remoteip + ":" + remoteport); continue; } if (channel.stream.port == -1) { channel.stream.port = remoteport; //NATing JFLog.log("RTP : NAT port = " + channel.stream.getPort()); } channel.processRTP(data, 0, len); } } catch (SocketException e) { if (active) { JFLog.log(e); } active = false; } catch (Exception e) { JFLog.log(e); active = false; } } } } /** Keep Alive - call every 30 seconds to keep active. */ public void keepalive() { if (!active) return; now = System.currentTimeMillis(); //do refreshes a little sooner (75 seconds) (in case nonce changes) if (useTURN && (now + 75 * 1000) > turnAllocExpires) { //request another 10 mins stun1.requestRefresh(600); stun2.requestRefresh(600); } for(int a=0;a<channels.size();a++) { RTPChannel channel = channels.get(a); channel.keepalive(now); } } /** * Sets raw mode<br> Packets are not decoded, they are passed directly thru * RTPInterface.rtpPacket()<br> This is used by PBX to relay RTP packets * between call originator and terminator.<br> */ public void setRawMode(boolean state) { this.rawMode = state; } /** * Returns current raw mode */ public boolean getRawMode() { return rawMode; } public static String getTurnIP() { return turnIP; } public static String genIceufrag() { StringBuilder sb = new StringBuilder(); Random r = new Random(); for(int a=0;a<16;a++) { sb.append((char)('a' + r.nextInt(26))); } return sb.toString(); } public static String genIcepwd() { StringBuilder sb = new StringBuilder(); Random r = new Random(); for(int a=0;a<16;a++) { sb.append((char)('a' + r.nextInt(26))); } return sb.toString(); } public void setInterface(RTPInterface iface) { this.iface = iface; } public String toString() { int remoteport = -1; RTPChannel channel = getDefaultChannel(); if (channel != null) { remoteport = channel.getremoteport(); } return "RTP:{localport=" + localrtpport + ",remoteport=" + remoteport + ",mode=" + rawMode + "}"; } }