/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Vector;
import java.text.ParseException;
public class SdpParser {
private SdpInfo sdpInfo;
private Vector supportedMedia = new Vector();
public SdpParser() {
}
public synchronized SdpInfo parseSdp(String sdpData) throws ParseException {
/*
* parse sdpData
*/
String t = "c=IN IP4 ";
int start = sdpData.indexOf(t);
int finish = sdpData.indexOf("\r", start);
if (start < 0 || finish < 0) {
throw new ParseException("Invalid remote SDP", 0);
}
String remoteHost = sdpData.substring(start + t.length(), finish).trim();
t = "m=audio";
start = sdpData.indexOf(t);
String s = sdpData.substring(start + t.length());
t = "RTP/AVP ";
finish = s.indexOf(t);
String port = s.substring(0, finish).trim();
if (start < 0 || finish < 0) {
throw new ParseException("Invalid remote SDP", 0);
}
int remotePort;
try {
remotePort = Integer.parseInt(port);
} catch (NumberFormatException e) {
Logger.println("Invalid remote port in sdp " + port
+ " sdpData " + sdpData);
throw new ParseException("Invalid remote port in sdp " + port
+ " sdpData " + sdpData, 0);
}
start = finish + t.length();
finish = s.indexOf("\r\n");
s = s.substring(start, finish); // point at payloads
/*
* Get all supported RTP Payloads
*/
String[] payloads = s.split("[\" \"]");
String[] sdp = sdpData.split("[\r\n]");
MediaInfo mediaInfo = new MediaInfo(RtpPacket.PCMU_PAYLOAD,
0, 8000, 1, false);
supportedMedia.add(mediaInfo); // we always support payload 0
byte telephoneEventPayload = 0;
/*
* Get all "a=rtpmap:" entries, stop when we hit a non-rtpmap entry
*/
for (int i = 0 ; i < sdp.length; i++) {
s = sdp[i];
if (s.indexOf("a=rtpmap:") != 0) {
continue;
}
RtpmapParser rtpmapParser = new RtpmapParser(s);
mediaInfo = rtpmapParser.getMediaInfo();
if (mediaInfo == null) {
//Logger.println("no media info for " + s);
continue; // skip this entry
}
if (mediaInfo.isTelephoneEventPayload()) {
telephoneEventPayload = mediaInfo.getPayload();
}
supportedMedia.add(mediaInfo);
}
/*
* At this point, payloads[] contains all of the supported payloads
* and the Vector supportedMedia contains the MediaInfo's for
* all supported payloads.
*
* For each payload, find the corresponding MediaInfo and
* select the appropriate one.
*/
mediaInfo = null;
boolean preferredMediaSpecified = false;
t = "a=PreferredPayload:";
if ((start = sdpData.indexOf(t)) >= 0) {
s = sdpData.substring(start + t.length());
finish = s.indexOf("\r\n");
if (finish > 0) {
int payload;
s = s.substring(0, finish);
payload = Integer.parseInt(s);
try {
mediaInfo = getMediaInfo(payload);
} catch (ParseException e) {
}
preferredMediaSpecified = true;
}
}
if (mediaInfo == null) {
for (int i = 0; i < payloads.length; i++) {
int payload = 0;
try {
payload = Integer.parseInt(payloads[i]);
} catch (NumberFormatException e) {
Logger.println("Invalid payload in rtpmap: " + payloads[i]);
throw new ParseException("Invalid payload int rtpmap: "
+ payloads[i], 0);
}
if (payload != 0 && (payload < 96 || payload > 127)) {
/*
* Not one we can deal with
*/
continue;
}
/*
* See if it's a supported payload
*/
MediaInfo m = null;
try {
m = getMediaInfo(payload);
} catch (ParseException e) {
Logger.println("ignoring undefined payload " + payload);
continue;
}
if (m.isTelephoneEventPayload()) {
continue;
}
if (mediaInfo == null ||
mediaInfo.getSampleRate() < m.getSampleRate()) {
mediaInfo = m;
} else if (mediaInfo.getSampleRate() == m.getSampleRate()) {
if (mediaInfo.getChannels() < m.getChannels()) {
mediaInfo = m;
}
}
}
}
if (mediaInfo == null) {
Logger.println("No suitable media payload in sdp data "
+ sdpData);
throw new ParseException("No suitable media payload in sdp data "
+ sdpData, 0);
}
sdpInfo = new SdpInfo(
remoteHost, remotePort, telephoneEventPayload, supportedMedia,
mediaInfo, preferredMediaSpecified);
t = "a=transmitPayload:";
if ((start = sdpData.indexOf(t)) >= 0) {
s = sdpData.substring(start + t.length());
finish = s.indexOf("\r\n");
if (finish > 0) {
int payload;
s = s.substring(0, finish);
payload = Integer.parseInt(s);
try {
sdpInfo.setTransmitMediaInfo(getMediaInfo(payload));
Logger.println("Set xmit mediaInfo to "
+ sdpInfo.getTransmitMediaInfo());
} catch (ParseException e) {
}
}
}
int ix;
t = "a=transmitMediaInfoOk";
if ((ix = sdpData.indexOf(t)) >= 0) {
sdpInfo.setTransmitMediaInfoOk(true);
}
t = "a=userName:";
if ((ix = sdpData.indexOf(t)) >= 0) {
String userName = sdpData.substring(ix + t.length());
finish = userName.indexOf("\n");
if (finish > 0) {
sdpInfo.setUserName(userName.substring(0, finish).trim());
} else {
/*
* This is a workaround for a bug where "\r\n" are missing
* from the SDP.
* XXX This assumes "userName:" is last in the sdp.
*/
sdpInfo.setUserName(userName.substring(0).trim());
}
}
t = "a=callId:";
if ((ix = sdpData.indexOf(t)) >= 0) {
String callId = sdpData.substring(ix + t.length());
finish = callId.indexOf("\n");
if (finish > 0) {
sdpInfo.setCallId(
callId.substring(0, finish).trim());
}
}
t = "a=conferenceId:";
if ((ix = sdpData.indexOf(t)) >= 0) {
String conferenceId = sdpData.substring(ix + t.length());
finish = conferenceId.indexOf("\n");
if (finish > 0) {
sdpInfo.setConferenceId(
conferenceId.substring(0, finish).trim());
} else {
/*
* This is a workaround for a bug where "\r\n" are missing
* from the SDP.
* XXX This assumes "conferenceId:" is last in the sdp.
*/
sdpInfo.setConferenceId(conferenceId.substring(0).trim());
}
}
if (sdpData.indexOf("a=distributedBridge") >= 0) {
sdpInfo.setDistributedBridge();
}
t = "a=rtcpAddress:";
if ((ix = sdpData.indexOf(t)) >= 0) {
s = sdpData.substring(ix + t.length());
finish = s.indexOf("\n");
if (finish > 0) {
s = s.substring(0, finish).trim();
} else {
s = s.substring(0).trim();
}
String[] tokens = s.split(":");
if (tokens.length != 2) {
throw new ParseException("Invalid rtcp address in sdp "
+ " sdpData " + sdpData, 0);
}
try {
sdpInfo.setRtcpAddress(new InetSocketAddress(
InetAddress.getByName(tokens[0]), Integer.parseInt(tokens[1])));
} catch (UnknownHostException e) {
throw new ParseException("Invalid rtcp host address in sdp "
+ " sdpData " + sdpData, 0);
} catch (NumberFormatException e) {
throw new ParseException("Invalid rtcp port in sdp "
+ " sdpData " + sdpData, 0);
}
}
return sdpInfo;
}
private MediaInfo getMediaInfo(int payload) throws ParseException {
for (int i = 0; i < supportedMedia.size(); i++) {
MediaInfo mediaInfo = (MediaInfo) supportedMedia.elementAt(i);
if (mediaInfo.getPayload() == payload) {
return mediaInfo;
}
}
throw new ParseException("Unsupported payload " + payload, 0);
}
}
class RtpmapParser {
private MediaInfo mediaInfo;
/*
* An rtpmap entry looks like this:
*
* a=rtpmap:<payload> <PCMU | PCM | SPEEX>/<sampleRate>/<channels>
* or
* a=rtpmap:<payload> telephone-event/8000/1
*/
public RtpmapParser(String rtpmap) throws ParseException {
byte payload;
int encoding;
int sampleRate;
int channels;
byte telephoneEventPayload;
int start;
int finish;
finish = rtpmap.indexOf(" ");
if (finish < 0) {
Logger.println("Invalid rtpmap: " + rtpmap);
throw new ParseException("Invalid rtpmap: " + rtpmap, 0);
}
try {
payload = (byte)Integer.parseInt(rtpmap.substring(9, finish));
} catch (NumberFormatException e) {
Logger.println("Invalid payload in rtpmap: " + rtpmap);
throw new ParseException("Invalid payload in rtpmap: "
+ rtpmap, 0);
}
String s = rtpmap.substring(finish + 1);
finish = s.indexOf("telephone-event");
if (finish >= 0) {
mediaInfo = new MediaInfo(payload, 0, 8000, 1, true);
telephoneEventPayload = payload;
return;
}
finish = s.indexOf("CN/");
if (finish >= 0) {
return; // ignore this entry
}
start = s.indexOf("PCM/");
if (start >= 0) {
s = s.substring(start + 4);
encoding = RtpPacket.PCM_ENCODING;
} else {
start = s.indexOf("PCMU/");
if (start >= 0) {
s = s.substring(start + 5);
encoding = RtpPacket.PCMU_ENCODING;
} else {
start = s.indexOf("SPEEX/");
if (start < 0) {
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println("Ignoring rtpmap entry: "
+ payload + " " + s);
}
return; // ignore this entry
}
s = s.substring(start + 6);
encoding = RtpPacket.SPEEX_ENCODING;
}
}
finish = s.indexOf("/");
boolean channelsPresent = true;
String rate;
if (finish < 0) {
channelsPresent = false;
rate = s.substring(start);
} else {
rate = s.substring(start, finish); // point at sample rate
}
try {
sampleRate = Integer.parseInt(rate);
} catch (NumberFormatException e) {
Logger.println("Invalid sample rate in rtpmap: " + rtpmap);
throw new ParseException("Invalid sample rate in rtpmap: "
+ rtpmap, 0);
}
if (channelsPresent) {
s = s.substring(finish + 1); // point at channels
try {
channels = Integer.parseInt(s);
} catch (NumberFormatException e) {
Logger.println("Invalid channels in rtpmap: " + rtpmap);
throw new ParseException("Invalid channels in rtpmap: "
+ rtpmap, 0);
}
} else {
channels = 1;
}
mediaInfo = new MediaInfo(payload, encoding, sampleRate,
channels, false);
}
public MediaInfo getMediaInfo() {
return mediaInfo;
}
}