package jpbx.core;
/** WebRTC
*
* @author pquiring
*
* Created : Jan 4, 2014
*/
import java.io.*;
import java.util.*;
import javaforce.*;
import javaforce.service.*;
import javaforce.voip.*;
public class WebRTC implements WebSocketHandler, SIPClientInterface {
private static byte crt[], privateKey[];
private static String fingerprintSHA256;
public void init() {
try {
char password[] = "password".toCharArray();
FileInputStream fis = new FileInputStream(Paths.etc + "jpbx.key");
KeyMgmt key = new KeyMgmt();
key.open(fis, password);
fis.close();
crt = key.getCRT("jpbxlite").getEncoded();
fingerprintSHA256 = KeyMgmt.fingerprintSHA256(crt);
ArrayList<byte[]> chain = new ArrayList<byte[]>();
chain.add(crt);
java.security.cert.Certificate root = key.getCRT("root");
if (root != null) {
chain.add(root.getEncoded());
}
privateKey = key.getKEY("jpbxlite", password).getEncoded();
SRTPChannel.initDTLS(chain, privateKey, false);
if (false) {
//see bouncy castle DTLSClientTest
RTP testRTP = new RTP();
testRTP.init(null, 5556);
testRTP.start();
SDP sdp = new SDP();
SDP.Stream stream = sdp.addStream(SDP.Type.audio);
stream.addCodec(RTP.CODEC_G711u);
stream.ip = "10.1.1.2";
stream.port = -1;
RTPChannel testCH = testRTP.createChannel(stream);
testCH.start();
}
} catch (Exception e) {
JFLog.log(e);
}
}
public void doWebRTC1(WebRequest req, WebResponse res) throws Exception {
String args[] = req.getQueryString().split("&");
String user = "", room = "";
for(int a=0;a<args.length;a++) {
int x = args[a].indexOf("=") + 1;
if (args[a].startsWith("user=")) user = args[a].substring(x);
if (args[a].startsWith("room=")) room = args[a].substring(x);
}
RTC rtc = rooms.get(room);
JFLog.log("room:" + room + ":rtc=" + rtc);
if (rtc != null) {
if (rtc.inuse) {
StringBuilder html = new StringBuilder();
html.append("Room is full");
res.getOutputStream().write(html.toString().getBytes());
return;
} else {
JFLog.log("Room has one other guest:" + room);
}
} else {
JFLog.log("room is empty:" + room);
}
Random r = new Random();
if (user.length() == 0) {
user = "1010"; //Integer.toString(Math.abs(r.nextInt()));
}
if (room.length() == 0) {
room = Integer.toString(Math.abs(r.nextInt()));
}
StringBuilder html = new StringBuilder();
html.append("<html>");
html.append("<head>");
html.append(" <title>jPBXlite</title>");
html.append(" <link rel=stylesheet href='/static/webrtc.css' type='text/css'>");
html.append(" <script src='/static/adapter.js'></script>");
html.append(" <script type='text/javascript' src='/static/webrtc1.js'></script>");
html.append("</head>");
html.append("<body leftmargin=1 rightmargin=1 topmargin=1 bottommargin=1 vlink=ffffff link=ffffff alink=ffffff width=100% height=100%>");
html.append("<div id='container' ondblclick='enterFullScreen()' style='width: 608px; height: 456px; left: 336px; top: 0px;'>");
html.append(" <div id='card'>");
html.append(" <div id='local'>");
html.append(" <video id='localVideo' muted='true' autoplay='autoplay' style='opacity: 1;'></video>");
html.append(" </div>");
html.append(" <div id='remote'>");
html.append(" <video id='remoteVideo' autoplay='autoplay'></video>");
html.append(" <div id='mini'>");
html.append(" <video id='miniVideo' muted='true' autoplay='autoplay'></video>");
html.append(" </div>");
html.append(" </div>");
html.append(" </div>");
html.append("</div>");
html.append("<footer id='status'></footer>");
html.append("<div id='infoDiv'></div>");
html.append("<script>");
html.append(" var errorMessages = [];");
html.append(" var user = '" + user + "';");
html.append(" var roomKey = '" + room + "';");
html.append(" var roomLink = 'http://" + req.getHost() + "/webrtc1?room=" + room + "';");
html.append(" var initiator = " + (rtc == null ? "0" : "1") + ";");
html.append(" var pcConfig = null;"); //'iceServers': [{'url': 'stun:stun.services.mozilla.com'}]};");
html.append(" var pcConstraints = {};"); //'optional': [{'DtlsSrtpKeyAgreement': true}]};");
html.append(" var offerConstraints = {'optional': [], 'mandatory': {}};");
html.append(" var mediaConstraints = {'audio': true, 'video': true};");
html.append(" var stereo = false;");
html.append(" var audio_send_codec = 'PCMU/8000';");
html.append(" var audio_receive_codec = 'PCMU/8000';");
html.append(" var webSocketURL = 'ws://" + req.getHost() + ":" + WebConfig.http_port + "/webrtcsocket';");
html.append(" setTimeout(initialize, 1);");
html.append("</script>");
res.getOutputStream().write(html.toString().getBytes());
}
public void doWebRTC2(WebRequest req, WebResponse res) throws Exception {
int postLength = JF.atoi(req.getHeader("Content-Length"));
byte post[] = JF.readAll(req.getInputStream(), postLength);
String query = new String(post);
String args[] = query.split("&");
String user = "", pass = "", dial = "";
for(int a=0;a<args.length;a++) {
int x = args[a].indexOf("=") + 1;
if (args[a].startsWith("user=")) user = args[a].substring(x);
if (args[a].startsWith("pass=")) pass = args[a].substring(x);
if (args[a].startsWith("dial=")) dial = args[a].substring(x);
}
StringBuilder html = new StringBuilder();
html.append("<html>");
html.append("<head>");
html.append(" <title>jPBXlite</title>");
html.append(" <link rel=stylesheet href='/static/webrtc.css' type='text/css'>");
html.append(" <script src='/static/adapter.js'></script>");
html.append(" <script type='text/javascript' src='/static/webrtc2.js'></script>");
html.append("</head>");
html.append("<body leftmargin=1 rightmargin=1 topmargin=1 bottommargin=1 vlink=ffffff link=ffffff alink=ffffff width=100% height=100%>");
html.append("<div id='container' ondblclick='enterFullScreen()' style='width: 608px; height: 456px; left: 336px; top: 0px;'>");
html.append(" <div id='card'>");
html.append(" <div id='local'>");
html.append(" <video id='localVideo' muted='true' autoplay='autoplay' style='opacity: 1;'></video>");
html.append(" </div>");
html.append(" <div id='remote'>");
html.append(" <video id='remoteVideo' autoplay='autoplay'></video>");
html.append(" <div id='mini'>");
html.append(" <video id='miniVideo' muted='true' autoplay='autoplay'></video>");
html.append(" </div>");
html.append(" </div>");
html.append(" </div>");
html.append("</div>");
html.append("<footer id='status'></footer>");
html.append("<div id='infoDiv'></div>");
html.append("<script>");
html.append(" var errorMessages = [];");
html.append(" var user = '" + user + "';");
html.append(" var pass = '" + pass + "';");
html.append(" var dial = '" + dial + "';");
html.append(" var initiator = 1;");
html.append(" var pcConfig = null;");
html.append(" var pcConstraints = {};");
html.append(" var offerConstraints = {'optional': [], 'mandatory': {}};");
html.append(" var mediaConstraints = {'audio': true, 'video': true};");
html.append(" var stereo = false;");
html.append(" var audio_send_codec = 'PCMU/8000';");
html.append(" var audio_receive_codec = 'PCMU/8000';");
html.append(" var webSocketURL = 'wss://" + req.getHost() + ":" + WebConfig.https_port + "/webrtcsocket';");
html.append(" setTimeout(initialize, 1);");
html.append("</script>");
res.getOutputStream().write(html.toString().getBytes());
}
private class RTC {
public WebSocket sock;
public SIPClient sip;
public String callid;
public boolean talking;
public boolean inuse;
public RTC other;
}
private static HashMap<String, RTC>rooms = new HashMap<String, RTC>(); //share between http and https
private int sipmin = 6000;
private int sipmax = 7000;
private int sipnext = sipmin;
private static boolean use_sip = true; //enable webRTC sip support (experimental)
private synchronized int getlocalport() {
int port = sipnext++;
if (sipnext == sipmax) sipnext = sipmin;
return port;
}
public boolean doWebSocketConnect(WebSocket sock) {
if (!sock.getURL().equals("/webrtcsocket")) return false;
RTC rtc = new RTC();
rtc.sock = sock;
sock.userobj = rtc;
return true;
}
public void doWebSocketClosed(WebSocket sock) {
if (sock.userobj == null) return;
RTC rtc = (RTC)sock.userobj;
if (rtc.talking) {
rtc.sip.bye(rtc.callid);
rtc.talking = false;
}
if (use_sip) {
rtc.sip.uninit();
}
}
public void doWebSocketMessage(WebSocket sock, byte data[], int msg_type) {
if (sock.userobj == null) return;
//parse JSON message
// JFLog.log("WebSocketMessage:" + new String(data));
JFLog.log("RTC:JSON=" + new String(data));
try {
RTC rtc = (RTC)sock.userobj;
JSON.Element json = JSON.parse(new String(data));
//find type
String type = "", sdp = "", room = "", user = "", pass = "", dial = "";
for(int a=0;a<json.children.size();a++) {
JSON.Element e = json.children.get(a);
String key = e.key;
String val = e.value;
if (key.equals("type")) type = val;
else if (key.equals("sdp")) sdp = val;
else if (key.equals("room")) room = val;
else if (key.equals("user")) user = val;
else if (key.equals("pass")) pass = val;
else if (key.equals("dial")) dial = val;
}
if (type.equals("hello")) {
JFLog.log("hello:" + room);
rooms.put(room, rtc);
} else if (type.equals("register")) {
rtc.sip = new SIPClient();
rtc.sip.userobj = rtc;
rtc.sip.init("127.0.0.1", WebConfig.sip_port, getlocalport(), this, SIP.Transport.UDP);
rtc.sip.register(user, user, null, pass);
} else if (type.equals("offer")) {
RTC rtc2 = rooms.get(room);
StringBuilder json2 = new StringBuilder();
json2.append("{");
json2.append("\"type\":" + "\"offer\"");
json2.append(",\"sdp\":\"" + sdp + "\"");
json2.append("}");
rtc2.sock.write(json2.toString().getBytes(), WebSocket.TYPE_TEXT);
rtc2.other = rtc;
rtc2.inuse = true;
} else if (type.equals("invite")) {
sdp = "SIP/2.0 ???\r\n\r\n" + sdp.replaceAll("\\\\r\\\\n", "\r\n");
String lns[] = sdp.split("\r\n");
/* for(int a=0;a<lns.length;a++) {
//save some params
String ln = lns[a];
if (ln.startsWith("a=ice-ufrag:")) rtc.iceufrag = ln;
else if (ln.startsWith("a=ice-pwd:")) rtc.icepwd = ln;
else if (ln.startsWith("a=fingerprint:")) rtc.fingerprint = ln;
}*/
//send an invite to room (IVR)
JFLog.log("RTC:INVITE:SDP=" + sdp);
rtc.sip.invite(dial, SIP.getSDP(lns));
} else if (type.equals("bye")) {
if (rtc.sip != null) {
if (rtc.callid != null) {
rtc.sip.bye(rtc.callid);
rtc.callid = null;
}
} else {
rooms.remove(room);
}
} else if (type.equals("answer")) {
RTC rtc2 = rooms.get(room);
StringBuilder json2 = new StringBuilder();
json2.append("{");
json2.append("\"type\":" + "\"answer\"");
json2.append(",\"sdp\":\"" + sdp + "\"");
json2.append("}");
rtc2.other.sock.write(json2.toString().getBytes(), WebSocket.TYPE_TEXT);
}
} catch (Exception e) {
JFLog.log(e);
}
}
public void onRegister(SIPClient sip, boolean success) {
}
public void onTrying(SIPClient sip, String callid) {
}
public void onRinging(SIPClient sip, String callid) {
}
public void onSuccess(SIPClient sip, String callid, SDP sdp, boolean complete) {
if (complete) {
RTC rtc = (RTC)sip.userobj;
String sdpstr = convertSDPtoWebSDP(rtc,sip.getSDP(callid));
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"type\":" + "\"answer\"");
json.append(",\"sdp\":\"" + sdpstr + "\"");
json.append("}");
JFLog.log("RTC:onSuccess:SDP=" + sdpstr.replaceAll("\\\\r\\\\n", "\r\n"));
rtc.sock.write(json.toString().getBytes(), WebSocket.TYPE_TEXT);
}
}
public void onBye(SIPClient sip, String callid) {
}
public int onInvite(SIPClient sip, String callid, String toName, String toNumber, SDP sdp) {
RTC rtc = (RTC)sip.userobj;
if (rtc.talking) {
//reinvite
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"type\":" + "\"answer\"");
json.append(",\"sdp\":\"" + convertSDPtoWebSDP(rtc, sip.getSDP(callid)) + "\"");
json.append("}");
rtc.sock.write(json.toString().getBytes(), WebSocket.TYPE_TEXT);
return 200;
}
return 486; //busy
}
public void onCancel(SIPClient sip, String callid, int code) {
}
public void onRefer(SIPClient sip, String callid) {
}
public void onNotify(SIPClient sip, String callid, String event, String msg) {
}
public void onAck(SIPClient sip, String callid, SDP sdp) {
}
//http://tools.ietf.org/html/rfc5763 - Secure RTP (fingerprint)
private String convertSDPtoWebSDP(RTC rtc, String sdp) {
String lns[] = sdp.split("\r\n");
StringBuilder out = new StringBuilder();
boolean fingerprint = false;
boolean m = false;
String ip = null;
int port = -1;
int id = 1234567890;
for(int a=0;a<lns.length;a++) {
String ln = lns[a];
if (ln.startsWith("a=content:")) continue; //unrecognized
if (ln.startsWith("c=")) {
//c=IN IP4 <ip>
ip = ln.substring(9);
}
if (ln.startsWith("m=")) {
ln = ln.replaceAll("RTP/AVP", "RTP/SAVPF"); //???
//m=audio port ...
if (!fingerprint) {
//add a bogus ice params and DTLS fingerprint before first m=
out.append("a=ice-ufrag:12345678");
out.append("\\r\\n");
out.append("a=ice-pwd:javaforce");
out.append("\\r\\n");
out.append("a=fingerprint:sha-256 " + fingerprintSHA256);
out.append("\\r\\n");
fingerprint = true;
}
if (m) {
//output candidate for last stream
out.append("a=setup:actpass"); //http://tools.ietf.org/html/rfc4145
out.append("\\r\\n");
out.append("a=candidate: 0 1 UDP " + (id++) + " " + ip + " " + port + " typ host"); // raddr " + ip + " rport " + port);
out.append("\\r\\n");
out.append("a=candidate: 0 2 UDP " + (id++) + " " + ip + " " + (port+1) + " typ host"); // raddr " + ip + " rport " + port);
out.append("\\r\\n");
out.append("a=rtcp-mux");
out.append("\\r\\n");
}
m = true;
port = JF.atoi(ln.split(" ")[1]);
}
out.append(ln);
out.append("\\r\\n");
}
if (m) {
//output candidate for last stream
out.append("a=setup:actpass");
out.append("\\r\\n");
out.append("a=candidate: 0 1 UDP " + (id++) + " " + ip + " " + port + " typ host"); // raddr " + ip + " rport " + port);
out.append("\\r\\n");
out.append("a=candidate: 0 2 UDP " + (id++) + " " + ip + " " + (port+1) + " typ host"); // raddr " + ip + " rport " + port);
out.append("\\r\\n");
out.append("a=rtcp-mux");
out.append("\\r\\n");
}
return out.toString();
}
}