| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * 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 android.net.sip; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Locale; |
| |
| /** |
| * An object used to manipulate messages of Session Description Protocol (SDP). |
| * It is mainly designed for the uses of Session Initiation Protocol (SIP). |
| * Therefore, it only handles connection addresses ("c="), bandwidth limits, |
| * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this |
| * implementation does not support multicast sessions. |
| * |
| * <p>Here is an example code to create a session description.</p> |
| * <pre> |
| * SimpleSessionDescription description = new SimpleSessionDescription( |
| * System.currentTimeMillis(), "1.2.3.4"); |
| * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP"); |
| * media.setRtpPayload(0, "PCMU/8000", null); |
| * media.setRtpPayload(8, "PCMA/8000", null); |
| * media.setRtpPayload(127, "telephone-event/8000", "0-15"); |
| * media.setAttribute("sendrecv", ""); |
| * </pre> |
| * <p>Invoking <code>description.encode()</code> will produce a result like the |
| * one below.</p> |
| * <pre> |
| * v=0 |
| * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4 |
| * s=- |
| * c=IN IP4 1.2.3.4 |
| * t=0 0 |
| * m=audio 56789 RTP/AVP 0 8 127 |
| * a=rtpmap:0 PCMU/8000 |
| * a=rtpmap:8 PCMA/8000 |
| * a=rtpmap:127 telephone-event/8000 |
| * a=fmtp:127 0-15 |
| * a=sendrecv |
| * </pre> |
| * @hide |
| */ |
| public class SimpleSessionDescription { |
| private final Fields mFields = new Fields("voscbtka"); |
| private final ArrayList<Media> mMedia = new ArrayList<Media>(); |
| |
| /** |
| * Creates a minimal session description from the given session ID and |
| * unicast address. The address is used in the origin field ("o=") and the |
| * connection field ("c="). See {@link SimpleSessionDescription} for an |
| * example of its usage. |
| */ |
| public SimpleSessionDescription(long sessionId, String address) { |
| address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address; |
| mFields.parse("v=0"); |
| mFields.parse(String.format(Locale.US, "o=- %d %d %s", sessionId, |
| System.currentTimeMillis(), address)); |
| mFields.parse("s=-"); |
| mFields.parse("t=0 0"); |
| mFields.parse("c=" + address); |
| } |
| |
| /** |
| * Creates a session description from the given message. |
| * |
| * @throws IllegalArgumentException if message is invalid. |
| */ |
| public SimpleSessionDescription(String message) { |
| String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+"); |
| Fields fields = mFields; |
| |
| for (String line : lines) { |
| try { |
| if (line.charAt(1) != '=') { |
| throw new IllegalArgumentException(); |
| } |
| if (line.charAt(0) == 'm') { |
| String[] parts = line.substring(2).split(" ", 4); |
| String[] ports = parts[1].split("/", 2); |
| Media media = newMedia(parts[0], Integer.parseInt(ports[0]), |
| (ports.length < 2) ? 1 : Integer.parseInt(ports[1]), |
| parts[2]); |
| for (String format : parts[3].split(" ")) { |
| media.setFormat(format, null); |
| } |
| fields = media; |
| } else { |
| fields.parse(line); |
| } |
| } catch (Exception e) { |
| throw new IllegalArgumentException("Invalid SDP: " + line); |
| } |
| } |
| } |
| |
| /** |
| * Creates a new media description in this session description. |
| * |
| * @param type The media type, e.g. {@code "audio"}. |
| * @param port The first transport port used by this media. |
| * @param portCount The number of contiguous ports used by this media. |
| * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}. |
| */ |
| public Media newMedia(String type, int port, int portCount, |
| String protocol) { |
| Media media = new Media(type, port, portCount, protocol); |
| mMedia.add(media); |
| return media; |
| } |
| |
| /** |
| * Returns all the media descriptions in this session description. |
| */ |
| public Media[] getMedia() { |
| return mMedia.toArray(new Media[mMedia.size()]); |
| } |
| |
| /** |
| * Encodes the session description and all its media descriptions in a |
| * string. Note that the result might be incomplete if a required field |
| * has never been added before. |
| */ |
| public String encode() { |
| StringBuilder buffer = new StringBuilder(); |
| mFields.write(buffer); |
| for (Media media : mMedia) { |
| media.write(buffer); |
| } |
| return buffer.toString(); |
| } |
| |
| /** |
| * Returns the connection address or {@code null} if it is not present. |
| */ |
| public String getAddress() { |
| return mFields.getAddress(); |
| } |
| |
| /** |
| * Sets the connection address. The field will be removed if the address |
| * is {@code null}. |
| */ |
| public void setAddress(String address) { |
| mFields.setAddress(address); |
| } |
| |
| /** |
| * Returns the encryption method or {@code null} if it is not present. |
| */ |
| public String getEncryptionMethod() { |
| return mFields.getEncryptionMethod(); |
| } |
| |
| /** |
| * Returns the encryption key or {@code null} if it is not present. |
| */ |
| public String getEncryptionKey() { |
| return mFields.getEncryptionKey(); |
| } |
| |
| /** |
| * Sets the encryption method and the encryption key. The field will be |
| * removed if the method is {@code null}. |
| */ |
| public void setEncryption(String method, String key) { |
| mFields.setEncryption(method, key); |
| } |
| |
| /** |
| * Returns the types of the bandwidth limits. |
| */ |
| public String[] getBandwidthTypes() { |
| return mFields.getBandwidthTypes(); |
| } |
| |
| /** |
| * Returns the bandwidth limit of the given type or {@code -1} if it is not |
| * present. |
| */ |
| public int getBandwidth(String type) { |
| return mFields.getBandwidth(type); |
| } |
| |
| /** |
| * Sets the bandwith limit for the given type. The field will be removed if |
| * the value is negative. |
| */ |
| public void setBandwidth(String type, int value) { |
| mFields.setBandwidth(type, value); |
| } |
| |
| /** |
| * Returns the names of all the attributes. |
| */ |
| public String[] getAttributeNames() { |
| return mFields.getAttributeNames(); |
| } |
| |
| /** |
| * Returns the attribute of the given name or {@code null} if it is not |
| * present. |
| */ |
| public String getAttribute(String name) { |
| return mFields.getAttribute(name); |
| } |
| |
| /** |
| * Sets the attribute for the given name. The field will be removed if |
| * the value is {@code null}. To set a binary attribute, use an empty |
| * string as the value. |
| */ |
| public void setAttribute(String name, String value) { |
| mFields.setAttribute(name, value); |
| } |
| |
| /** |
| * This class represents a media description of a session description. It |
| * can only be created by {@link SimpleSessionDescription#newMedia}. Since |
| * the syntax is more restricted for RTP based protocols, two sets of access |
| * methods are implemented. See {@link SimpleSessionDescription} for an |
| * example of its usage. |
| */ |
| public static class Media extends Fields { |
| private final String mType; |
| private final int mPort; |
| private final int mPortCount; |
| private final String mProtocol; |
| private ArrayList<String> mFormats = new ArrayList<String>(); |
| |
| private Media(String type, int port, int portCount, String protocol) { |
| super("icbka"); |
| mType = type; |
| mPort = port; |
| mPortCount = portCount; |
| mProtocol = protocol; |
| } |
| |
| /** |
| * Returns the media type. |
| */ |
| public String getType() { |
| return mType; |
| } |
| |
| /** |
| * Returns the first transport port used by this media. |
| */ |
| public int getPort() { |
| return mPort; |
| } |
| |
| /** |
| * Returns the number of contiguous ports used by this media. |
| */ |
| public int getPortCount() { |
| return mPortCount; |
| } |
| |
| /** |
| * Returns the transport protocol. |
| */ |
| public String getProtocol() { |
| return mProtocol; |
| } |
| |
| /** |
| * Returns the media formats. |
| */ |
| public String[] getFormats() { |
| return mFormats.toArray(new String[mFormats.size()]); |
| } |
| |
| /** |
| * Returns the {@code fmtp} attribute of the given format or |
| * {@code null} if it is not present. |
| */ |
| public String getFmtp(String format) { |
| return super.get("a=fmtp:" + format, ' '); |
| } |
| |
| /** |
| * Sets a format and its {@code fmtp} attribute. If the attribute is |
| * {@code null}, the corresponding field will be removed. |
| */ |
| public void setFormat(String format, String fmtp) { |
| mFormats.remove(format); |
| mFormats.add(format); |
| super.set("a=rtpmap:" + format, ' ', null); |
| super.set("a=fmtp:" + format, ' ', fmtp); |
| } |
| |
| /** |
| * Removes a format and its {@code fmtp} attribute. |
| */ |
| public void removeFormat(String format) { |
| mFormats.remove(format); |
| super.set("a=rtpmap:" + format, ' ', null); |
| super.set("a=fmtp:" + format, ' ', null); |
| } |
| |
| /** |
| * Returns the RTP payload types. |
| */ |
| public int[] getRtpPayloadTypes() { |
| int[] types = new int[mFormats.size()]; |
| int length = 0; |
| for (String format : mFormats) { |
| try { |
| types[length] = Integer.parseInt(format); |
| ++length; |
| } catch (NumberFormatException e) { } |
| } |
| return Arrays.copyOf(types, length); |
| } |
| |
| /** |
| * Returns the {@code rtpmap} attribute of the given RTP payload type |
| * or {@code null} if it is not present. |
| */ |
| public String getRtpmap(int type) { |
| return super.get("a=rtpmap:" + type, ' '); |
| } |
| |
| /** |
| * Returns the {@code fmtp} attribute of the given RTP payload type or |
| * {@code null} if it is not present. |
| */ |
| public String getFmtp(int type) { |
| return super.get("a=fmtp:" + type, ' '); |
| } |
| |
| /** |
| * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp} |
| * attributes. If any of the attributes is {@code null}, the |
| * corresponding field will be removed. See |
| * {@link SimpleSessionDescription} for an example of its usage. |
| */ |
| public void setRtpPayload(int type, String rtpmap, String fmtp) { |
| String format = String.valueOf(type); |
| mFormats.remove(format); |
| mFormats.add(format); |
| super.set("a=rtpmap:" + format, ' ', rtpmap); |
| super.set("a=fmtp:" + format, ' ', fmtp); |
| } |
| |
| /** |
| * Removes a RTP payload and its {@code rtpmap} and {@code fmtp} |
| * attributes. |
| */ |
| public void removeRtpPayload(int type) { |
| removeFormat(String.valueOf(type)); |
| } |
| |
| private void write(StringBuilder buffer) { |
| buffer.append("m=").append(mType).append(' ').append(mPort); |
| if (mPortCount != 1) { |
| buffer.append('/').append(mPortCount); |
| } |
| buffer.append(' ').append(mProtocol); |
| for (String format : mFormats) { |
| buffer.append(' ').append(format); |
| } |
| buffer.append("\r\n"); |
| super.write(buffer); |
| } |
| } |
| |
| /** |
| * This class acts as a set of fields, and the size of the set is expected |
| * to be small. Therefore, it uses a simple list instead of maps. Each field |
| * has three parts: a key, a delimiter, and a value. Delimiters are special |
| * because they are not included in binary attributes. As a result, the |
| * private methods, which are the building blocks of this class, all take |
| * the delimiter as an argument. |
| */ |
| private static class Fields { |
| private final String mOrder; |
| private final ArrayList<String> mLines = new ArrayList<String>(); |
| |
| Fields(String order) { |
| mOrder = order; |
| } |
| |
| /** |
| * Returns the connection address or {@code null} if it is not present. |
| */ |
| public String getAddress() { |
| String address = get("c", '='); |
| if (address == null) { |
| return null; |
| } |
| String[] parts = address.split(" "); |
| if (parts.length != 3) { |
| return null; |
| } |
| int slash = parts[2].indexOf('/'); |
| return (slash < 0) ? parts[2] : parts[2].substring(0, slash); |
| } |
| |
| /** |
| * Sets the connection address. The field will be removed if the address |
| * is {@code null}. |
| */ |
| public void setAddress(String address) { |
| if (address != null) { |
| address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + |
| address; |
| } |
| set("c", '=', address); |
| } |
| |
| /** |
| * Returns the encryption method or {@code null} if it is not present. |
| */ |
| public String getEncryptionMethod() { |
| String encryption = get("k", '='); |
| if (encryption == null) { |
| return null; |
| } |
| int colon = encryption.indexOf(':'); |
| return (colon == -1) ? encryption : encryption.substring(0, colon); |
| } |
| |
| /** |
| * Returns the encryption key or {@code null} if it is not present. |
| */ |
| public String getEncryptionKey() { |
| String encryption = get("k", '='); |
| if (encryption == null) { |
| return null; |
| } |
| int colon = encryption.indexOf(':'); |
| return (colon == -1) ? null : encryption.substring(0, colon + 1); |
| } |
| |
| /** |
| * Sets the encryption method and the encryption key. The field will be |
| * removed if the method is {@code null}. |
| */ |
| public void setEncryption(String method, String key) { |
| set("k", '=', (method == null || key == null) ? |
| method : method + ':' + key); |
| } |
| |
| /** |
| * Returns the types of the bandwidth limits. |
| */ |
| public String[] getBandwidthTypes() { |
| return cut("b=", ':'); |
| } |
| |
| /** |
| * Returns the bandwidth limit of the given type or {@code -1} if it is |
| * not present. |
| */ |
| public int getBandwidth(String type) { |
| String value = get("b=" + type, ':'); |
| if (value != null) { |
| try { |
| return Integer.parseInt(value); |
| } catch (NumberFormatException e) { } |
| setBandwidth(type, -1); |
| } |
| return -1; |
| } |
| |
| /** |
| * Sets the bandwith limit for the given type. The field will be removed |
| * if the value is negative. |
| */ |
| public void setBandwidth(String type, int value) { |
| set("b=" + type, ':', (value < 0) ? null : String.valueOf(value)); |
| } |
| |
| /** |
| * Returns the names of all the attributes. |
| */ |
| public String[] getAttributeNames() { |
| return cut("a=", ':'); |
| } |
| |
| /** |
| * Returns the attribute of the given name or {@code null} if it is not |
| * present. |
| */ |
| public String getAttribute(String name) { |
| return get("a=" + name, ':'); |
| } |
| |
| /** |
| * Sets the attribute for the given name. The field will be removed if |
| * the value is {@code null}. To set a binary attribute, use an empty |
| * string as the value. |
| */ |
| public void setAttribute(String name, String value) { |
| set("a=" + name, ':', value); |
| } |
| |
| private void write(StringBuilder buffer) { |
| for (int i = 0; i < mOrder.length(); ++i) { |
| char type = mOrder.charAt(i); |
| for (String line : mLines) { |
| if (line.charAt(0) == type) { |
| buffer.append(line).append("\r\n"); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Invokes {@link #set} after splitting the line into three parts. |
| */ |
| private void parse(String line) { |
| char type = line.charAt(0); |
| if (mOrder.indexOf(type) == -1) { |
| return; |
| } |
| char delimiter = '='; |
| if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) { |
| delimiter = ' '; |
| } else if (type == 'b' || type == 'a') { |
| delimiter = ':'; |
| } |
| int i = line.indexOf(delimiter); |
| if (i == -1) { |
| set(line, delimiter, ""); |
| } else { |
| set(line.substring(0, i), delimiter, line.substring(i + 1)); |
| } |
| } |
| |
| /** |
| * Finds the key with the given prefix and returns its suffix. |
| */ |
| private String[] cut(String prefix, char delimiter) { |
| String[] names = new String[mLines.size()]; |
| int length = 0; |
| for (String line : mLines) { |
| if (line.startsWith(prefix)) { |
| int i = line.indexOf(delimiter); |
| if (i == -1) { |
| i = line.length(); |
| } |
| names[length] = line.substring(prefix.length(), i); |
| ++length; |
| } |
| } |
| return Arrays.copyOf(names, length); |
| } |
| |
| /** |
| * Returns the index of the key. |
| */ |
| private int find(String key, char delimiter) { |
| int length = key.length(); |
| for (int i = mLines.size() - 1; i >= 0; --i) { |
| String line = mLines.get(i); |
| if (line.startsWith(key) && (line.length() == length || |
| line.charAt(length) == delimiter)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Sets the key with the value or removes the key if the value is |
| * {@code null}. |
| */ |
| private void set(String key, char delimiter, String value) { |
| int index = find(key, delimiter); |
| if (value != null) { |
| if (value.length() != 0) { |
| key = key + delimiter + value; |
| } |
| if (index == -1) { |
| mLines.add(key); |
| } else { |
| mLines.set(index, key); |
| } |
| } else if (index != -1) { |
| mLines.remove(index); |
| } |
| } |
| |
| /** |
| * Returns the value of the key. |
| */ |
| private String get(String key, char delimiter) { |
| int index = find(key, delimiter); |
| if (index == -1) { |
| return null; |
| } |
| String line = mLines.get(index); |
| int length = key.length(); |
| return (line.length() == length) ? "" : line.substring(length + 1); |
| } |
| } |
| } |