RtpDescription.java

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