• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.sip;
18 
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 
22 /**
23  * An object used to manipulate messages of Session Description Protocol (SDP).
24  * It is mainly designed for the uses of Session Initiation Protocol (SIP).
25  * Therefore, it only handles connection addresses ("c="), bandwidth limits,
26  * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this
27  * implementation does not support multicast sessions.
28  *
29  * <p>Here is an example code to create a session description.</p>
30  * <pre>
31  * SimpleSessionDescription description = new SimpleSessionDescription(
32  *     System.currentTimeMillis(), "1.2.3.4");
33  * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP");
34  * media.setRtpPayload(0, "PCMU/8000", null);
35  * media.setRtpPayload(8, "PCMA/8000", null);
36  * media.setRtpPayload(127, "telephone-event/8000", "0-15");
37  * media.setAttribute("sendrecv", "");
38  * </pre>
39  * <p>Invoking <code>description.encode()</code> will produce a result like the
40  * one below.</p>
41  * <pre>
42  * v=0
43  * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4
44  * s=-
45  * c=IN IP4 1.2.3.4
46  * t=0 0
47  * m=audio 56789 RTP/AVP 0 8 127
48  * a=rtpmap:0 PCMU/8000
49  * a=rtpmap:8 PCMA/8000
50  * a=rtpmap:127 telephone-event/8000
51  * a=fmtp:127 0-15
52  * a=sendrecv
53  * </pre>
54  * @hide
55  */
56 public class SimpleSessionDescription {
57     private final Fields mFields = new Fields("voscbtka");
58     private final ArrayList<Media> mMedia = new ArrayList<Media>();
59 
60     /**
61      * Creates a minimal session description from the given session ID and
62      * unicast address. The address is used in the origin field ("o=") and the
63      * connection field ("c="). See {@link SimpleSessionDescription} for an
64      * example of its usage.
65      */
SimpleSessionDescription(long sessionId, String address)66     public SimpleSessionDescription(long sessionId, String address) {
67         address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address;
68         mFields.parse("v=0");
69         mFields.parse(String.format("o=- %d %d %s", sessionId,
70                 System.currentTimeMillis(), address));
71         mFields.parse("s=-");
72         mFields.parse("t=0 0");
73         mFields.parse("c=" + address);
74     }
75 
76     /**
77      * Creates a session description from the given message.
78      *
79      * @throws IllegalArgumentException if message is invalid.
80      */
SimpleSessionDescription(String message)81     public SimpleSessionDescription(String message) {
82         String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+");
83         Fields fields = mFields;
84 
85         for (String line : lines) {
86             try {
87                 if (line.charAt(1) != '=') {
88                     throw new IllegalArgumentException();
89                 }
90                 if (line.charAt(0) == 'm') {
91                     String[] parts = line.substring(2).split(" ", 4);
92                     String[] ports = parts[1].split("/", 2);
93                     Media media = newMedia(parts[0], Integer.parseInt(ports[0]),
94                             (ports.length < 2) ? 1 : Integer.parseInt(ports[1]),
95                             parts[2]);
96                     for (String format : parts[3].split(" ")) {
97                         media.setFormat(format, null);
98                     }
99                     fields = media;
100                 } else {
101                     fields.parse(line);
102                 }
103             } catch (Exception e) {
104                 throw new IllegalArgumentException("Invalid SDP: " + line);
105             }
106         }
107     }
108 
109     /**
110      * Creates a new media description in this session description.
111      *
112      * @param type The media type, e.g. {@code "audio"}.
113      * @param port The first transport port used by this media.
114      * @param portCount The number of contiguous ports used by this media.
115      * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
116      */
newMedia(String type, int port, int portCount, String protocol)117     public Media newMedia(String type, int port, int portCount,
118             String protocol) {
119         Media media = new Media(type, port, portCount, protocol);
120         mMedia.add(media);
121         return media;
122     }
123 
124     /**
125      * Returns all the media descriptions in this session description.
126      */
getMedia()127     public Media[] getMedia() {
128         return mMedia.toArray(new Media[mMedia.size()]);
129     }
130 
131     /**
132      * Encodes the session description and all its media descriptions in a
133      * string. Note that the result might be incomplete if a required field
134      * has never been added before.
135      */
encode()136     public String encode() {
137         StringBuilder buffer = new StringBuilder();
138         mFields.write(buffer);
139         for (Media media : mMedia) {
140             media.write(buffer);
141         }
142         return buffer.toString();
143     }
144 
145     /**
146      * Returns the connection address or {@code null} if it is not present.
147      */
getAddress()148     public String getAddress() {
149         return mFields.getAddress();
150     }
151 
152     /**
153      * Sets the connection address. The field will be removed if the address
154      * is {@code null}.
155      */
setAddress(String address)156     public void setAddress(String address) {
157         mFields.setAddress(address);
158     }
159 
160     /**
161      * Returns the encryption method or {@code null} if it is not present.
162      */
getEncryptionMethod()163     public String getEncryptionMethod() {
164         return mFields.getEncryptionMethod();
165     }
166 
167     /**
168      * Returns the encryption key or {@code null} if it is not present.
169      */
getEncryptionKey()170     public String getEncryptionKey() {
171         return mFields.getEncryptionKey();
172     }
173 
174     /**
175      * Sets the encryption method and the encryption key. The field will be
176      * removed if the method is {@code null}.
177      */
setEncryption(String method, String key)178     public void setEncryption(String method, String key) {
179         mFields.setEncryption(method, key);
180     }
181 
182     /**
183      * Returns the types of the bandwidth limits.
184      */
getBandwidthTypes()185     public String[] getBandwidthTypes() {
186         return mFields.getBandwidthTypes();
187     }
188 
189     /**
190      * Returns the bandwidth limit of the given type or {@code -1} if it is not
191      * present.
192      */
getBandwidth(String type)193     public int getBandwidth(String type) {
194         return mFields.getBandwidth(type);
195     }
196 
197     /**
198      * Sets the bandwith limit for the given type. The field will be removed if
199      * the value is negative.
200      */
setBandwidth(String type, int value)201     public void setBandwidth(String type, int value) {
202         mFields.setBandwidth(type, value);
203     }
204 
205     /**
206      * Returns the names of all the attributes.
207      */
getAttributeNames()208     public String[] getAttributeNames() {
209         return mFields.getAttributeNames();
210     }
211 
212     /**
213      * Returns the attribute of the given name or {@code null} if it is not
214      * present.
215      */
getAttribute(String name)216     public String getAttribute(String name) {
217         return mFields.getAttribute(name);
218     }
219 
220     /**
221      * Sets the attribute for the given name. The field will be removed if
222      * the value is {@code null}. To set a binary attribute, use an empty
223      * string as the value.
224      */
setAttribute(String name, String value)225     public void setAttribute(String name, String value) {
226         mFields.setAttribute(name, value);
227     }
228 
229     /**
230      * This class represents a media description of a session description. It
231      * can only be created by {@link SimpleSessionDescription#newMedia}. Since
232      * the syntax is more restricted for RTP based protocols, two sets of access
233      * methods are implemented. See {@link SimpleSessionDescription} for an
234      * example of its usage.
235      */
236     public static class Media extends Fields {
237         private final String mType;
238         private final int mPort;
239         private final int mPortCount;
240         private final String mProtocol;
241         private ArrayList<String> mFormats = new ArrayList<String>();
242 
Media(String type, int port, int portCount, String protocol)243         private Media(String type, int port, int portCount, String protocol) {
244             super("icbka");
245             mType = type;
246             mPort = port;
247             mPortCount = portCount;
248             mProtocol = protocol;
249         }
250 
251         /**
252          * Returns the media type.
253          */
getType()254         public String getType() {
255             return mType;
256         }
257 
258         /**
259          * Returns the first transport port used by this media.
260          */
getPort()261         public int getPort() {
262             return mPort;
263         }
264 
265         /**
266          * Returns the number of contiguous ports used by this media.
267          */
getPortCount()268         public int getPortCount() {
269             return mPortCount;
270         }
271 
272         /**
273          * Returns the transport protocol.
274          */
getProtocol()275         public String getProtocol() {
276             return mProtocol;
277         }
278 
279         /**
280          * Returns the media formats.
281          */
getFormats()282         public String[] getFormats() {
283             return mFormats.toArray(new String[mFormats.size()]);
284         }
285 
286         /**
287          * Returns the {@code fmtp} attribute of the given format or
288          * {@code null} if it is not present.
289          */
getFmtp(String format)290         public String getFmtp(String format) {
291             return super.get("a=fmtp:" + format, ' ');
292         }
293 
294         /**
295          * Sets a format and its {@code fmtp} attribute. If the attribute is
296          * {@code null}, the corresponding field will be removed.
297          */
setFormat(String format, String fmtp)298         public void setFormat(String format, String fmtp) {
299             mFormats.remove(format);
300             mFormats.add(format);
301             super.set("a=rtpmap:" + format, ' ', null);
302             super.set("a=fmtp:" + format, ' ', fmtp);
303         }
304 
305         /**
306          * Removes a format and its {@code fmtp} attribute.
307          */
removeFormat(String format)308         public void removeFormat(String format) {
309             mFormats.remove(format);
310             super.set("a=rtpmap:" + format, ' ', null);
311             super.set("a=fmtp:" + format, ' ', null);
312         }
313 
314         /**
315          * Returns the RTP payload types.
316          */
getRtpPayloadTypes()317         public int[] getRtpPayloadTypes() {
318             int[] types = new int[mFormats.size()];
319             int length = 0;
320             for (String format : mFormats) {
321                 try {
322                     types[length] = Integer.parseInt(format);
323                     ++length;
324                 } catch (NumberFormatException e) { }
325             }
326             return Arrays.copyOf(types, length);
327         }
328 
329         /**
330          * Returns the {@code rtpmap} attribute of the given RTP payload type
331          * or {@code null} if it is not present.
332          */
getRtpmap(int type)333         public String getRtpmap(int type) {
334             return super.get("a=rtpmap:" + type, ' ');
335         }
336 
337         /**
338          * Returns the {@code fmtp} attribute of the given RTP payload type or
339          * {@code null} if it is not present.
340          */
getFmtp(int type)341         public String getFmtp(int type) {
342             return super.get("a=fmtp:" + type, ' ');
343         }
344 
345         /**
346          * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp}
347          * attributes. If any of the attributes is {@code null}, the
348          * corresponding field will be removed. See
349          * {@link SimpleSessionDescription} for an example of its usage.
350          */
setRtpPayload(int type, String rtpmap, String fmtp)351         public void setRtpPayload(int type, String rtpmap, String fmtp) {
352             String format = String.valueOf(type);
353             mFormats.remove(format);
354             mFormats.add(format);
355             super.set("a=rtpmap:" + format, ' ', rtpmap);
356             super.set("a=fmtp:" + format, ' ', fmtp);
357         }
358 
359         /**
360          * Removes a RTP payload and its {@code rtpmap} and {@code fmtp}
361          * attributes.
362          */
removeRtpPayload(int type)363         public void removeRtpPayload(int type) {
364             removeFormat(String.valueOf(type));
365         }
366 
write(StringBuilder buffer)367         private void write(StringBuilder buffer) {
368             buffer.append("m=").append(mType).append(' ').append(mPort);
369             if (mPortCount != 1) {
370                 buffer.append('/').append(mPortCount);
371             }
372             buffer.append(' ').append(mProtocol);
373             for (String format : mFormats) {
374                 buffer.append(' ').append(format);
375             }
376             buffer.append("\r\n");
377             super.write(buffer);
378         }
379     }
380 
381     /**
382      * This class acts as a set of fields, and the size of the set is expected
383      * to be small. Therefore, it uses a simple list instead of maps. Each field
384      * has three parts: a key, a delimiter, and a value. Delimiters are special
385      * because they are not included in binary attributes. As a result, the
386      * private methods, which are the building blocks of this class, all take
387      * the delimiter as an argument.
388      */
389     private static class Fields {
390         private final String mOrder;
391         private final ArrayList<String> mLines = new ArrayList<String>();
392 
Fields(String order)393         Fields(String order) {
394             mOrder = order;
395         }
396 
397         /**
398          * Returns the connection address or {@code null} if it is not present.
399          */
getAddress()400         public String getAddress() {
401             String address = get("c", '=');
402             if (address == null) {
403                 return null;
404             }
405             String[] parts = address.split(" ");
406             if (parts.length != 3) {
407                 return null;
408             }
409             int slash = parts[2].indexOf('/');
410             return (slash < 0) ? parts[2] : parts[2].substring(0, slash);
411         }
412 
413         /**
414          * Sets the connection address. The field will be removed if the address
415          * is {@code null}.
416          */
setAddress(String address)417         public void setAddress(String address) {
418             if (address != null) {
419                 address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") +
420                         address;
421             }
422             set("c", '=', address);
423         }
424 
425         /**
426          * Returns the encryption method or {@code null} if it is not present.
427          */
getEncryptionMethod()428         public String getEncryptionMethod() {
429             String encryption = get("k", '=');
430             if (encryption == null) {
431                 return null;
432             }
433             int colon = encryption.indexOf(':');
434             return (colon == -1) ? encryption : encryption.substring(0, colon);
435         }
436 
437         /**
438          * Returns the encryption key or {@code null} if it is not present.
439          */
getEncryptionKey()440         public String getEncryptionKey() {
441             String encryption = get("k", '=');
442             if (encryption == null) {
443                 return null;
444             }
445             int colon = encryption.indexOf(':');
446             return (colon == -1) ? null : encryption.substring(0, colon + 1);
447         }
448 
449         /**
450          * Sets the encryption method and the encryption key. The field will be
451          * removed if the method is {@code null}.
452          */
setEncryption(String method, String key)453         public void setEncryption(String method, String key) {
454             set("k", '=', (method == null || key == null) ?
455                     method : method + ':' + key);
456         }
457 
458         /**
459          * Returns the types of the bandwidth limits.
460          */
getBandwidthTypes()461         public String[] getBandwidthTypes() {
462             return cut("b=", ':');
463         }
464 
465         /**
466          * Returns the bandwidth limit of the given type or {@code -1} if it is
467          * not present.
468          */
getBandwidth(String type)469         public int getBandwidth(String type) {
470             String value = get("b=" + type, ':');
471             if (value != null) {
472                 try {
473                     return Integer.parseInt(value);
474                 } catch (NumberFormatException e) { }
475                 setBandwidth(type, -1);
476             }
477             return -1;
478         }
479 
480         /**
481          * Sets the bandwith limit for the given type. The field will be removed
482          * if the value is negative.
483          */
setBandwidth(String type, int value)484         public void setBandwidth(String type, int value) {
485             set("b=" + type, ':', (value < 0) ? null : String.valueOf(value));
486         }
487 
488         /**
489          * Returns the names of all the attributes.
490          */
getAttributeNames()491         public String[] getAttributeNames() {
492             return cut("a=", ':');
493         }
494 
495         /**
496          * Returns the attribute of the given name or {@code null} if it is not
497          * present.
498          */
getAttribute(String name)499         public String getAttribute(String name) {
500             return get("a=" + name, ':');
501         }
502 
503         /**
504          * Sets the attribute for the given name. The field will be removed if
505          * the value is {@code null}. To set a binary attribute, use an empty
506          * string as the value.
507          */
setAttribute(String name, String value)508         public void setAttribute(String name, String value) {
509             set("a=" + name, ':', value);
510         }
511 
write(StringBuilder buffer)512         private void write(StringBuilder buffer) {
513             for (int i = 0; i < mOrder.length(); ++i) {
514                 char type = mOrder.charAt(i);
515                 for (String line : mLines) {
516                     if (line.charAt(0) == type) {
517                         buffer.append(line).append("\r\n");
518                     }
519                 }
520             }
521         }
522 
523         /**
524          * Invokes {@link #set} after splitting the line into three parts.
525          */
parse(String line)526         private void parse(String line) {
527             char type = line.charAt(0);
528             if (mOrder.indexOf(type) == -1) {
529                 return;
530             }
531             char delimiter = '=';
532             if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) {
533                 delimiter = ' ';
534             } else if (type == 'b' || type == 'a') {
535                 delimiter = ':';
536             }
537             int i = line.indexOf(delimiter);
538             if (i == -1) {
539                 set(line, delimiter, "");
540             } else {
541                 set(line.substring(0, i), delimiter, line.substring(i + 1));
542             }
543         }
544 
545         /**
546          * Finds the key with the given prefix and returns its suffix.
547          */
cut(String prefix, char delimiter)548         private String[] cut(String prefix, char delimiter) {
549             String[] names = new String[mLines.size()];
550             int length = 0;
551             for (String line : mLines) {
552                 if (line.startsWith(prefix)) {
553                     int i = line.indexOf(delimiter);
554                     if (i == -1) {
555                         i = line.length();
556                     }
557                     names[length] = line.substring(prefix.length(), i);
558                     ++length;
559                 }
560             }
561             return Arrays.copyOf(names, length);
562         }
563 
564         /**
565          * Returns the index of the key.
566          */
find(String key, char delimiter)567         private int find(String key, char delimiter) {
568             int length = key.length();
569             for (int i = mLines.size() - 1; i >= 0; --i) {
570                 String line = mLines.get(i);
571                 if (line.startsWith(key) && (line.length() == length ||
572                         line.charAt(length) == delimiter)) {
573                     return i;
574                 }
575             }
576             return -1;
577         }
578 
579         /**
580          * Sets the key with the value or removes the key if the value is
581          * {@code null}.
582          */
set(String key, char delimiter, String value)583         private void set(String key, char delimiter, String value) {
584             int index = find(key, delimiter);
585             if (value != null) {
586                 if (value.length() != 0) {
587                     key = key + delimiter + value;
588                 }
589                 if (index == -1) {
590                     mLines.add(key);
591                 } else {
592                     mLines.set(index, key);
593                 }
594             } else if (index != -1) {
595                 mLines.remove(index);
596             }
597         }
598 
599         /**
600          * Returns the value of the key.
601          */
get(String key, char delimiter)602         private String get(String key, char delimiter) {
603             int index = find(key, delimiter);
604             if (index == -1) {
605                 return null;
606             }
607             String line = mLines.get(index);
608             int length = key.length();
609             return (line.length() == length) ? "" : line.substring(length + 1);
610         }
611     }
612 }
613