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