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