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