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