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