/** * 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.util.Date; import java.util.LinkedList; import java.util.Vector; import javax.sdp.Attribute; import javax.sdp.MediaDescription; import javax.sdp.SdpException; import javax.sdp.SdpFactory; import javax.sdp.SdpParseException; import javax.sdp.SessionDescription; import javax.sdp.Time; import javax.sip.ClientTransaction; import javax.sip.Dialog; import javax.sip.ServerTransaction; import javax.sip.message.Message; import org.apache.log4j.Logger; import org.jabberstudio.jso.JID; import org.jabberstudio.jso.NSI; import org.jabberstudio.jso.Packet; import org.jabberstudio.jso.StreamElement; /** * * Represents a call, contains the information for the Jingle as well as the sip side of the call * */ public class CallSession { private static LinkedList<Payload> supported = new LinkedList<Payload>(); public static Payload PAYLOAD_SPEEX = new Payload(99, "speex", 16000, 22000); public static Payload PAYLOAD_SPEEX2 = new Payload(98, "speex", 8000, 11000); public static Payload PAYLOAD_PCMU = new Payload(0, "PCMU", 8000, 64000); public static Payload PAYLOAD_PCMA = new Payload(8, "PCMA", 8000, 64000); public static Payload PAYLOAD_G723 = new Payload(4, "G723", 8000, 6300); public static VPayload PAYLOAD_H263 = new VPayload(34, "H263", 90000, 512000, 320, 200, 15); public static VPayload PAYLOAD_H264 = new VPayload(97, "H264", 90000, 512000, 640, 480, 15); public static VPayload PAYLOAD_H264SVC = new VPayload(96, "H264-SVC", 90000, 512000, 640, 480, 15); static { supported.add(PAYLOAD_SPEEX); supported.add(PAYLOAD_SPEEX2); supported.add(PAYLOAD_PCMU); supported.add(PAYLOAD_PCMA); supported.add(PAYLOAD_G723); supported.add(PAYLOAD_H264); supported.add(PAYLOAD_H263); supported.add(PAYLOAD_H264SVC); } public static class Payload { int id; String name; int clockRate; int bitRate; public Payload(int id, String name, int clockRate, int bitRate) { this.id = id; this.name = name; this.clockRate = clockRate; this.bitRate = bitRate; } } public static class VPayload extends Payload { int width; int height; int framerate; public VPayload(int id, String name, int clockRate, int bitRate, int width, int height, int framerate) { super(id, name, clockRate, bitRate); this.width = width; this.height = height; this.framerate = framerate; } } String jabberSessionId; JID jabberRemote; JID jabberLocal; String jabberInitiator; String candidateUser; String candidateVUser; boolean sentTransport = false; boolean sentVTransport = false; boolean callAccepted = false; Dialog sipDialog; ServerTransaction inviteTransaction; ClientTransaction inviteOutTransaction; public RtpRelay relay; public RtpRelay vRelay; LinkedList<Payload> offerPayloads = new LinkedList<Payload>(); LinkedList<Payload> answerPayloads = new LinkedList<Payload>(); LinkedList<VPayload> offerVPayloads = new LinkedList<VPayload>(); LinkedList<VPayload> answerVPayloads = new LinkedList<VPayload>(); Logger logger = Logger.getLogger(this.getClass()); private static int nextInternalCallId = 0; public String internalCallId; public CallSession() { internalCallId = "CS" + String.format("%08x", nextInternalCallId++); try { relay = new RtpRelay(this, false); } catch (IOException e) { logger.error("Can't setup rtp relay", e); } } private boolean isSupportedPayload(Payload payload) { for (Payload p : supported) { if (p.name.equalsIgnoreCase(payload.name) && payload.clockRate == p.clockRate) { return true; } } return false; } public Payload getByName(String name, int clockRate) { for (Payload p : supported) { if (p.name.equalsIgnoreCase(name) && p.clockRate == clockRate) { return p; } } return null; } public Payload getByName(String name) { for (Payload p : supported) { if (p instanceof VPayload) { VPayload vp = (VPayload) p; if (p.name.equalsIgnoreCase(name)) { return vp; } } } return null; } public Payload getById(int id) { for (Payload p : supported) { if (p.id == id) { return p; } } return null; } public void parseInitiate(Packet p, boolean jingle) { if(!jingle) { StreamElement session = p.getFirstElement(new NSI("session", "http://www.google.com/session")); jabberSessionId = session.getID(); jabberRemote = p.getFrom(); jabberLocal = p.getTo(); jabberInitiator = session.getAttributeValue("initiator"); parseSession(session, true); } else { StreamElement session = p.getFirstElement("jingle"); jabberSessionId = session.getAttributeValue("sid"); jabberRemote = p.getFrom(); jabberLocal = p.getTo(); jabberInitiator = session.getAttributeValue("initiator"); parseJingleSession(session, true); } } public void parseAccept(Packet p, boolean jingle) { if(jingle) { StreamElement session = p.getFirstElement("jingle"); parseJingleSession(session, false); } else { StreamElement session = p.getFirstElement(new NSI("session", "http://www.google.com/session")); parseSession(session, false); } } private void parseJingleSession(StreamElement session, boolean offer) { // StreamElement content = session.getFirstElement("content"); for(Object contObj : session.listElements("content")) { // loop content StreamElement content = (StreamElement) contObj; for(Object descObj : content.listElements("description")) { StreamElement desc = (StreamElement) descObj; boolean video = false; // Parse Video Description if( desc.getAttributeValue("media") != null && desc.getAttributeValue("media").equals("video") ) { video = true; logger.info("[[" + internalCallId + "]] Video call detected, enabling video rtp stream"); if (vRelay == null) { try { vRelay = new RtpRelay(this, true); } catch (IOException e) { logger.error("Can't setup video rtp relay", e); } } for (Object opt : desc.listElements("payload-type")) { StreamElement pt = (StreamElement) opt; try { int id = Integer.parseInt(pt.getAttributeValue("id")); String name = pt.getAttributeValue("name"); logger.debug("[[" + internalCallId + "]] found payload: " + name ); int framerate = 0; int width = 0; int height = 0; for (Object vparamObj : desc.listElements("parameter")) { StreamElement vparams = (StreamElement) vparamObj; if (vparams.getAttributeValue("framerate") != null) { framerate = Integer.parseInt(vparams.getAttributeValue("framerate")); } if (vparams.getAttributeValue("width") != null) { width = Integer.parseInt(vparams.getAttributeValue("width")); } if (vparams.getAttributeValue("height") != null) { height = Integer.parseInt(vparams.getAttributeValue("height")); } } // add video payload Payload p = getByName(name); if (p != null && p instanceof VPayload) { VPayload tmp = (VPayload) p; VPayload vp = null; // save the rtp map id, but load in our offical config.... if (framerate != 0 && width != 0 && height != 0) { vp = new VPayload(id, name, tmp.clockRate, tmp.bitRate, width, height, framerate); } else { vp = new VPayload(id, tmp.name, tmp.clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate); } if (offer) { offerVPayloads.add(vp); } else { answerVPayloads.add(vp); } } } catch (NumberFormatException e) { // ignore tags we don't understand (but write full log, in case we need to investigate) logger.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e); logger.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString()); logger.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString()); } } // Parse Audio Description } else if ( desc.getAttributeValue("media") != null && desc.getAttributeValue("media").equals("audio") ) { logger.info("[[" + internalCallId + "]] Audio call detected"); for (Object opt : desc.listElements("payload-type")) { StreamElement pt = (StreamElement) opt; try { int id = Integer.parseInt(pt.getAttributeValue("id")); String name = pt.getAttributeValue("name"); logger.debug("[[" + internalCallId + "]] found payload: " + name ); int clockrate = 0; if (pt.getAttributeValue("clockrate") != null) { clockrate = Integer.parseInt(pt.getAttributeValue("clockrate")); } int bitrate = 0; if (pt.getAttributeValue("bitrate") != null) { bitrate = Integer.parseInt(pt.getAttributeValue("bitrate")); } // add audio payload Payload payload = new Payload(id, name, clockrate, bitrate); if (isSupportedPayload(payload)) { if (offer) { offerPayloads.add(payload); } else { answerPayloads.add(payload); } } } catch (NumberFormatException e) { // ignore tags we don't understand (but write full log, in case we need to investigate) logger.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e); logger.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString()); logger.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString()); } } } } } } private void parseSession(StreamElement session, boolean offer) { StreamElement description = session.getFirstElement("description"); if (description.getNamespaceURI().equals("http://www.google.com/session/video")) { logger.info("[[" + internalCallId + "]] Video call detected, enabling video rtp stream"); if (vRelay == null) { try { vRelay = new RtpRelay(this, true); } catch (IOException e) { logger.error("Can't setup video rtp relay", e); } } } for (Object opt : description.listElements()) { StreamElement pt = (StreamElement) opt; if (pt.getNamespaceURI().equals("http://www.google.com/session/video") && pt.getLocalName().equals("payload-type")) { try { int id = Integer.parseInt(pt.getAttributeValue("id")); String name = pt.getAttributeValue("name"); // int width = Integer.parseInt(pt.getAttributeValue("width")); // int height = Integer.parseInt(pt.getAttributeValue("height")); //int framerate = Integer.parseInt(pt.getAttributeValue("framerate")); Payload p = getByName(name); if (p != null && p instanceof VPayload) { VPayload tmp = (VPayload) p; // save the rtp map id, but load in our offical config.... VPayload vp = new VPayload(id, tmp.name, tmp.clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate); if (offer) { offerVPayloads.add(vp); } else { answerVPayloads.add(vp); } } } catch (NumberFormatException e) { // ignore tags we don't understand (but write full log, in case we need to investigate) logger.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e); logger.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString()); logger.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString()); } } else if(pt.getNamespaceURI().equals("http://www.google.com/session/phone") && pt.getLocalName().equals("payload-type")) { try { int id = Integer.parseInt(pt.getAttributeValue("id")); String name = pt.getAttributeValue("name"); int clockrate = 0; if (pt.getAttributeValue("clockrate") != null) { clockrate = Integer.parseInt(pt.getAttributeValue("clockrate")); } int bitrate = 0; if (pt.getAttributeValue("bitrate") != null) { bitrate = Integer.parseInt(pt.getAttributeValue("bitrate")); } Payload payload = new Payload(id, name, clockrate, bitrate); if (isSupportedPayload(payload)) { if (offer) { offerPayloads.add(payload); } else { answerPayloads.add(payload); } } } catch (NumberFormatException e) { // ignore tags we don't understand (but write full log, in case we need to investigate) logger.warn("[[" + internalCallId + "]] failed to parse tag in session : ", e); logger.debug("[[" + internalCallId + "]] NumberFormatException -> session contents : " + session.toString()); logger.debug("[[" + internalCallId + "]] NumberFormatException -> description item contents : " + pt.toString()); } } } } public SessionDescription buildSDP(boolean offer) { SdpFactory sdpFactory = SdpFactory.getInstance(); try { SessionDescription sd = sdpFactory.createSessionDescription(); sd.setVersion(sdpFactory.createVersion(0)); long ntpts = SdpFactory.getNtpTime(new Date()); sd.setOrigin(sdpFactory.createOrigin("JabberGW", ntpts, ntpts, "IN", "IP4", SipService.getLocalIP())); sd.setSessionName(sdpFactory.createSessionName("Jabber Call")); Vector<Time> times = new Vector<Time>(); times.add(sdpFactory.createTime()); sd.setTimeDescriptions(times); sd.setConnection(sdpFactory.createConnection(SipService.getLocalIP())); int [] formats; Vector<Attribute> attributes = new Vector<Attribute>(); if (offer) { formats = new int[offerPayloads.size() + 1]; int i = 0; for (Payload p : offerPayloads) { formats[i++] = p.id; attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate)); } } else { formats = new int[answerPayloads.size() + 1]; int i = 0; for (Payload p : answerPayloads) { formats[i++] = p.id; attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate)); } } formats[formats.length - 1] = 101; attributes.add(sdpFactory.createAttribute("rtpmap", "101 telephone-event/8000")); attributes.add(sdpFactory.createAttribute("fmtp", "101 0-15")); MediaDescription md = sdpFactory.createMediaDescription("audio", this.relay.getSipPort(), 1, "RTP/AVP", formats); md.setAttributes(attributes); Vector<MediaDescription> mds = new Vector<MediaDescription>(); mds.add(md); if (vRelay != null) { // video call, add video m-line attributes = new Vector<Attribute>(); if (offer) { formats = new int[offerVPayloads.size()]; int i = 0; for (Payload p : offerVPayloads) { formats[i++] = p.id; attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate)); attributes.add(sdpFactory.createAttribute("fmtp", Integer.toString(p.id) + " packetization-rate=1")); } } else { formats = new int[answerVPayloads.size()]; int i = 0; for (Payload p : answerVPayloads) { formats[i++] = p.id; attributes.add(sdpFactory.createAttribute("rtpmap", Integer.toString(p.id) + " " + p.name + "/" + p.clockRate)); attributes.add(sdpFactory.createAttribute("fmtp", Integer.toString(p.id) + " packetization-rate=1")); } } attributes.add(sdpFactory.createAttribute("framerate", "30")); attributes.add(sdpFactory.createAttribute("rtcp", Integer.toString(this.vRelay.getSipRtcpPort()))); md.setBandwidth("AS", 960); md = sdpFactory.createMediaDescription("video", this.vRelay.getSipPort(), 1, "RTP/AVP", formats); md.setAttributes(attributes); mds.add(md); } sd.setMediaDescriptions(mds); return sd; } catch (SdpException e) { logger.error("Error building SDP", e); } return null; } public void parseSDP(String sdp, boolean offer) { SdpFactory sdpFactory = SdpFactory.getInstance(); try { SessionDescription sd = sdpFactory.createSessionDescription(sdp); @SuppressWarnings("unchecked") Vector<MediaDescription> mdesc = (Vector<MediaDescription>) sd.getMediaDescriptions(false); for (MediaDescription md : mdesc) { javax.sdp.Media media = md.getMedia(); if (media.getMediaType().equals("video") && media.getMediaPort() == 0 && vRelay != null ) { logger.debug("[[" + internalCallId + "]] Video mapping conflict!! (SDP VIDEO PORT = 0)"); // Issue: An video capable XMPP contact video calling a non-video capable SIP endpoint // Options: remote error (current), proper terminate session, or update candidates for XMPP side } if (media.getMediaType().equals("video") && media.getMediaPort() != 0 ) { logger.info("[[" + internalCallId + "]] Video sdp detected! starting video rtp stream..."); if (vRelay == null) { try { vRelay = new RtpRelay(this, true); } catch (IOException e) { logger.error("unable to create video relay!", e); } } int remotePort = media.getMediaPort(); String remoteParty = null; if (md.getConnection() != null) { remoteParty = md.getConnection().getAddress(); } else { remoteParty = sd.getConnection().getAddress(); } vRelay.setSipDest(remoteParty, remotePort); @SuppressWarnings("unchecked") Vector<Attribute> attributes = (Vector<Attribute>) md.getAttributes(false); for (Attribute attrib : attributes) { if (attrib.getName().equals("rtpmap")) { logger.debug("[[" + internalCallId + "]] Got attribute value " + attrib.getValue()); String fields[] = attrib.getValue().split(" ", 2); int codec = Integer.parseInt(fields[0]); String name = fields[1].split("/")[0]; int clockRate = Integer.parseInt(fields[1].split("/")[1]); logger.debug("[[" + internalCallId + "]] Payload " + codec + " rate " + clockRate + " is mapped to " + name); if (codec >= 96) { Payload bitRatePayload = getByName(name, clockRate); if (bitRatePayload != null && bitRatePayload instanceof VPayload) { VPayload tmp = (VPayload) bitRatePayload; VPayload p = new VPayload(codec, tmp.name, clockRate, tmp.bitRate, tmp.width, tmp.height, tmp.framerate); if (offer) { offerVPayloads.add(p); } else { answerVPayloads.add(p); } } } } } } else { int remotePort = media.getMediaPort(); String remoteParty = null; if (md.getConnection() != null) { remoteParty = md.getConnection().getAddress(); } else { remoteParty = sd.getConnection().getAddress(); } relay.setSipDest(remoteParty, remotePort); @SuppressWarnings("unchecked") Vector<String> codecs = (Vector<String>) media.getMediaFormats(false); for (String codec : codecs) { int id = Integer.parseInt(codec); logger.debug("[[" + internalCallId + "]] Got a codec " + id); if (id < 97) { Payload p = getById(id); if (p != null) { if (offer) { offerPayloads.add(p); } else { answerPayloads.add(p); } } } } @SuppressWarnings("unchecked") Vector<Attribute> attributes = (Vector<Attribute>) md.getAttributes(false); for (Attribute attrib : attributes) { if (attrib.getName().equals("rtpmap")) { logger.debug("[[" + internalCallId + "]] Got attribute value " + attrib.getValue()); String fields[] = attrib.getValue().split(" ", 2); int codec = Integer.parseInt(fields[0]); String name = fields[1].split("/")[0]; int clockRate = Integer.parseInt(fields[1].split("/")[1]); logger.debug("[[" + internalCallId + "]] Payload " + codec + " rate " + clockRate + " is mapped to " + name); if (codec >= 96) { Payload bitRatePayload = getByName(name, clockRate); if (bitRatePayload != null) { Payload p = new Payload(codec, name, clockRate, bitRatePayload.bitRate); if (offer) { offerPayloads.add(p); } else { answerPayloads.add(p); } } } } } } } } catch (SdpParseException e) { logger.error("Unable to parse SDP!", e); } catch (SdpException e) { logger.error("Unable to parse SDP!", e); } } public void parseInvite(Message message, Dialog d, ServerTransaction trans) { sipDialog = d; inviteTransaction = trans; parseSDP(new String(message.getRawContent()), true); } }