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