SimpleSessionDescription.java revision e6c0c109588771a97aba51d06fdf73557b06dfd3
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
17package android.net.sip;
18
19import java.util.ArrayList;
20import 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 */
56public 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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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         */
254        public String getType() {
255            return mType;
256        }
257
258        /**
259         * Returns the first transport port used by this media.
260         */
261        public int getPort() {
262            return mPort;
263        }
264
265        /**
266         * Returns the number of contiguous ports used by this media.
267         */
268        public int getPortCount() {
269            return mPortCount;
270        }
271
272        /**
273         * Returns the transport protocol.
274         */
275        public String getProtocol() {
276            return mProtocol;
277        }
278
279        /**
280         * Returns the media formats.
281         */
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         */
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         */
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         */
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         */
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         */
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         */
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 {@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         */
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         */
363        public void removeRtpPayload(int type) {
364            removeFormat(String.valueOf(type));
365        }
366
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
393        Fields(String order) {
394            mOrder = order;
395        }
396
397        /**
398         * Returns the connection address or {@code null} if it is not present.
399         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
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         */
508        public void setAttribute(String name, String value) {
509            set("a=" + name, ':', value);
510        }
511
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         */
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         */
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         */
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         */
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         */
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