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