1package eu.siacs.conversations.xmpp.jingle.stanzas;
2
3import android.util.Pair;
4import com.google.common.base.Preconditions;
5import com.google.common.base.Strings;
6import com.google.common.collect.ArrayListMultimap;
7import com.google.common.collect.ImmutableList;
8import com.google.common.collect.Iterables;
9import com.google.common.collect.Sets;
10import eu.siacs.conversations.xml.Element;
11import eu.siacs.conversations.xml.Namespace;
12import eu.siacs.conversations.xmpp.jingle.Media;
13import eu.siacs.conversations.xmpp.jingle.SessionDescription;
14import java.util.Collection;
15import java.util.HashMap;
16import java.util.List;
17import java.util.Map;
18import java.util.Set;
19
20public class RtpDescription extends GenericDescription {
21
22 private RtpDescription(final String media) {
23 super("description", Namespace.JINGLE_APPS_RTP);
24 this.setAttribute("media", media);
25 }
26
27 private RtpDescription() {
28 super("description", Namespace.JINGLE_APPS_RTP);
29 }
30
31 public static RtpDescription stub(final Media media) {
32 return new RtpDescription(media.toString());
33 }
34
35 public Media getMedia() {
36 return Media.of(this.getAttribute("media"));
37 }
38
39 public List<PayloadType> getPayloadTypes() {
40 final ImmutableList.Builder<PayloadType> builder = new ImmutableList.Builder<>();
41 for (Element child : getChildren()) {
42 if ("payload-type".equals(child.getName())) {
43 builder.add(PayloadType.of(child));
44 }
45 }
46 return builder.build();
47 }
48
49 public List<FeedbackNegotiation> getFeedbackNegotiations() {
50 return FeedbackNegotiation.fromChildren(this.getChildren());
51 }
52
53 public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
54 return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
55 }
56
57 public List<RtpHeaderExtension> getHeaderExtensions() {
58 final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
59 for (final Element child : getChildren()) {
60 if ("rtp-hdrext".equals(child.getName())
61 && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) {
62 builder.add(RtpHeaderExtension.upgrade(child));
63 }
64 }
65 return builder.build();
66 }
67
68 public List<Source> getSources() {
69 final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
70 for (final Element child : getChildren()) {
71 if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
72 builder.add(Source.upgrade(child));
73 }
74 }
75 return builder.build();
76 }
77
78 public List<SourceGroup> getSourceGroups() {
79 final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
80 for (final Element child : getChildren()) {
81 if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
82 builder.add(SourceGroup.upgrade(child));
83 }
84 }
85 return builder.build();
86 }
87
88 public static RtpDescription upgrade(final Element element) {
89 Preconditions.checkArgument(
90 "description".equals(element.getName()),
91 "Name of provided element is not description");
92 Preconditions.checkArgument(
93 Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()),
94 "Element does not match the jingle rtp namespace");
95 final RtpDescription description = new RtpDescription();
96 description.setAttributes(element.getAttributes());
97 description.setChildren(element.getChildren());
98 return description;
99 }
100
101 public static class FeedbackNegotiation extends Element {
102 private FeedbackNegotiation() {
103 super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
104 }
105
106 public FeedbackNegotiation(String type, String subType) {
107 super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
108 this.setAttribute("type", type);
109 if (subType != null) {
110 this.setAttribute("subtype", subType);
111 }
112 }
113
114 public String getType() {
115 return this.getAttribute("type");
116 }
117
118 public String getSubType() {
119 return this.getAttribute("subtype");
120 }
121
122 private static FeedbackNegotiation upgrade(final Element element) {
123 Preconditions.checkArgument("rtcp-fb".equals(element.getName()));
124 Preconditions.checkArgument(
125 Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
126 final FeedbackNegotiation feedback = new FeedbackNegotiation();
127 feedback.setAttributes(element.getAttributes());
128 feedback.setChildren(element.getChildren());
129 return feedback;
130 }
131
132 public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
133 ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
134 for (final Element child : children) {
135 if ("rtcp-fb".equals(child.getName())
136 && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
137 builder.add(upgrade(child));
138 }
139 }
140 return builder.build();
141 }
142 }
143
144 public static class FeedbackNegotiationTrrInt extends Element {
145
146 private FeedbackNegotiationTrrInt(int value) {
147 super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
148 this.setAttribute("value", value);
149 }
150
151 private FeedbackNegotiationTrrInt() {
152 super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
153 }
154
155 public int getValue() {
156 final String value = getAttribute("value");
157 return Integer.parseInt(value);
158 }
159
160 private static FeedbackNegotiationTrrInt upgrade(final Element element) {
161 Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
162 Preconditions.checkArgument(
163 Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
164 final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
165 trr.setAttributes(element.getAttributes());
166 trr.setChildren(element.getChildren());
167 return trr;
168 }
169
170 public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
171 ImmutableList.Builder<FeedbackNegotiationTrrInt> builder =
172 new ImmutableList.Builder<>();
173 for (final Element child : children) {
174 if ("rtcp-fb-trr-int".equals(child.getName())
175 && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
176 builder.add(upgrade(child));
177 }
178 }
179 return builder.build();
180 }
181 }
182
183 // XEP-0294: Jingle RTP Header Extensions Negotiation
184 // maps to `extmap:$id $uri`
185 public static class RtpHeaderExtension extends Element {
186
187 private RtpHeaderExtension() {
188 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
189 }
190
191 public RtpHeaderExtension(String id, String uri) {
192 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
193 this.setAttribute("id", id);
194 this.setAttribute("uri", uri);
195 }
196
197 public String getId() {
198 return this.getAttribute("id");
199 }
200
201 public String getUri() {
202 return this.getAttribute("uri");
203 }
204
205 public static RtpHeaderExtension upgrade(final Element element) {
206 Preconditions.checkArgument("rtp-hdrext".equals(element.getName()));
207 Preconditions.checkArgument(
208 Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace()));
209 final RtpHeaderExtension extension = new RtpHeaderExtension();
210 extension.setAttributes(element.getAttributes());
211 extension.setChildren(element.getChildren());
212 return extension;
213 }
214
215 public static RtpHeaderExtension ofSdpString(final String sdp) {
216 final String[] pair = sdp.split(" ", 2);
217 if (pair.length == 2) {
218 final String id = pair[0];
219 final String uri = pair[1];
220 return new RtpHeaderExtension(id, uri);
221 } else {
222 return null;
223 }
224 }
225 }
226
227 // maps to `rtpmap:$id $name/$clockrate/$channels`
228 public static class PayloadType extends Element {
229
230 private PayloadType() {
231 super("payload-type", Namespace.JINGLE_APPS_RTP);
232 }
233
234 public PayloadType(String id, String name, int clockRate, int channels) {
235 super("payload-type", Namespace.JINGLE_APPS_RTP);
236 this.setAttribute("id", id);
237 this.setAttribute("name", name);
238 this.setAttribute("clockrate", clockRate);
239 if (channels != 1) {
240 this.setAttribute("channels", channels);
241 }
242 }
243
244 public String toSdpAttribute() {
245 final int channels = getChannels();
246 final String name = getPayloadTypeName();
247 Preconditions.checkArgument(name != null, "Payload-type name must not be empty");
248 SessionDescription.checkNoWhitespace(
249 name, "payload-type name must not contain whitespaces");
250 return getId()
251 + " "
252 + name
253 + "/"
254 + getClockRate()
255 + (channels == 1 ? "" : "/" + channels);
256 }
257
258 public int getIntId() {
259 final String id = this.getAttribute("id");
260 return id == null ? 0 : SessionDescription.ignorantIntParser(id);
261 }
262
263 public String getId() {
264 return this.getAttribute("id");
265 }
266
267 public String getPayloadTypeName() {
268 return this.getAttribute("name");
269 }
270
271 public int getClockRate() {
272 final String clockRate = this.getAttribute("clockrate");
273 if (clockRate == null) {
274 return 0;
275 }
276 try {
277 return Integer.parseInt(clockRate);
278 } catch (NumberFormatException e) {
279 return 0;
280 }
281 }
282
283 public int getChannels() {
284 final String channels = this.getAttribute("channels");
285 if (channels == null) {
286 return 1; // The number of channels; if omitted, it MUST be assumed to contain one
287 // channel
288 }
289 try {
290 return Integer.parseInt(channels);
291 } catch (NumberFormatException e) {
292 return 1;
293 }
294 }
295
296 public List<Parameter> getParameters() {
297 final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
298 for (Element child : getChildren()) {
299 if ("parameter".equals(child.getName())) {
300 builder.add(Parameter.of(child));
301 }
302 }
303 return builder.build();
304 }
305
306 public List<FeedbackNegotiation> getFeedbackNegotiations() {
307 return FeedbackNegotiation.fromChildren(this.getChildren());
308 }
309
310 public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
311 return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
312 }
313
314 public static PayloadType of(final Element element) {
315 Preconditions.checkArgument(
316 "payload-type".equals(element.getName()),
317 "element name must be called payload-type");
318 PayloadType payloadType = new PayloadType();
319 payloadType.setAttributes(element.getAttributes());
320 payloadType.setChildren(element.getChildren());
321 return payloadType;
322 }
323
324 public static PayloadType ofSdpString(final String sdp) {
325 final String[] pair = sdp.split(" ", 2);
326 if (pair.length == 2) {
327 final String id = pair[0];
328 final String[] parts = pair[1].split("/");
329 if (parts.length >= 2) {
330 final String name = parts[0];
331 final int clockRate = SessionDescription.ignorantIntParser(parts[1]);
332 final int channels;
333 if (parts.length >= 3) {
334 channels = SessionDescription.ignorantIntParser(parts[2]);
335 } else {
336 channels = 1;
337 }
338 return new PayloadType(id, name, clockRate, channels);
339 }
340 }
341 return null;
342 }
343
344 public void addParameters(List<Parameter> parameters) {
345 addChildren(parameters);
346 }
347 }
348
349 // map to `fmtp $id key=value;key=value
350 // where id is the id of the parent payload-type
351 public static class Parameter extends Element {
352
353 private Parameter() {
354 super("parameter", Namespace.JINGLE_APPS_RTP);
355 }
356
357 public Parameter(String name, String value) {
358 super("parameter", Namespace.JINGLE_APPS_RTP);
359 this.setAttribute("name", name);
360 this.setAttribute("value", value);
361 }
362
363 public String getParameterName() {
364 return this.getAttribute("name");
365 }
366
367 public String getParameterValue() {
368 return this.getAttribute("value");
369 }
370
371 public static Parameter of(final Element element) {
372 Preconditions.checkArgument(
373 "parameter".equals(element.getName()), "element name must be called parameter");
374 Parameter parameter = new Parameter();
375 parameter.setAttributes(element.getAttributes());
376 parameter.setChildren(element.getChildren());
377 return parameter;
378 }
379
380 public static String toSdpString(final String id, List<Parameter> parameters) {
381 final StringBuilder stringBuilder = new StringBuilder();
382 stringBuilder.append(id).append(' ');
383 for (int i = 0; i < parameters.size(); ++i) {
384 final Parameter p = parameters.get(i);
385 final String name = p.getParameterName();
386 Preconditions.checkArgument(
387 name != null, String.format("parameter for %s must have a name", id));
388 SessionDescription.checkNoWhitespace(
389 name,
390 String.format("parameter names for %s must not contain whitespaces", id));
391
392 final String value = p.getParameterValue();
393 Preconditions.checkArgument(
394 value != null, String.format("parameter for %s must have a value", id));
395 SessionDescription.checkNoWhitespace(
396 value,
397 String.format("parameter values for %s must not contain whitespaces", id));
398
399 stringBuilder.append(name).append('=').append(value);
400 if (i != parameters.size() - 1) {
401 stringBuilder.append(';');
402 }
403 }
404 return stringBuilder.toString();
405 }
406
407 public static String toSdpString(final String id, final Parameter parameter) {
408 final String name = parameter.getParameterName();
409 final String value = parameter.getParameterValue();
410 Preconditions.checkArgument(
411 value != null, String.format("parameter for %s must have a value", id));
412 SessionDescription.checkNoWhitespace(
413 value,
414 String.format("parameter values for %s must not contain whitespaces", id));
415 if (Strings.isNullOrEmpty(name)) {
416 return String.format("%s %s", id, value);
417 } else {
418 return String.format("%s %s=%s", id, name, value);
419 }
420 }
421
422 static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
423 final String[] pair = sdp.split(" ");
424 if (pair.length == 2) {
425 final String id = pair[0];
426 final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
427 for (final String parameter : pair[1].split(";")) {
428 final String[] parts = parameter.split("=", 2);
429 if (parts.length == 2) {
430 builder.add(new Parameter(parts[0], parts[1]));
431 }
432 }
433 return new Pair<>(id, builder.build());
434 } else {
435 return null;
436 }
437 }
438 }
439
440 // XEP-0339: Source-Specific Media Attributes in Jingle
441 // maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
442 public static class Source extends Element {
443
444 private Source() {
445 super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
446 }
447
448 public Source(String ssrcId, Collection<Parameter> parameters) {
449 super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
450 this.setAttribute("ssrc", ssrcId);
451 for (Parameter parameter : parameters) {
452 this.addChild(parameter);
453 }
454 }
455
456 public String getSsrcId() {
457 return this.getAttribute("ssrc");
458 }
459
460 public List<Parameter> getParameters() {
461 ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
462 for (Element child : getChildren()) {
463 if ("parameter".equals(child.getName())) {
464 builder.add(Parameter.upgrade(child));
465 }
466 }
467 return builder.build();
468 }
469
470 public static Source upgrade(final Element element) {
471 Preconditions.checkArgument("source".equals(element.getName()));
472 Preconditions.checkArgument(
473 Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
474 element.getNamespace()));
475 final Source source = new Source();
476 source.setChildren(element.getChildren());
477 source.setAttributes(element.getAttributes());
478 return source;
479 }
480
481 public static class Parameter extends Element {
482
483 public String getParameterName() {
484 return this.getAttribute("name");
485 }
486
487 public String getParameterValue() {
488 return this.getAttribute("value");
489 }
490
491 private Parameter() {
492 super("parameter");
493 }
494
495 public Parameter(final String attribute, final String value) {
496 super("parameter");
497 this.setAttribute("name", attribute);
498 if (value != null) {
499 this.setAttribute("value", value);
500 }
501 }
502
503 public static Parameter upgrade(final Element element) {
504 Preconditions.checkArgument("parameter".equals(element.getName()));
505 Parameter parameter = new Parameter();
506 parameter.setAttributes(element.getAttributes());
507 parameter.setChildren(element.getChildren());
508 return parameter;
509 }
510 }
511 }
512
513 public static class SourceGroup extends Element {
514
515 public SourceGroup(final String semantics, List<String> ssrcs) {
516 this();
517 this.setAttribute("semantics", semantics);
518 for (String ssrc : ssrcs) {
519 this.addChild("source").setAttribute("ssrc", ssrc);
520 }
521 }
522
523 private SourceGroup() {
524 super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
525 }
526
527 public String getSemantics() {
528 return this.getAttribute("semantics");
529 }
530
531 public List<String> getSsrcs() {
532 final ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
533 for (Element child : getChildren()) {
534 if ("source".equals(child.getName())) {
535 final String ssrc = child.getAttribute("ssrc");
536 if (Strings.isNullOrEmpty(ssrc)) {
537 continue;
538 }
539 builder.add(
540 SessionDescription.checkNoNewline(
541 ssrc,
542 "Source Specific media attributes can not contain newline"));
543 }
544 }
545 return builder.build();
546 }
547
548 public static SourceGroup upgrade(final Element element) {
549 Preconditions.checkArgument("ssrc-group".equals(element.getName()));
550 Preconditions.checkArgument(
551 Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
552 element.getNamespace()));
553 final SourceGroup group = new SourceGroup();
554 group.setChildren(element.getChildren());
555 group.setAttributes(element.getAttributes());
556 return group;
557 }
558 }
559
560 public static RtpDescription of(
561 final SessionDescription sessionDescription, final SessionDescription.Media media) {
562 final RtpDescription rtpDescription = new RtpDescription(media.media);
563 final Map<String, List<Parameter>> parameterMap = new HashMap<>();
564 final ArrayListMultimap<String, Element> feedbackNegotiationMap =
565 ArrayListMultimap.create();
566 final ArrayListMultimap<String, Source.Parameter> sourceParameterMap =
567 ArrayListMultimap.create();
568 final Set<String> attributes =
569 Sets.newHashSet(
570 Iterables.concat(
571 sessionDescription.attributes.keySet(), media.attributes.keySet()));
572 for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
573 final String[] parts = rtcpFb.split(" ");
574 if (parts.length >= 2) {
575 final String id = parts[0];
576 final String type = parts[1];
577 final String subType = parts.length >= 3 ? parts[2] : null;
578 if ("trr-int".equals(type)) {
579 if (subType != null) {
580 feedbackNegotiationMap.put(
581 id,
582 new FeedbackNegotiationTrrInt(
583 SessionDescription.ignorantIntParser(subType)));
584 }
585 } else {
586 feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
587 }
588 }
589 }
590 for (final String ssrc : media.attributes.get(("ssrc"))) {
591 final String[] parts = ssrc.split(" ", 2);
592 if (parts.length == 2) {
593 final String id = parts[0];
594 final String[] subParts = parts[1].split(":", 2);
595 final String attribute = subParts[0];
596 final String value = subParts.length == 2 ? subParts[1] : null;
597 sourceParameterMap.put(id, new Source.Parameter(attribute, value));
598 }
599 }
600 for (final String fmtp : media.attributes.get("fmtp")) {
601 final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
602 if (pair != null) {
603 parameterMap.put(pair.first, pair.second);
604 }
605 }
606 rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
607 for (final String rtpmap : media.attributes.get("rtpmap")) {
608 final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
609 if (payloadType != null) {
610 payloadType.addParameters(parameterMap.get(payloadType.getId()));
611 payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
612 rtpDescription.addChild(payloadType);
613 }
614 }
615 for (final String extmap : media.attributes.get("extmap")) {
616 final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
617 if (extension != null) {
618 rtpDescription.addChild(extension);
619 }
620 }
621 if (attributes.contains("extmap-allow-mixed")) {
622 rtpDescription.addChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
623 }
624 for (final String ssrcGroup : media.attributes.get("ssrc-group")) {
625 final String[] parts = ssrcGroup.split(" ");
626 if (parts.length >= 2) {
627 ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
628 final String semantics = parts[0];
629 for (int i = 1; i < parts.length; ++i) {
630 builder.add(parts[i]);
631 }
632 rtpDescription.addChild(new SourceGroup(semantics, builder.build()));
633 }
634 }
635 for (Map.Entry<String, Collection<Source.Parameter>> source :
636 sourceParameterMap.asMap().entrySet()) {
637 rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
638 }
639 if (media.attributes.containsKey("rtcp-mux")) {
640 rtpDescription.addChild("rtcp-mux");
641 }
642 return rtpDescription;
643 }
644}