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.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                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 Pair<String, List<Parameter>> ofSdpString(final String sdp) {
391            final String[] pair = sdp.split(" ");
392            if (pair.length == 2) {
393                final String id = pair[0];
394                ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
395                for (final String parameter : pair[1].split(";")) {
396                    final String[] parts = parameter.split("=", 2);
397                    if (parts.length == 2) {
398                        builder.add(new Parameter(parts[0], parts[1]));
399                    }
400                }
401                return new Pair<>(id, builder.build());
402            } else {
403                return null;
404            }
405        }
406    }
407
408    //XEP-0339: Source-Specific Media Attributes in Jingle
409    //maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
410    public static class Source extends Element {
411
412        private Source() {
413            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
414        }
415
416        public Source(String ssrcId, Collection<Parameter> parameters) {
417            super("source", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
418            this.setAttribute("ssrc", ssrcId);
419            for (Parameter parameter : parameters) {
420                this.addChild(parameter);
421            }
422        }
423
424        public String getSsrcId() {
425            return this.getAttribute("ssrc");
426        }
427
428        public List<Parameter> getParameters() {
429            ImmutableList.Builder<Parameter> builder = new ImmutableList.Builder<>();
430            for (Element child : this.children) {
431                if ("parameter".equals(child.getName())) {
432                    builder.add(Parameter.upgrade(child));
433                }
434            }
435            return builder.build();
436        }
437
438        public static Source upgrade(final Element element) {
439            Preconditions.checkArgument("source".equals(element.getName()));
440            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
441            final Source source = new Source();
442            source.setChildren(element.getChildren());
443            source.setAttributes(element.getAttributes());
444            return source;
445        }
446
447        public static class Parameter extends Element {
448
449            public String getParameterName() {
450                return this.getAttribute("name");
451            }
452
453            public String getParameterValue() {
454                return this.getAttribute("value");
455            }
456
457            private Parameter() {
458                super("parameter");
459            }
460
461            public Parameter(final String attribute, final String value) {
462                super("parameter");
463                this.setAttribute("name", attribute);
464                if (value != null) {
465                    this.setAttribute("value", value);
466                }
467            }
468
469            public static Parameter upgrade(final Element element) {
470                Preconditions.checkArgument("parameter".equals(element.getName()));
471                Parameter parameter = new Parameter();
472                parameter.setAttributes(element.getAttributes());
473                parameter.setChildren(element.getChildren());
474                return parameter;
475            }
476        }
477
478    }
479
480    public static class SourceGroup extends Element {
481
482        private SourceGroup() {
483            super("ssrc-group", Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES);
484        }
485
486        public String getSemantics() {
487            return this.getAttribute("semantics");
488        }
489
490        public List<String> getSsrcs() {
491            ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
492            for (Element child : this.children) {
493                if ("source".equals(child.getName())) {
494                    final String ssrc = child.getAttribute("ssrc");
495                    if (ssrc != null) {
496                        builder.add(ssrc);
497                    }
498                }
499            }
500            return builder.build();
501        }
502
503        public static SourceGroup upgrade(final Element element) {
504            Preconditions.checkArgument("ssrc-group".equals(element.getName()));
505            Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
506            final SourceGroup group = new SourceGroup();
507            group.setChildren(element.getChildren());
508            group.setAttributes(element.getAttributes());
509            return group;
510        }
511    }
512
513    public static RtpDescription of(final SessionDescription.Media media) {
514        final RtpDescription rtpDescription = new RtpDescription(media.media);
515        final Map<String, List<Parameter>> parameterMap = new HashMap<>();
516        final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
517        final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
518        for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
519            final String[] parts = rtcpFb.split(" ");
520            if (parts.length >= 2) {
521                final String id = parts[0];
522                final String type = parts[1];
523                final String subType = parts.length >= 3 ? parts[2] : null;
524                if ("trr-int".equals(type)) {
525                    if (subType != null) {
526                        feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
527                    }
528                } else {
529                    feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
530                }
531            }
532        }
533        for (final String ssrc : media.attributes.get(("ssrc"))) {
534            final String[] parts = ssrc.split(" ", 2);
535            if (parts.length == 2) {
536                final String id = parts[0];
537                final String[] subParts = parts[1].split(":", 2);
538                final String attribute = subParts[0];
539                final String value = subParts.length == 2 ? subParts[1] : null;
540                sourceParameterMap.put(id, new Source.Parameter(attribute, value));
541            }
542        }
543        for (final String fmtp : media.attributes.get("fmtp")) {
544            final Pair<String, List<Parameter>> pair = Parameter.ofSdpString(fmtp);
545            if (pair != null) {
546                parameterMap.put(pair.first, pair.second);
547            }
548        }
549        rtpDescription.addChildren(feedbackNegotiationMap.get("*"));
550        for (final String rtpmap : media.attributes.get("rtpmap")) {
551            final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
552            if (payloadType != null) {
553                payloadType.addParameters(parameterMap.get(payloadType.getId()));
554                payloadType.addChildren(feedbackNegotiationMap.get(payloadType.getId()));
555                rtpDescription.addChild(payloadType);
556            }
557        }
558        for (final String extmap : media.attributes.get("extmap")) {
559            final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
560            if (extension != null) {
561                rtpDescription.addChild(extension);
562            }
563        }
564        for (Map.Entry<String, Collection<Source.Parameter>> source : sourceParameterMap.asMap().entrySet()) {
565            rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
566        }
567        if (media.attributes.containsKey("rtcp-mux")) {
568            rtpDescription.addChild("rtcp-mux");
569        }
570        return rtpDescription;
571    }
572
573    private void addChildren(List<Element> elements) {
574        if (elements != null) {
575            this.children.addAll(elements);
576        }
577    }
578}