RtpDescription.java

  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}