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