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