diff --git a/src/main/java/eu/siacs/conversations/xml/Namespace.java b/src/main/java/eu/siacs/conversations/xml/Namespace.java index 40a89cb07..237cb4070 100644 --- a/src/main/java/eu/siacs/conversations/xml/Namespace.java +++ b/src/main/java/eu/siacs/conversations/xml/Namespace.java @@ -38,6 +38,7 @@ public final class Namespace { public static final String JINGLE_APPS_DTLS = "urn:xmpp:jingle:apps:dtls:0"; 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 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 d178fd2f5..df2fb9823 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java @@ -73,6 +73,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { final State oldState = this.state; if (transition(State.SESSION_INITIALIZED)) { if (oldState == State.PROCEED) { + processContents(contents); sendSessionAccept(); } else { //TODO start ringing @@ -82,6 +83,23 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } + private void processContents(final Map contents) { + for(Map.Entry content : contents.entrySet()) { + final DescriptionTransport descriptionTransport = content.getValue(); + final RtpDescription rtpDescription = descriptionTransport.description; + Log.d(Config.LOGTAG,"receive content with name "+content.getKey()+" and media="+rtpDescription.getMedia()); + for(RtpDescription.PayloadType payloadType : rtpDescription.getPayloadTypes()) { + Log.d(Config.LOGTAG,"payload type: "+payloadType.toString()); + } + for(RtpDescription.RtpHeaderExtension extension : rtpDescription.getHeaderExtensions()) { + Log.d(Config.LOGTAG,"extension: "+extension.toString()); + } + final IceUdpTransportInfo iceUdpTransportInfo = descriptionTransport.transport; + Log.d(Config.LOGTAG,"transport: "+descriptionTransport.transport); + Log.d(Config.LOGTAG,"fingerprint "+iceUdpTransportInfo.getFingerprint()); + } + } + void deliveryMessage(final Jid from, final Element message) { Log.d(Config.LOGTAG, id.account.getJid().asBareJid() + ": delivered message to JingleRtpConnection " + message); switch (message.getName()) { @@ -175,7 +193,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { } } - private static class DescriptionTransport { + public static class DescriptionTransport { private final RtpDescription description; private final IceUdpTransportInfo transport; @@ -192,6 +210,7 @@ public class JingleRtpConnection extends AbstractJingleConnection { if (description instanceof RtpDescription) { rtpDescription = (RtpDescription) description; } else { + Log.d(Config.LOGTAG,"description was "+description); throw new IllegalArgumentException("Content does not contain RtpDescription"); } if (transportInfo instanceof IceUdpTransportInfo) { @@ -203,13 +222,13 @@ public class JingleRtpConnection extends AbstractJingleConnection { } public static Map of(final Map contents) { - return Maps.transformValues(contents, new Function() { + return ImmutableMap.copyOf(Maps.transformValues(contents, new Function() { @NullableDecl @Override public DescriptionTransport apply(@NullableDecl Content content) { return content == null ? null : of(content); } - }); + })); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java new file mode 100644 index 000000000..f70f3d299 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SdpUtils.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.xmpp.jingle; + +import java.util.Map; + +public class SdpUtils { + + public static String toSdpString(Map contents) { + return ""; + } + +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index a815155dd..f27efb1e2 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -51,9 +51,11 @@ public class Content extends Element { if (description == null) { return null; } - final String xmlns = description.getNamespace(); - if (FileTransferDescription.NAMESPACES.contains(xmlns)) { + final String namespace = description.getNamespace(); + if (FileTransferDescription.NAMESPACES.contains(namespace)) { return FileTransferDescription.upgrade(description); + } else if (Namespace.JINGLE_APPS_RTP.equals(namespace)) { + return RtpDescription.upgrade(description); } else { return GenericDescription.upgrade(description); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 00beac65c..84734b924 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -1,6 +1,9 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -11,6 +14,21 @@ public class IceUdpTransportInfo extends GenericTransportInfo { super(name, xmlns); } + public Fingerprint getFingerprint() { + final Element fingerprint = this.findChild("fingerprint", Namespace.JINGLE_APPS_DTLS); + return fingerprint == null ? null : Fingerprint.upgrade(fingerprint); + } + + public List getCandidates() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(final Element child : getChildren()) { + if ("candidate".equals(child.getName())) { + builder.add(Candidate.upgrade(child)); + } + } + return builder.build(); + } + public static IceUdpTransportInfo upgrade(final Element element) { Preconditions.checkArgument("transport".equals(element.getName()), "Name of provided element is not transport"); Preconditions.checkArgument(Namespace.JINGLE_TRANSPORT_ICE_UDP.equals(element.getNamespace()), "Element does not match ice-udp transport namespace"); @@ -19,4 +37,104 @@ public class IceUdpTransportInfo extends GenericTransportInfo { transportInfo.setChildren(element.getChildren()); return transportInfo; } + + public static class Candidate extends Element { + + private Candidate() { + super("candidate"); + } + + public int getComponent() { + return getAttributeAsInt("component"); + } + + public int getFoundation() { + return getAttributeAsInt("foundation"); + } + + public int getGeneration() { + return getAttributeAsInt("generation"); + } + + public String getId() { + return getAttribute("id"); + } + + public String getIp() { + return getAttribute("ip"); + } + + public int getNetwork() { + return getAttributeAsInt("network"); + } + + public int getPort() { + return getAttributeAsInt("port"); + } + + public int getPriority() { + return getAttributeAsInt("priority"); + } + + public String getProtocol() { + return getAttribute("protocol"); + } + + public String getRelAddr() { + return getAttribute("rel-addr"); + } + + public int getRelPort() { + return getAttributeAsInt("rel-port"); + } + + public String getType() { //TODO might be converted to enum + return getAttribute("type"); + } + + private int getAttributeAsInt(final String name) { + final String value = this.getAttribute(name); + if (value == null) { + return 0; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + public static Candidate upgrade(final Element element) { + Preconditions.checkArgument("candidate".equals(element.getName())); + final Candidate candidate = new Candidate(); + candidate.setAttributes(element.getAttributes()); + candidate.setChildren(element.getChildren()); + return candidate; + } + } + + + public static class Fingerprint extends Element { + + public String getHash() { + return this.getAttribute("hash"); + } + + public String getSetup() { + return this.getAttribute("setup"); + } + + private Fingerprint() { + super("fingerprint", Namespace.JINGLE_APPS_DTLS); + } + + public static Fingerprint upgrade(final Element element) { + Preconditions.checkArgument("fingerprint".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_APPS_DTLS.equals(element.getNamespace())); + final Fingerprint fingerprint = new Fingerprint(); + fingerprint.setAttributes(element.getAttributes()); + fingerprint.setContent(element.getContent()); + return fingerprint; + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java index a76c841bf..e4fb88ecb 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/JinglePacket.java @@ -38,7 +38,7 @@ public class JinglePacket extends IqPacket { return jinglePacket; } - //TODO can have multiple contents + //TODO deprecate this somehow and make file transfer fail if there are multiple (or something) public Content getJingleContent() { final Element content = getJingleChild("content"); return content == null ? null : Content.upgrade(content); 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 fd66f00ee..15a208205 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,6 +1,10 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; + +import java.util.List; +import java.util.Locale; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -12,6 +16,30 @@ public class RtpDescription extends GenericDescription { super(name, namespace); } + public Media getMedia() { + return Media.of(this.getAttribute("media")); + } + + public List getPayloadTypes() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(Element child : getChildren()) { + if ("payload-type".equals(child.getName())) { + builder.add(PayloadType.of(child)); + } + } + return builder.build(); + } + + public List getHeaderExtensions() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for(final Element child : getChildren()) { + if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) { + builder.add(RtpHeaderExtension.upgrade(child)); + } + } + return builder.build(); + } + 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"); @@ -20,4 +48,133 @@ public class RtpDescription extends GenericDescription { description.setChildren(element.getChildren()); return description; } + + //TODO: support for https://xmpp.org/extensions/xep-0293.html + + + public static class RtpHeaderExtension extends Element { + + private RtpHeaderExtension() { + super("rtp-hdrext", Namespace.JINGLE_RTP_HEADER_EXTENSIONS); + } + + public String getId() { + return this.getAttribute("id"); + } + + public String getUri() { + return this.getAttribute("uri"); + } + + public static RtpHeaderExtension upgrade(final Element element) { + Preconditions.checkArgument("rtp-hdrext".equals(element.getName())); + Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace())); + final RtpHeaderExtension extension = new RtpHeaderExtension(); + extension.setAttributes(element.getAttributes()); + extension.setChildren(element.getChildren()); + return extension; + } + } + + public static class PayloadType extends Element { + + private PayloadType(String name, String xmlns) { + super(name, xmlns); + } + public String getId() { + return this.getAttribute("id"); + } + + public String getPayloadTypeName() { + return this.getAttribute("name"); + } + + public int getClockRate() { + final String clockRate = this.getAttribute("clockrate"); + if (clockRate == null) { + return 0; + } + try { + return Integer.parseInt(clockRate); + } catch (NumberFormatException e) { + return 0; + } + } + + public int getChannels() { + final String channels = this.getAttribute("channels"); + if (channels == null) { + return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel + } + try { + return Integer.parseInt(channels); + } catch (NumberFormatException e) { + return 1; + } + } + + public List getParameters() { + final ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (Element child : getChildren()) { + if ("parameter".equals(child.getName())) { + builder.add(Parameter.of(child)); + } + } + return builder.build(); + } + + 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.setAttributes(element.getAttributes()); + payloadType.setChildren(element.getChildren()); + return payloadType; + } + } + + public static class Parameter extends Element { + + private Parameter() { + super("parameter", Namespace.JINGLE_APPS_RTP); + } + + public Parameter(String name, String value) { + super("parameter", Namespace.JINGLE_APPS_RTP); + this.setAttribute("name", name); + this.setAttribute("value", value); + } + + public String getParameterName() { + return this.getAttribute("name"); + } + + public String getParameterValue() { + return this.getAttribute("value"); + } + + public static Parameter of(final Element element) { + Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter"); + Parameter parameter = new Parameter(); + parameter.setAttributes(element.getAttributes()); + parameter.setChildren(element.getChildren()); + return parameter; + } + } + + public enum Media { + VIDEO, AUDIO, UNKNOWN; + + @Override + public String toString() { + return super.toString().toLowerCase(Locale.ROOT); + } + + public static Media of(String value) { + try { + return value == null ? UNKNOWN : Media.valueOf(value.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException e) { + return UNKNOWN; + } + } + } }