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