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.Locale;
 13import java.util.Map;
 14
 15import eu.siacs.conversations.xml.Element;
 16import eu.siacs.conversations.xml.Namespace;
 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            return getId()+" "+getPayloadTypeName()+"/"+getClockRate()+(channels == 1 ? "" : "/"+channels);
236        }
237
238        public int getIntId() {
239            final String id = this.getAttribute("id");
240            return id == null ? 0 : SessionDescription.ignorantIntParser(id);
241        }
242
243        public String getId() {
244            return this.getAttribute("id");
245        }
246
247
248        public String getPayloadTypeName() {
249            return this.getAttribute("name");
250        }
251
252        public int getClockRate() {
253            final String clockRate = this.getAttribute("clockrate");
254            if (clockRate == null) {
255                return 0;
256            }
257            try {
258                return Integer.parseInt(clockRate);
259            } catch (NumberFormatException e) {
260                return 0;
261            }
262        }
263
264        public int getChannels() {
265            final String channels = this.getAttribute("channels");
266            if (channels == null) {
267                return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel
268            }
269            try {
270                return Integer.parseInt(channels);
271            } catch (NumberFormatException e) {
272                return 1;
273            }
274        }
275
276        public List<Parameter> getParameters() {
277            final ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
278            for (Element child : getChildren()) {
279                if ("parameter".equals(child.getName())) {
280                    builder.add(Parameter.of(child));
281                }
282            }
283            return builder.build();
284        }
285
286        public List<FeedbackNegotiation> getFeedbackNegotiations() {
287            return FeedbackNegotiation.fromChildren(this.getChildren());
288        }
289
290        public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
291            return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
292        }
293
294        public static PayloadType of(final Element element) {
295            Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type");
296            PayloadType payloadType = new PayloadType();
297            payloadType.setAttributes(element.getAttributes());
298            payloadType.setChildren(element.getChildren());
299            return payloadType;
300        }
301
302        public static PayloadType ofSdpString(final String sdp) {
303            final String[] pair = sdp.split(" ", 2);
304            if (pair.length == 2) {
305                final String id = pair[0];
306                final String[] parts = pair[1].split("/");
307                if (parts.length >= 2) {
308                    final String name = parts[0];
309                    final int clockRate = SessionDescription.ignorantIntParser(parts[1]);
310                    final int channels;
311                    if (parts.length >= 3) {
312                        channels = SessionDescription.ignorantIntParser(parts[2]);
313                    } else {
314                        channels = 1;
315                    }
316                    return new PayloadType(id, name, clockRate, channels);
317                }
318            }
319            return null;
320        }
321
322        public void addChildren(final List<Element> children) {
323            if (children != null) {
324                this.children.addAll(children);
325            }
326        }
327
328        public void addParameters(List<Parameter> parameters) {
329            if (parameters != null) {
330                this.children.addAll(parameters);
331            }
332        }
333    }
334
335    //map to `fmtp $id key=value;key=value
336    //where id is the id of the parent payload-type
337    public static class Parameter extends Element {
338
339        private Parameter() {
340            super("parameter", Namespace.JINGLE_APPS_RTP);
341        }
342
343        public Parameter(String name, String value) {
344            super("parameter", Namespace.JINGLE_APPS_RTP);
345            this.setAttribute("name", name);
346            this.setAttribute("value", value);
347        }
348
349        public String getParameterName() {
350            return this.getAttribute("name");
351        }
352
353        public String getParameterValue() {
354            return this.getAttribute("value");
355        }
356
357        public static Parameter of(final Element element) {
358            Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter");
359            Parameter parameter = new Parameter();
360            parameter.setAttributes(element.getAttributes());
361            parameter.setChildren(element.getChildren());
362            return parameter;
363        }
364
365        public static String toSdpString(final String id, List<Parameter> parameters) {
366            final StringBuilder stringBuilder = new StringBuilder();
367            stringBuilder.append(id).append(' ');
368            for(int i = 0; i < parameters.size(); ++i) {
369                Parameter p = parameters.get(i);
370                stringBuilder.append(p.getParameterName()).append('=').append(p.getParameterValue());
371                if (i != parameters.size() - 1) {
372                    stringBuilder.append(';');
373                }
374            }
375            return stringBuilder.toString();
376        }
377
378        public static Pair<String, List<Parameter>> ofSdpString(final String sdp) {
379            final String[] pair = sdp.split(" ");
380            if (pair.length == 2) {
381                final String id = pair[0];
382                ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
383                for (final String parameter : pair[1].split(";")) {
384                    final String[] parts = parameter.split("=", 2);
385                    if (parts.length == 2) {
386                        builder.add(new Parameter(parts[0], parts[1]));
387                    }
388                }
389                return new Pair<>(id, builder.build());
390            } else {
391                return null;
392            }
393        }
394    }
395
396    //XEP-0339: Source-Specific Media Attributes in Jingle
397    //maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
398    public static class Source extends Element {
399
400        private Source() {
401            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
402        }
403
404        public Source(String ssrcId, Collection<Parameter> parameters) {
405            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
406            this.setAttribute("ssrc", ssrcId);
407            for (Parameter parameter : parameters) {
408                this.addChild(parameter);
409            }
410        }
411
412        public String getSsrcId() {
413            return this.getAttribute("ssrc");
414        }
415
416        public List<Parameter> getParameters() {
417            ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
418            for (Element child : this.children) {
419                if ("parameter".equals(child.getName())) {
420                    builder.add(Parameter.upgrade(child));
421                }
422            }
423            return builder.build();
424        }
425
426        public static Source upgrade(final Element element) {
427            Preconditions.checkArgument("source".equals(element.getName()));
428            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
429            final Source source = new Source();
430            source.setChildren(element.getChildren());
431            source.setAttributes(element.getAttributes());
432            return source;
433        }
434
435        public static class Parameter extends Element {
436
437            public String getParameterName() {
438                return this.getAttribute("name");
439            }
440
441            public String getParameterValue() {
442                return this.getAttribute("value");
443            }
444
445            private Parameter() {
446                super("parameter");
447            }
448
449            public Parameter(final String attribute, final String value) {
450                super("parameter");
451                this.setAttribute("name", attribute);
452                if (value != null) {
453                    this.setAttribute("value", value);
454                }
455            }
456
457            public static Parameter upgrade(final Element element) {
458                Preconditions.checkArgument("parameter".equals(element.getName()));
459                Parameter parameter = new Parameter();
460                parameter.setAttributes(element.getAttributes());
461                parameter.setChildren(element.getChildren());
462                return parameter;
463            }
464        }
465
466    }
467
468    public static class SourceGroup extends Element {
469
470        private SourceGroup() {
471            super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
472        }
473
474        public String getSemantics() {
475            return this.getAttribute("semantics");
476        }
477
478        public List<String> getSsrcs() {
479            ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
480            for(Element child : this.children) {
481                if ("source".equals(child.getName())) {
482                    final String ssrc = child.getAttribute("ssrc");
483                    if (ssrc != null) {
484                        builder.add(ssrc);
485                    }
486                }
487            }
488            return builder.build();
489        }
490
491        public static SourceGroup upgrade(final Element element) {
492            Preconditions.checkArgument("ssrc-group".equals(element.getName()));
493            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
494            final SourceGroup group = new SourceGroup();
495            group.setChildren(element.getChildren());
496            group.setAttributes(element.getAttributes());
497            return group;
498        }
499    }
500
501    public enum Media {
502        VIDEO, AUDIO, UNKNOWN;
503
504        @Override
505        public String toString() {
506            return super.toString().toLowerCase(Locale.ROOT);
507        }
508
509        public static Media of(String value) {
510            try {
511                return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT));
512            } catch (IllegalArgumentException e) {
513                return UNKNOWN;
514            }
515        }
516    }
517
518    public static RtpDescription of(final SessionDescription.Media media) {
519        final RtpDescription rtpDescription = new RtpDescription(media.media);
520        final Map<String, List<Parameter>> parameterMap = new HashMap<>();
521        final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
522        final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
523        for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
524            final String[] parts = rtcpFb.split(" ");
525            if (parts.length >= 2) {
526                final String id = parts[0];
527                final String type = parts[1];
528                final String subType = parts.length >= 3 ? parts[2] : null;
529                if ("trr-int".equals(type)) {
530                    if (subType != null) {
531                        feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
532                    }
533                } else {
534                    feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
535                }
536            }
537        }
538        for (final String ssrc : media.attributes.get(("ssrc"))) {
539            final String[] parts = ssrc.split(" ", 2);
540            if (parts.length == 2) {
541                final String id = parts[0];
542                final String[] subParts = parts[1].split(":", 2);
543                final String attribute = subParts[0];
544                final String value = subParts.length == 2 ? subParts[1] : null;
545                sourceParameterMap.put(id, new Source.Parameter(attribute, value));
546            }
547        }
548        for (final String fmtp : media.attributes.get("fmtp")) {
549            final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
550            if (pair != null) {
551                parameterMap.put(pair.first, pair.second);
552            }
553        }
554        rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
555        for (final String rtpmap : media.attributes.get("rtpmap")) {
556            final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
557            if (payloadType != null) {
558                payloadType.addParameters(parameterMap.get(payloadType.getId()));
559                payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
560                rtpDescription.addChild(payloadType);
561            }
562        }
563        for (final String extmap : media.attributes.get("extmap")) {
564            final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
565            if (extension != null) {
566                rtpDescription.addChild(extension);
567            }
568        }
569        for (Map.Entry<String, Collection<Source.Parameter>> source : sourceParameterMap.asMap().entrySet()) {
570            rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
571        }
572        return rtpDescription;
573    }
574
575    private void addChildren(List<Element> elements) {
576        if (elements != null) {
577            this.children.addAll(elements);
578        }
579    }
580}