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 : this.children) {
 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 : this.children) {
 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 addChildren(final List<Element> children) {
330            if (children != null) {
331                this.children.addAll(children);
332            }
333        }
334
335        public void addParameters(List<Parameter> parameters) {
336            if (parameters != null) {
337                this.children.addAll(parameters);
338            }
339        }
340    }
341
342    //map to `fmtp $id key=value;key=value
343    //where id is the id of the parent payload-type
344    public static class Parameter extends Element {
345
346        private Parameter() {
347            super("parameter", Namespace.JINGLE_APPS_RTP);
348        }
349
350        public Parameter(String name, String value) {
351            super("parameter", Namespace.JINGLE_APPS_RTP);
352            this.setAttribute("name", name);
353            this.setAttribute("value", value);
354        }
355
356        public String getParameterName() {
357            return this.getAttribute("name");
358        }
359
360        public String getParameterValue() {
361            return this.getAttribute("value");
362        }
363
364        public static Parameter of(final Element element) {
365            Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter");
366            Parameter parameter = new Parameter();
367            parameter.setAttributes(element.getAttributes());
368            parameter.setChildren(element.getChildren());
369            return parameter;
370        }
371
372        public static String toSdpString(final String id, List<Parameter> parameters) {
373            final StringBuilder stringBuilder = new StringBuilder();
374            stringBuilder.append(id).append(' ');
375            for (int i = 0; i < parameters.size(); ++i) {
376                final Parameter p = parameters.get(i);
377                final String name = p.getParameterName();
378                Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id));
379                SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id));
380
381                final String value = p.getParameterValue();
382                Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
383                SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
384
385                stringBuilder.append(name).append('=').append(value);
386                if (i != parameters.size() - 1) {
387                    stringBuilder.append(';');
388                }
389            }
390            return stringBuilder.toString();
391        }
392
393        public static String toSdpString(final String id, final Parameter parameter) {
394            final String name = parameter.getParameterName();
395            final String value = parameter.getParameterValue();
396            Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
397            SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
398            if (Strings.isNullOrEmpty(name)) {
399                return String.format("%s %s", id, value);
400            } else {
401                return String.format("%s %s=%s", id, name, value);
402            }
403        }
404
405        static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
406            final String[] pair = sdp.split(" ");
407            if (pair.length == 2) {
408                final String id = pair[0];
409                final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
410                for (final String parameter : pair[1].split(";")) {
411                    final String[] parts = parameter.split("=", 2);
412                    if (parts.length == 2) {
413                        builder.add(new Parameter(parts[0], parts[1]));
414                    }
415                }
416                return new Pair<>(id, builder.build());
417            } else {
418                return null;
419            }
420        }
421    }
422
423    //XEP-0339: Source-Specific Media Attributes in Jingle
424    //maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
425    public static class Source extends Element {
426
427        private Source() {
428            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
429        }
430
431        public Source(String ssrcId, Collection<Parameter> parameters) {
432            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
433            this.setAttribute("ssrc", ssrcId);
434            for (Parameter parameter : parameters) {
435                this.addChild(parameter);
436            }
437        }
438
439        public String getSsrcId() {
440            return this.getAttribute("ssrc");
441        }
442
443        public List<Parameter> getParameters() {
444            ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
445            for (Element child : this.children) {
446                if ("parameter".equals(child.getName())) {
447                    builder.add(Parameter.upgrade(child));
448                }
449            }
450            return builder.build();
451        }
452
453        public static Source upgrade(final Element element) {
454            Preconditions.checkArgument("source".equals(element.getName()));
455            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
456            final Source source = new Source();
457            source.setChildren(element.getChildren());
458            source.setAttributes(element.getAttributes());
459            return source;
460        }
461
462        public static class Parameter extends Element {
463
464            public String getParameterName() {
465                return this.getAttribute("name");
466            }
467
468            public String getParameterValue() {
469                return this.getAttribute("value");
470            }
471
472            private Parameter() {
473                super("parameter");
474            }
475
476            public Parameter(final String attribute, final String value) {
477                super("parameter");
478                this.setAttribute("name", attribute);
479                if (value != null) {
480                    this.setAttribute("value", value);
481                }
482            }
483
484            public static Parameter upgrade(final Element element) {
485                Preconditions.checkArgument("parameter".equals(element.getName()));
486                Parameter parameter = new Parameter();
487                parameter.setAttributes(element.getAttributes());
488                parameter.setChildren(element.getChildren());
489                return parameter;
490            }
491        }
492
493    }
494
495    public static class SourceGroup extends Element {
496
497        public SourceGroup(final String semantics, List<String> ssrcs) {
498            this();
499            this.setAttribute("semantics", semantics);
500            for (String ssrc : ssrcs) {
501                this.addChild("source").setAttribute("ssrc", ssrc);
502            }
503        }
504
505        private SourceGroup() {
506            super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
507        }
508
509        public String getSemantics() {
510            return this.getAttribute("semantics");
511        }
512
513        public List<String> getSsrcs() {
514            ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
515            for (Element child : this.children) {
516                if ("source".equals(child.getName())) {
517                    final String ssrc = child.getAttribute("ssrc");
518                    if (ssrc != null) {
519                        builder.add(ssrc);
520                    }
521                }
522            }
523            return builder.build();
524        }
525
526        public static SourceGroup upgrade(final Element element) {
527            Preconditions.checkArgument("ssrc-group".equals(element.getName()));
528            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
529            final SourceGroup group = new SourceGroup();
530            group.setChildren(element.getChildren());
531            group.setAttributes(element.getAttributes());
532            return group;
533        }
534    }
535
536    public static RtpDescription of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
537        final RtpDescription rtpDescription = new RtpDescription(media.media);
538        final Map<String, List<Parameter>> parameterMap = new HashMap<>();
539        final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
540        final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
541        final Set<String> attributes = Sets.newHashSet(Iterables.concat(
542                sessionDescription.attributes.keySet(),
543                media.attributes.keySet()
544        ));
545        for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
546            final String[] parts = rtcpFb.split(" ");
547            if (parts.length >= 2) {
548                final String id = parts[0];
549                final String type = parts[1];
550                final String subType = parts.length >= 3 ? parts[2] : null;
551                if ("trr-int".equals(type)) {
552                    if (subType != null) {
553                        feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
554                    }
555                } else {
556                    feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
557                }
558            }
559        }
560        for (final String ssrc : media.attributes.get(("ssrc"))) {
561            final String[] parts = ssrc.split(" ", 2);
562            if (parts.length == 2) {
563                final String id = parts[0];
564                final String[] subParts = parts[1].split(":", 2);
565                final String attribute = subParts[0];
566                final String value = subParts.length == 2 ? subParts[1] : null;
567                sourceParameterMap.put(id, new Source.Parameter(attribute, value));
568            }
569        }
570        for (final String fmtp : media.attributes.get("fmtp")) {
571            final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
572            if (pair != null) {
573                parameterMap.put(pair.first, pair.second);
574            }
575        }
576        rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
577        for (final String rtpmap : media.attributes.get("rtpmap")) {
578            final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
579            if (payloadType != null) {
580                payloadType.addParameters(parameterMap.get(payloadType.getId()));
581                payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
582                rtpDescription.addChild(payloadType);
583            }
584        }
585        for (final String extmap : media.attributes.get("extmap")) {
586            final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
587            if (extension != null) {
588                rtpDescription.addChild(extension);
589            }
590        }
591        if (attributes.contains("extmap-allow-mixed")) {
592            rtpDescription.addChild("extmap-allow-mixed", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
593        }
594        for (final String ssrcGroup : media.attributes.get("ssrc-group")) {
595            final String[] parts = ssrcGroup.split(" ");
596            if (parts.length >= 2) {
597                ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
598                final String semantics = parts[0];
599                for (int i = 1; i < parts.length; ++i) {
600                    builder.add(parts[i]);
601                }
602                rtpDescription.addChild(new SourceGroup(semantics, builder.build()));
603            }
604        }
605        for (Map.Entry<String, Collection<Source.Parameter>> source : sourceParameterMap.asMap().entrySet()) {
606            rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
607        }
608        if (media.attributes.containsKey("rtcp-mux")) {
609            rtpDescription.addChild("rtcp-mux");
610        }
611        return rtpDescription;
612    }
613
614    private void addChildren(List<Element> elements) {
615        if (elements != null) {
616            this.children.addAll(elements);
617        }
618    }
619}