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 return Integer.parseInt(value);
149
150 }
151
152 private static FeedbackNegotiationTrrInt upgrade(final Element element) {
153 Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
154 Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
155 final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
156 trr.setAttributes(element.getAttributes());
157 trr.setChildren(element.getChildren());
158 return trr;
159 }
160
161 public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
162 ImmutableList.Builder<FeedbackNegotiationTrrInt> builder = new ImmutableList.Builder<>();
163 for (final Element child : children) {
164 if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
165 builder.add(upgrade(child));
166 }
167 }
168 return builder.build();
169 }
170 }
171
172
173 //XEP-0294: Jingle RTP Header Extensions Negotiation
174 //maps to `extmap:$id $uri`
175 public static class RtpHeaderExtension extends Element {
176
177 private RtpHeaderExtension() {
178 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
179 }
180
181 public RtpHeaderExtension(String id, String uri) {
182 super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
183 this.setAttribute("id", id);
184 this.setAttribute("uri", uri);
185 }
186
187 public String getId() {
188 return this.getAttribute("id");
189 }
190
191 public String getUri() {
192 return this.getAttribute("uri");
193 }
194
195 public static RtpHeaderExtension upgrade(final Element element) {
196 Preconditions.checkArgument("rtp-hdrext".equals(element.getName()));
197 Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace()));
198 final RtpHeaderExtension extension = new RtpHeaderExtension();
199 extension.setAttributes(element.getAttributes());
200 extension.setChildren(element.getChildren());
201 return extension;
202 }
203
204 public static RtpHeaderExtension ofSdpString(final String sdp) {
205 final String[] pair = sdp.split(" ", 2);
206 if (pair.length == 2) {
207 final String id = pair[0];
208 final String uri = pair[1];
209 return new RtpHeaderExtension(id, uri);
210 } else {
211 return null;
212 }
213 }
214 }
215
216 //maps to `rtpmap:$id $name/$clockrate/$channels`
217 public static class PayloadType extends Element {
218
219 private PayloadType() {
220 super("payload-type", Namespace.JINGLE_APPS_RTP);
221 }
222
223 public PayloadType(String id, String name, int clockRate, int channels) {
224 super("payload-type", Namespace.JINGLE_APPS_RTP);
225 this.setAttribute("id", id);
226 this.setAttribute("name", name);
227 this.setAttribute("clockrate", clockRate);
228 if (channels != 1) {
229 this.setAttribute("channels", channels);
230 }
231 }
232
233 public String toSdpAttribute() {
234 final int channels = getChannels();
235 return getId()+" "+getPayloadTypeName()+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels);
236 }
237
238 public int getIntId() {
239 final String id = this.getAttribute("id");
240 return id == null ? 0 : SessionDescription.ignorantIntParser(id);
241 }
242
243 public String getId() {
244 return this.getAttribute("id");
245 }
246
247
248 public String getPayloadTypeName() {
249 return this.getAttribute("name");
250 }
251
252 public int getClockRate() {
253 final String clockRate = this.getAttribute("clockrate");
254 if (clockRate == null) {
255 return 0;
256 }
257 try {
258 return Integer.parseInt(clockRate);
259 } catch (NumberFormatException e) {
260 return 0;
261 }
262 }
263
264 public int getChannels() {
265 final String channels = this.getAttribute("channels");
266 if (channels == null) {
267 return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel
268 }
269 try {
270 return Integer.parseInt(channels);
271 } catch (NumberFormatException e) {
272 return 1;
273 }
274 }
275
276 public List<Parameter> getParameters() {
277 final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
278 for (Element child : getChildren()) {
279 if ("parameter".equals(child.getName())) {
280 builder.add(Parameter.of(child));
281 }
282 }
283 return builder.build();
284 }
285
286 public List<FeedbackNegotiation> getFeedbackNegotiations() {
287 return FeedbackNegotiation.fromChildren(this.getChildren());
288 }
289
290 public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
291 return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
292 }
293
294 public static PayloadType of(final Element element) {
295 Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type");
296 PayloadType payloadType = new PayloadType();
297 payloadType.setAttributes(element.getAttributes());
298 payloadType.setChildren(element.getChildren());
299 return payloadType;
300 }
301
302 public static PayloadType ofSdpString(final String sdp) {
303 final String[] pair = sdp.split(" ", 2);
304 if (pair.length == 2) {
305 final String id = pair[0];
306 final String[] parts = pair[1].split("/");
307 if (parts.length >= 2) {
308 final String name = parts[0];
309 final int clockRate = SessionDescription.ignorantIntParser(parts[1]);
310 final int channels;
311 if (parts.length >= 3) {
312 channels = SessionDescription.ignorantIntParser(parts[2]);
313 } else {
314 channels = 1;
315 }
316 return new PayloadType(id, name, clockRate, channels);
317 }
318 }
319 return null;
320 }
321
322 public void addChildren(final List<Element> children) {
323 if (children != null) {
324 this.children.addAll(children);
325 }
326 }
327
328 public void addParameters(List<Parameter> parameters) {
329 if (parameters != null) {
330 this.children.addAll(parameters);
331 }
332 }
333 }
334
335 //map to `fmtp $id key=value;key=value
336 //where id is the id of the parent payload-type
337 public static class Parameter extends Element {
338
339 private Parameter() {
340 super("parameter", Namespace.JINGLE_APPS_RTP);
341 }
342
343 public Parameter(String name, String value) {
344 super("parameter", Namespace.JINGLE_APPS_RTP);
345 this.setAttribute("name", name);
346 this.setAttribute("value", value);
347 }
348
349 public String getParameterName() {
350 return this.getAttribute("name");
351 }
352
353 public String getParameterValue() {
354 return this.getAttribute("value");
355 }
356
357 public static Parameter of(final Element element) {
358 Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter");
359 Parameter parameter = new Parameter();
360 parameter.setAttributes(element.getAttributes());
361 parameter.setChildren(element.getChildren());
362 return parameter;
363 }
364
365 public static String toSdpString(final String id, List<Parameter> parameters) {
366 final StringBuilder stringBuilder = new StringBuilder();
367 stringBuilder.append(id).append(' ');
368 for(int i = 0; i < parameters.size(); ++i) {
369 Parameter p = parameters.get(i);
370 stringBuilder.append(p.getParameterName()).append('=').append(p.getParameterValue());
371 if (i != parameters.size() - 1) {
372 stringBuilder.append(';');
373 }
374 }
375 return stringBuilder.toString();
376 }
377
378 public static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
379 final String[] pair = sdp.split(" ");
380 if (pair.length == 2) {
381 final String id = pair[0];
382 ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
383 for (final String parameter : pair[1].split(";")) {
384 final String[] parts = parameter.split("=", 2);
385 if (parts.length == 2) {
386 builder.add(new Parameter(parts[0], parts[1]));
387 }
388 }
389 return new Pair<>(id, builder.build());
390 } else {
391 return null;
392 }
393 }
394 }
395
396 //XEP-0339: Source-Specific Media Attributes in Jingle
397 //maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
398 public static class Source extends Element {
399
400 private Source() {
401 super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
402 }
403
404 public Source(String ssrcId, Collection<Parameter> parameters) {
405 super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
406 this.setAttribute("ssrc", ssrcId);
407 for (Parameter parameter : parameters) {
408 this.addChild(parameter);
409 }
410 }
411
412 public String getSsrcId() {
413 return this.getAttribute("ssrc");
414 }
415
416 public List<Parameter> getParameters() {
417 ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
418 for (Element child : this.children) {
419 if ("parameter".equals(child.getName())) {
420 builder.add(Parameter.upgrade(child));
421 }
422 }
423 return builder.build();
424 }
425
426 public static Source upgrade(final Element element) {
427 Preconditions.checkArgument("source".equals(element.getName()));
428 Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
429 final Source source = new Source();
430 source.setChildren(element.getChildren());
431 source.setAttributes(element.getAttributes());
432 return source;
433 }
434
435 public static class Parameter extends Element {
436
437 public String getParameterName() {
438 return this.getAttribute("name");
439 }
440
441 public String getParameterValue() {
442 return this.getAttribute("value");
443 }
444
445 private Parameter() {
446 super("parameter");
447 }
448
449 public Parameter(final String attribute, final String value) {
450 super("parameter");
451 this.setAttribute("name", attribute);
452 if (value != null) {
453 this.setAttribute("value", value);
454 }
455 }
456
457 public static Parameter upgrade(final Element element) {
458 Preconditions.checkArgument("parameter".equals(element.getName()));
459 Parameter parameter = new Parameter();
460 parameter.setAttributes(element.getAttributes());
461 parameter.setChildren(element.getChildren());
462 return parameter;
463 }
464 }
465
466 }
467
468 public static class SourceGroup extends Element {
469
470 private SourceGroup() {
471 super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
472 }
473
474 public String getSemantics() {
475 return this.getAttribute("semantics");
476 }
477
478 public List<String> getSsrcs() {
479 ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
480 for(Element child : this.children) {
481 if ("source".equals(child.getName())) {
482 final String ssrc = child.getAttribute("ssrc");
483 if (ssrc != null) {
484 builder.add(ssrc);
485 }
486 }
487 }
488 return builder.build();
489 }
490
491 public static SourceGroup upgrade(final Element element) {
492 Preconditions.checkArgument("ssrc-group".equals(element.getName()));
493 Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
494 final SourceGroup group = new SourceGroup();
495 group.setChildren(element.getChildren());
496 group.setAttributes(element.getAttributes());
497 return group;
498 }
499 }
500
501 public enum Media {
502 VIDEO, AUDIO, UNKNOWN;
503
504 @Override
505 public String toString() {
506 return super.toString().toLowerCase(Locale.ROOT);
507 }
508
509 public static Media of(String value) {
510 try {
511 return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT));
512 } catch (IllegalArgumentException e) {
513 return UNKNOWN;
514 }
515 }
516 }
517
518 public static RtpDescription of(final SessionDescription.Media media) {
519 final RtpDescription rtpDescription = new RtpDescription(media.media);
520 final Map<String, List<Parameter>> parameterMap = new HashMap<>();
521 final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
522 final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
523 for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
524 final String[] parts = rtcpFb.split(" ");
525 if (parts.length >= 2) {
526 final String id = parts[0];
527 final String type = parts[1];
528 final String subType = parts.length >= 3 ? parts[2] : null;
529 if ("trr-int".equals(type)) {
530 if (subType != null) {
531 feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
532 }
533 } else {
534 feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
535 }
536 }
537 }
538 for (final String ssrc : media.attributes.get(("ssrc"))) {
539 final String[] parts = ssrc.split(" ", 2);
540 if (parts.length == 2) {
541 final String id = parts[0];
542 final String[] subParts = parts[1].split(":", 2);
543 final String attribute = subParts[0];
544 final String value = subParts.length == 2 ? subParts[1] : null;
545 sourceParameterMap.put(id, new Source.Parameter(attribute, value));
546 }
547 }
548 for (final String fmtp : media.attributes.get("fmtp")) {
549 final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
550 if (pair != null) {
551 parameterMap.put(pair.first, pair.second);
552 }
553 }
554 rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
555 for (final String rtpmap : media.attributes.get("rtpmap")) {
556 final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
557 if (payloadType != null) {
558 payloadType.addParameters(parameterMap.get(payloadType.getId()));
559 payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
560 rtpDescription.addChild(payloadType);
561 }
562 }
563 for (final String extmap : media.attributes.get("extmap")) {
564 final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
565 if (extension != null) {
566 rtpDescription.addChild(extension);
567 }
568 }
569 for (Map.Entry<String, Collection<Source.Parameter>> source : sourceParameterMap.asMap().entrySet()) {
570 rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
571 }
572 return rtpDescription;
573 }
574
575 private void addChildren(List<Element> elements) {
576 if (elements != null) {
577 this.children.addAll(elements);
578 }
579 }
580}