RtpDescription.java

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