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