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