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