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