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