/*
* JBoss, Home of Professional Open Source
* Copyright 2011, Red Hat, Inc. and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.restcomm.media.rtp.sdp;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.restcomm.media.sdp.format.AVProfile;
import org.restcomm.media.sdp.format.RTPFormat;
import org.restcomm.media.sdp.format.RTPFormats;
import org.restcomm.media.spi.format.ApplicationFormat;
import org.restcomm.media.spi.format.AudioFormat;
import org.restcomm.media.spi.format.EncodingName;
import org.restcomm.media.spi.format.FormatFactory;
import org.restcomm.media.spi.format.VideoFormat;
import org.restcomm.media.spi.utils.Text;
/**
* Media descriptor attribute.
*
* @author kulikov
*
* @deprecated use new /io/sdp library
*/
public class MediaDescriptorField {
private Text mediaType;
private int port;
private Text profile;
private ConnectionField connection;
private boolean rtcpMux = false;
private boolean ice = false;
private Text iceUfrag;
private Text icePwd;
private List<CandidateField> candidates = new ArrayList<CandidateField>();
private RTPFormats formats = new RTPFormats(15);
// optional SDP attribute used for WebRTC session encryption
private Text webRTCFingerprint;
// legacy unencrypted RTP media profile
public final static String RTP_AVP_PROFILE = "RTP/AVP";
// legacy encrypted SRTP media profile
public final static String RTP_SAVP_PROFILE = "RTP/SAVP";
// WebRTC (DTLS SRTP encrypted) RTP media profile
public final static String RTP_SAVPF_PROFILE = "RTP/SAVPF";
/**
* Reads values from specified text line
*
* @param line the text description of the media.
* @throws ParseException
*/
protected void setDescriptor(Text line) throws ParseException {
line.trim();
try {
//split using equal sign
Iterator<Text> it = line.split('=').iterator();
//skip first token (m)
Text t = it.next();
//select second token (media_type port profile formats)
t = it.next();
//split using white spaces
it = t.split(' ').iterator();
//media type
mediaType = it.next();
mediaType.trim();
//port
t = it.next();
t.trim();
port = t.toInteger();
//profile
profile = it.next();
profile.trim();
//formats
while (it.hasNext()) {
t = it.next();
t.trim();
RTPFormat fmt = AVProfile.getFormat(t.toInteger(),mediaType);
if (fmt != null && !formats.contains(fmt.getFormat())) {
formats.add(fmt.clone());
}
}
} catch (Exception e) {
throw new ParseException("Could not parse media descriptor", 0);
}
}
/**
* Parses attribute.
*
* @param attribute the attribute to parse
*/
protected void addAttribute(Text attribute) {
if (attribute.startsWith(SessionDescription.RTPMAP)) {
addRtpMapAttribute(attribute);
return;
}
if (attribute.startsWith(SessionDescription.FMTP)) {
addFmtAttribute(attribute);
return;
}
if (attribute.startsWith(SessionDescription.WEBRTC_FINGERPRINT)) {
addFingerprintAttribute(attribute);
return;
}
if(attribute.startsWith(SessionDescription.ICE_UFRAG)) {
this.ice = true;
addIceUfragAttribute(attribute);
return;
}
if(attribute.startsWith(SessionDescription.ICE_PWD)) {
this.ice = true;
addIcePwdAttribute(attribute);
return;
}
if(attribute.startsWith(CandidateField.CANDIDATE_FIELD)) {
this.ice = true;
addCandidate(attribute);
return;
}
if(attribute.startsWith(RtcpMuxField.RTCP_MUX_FIELD)) {
this.rtcpMux = true;
return;
}
}
/**
* Parses an ice-ufrag attribute and registers it in the media session.<br>
* Example: <code>a=ice-ufrag:apy5ZU+12zHYMHGq</code>
* @param attribute
*/
private void addIceUfragAttribute(Text attribute) {
// Copy and trim the attribute
Text attr = new Text();
attribute.copy(attr);
attr.trim();
// Extract the value from the attribute
Iterator<Text> it = attribute.split(':').iterator();
Text token = it.next();
token = it.next();
token.trim();
this.iceUfrag = token;
}
/**
* Parses an ice-pwd attribute and registers it in the media session.<br>
* Example: <code>a=ice-pwd:BsDycZCv0plnGR+su8E+3kGJ</code>
* @param attribute
*/
private void addIcePwdAttribute(Text attribute) {
// Copy and trim the attribute
Text attr = new Text();
attribute.copy(attr);
attr.trim();
// Extract the value from the attribute
Iterator<Text> it = attribute.split(':').iterator();
Text token = it.next();
token = it.next();
token.trim();
this.icePwd = token;
}
/**
* Parses a candidate field for ICE and register it on internal list.
* @param attribute
*/
private void addCandidate(Text attribute) {
// Copy and trim the attribute
Text attr = new Text();
attribute.copy(attr);
attr.trim();
// Parse the candidate field and add it to list of candidates
CandidateField candidateField = new CandidateField(attr);
this.candidates.add(candidateField);
// Candidates must be listed by weight in descending order
Collections.sort(this.candidates, Collections.reverseOrder());
}
/**
* Register a new RTP MAP attribute.
* <p>Example: <code>a=rtpmap:126 telephone-event/8000</code></p>
*
* @param attribute
* The attribute line to be registered.
* @throws IllegalArgumentException
* If the attribute is not a valid RTP MAP line.
*/
private void addRtpMapAttribute(Text attribute) throws IllegalArgumentException {
if (!attribute.startsWith(SessionDescription.RTPMAP)) {
throw new IllegalArgumentException("Not a valid RTP MAP attribute"+attribute);
}
Iterator<Text> it = attribute.split(':').iterator();
Text token = it.next();
token = it.next();
token.trim();
//payload and format descriptor
it = token.split(' ').iterator();
//payload
token = it.next();
token.trim();
int payload = token.toInteger();
//format descriptor
token = it.next();
token.trim();
createFormat(payload, token);
}
/**
* Register a new FMT attribute.<br>
* Example: <code>a=fmtp:111 minptime=10</code>
*
* @param attribute
* The attribute line to be registered.
* @throws IllegalArgumentException
* If the attribute is not a valid FMT line.
*/
private void addFmtAttribute(Text attribute) throws IllegalArgumentException {
if (!attribute.startsWith(SessionDescription.FMTP)) {
throw new IllegalArgumentException("Not a valid FMT attribute"+attribute);
}
Iterator<Text> it = attribute.split(':').iterator();
Text token = it.next();
token = it.next();
token.trim();
//payload and format descriptor
it = token.split(' ').iterator();
//payload
token = it.next();
token.trim();
int payload = token.toInteger();
//format descriptor
token = it.next();
token.trim();
RTPFormat fmt = getFormat(payload);
if (fmt != null) {
//TODO : replace string with text
fmt.getFormat().setOptions(token);
}
}
/**
* Register a new fingerprint attribute for WebRTC calls.<br>
* Example: <code>a=fingerprint:sha-256 E5:52:E5:88:CC:B6:7A:D7:8E:...</code>
*
* @param attribute
* The attribute line to be registered.
* @throws IllegalArgumentException
* If the attribute is not a valid FMT line.
*/
private void addFingerprintAttribute(Text attribute) throws IllegalArgumentException {
if (!attribute.startsWith(SessionDescription.WEBRTC_FINGERPRINT)) {
throw new IllegalArgumentException("Not a valid fingerprint attribute"+attribute);
}
// Remove line type 'a=fingerprint:'
Text fingerprint = (Text) attribute.subSequence(SessionDescription.WEBRTC_FINGERPRINT.length(),attribute.length());
setWebRTCFingerprint(fingerprint);
}
/**
* Gets the connection attribute of this description.
*
* @return connection field
*/
public ConnectionField getConnection() {
return this.connection;
}
/**
* Modify connection attribute.
*
* @param line the text view of attribute.
* @throws ParseException
*/
protected void setConnection(Text line) throws ParseException {
connection = new ConnectionField();
connection.strain(line);
Collections.sort(this.candidates);
}
/**
* Gets the media types
*
* @return media type value
*/
public Text getMediaType() {
return mediaType;
}
/**
* Gets the port number.
*
* @return the port number
*/
public int getPort() {
return port;
}
/**
* Gets the profile
*
* @return profile value
*/
public Text getProfile() {
return profile;
}
/**
* Gets the list of formats offered.
*
* @return collection of formats.
*/
public RTPFormats getFormats() {
return formats;
}
/**
* Searches format with specified payload number.
*
* @param payload payload number.
* @return format with this payload number.
*/
private RTPFormat getFormat(int payload) {
return formats.find(payload);
}
/**
* Indicates whether the media channel supports RTCP-MUX or not.
*
* @return whether rtcp-mux is supported
*/
public boolean isRtcpMux() {
return rtcpMux;
}
/**
* Creates or updates format using payload number and text format description.
*
* @param payload the payload number of the format.
* @param description text description of the format
* @return format object
*/
private RTPFormat createFormat(int payload, Text description) {
MediaType mtype = MediaType.fromDescription(mediaType);
switch (mtype) {
case AUDIO:
return createAudioFormat(payload, description);
case VIDEO:
return createVideoFormat(payload, description);
case APPLICATION:
return createApplicationFormat(payload, description);
default:
return null;
}
}
/**
* Creates or updates audio format using payload number and text format description.
*
* @param payload the payload number of the format.
* @param description text description of the format
* @return format object
*/
private RTPFormat createAudioFormat(int payload, Text description) {
Iterator<Text> it = description.split('/').iterator();
//encoding name
Text token = it.next();
token.trim();
EncodingName name = new EncodingName(token);
//clock rate
//TODO : convert to sample rate
token = it.next();
token.trim();
int clockRate = token.toInteger();
//channels
int channels = 1;
if (it.hasNext()) {
token = it.next();
token.trim();
channels = token.toInteger();
}
RTPFormat rtpFormat = getFormat(payload);
if (rtpFormat == null) {
formats.add(new RTPFormat(payload, FormatFactory.createAudioFormat(name, clockRate, -1, channels)));
} else {
//TODO: recreate format anyway. it is illegal to use clock rate as sample rate
((AudioFormat)rtpFormat.getFormat()).setName(name);
((AudioFormat)rtpFormat.getFormat()).setSampleRate(clockRate);
((AudioFormat)rtpFormat.getFormat()).setChannels(channels);
}
return rtpFormat;
}
/**
* Creates or updates video format using payload number and text format description.
*
* @param payload the payload number of the format.
* @param description text description of the format
* @return format object
*/
private RTPFormat createVideoFormat(int payload, Text description) {
Iterator<Text> it = description.split('/').iterator();
//encoding name
Text token = it.next();
token.trim();
EncodingName name = new EncodingName(token);
//clock rate
//TODO : convert to frame rate
token = it.next();
token.trim();
int clockRate = token.toInteger();
RTPFormat rtpFormat = getFormat(payload);
if (rtpFormat == null) {
formats.add(new RTPFormat(payload, FormatFactory.createVideoFormat(name, clockRate)));
} else {
//TODO: recreate format anyway. it is illegal to use clock rate as frame rate
((VideoFormat)rtpFormat.getFormat()).setName(name);
((VideoFormat)rtpFormat.getFormat()).setFrameRate(clockRate);
}
return rtpFormat;
}
/**
* Creates or updates application format using payload number and text format description.
*
* @param payload the payload number of the format.
* @param description text description of the format
* @return format object
*/
private RTPFormat createApplicationFormat(int payload, Text description) {
Iterator<Text> it = description.split('/').iterator();
//encoding name
Text token = it.next();
token.trim();
EncodingName name = new EncodingName(token);
//clock rate
token = it.next();
token.trim();
RTPFormat rtpFormat = getFormat(payload);
if (rtpFormat == null) {
formats.add(new RTPFormat(payload, FormatFactory.createApplicationFormat(name)));
} else {
((ApplicationFormat)rtpFormat.getFormat()).setName(name);
}
return rtpFormat;
}
/**
*
* @return true if the media profile requires encryption
*/
public boolean isWebRTCProfile() {
return getProfile().toString().equals(RTP_SAVP_PROFILE) || getProfile().toString().equals(RTP_SAVPF_PROFILE);
}
public Text getWebRTCFingerprint() {
return webRTCFingerprint;
}
public void setWebRTCFingerprint(Text webRTCFingerprint) {
this.webRTCFingerprint = webRTCFingerprint;
}
public boolean isIce() {
return this.ice;
}
public Text getIceUfrag() {
return iceUfrag;
}
public Text getIcePwd() {
return icePwd;
}
public List<CandidateField> getCandidates() {
return candidates;
}
public CandidateField getMostRelevantCandidate() {
if(this.candidates == null || this.candidates.isEmpty()) {
return null;
}
return this.candidates.get(0);
}
}