From 18059345c874cd12ab47ee144e274306080b89f0 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Sat, 4 Apr 2020 16:51:51 +0200 Subject: [PATCH] payload-type and rtp-hdrext sdp parsing --- .../eu/siacs/conversations/xml/Namespace.java | 1 + .../xmpp/jingle/JingleRtpConnection.java | 6 +- .../xmpp/jingle/MediaBuilder.java | 6 +- .../xmpp/jingle/SessionDescription.java | 51 +++--- .../jingle/SessionDescriptionBuilder.java | 6 +- .../xmpp/jingle/stanzas/RtpDescription.java | 172 +++++++++++++++++- 6 files changed, 195 insertions(+), 47 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 237cb4070..d4d513202 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/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"; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 3424b1ec7..faba08e48 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/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()); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java index e92c6b100..67e275414 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/MediaBuilder.java +++ b/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 formats; private String connectionData; - private List attributes; + private ArrayListMultimap attributes; public MediaBuilder setMedia(String media) { this.media = media; @@ -35,7 +37,7 @@ public class MediaBuilder { return this; } - public MediaBuilder setAttributes(List attributes) { + public MediaBuilder setAttributes(ArrayListMultimap attributes) { this.attributes = attributes; return this; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index 8054f0a4b..4381a4816 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/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 attributes; + public final ArrayListMultimap attributes; public final List media; - public SessionDescription(int version, String name, String connectionData, List attributes, List media) { + public SessionDescription(int version, String name, String connectionData, ArrayListMultimap attributes, List 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 attributeBuilder = new ImmutableList.Builder<>(); + ArrayListMultimap attributeMap = ArrayListMultimap.create(); ImmutableList.Builder 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 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 Pair 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 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 class Media { @@ -131,9 +122,9 @@ public class SessionDescription { public final String protocol; public final List formats; public final String connectionData; - public final List attributes; + public final ArrayListMultimap attributes; - public Media(String media, int port, String protocol, List formats, String connectionData, List attributes) { + public Media(String media, int port, String protocol, List formats, String connectionData, ArrayListMultimap attributes) { this.media = media; this.port = port; this.protocol = protocol; diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java index d45c53461..edee2ed76 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescriptionBuilder.java +++ b/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 attributes; + private ArrayListMultimap attributes; private List media; public SessionDescriptionBuilder setVersion(int version) { @@ -24,7 +26,7 @@ public class SessionDescriptionBuilder { return this; } - public SessionDescriptionBuilder setAttributes(List attributes) { + public SessionDescriptionBuilder setAttributes(ArrayListMultimap attributes) { this.attributes = attributes; return this; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java index 8f79022aa..6c8a9b6d4 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/RtpDescription.java +++ b/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 getPayloadTypes() { final ImmutableList.Builder 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 getFeedbackNegotiations() { + return FeedbackNegotiation.fromChildren(this.getChildren()); + } + + public List feedbackNegotiationTrrInts() { + return FeedbackNegotiationTrrInt.fromChildren(this.getChildren()); + } + public List getHeaderExtensions() { final ImmutableList.Builder 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 fromChildren(final List children) { + ImmutableList.Builder 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 fromChildren(final List children) { + ImmutableList.Builder 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 getFeedbackNegotiations() { + return FeedbackNegotiation.fromChildren(this.getChildren()); + } + + public List 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; + } }