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