payload-type and rtp-hdrext sdp parsing

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/xml/Namespace.java                         |   1 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java       |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java              |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java        |  51 
src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java    | 172 
6 files changed, 195 insertions(+), 47 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/xml/Namespace.java 🔗

@@ -39,6 +39,7 @@ public final class Namespace {
     public static final String JINGLE_FEATURE_AUDIO = "urn:xmpp:jingle:apps:rtp:audio";
     public static final String JINGLE_FEATURE_VIDEO = "urn:xmpp:jingle:apps:rtp:video";
     public static final String JINGLE_RTP_HEADER_EXTENSIONS = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";
+    public static final String JINGLE_RTP_FEEDBACK_NEGOTIATION = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0";
     public static final String IBB = "http://jabber.org/protocol/ibb";
     public static final String PING = "urn:xmpp:ping";
     public static final String PUSH = "urn:xmpp:push:0";

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java 🔗

@@ -252,11 +252,9 @@ public class JingleRtpConnection extends AbstractJingleConnection {
             @Override
             public void onCreateSuccess(org.webrtc.SessionDescription description) {
                 final SessionDescription sessionDescription = SessionDescription.parse(description.description);
+                Log.d(Config.LOGTAG,"description: "+description.description);
                 for (SessionDescription.Media media : sessionDescription.media) {
-                    Log.d(Config.LOGTAG, "media: " + media.protocol);
-                    for (SessionDescription.Attribute attribute : media.attributes) {
-                        Log.d(Config.LOGTAG, "attribute key=" + attribute.key + ", value=" + attribute.value);
-                    }
+                    Log.d(Config.LOGTAG, RtpDescription.of(media).toString());
                 }
                 Log.d(Config.LOGTAG, sessionDescription.toString());
             }

src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java 🔗

@@ -1,5 +1,7 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import com.google.common.collect.ArrayListMultimap;
+
 import java.util.List;
 
 public class MediaBuilder {
@@ -8,7 +10,7 @@ public class MediaBuilder {
     private String protocol;
     private List<Integer> formats;
     private String connectionData;
-    private List<SessionDescription.Attribute> attributes;
+    private ArrayListMultimap<String,String> attributes;
 
     public MediaBuilder setMedia(String media) {
         this.media = media;
@@ -35,7 +37,7 @@ public class MediaBuilder {
         return this;
     }
 
-    public MediaBuilder setAttributes(List<SessionDescription.Attribute> attributes) {
+    public MediaBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
         this.attributes = attributes;
         return this;
     }

src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java 🔗

@@ -1,7 +1,9 @@
 package eu.siacs.conversations.xmpp.jingle;
 
 import android.util.Log;
+import android.util.Pair;
 
+import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
@@ -14,11 +16,11 @@ public class SessionDescription {
     public final int version;
     public final String name;
     public final String connectionData;
-    public final List<Attribute> attributes;
+    public final ArrayListMultimap<String, String> attributes;
     public final List<Media> media;
 
 
-    public SessionDescription(int version, String name, String connectionData, List<Attribute> attributes, List<Media> media) {
+    public SessionDescription(int version, String name, String connectionData, ArrayListMultimap<String, String> attributes, List<Media> media) {
         this.version = version;
         this.name = name;
         this.connectionData = connectionData;
@@ -34,10 +36,10 @@ public class SessionDescription {
     public static SessionDescription parse(final String input) {
         final SessionDescriptionBuilder sessionDescriptionBuilder = new SessionDescriptionBuilder();
         MediaBuilder currentMediaBuilder = null;
-        ImmutableList.Builder<Attribute> attributeBuilder = new ImmutableList.Builder<>();
+        ArrayListMultimap<String, String> attributeMap = ArrayListMultimap.create();
         ImmutableList.Builder<Media> mediaBuilder = new ImmutableList.Builder<>();
         for (final String line : input.split("\n")) {
-            final String[] pair = line.split("=", 2);
+            final String[] pair = line.trim().split("=", 2);
             if (pair.length < 2 || pair[0].length() != 1) {
                 Log.d(Config.LOGTAG, "skipping sdp parsing on line " + line);
                 continue;
@@ -59,17 +61,18 @@ public class SessionDescription {
                     sessionDescriptionBuilder.setName(value);
                     break;
                 case 'a':
-                    attributeBuilder.add(Attribute.parse(value));
+                    final Pair<String, String> attribute = parseAttribute(value);
+                    attributeMap.put(attribute.first, attribute.second);
                     break;
                 case 'm':
                     if (currentMediaBuilder == null) {
-                        sessionDescriptionBuilder.setAttributes(attributeBuilder.build());
+                        sessionDescriptionBuilder.setAttributes(attributeMap);
                         ;
                     } else {
-                        currentMediaBuilder.setAttributes(attributeBuilder.build());
+                        currentMediaBuilder.setAttributes(attributeMap);
                         mediaBuilder.add(currentMediaBuilder.createMedia());
                     }
-                    attributeBuilder = new ImmutableList.Builder<>();
+                    attributeMap = ArrayListMultimap.create();
                     currentMediaBuilder = new MediaBuilder();
                     final String[] parts = value.split(" ");
                     if (parts.length >= 3) {
@@ -89,14 +92,14 @@ public class SessionDescription {
 
         }
         if (currentMediaBuilder != null) {
-            currentMediaBuilder.setAttributes(attributeBuilder.build());
+            currentMediaBuilder.setAttributes(attributeMap);
             mediaBuilder.add(currentMediaBuilder.createMedia());
         }
         sessionDescriptionBuilder.setMedia(mediaBuilder.build());
         return sessionDescriptionBuilder.createSessionDescription();
     }
 
-    private static int ignorantIntParser(final String input) {
+    public static int ignorantIntParser(final String input) {
         try {
             return Integer.parseInt(input);
         } catch (NumberFormatException e) {
@@ -104,25 +107,13 @@ public class SessionDescription {
         }
     }
 
-    public static class Attribute {
-        public final String key;
-        public final String value;
-
-        public Attribute(String key, String value) {
-            this.key = key;
-            this.value = value;
-        }
-
-        public static Attribute parse(String input) {
-            final String[] pair = input.split(":", 2);
-            if (pair.length == 2) {
-                return new Attribute(pair[0], pair[1]);
-            } else {
-                return new Attribute(pair[0], null);
-            }
+    public static Pair<String, String> parseAttribute(final String input) {
+        final String[] pair = input.split(":", 2);
+        if (pair.length == 2) {
+            return new Pair<>(pair[0], pair[1]);
+        } else {
+            return new Pair<>(pair[0], "");
         }
-
-
     }
 
     public static class Media {
@@ -131,9 +122,9 @@ public class SessionDescription {
         public final String protocol;
         public final List<Integer> formats;
         public final String connectionData;
-        public final List<Attribute> attributes;
+        public final ArrayListMultimap<String, String> attributes;
 
-        public Media(String media, int port, String protocol, List<Integer> formats, String connectionData, List<Attribute> attributes) {
+        public Media(String media, int port, String protocol, List<Integer> formats, String connectionData, ArrayListMultimap<String, String> attributes) {
             this.media = media;
             this.port = port;
             this.protocol = protocol;

src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java 🔗

@@ -1,12 +1,14 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import com.google.common.collect.ArrayListMultimap;
+
 import java.util.List;
 
 public class SessionDescriptionBuilder {
     private int version;
     private String name;
     private String connectionData;
-    private List<SessionDescription.Attribute> attributes;
+    private ArrayListMultimap<String,String> attributes;
     private List<SessionDescription.Media> media;
 
     public SessionDescriptionBuilder setVersion(int version) {
@@ -24,7 +26,7 @@ public class SessionDescriptionBuilder {
         return this;
     }
 
-    public SessionDescriptionBuilder setAttributes(List<SessionDescription.Attribute> attributes) {
+    public SessionDescriptionBuilder setAttributes(ArrayListMultimap<String,String> attributes) {
         this.attributes = attributes;
         return this;
     }

src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java 🔗

@@ -1,19 +1,23 @@
 package eu.siacs.conversations.xmpp.jingle.stanzas;
 
+import android.util.Log;
+
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
 import java.util.Locale;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.jingle.SessionDescription;
 
 public class RtpDescription extends GenericDescription {
 
 
-    private RtpDescription(String name, String namespace) {
-        super(name, namespace);
+    private RtpDescription() {
+        super("description", Namespace.JINGLE_APPS_RTP);
     }
 
     public Media getMedia() {
@@ -22,7 +26,7 @@ public class RtpDescription extends GenericDescription {
 
     public List<PayloadType> getPayloadTypes() {
         final ImmutableList.Builder<PayloadType> builder = new ImmutableList.Builder<>();
-        for(Element child : getChildren()) {
+        for (Element child : getChildren()) {
             if ("payload-type".equals(child.getName())) {
                 builder.add(PayloadType.of(child));
             }
@@ -30,9 +34,17 @@ public class RtpDescription extends GenericDescription {
         return builder.build();
     }
 
+    public List<FeedbackNegotiation> getFeedbackNegotiations() {
+        return FeedbackNegotiation.fromChildren(this.getChildren());
+    }
+
+    public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
+        return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
+    }
+
     public List<RtpHeaderExtension> getHeaderExtensions() {
         final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
-        for(final Element child : getChildren()) {
+        for (final Element child : getChildren()) {
             if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) {
                 builder.add(RtpHeaderExtension.upgrade(child));
             }
@@ -43,13 +55,82 @@ public class RtpDescription extends GenericDescription {
     public static RtpDescription upgrade(final Element element) {
         Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
         Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace");
-        final RtpDescription description = new RtpDescription("description", Namespace.JINGLE_APPS_RTP);
+        final RtpDescription description = new RtpDescription();
         description.setAttributes(element.getAttributes());
         description.setChildren(element.getChildren());
         return description;
     }
 
-    //TODO: support for https://xmpp.org/extensions/xep-0293.html
+    public static class FeedbackNegotiation extends Element {
+        private FeedbackNegotiation() {
+            super("rtcp-fb", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
+        }
+
+        public String getType() {
+            return this.getAttribute("type");
+        }
+
+        public String getSubType() {
+            return this.getAttribute("subtype");
+        }
+
+        private static FeedbackNegotiation upgrade(final Element element) {
+            Preconditions.checkArgument("rtcp-fb".equals(element.getName()));
+            Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
+            final FeedbackNegotiation feedback = new FeedbackNegotiation();
+            feedback.setAttributes(element.getAttributes());
+            feedback.setChildren(element.getChildren());
+            return feedback;
+        }
+
+        public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
+            ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
+            for (final Element child : children) {
+                if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
+                    builder.add(upgrade(child));
+                }
+            }
+            return builder.build();
+        }
+
+    }
+
+    public static class FeedbackNegotiationTrrInt extends Element {
+        private FeedbackNegotiationTrrInt() {
+            super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
+        }
+
+        public int getValue() {
+            final String value = getAttribute("value");
+            if (value == null) {
+                return 0;
+            }
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                return 0;
+            }
+        }
+
+        private static FeedbackNegotiationTrrInt upgrade(final Element element) {
+            Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
+            Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
+            final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
+            trr.setAttributes(element.getAttributes());
+            trr.setChildren(element.getChildren());
+            return trr;
+        }
+
+        public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
+            ImmutableList.Builder<FeedbackNegotiationTrrInt> builder = new ImmutableList.Builder<>();
+            for (final Element child : children) {
+                if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
+                    builder.add(upgrade(child));
+                }
+            }
+            return builder.build();
+        }
+    }
 
 
     //XEP-0294: Jingle RTP Header Extensions Negotiation
@@ -60,6 +141,12 @@ public class RtpDescription extends GenericDescription {
             super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
         }
 
+        public RtpHeaderExtension(String id, String uri) {
+            super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS);
+            this.setAttribute("id", id);
+            this.setAttribute("uri", uri);
+        }
+
         public String getId() {
             return this.getAttribute("id");
         }
@@ -76,14 +163,36 @@ public class RtpDescription extends GenericDescription {
             extension.setChildren(element.getChildren());
             return extension;
         }
+
+        public static RtpHeaderExtension ofSdpString(final String sdp) {
+            final String[] pair = sdp.split(" ", 2);
+            if (pair.length == 2) {
+                final String id = pair[0];
+                final String uri = pair[1];
+                return new RtpHeaderExtension(id,uri);
+            } else {
+                return null;
+            }
+        }
     }
 
     //maps to `rtpmap $id $name/$clockrate/$channels`
     public static class PayloadType extends Element {
 
-        private PayloadType(String name, String xmlns) {
-            super(name, xmlns);
+        private PayloadType() {
+            super("payload-type", Namespace.JINGLE_APPS_RTP);
+        }
+
+        public PayloadType(String id, String name, int clockRate, int channels) {
+            super("payload-type", Namespace.JINGLE_APPS_RTP);
+            this.setAttribute("id",id);
+            this.setAttribute("name", name);
+            this.setAttribute("clockrate", clockRate);
+            if (channels != 1) {
+                this.setAttribute("channels", channels);
+            }
         }
+
         public String getId() {
             return this.getAttribute("id");
         }
@@ -126,13 +235,41 @@ public class RtpDescription extends GenericDescription {
             return builder.build();
         }
 
+        public List<FeedbackNegotiation> getFeedbackNegotiations() {
+            return FeedbackNegotiation.fromChildren(this.getChildren());
+        }
+
+        public List<FeedbackNegotiationTrrInt> feedbackNegotiationTrrInts() {
+            return FeedbackNegotiationTrrInt.fromChildren(this.getChildren());
+        }
+
         public static PayloadType of(final Element element) {
             Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type");
-            PayloadType payloadType = new PayloadType("payload-type", Namespace.JINGLE_APPS_RTP);
+            PayloadType payloadType = new PayloadType();
             payloadType.setAttributes(element.getAttributes());
             payloadType.setChildren(element.getChildren());
             return payloadType;
         }
+
+        public static PayloadType ofSdpString(final String sdp) {
+            final String[] pair = sdp.split(" ",2);
+            if (pair.length == 2) {
+                final String id = pair[0];
+                final String[] parts = pair[1].split("/");
+                if (parts.length >= 2) {
+                    final String name = parts[0];
+                    final int clockRate = SessionDescription.ignorantIntParser(parts[1]);
+                    final int channels;
+                    if (parts.length >= 3) {
+                        channels = SessionDescription.ignorantIntParser(parts[2]);
+                    } else {
+                        channels =1;
+                    }
+                    return new PayloadType(id,name,clockRate,channels);
+                }
+            }
+            return null;
+        }
     }
 
     //map to `fmtp $id key=value;key=value
@@ -182,4 +319,21 @@ public class RtpDescription extends GenericDescription {
             }
         }
     }
+
+    public static RtpDescription of(final SessionDescription.Media media) {
+        final RtpDescription rtpDescription = new RtpDescription();
+        for(final String rtpmap : media.attributes.get("rtpmap")) {
+            final PayloadType payloadType = PayloadType.ofSdpString(rtpmap);
+            if (payloadType != null) {
+                rtpDescription.addChild(payloadType);
+            }
+        }
+        for(final String extmap : media.attributes.get("extmap")) {
+            final RtpHeaderExtension extension = RtpHeaderExtension.ofSdpString(extmap);
+            if (extension != null) {
+                rtpDescription.addChild(extension);
+            }
+        }
+        return rtpDescription;
+    }
 }