From d7eae78372aea65d62b5f3bbd438f4937b70fe27 Mon Sep 17 00:00:00 2001 From: cruizba Date: Wed, 2 Feb 2022 18:08:35 +0100 Subject: [PATCH] Initial logic in openvidu-java-client to add to the ConnectionProperties class a new 'customIceServers' parameter --- .../io/openvidu/java/client/Connection.java | 24 ++- .../java/client/ConnectionProperties.java | 35 +++- .../java/client/IceServerProperties.java | 170 ++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java index b838e43e..fffa61b0 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Connection.java @@ -25,7 +25,9 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; /** * See {@link io.openvidu.java.client.Session#getConnections()} @@ -428,8 +430,28 @@ public class Connection { Integer networkCache = (json.has("networkCache") && !json.get("networkCache").isJsonNull()) ? json.get("networkCache").getAsInt() : null; + + // External Ice Servers + List customIceServers = new ArrayList<>(); + if (json.has("customIceServers") && json.get("customIceServers").isJsonArray()) { + JsonArray customIceServersJsonArray = json.get("customIceServers").getAsJsonArray(); + customIceServersJsonArray.forEach(iceJsonElem -> { + JsonObject iceJsonObj = iceJsonElem.getAsJsonObject(); + String url = (iceJsonObj.has("urls") && !iceJsonObj.get("urls").isJsonNull()) + ? json.get("urls").getAsString() + : null; + String username = (iceJsonObj.has("username") && !iceJsonObj.get("username").isJsonNull()) + ? json.get("username").getAsString() + : null; + String credential = (iceJsonObj.has("credential") && !iceJsonObj.get("credential").isJsonNull()) + ? json.get("credential").getAsString() + : null; + customIceServers.add(new IceServerProperties.Builder().url(url).username(username).credential(credential).build()); + }); + } + this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate, - onlyPlayWithSubscribers, networkCache); + onlyPlayWithSubscribers, networkCache, customIceServers); return this; } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java index b68504c3..1da3b382 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/ConnectionProperties.java @@ -1,8 +1,12 @@ package io.openvidu.java.client; +import com.google.gson.JsonArray; import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import java.util.ArrayList; +import java.util.List; + /** * See * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)} @@ -22,6 +26,9 @@ public class ConnectionProperties { private Boolean onlyPlayWithSubscribers; private Integer networkCache; + // External Turn Service + private List customIceServers; + /** * * Builder for {@link io.openvidu.java.client.ConnectionProperties} @@ -42,12 +49,16 @@ public class ConnectionProperties { private Boolean onlyPlayWithSubscribers; private Integer networkCache; + // External Turn Service + private List customIceServers = new ArrayList<>(); + /** * Builder for {@link io.openvidu.java.client.ConnectionProperties}. */ public ConnectionProperties build() { return new ConnectionProperties(this.type, this.data, this.record, this.role, this.kurentoOptions, - this.rtspUri, this.adaptativeBitrate, this.onlyPlayWithSubscribers, this.networkCache); + this.rtspUri, this.adaptativeBitrate, this.onlyPlayWithSubscribers, this.networkCache, + this.customIceServers); } /** @@ -219,11 +230,17 @@ public class ConnectionProperties { this.networkCache = networkCache; return this; } + + // TODO: Comment + public Builder addCustomIceServer(IceServerProperties iceServerProperties) { + this.customIceServers.add(iceServerProperties); + return this; + } } ConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role, KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, - Integer networkCache) { + Integer networkCache, List customIceServers) { this.type = type; this.data = data; this.record = record; @@ -233,6 +250,7 @@ public class ConnectionProperties { this.adaptativeBitrate = adaptativeBitrate; this.onlyPlayWithSubscribers = onlyPlayWithSubscribers; this.networkCache = networkCache; + this.customIceServers = customIceServers; } /** @@ -346,6 +364,11 @@ public class ConnectionProperties { return this.networkCache; } + // TODO: Comment + public List getCustomIceServers() { + return new ArrayList<>(this.customIceServers); + } + public JsonObject toJson(String sessionId) { JsonObject json = new JsonObject(); json.addProperty("session", sessionId); @@ -397,6 +420,14 @@ public class ConnectionProperties { } else { json.add("networkCache", JsonNull.INSTANCE); } + + // Ice Servers + JsonArray customIceServersJsonList = new JsonArray(); + customIceServers.forEach((customIceServer) -> { + customIceServersJsonList.add(customIceServer.toJson()); + }); + json.add("customIceServers", customIceServersJsonList); + return json; } diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java new file mode 100644 index 00000000..b6a5de9e --- /dev/null +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/IceServerProperties.java @@ -0,0 +1,170 @@ +package io.openvidu.java.client; + +import com.google.gson.JsonObject; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class IceServerProperties { + + private String url; + private String username; + private String credential; + + public String getUrl() { + return url; + } + + public String getUsername() { + return username; + } + + public String getCredential() { + return credential; + } + + private IceServerProperties(String url, String username, String credential) { + this.url = url; + this.username = username; + this.credential = credential; + } + + /** + * Ice server properties following RTCIceServers format: + * https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer/urls + * @return + */ + public JsonObject toJson() { + JsonObject json = new JsonObject(); + json.addProperty("urls", getUrl()); + if (getUsername() != null && !getUsername().isEmpty()) { + json.addProperty("username", getUsername()); + } + if (getCredential() != null && !getCredential().isEmpty()) { + json.addProperty("credential", getCredential()); + } + return json; + } + + public static class Builder { + + private String url; + private String username; + private String credential; + + public IceServerProperties.Builder url(String url) { + this.url = url; + return this; + } + + public IceServerProperties.Builder username(String userName) { + this.username = userName; + return this; + } + + public IceServerProperties.Builder credential(String credential) { + this.credential = credential; + return this; + } + + + public IceServerProperties build() throws IllegalArgumentException { + if (this.url == null) { + throw new IllegalArgumentException("External turn url cannot be null"); + } + this.checkValidStunTurn(this.url); + if (this.username == null ^ this.credential == null) { + // If one is null when the other is defined, fail... + throw new IllegalArgumentException("You need to define username and credentials if you define one of them"); + } + if (this.username != null && this.credential != null && this.url.startsWith("stun")) { + // Credentials can not be defined using stun + throw new IllegalArgumentException("Credentials can not be defined while using stun."); + } + return new IceServerProperties(this.url, this.username, this.credential); + } + + /** Parsing Turn Stun Uri based on: + * - https://datatracker.ietf.org/doc/html/rfc7065#section-3.1 + * - https://datatracker.ietf.org/doc/html/rfc7064#section-3.1 + */ + private void checkValidStunTurn(String uri) throws IllegalArgumentException { + final String TCP_TRANSPORT_SUFFIX = "?transport=tcp"; + final String UDP_TRANSPORT_SUFFIX = "?transport=udp"; + + // Protocols which accepts transport=tcp and transport=udp + final Set TURN_PROTOCOLS = new HashSet<>(Arrays.asList( + "turn", + "turns" + )); + final Set STUN_PROTOCOLS = new HashSet<>(Arrays.asList( + "stun", + "stuns" + )); + + // Fails if no colons + int firstColonPos = uri.indexOf(':'); + if (firstColonPos == -1) { + throw new IllegalArgumentException("Not a valid TURN/STUN uri provided. " + + "No colons found in: '" + uri + "'"); + } + + // Get protocol and check + String protocol = uri.substring(0, firstColonPos); + if (!TURN_PROTOCOLS.contains(protocol) && !STUN_PROTOCOLS.contains(protocol)) { + throw new IllegalArgumentException("The protocol '" + protocol + "' is invalid. Only valid values are: " + + TURN_PROTOCOLS + " " + STUN_PROTOCOLS); + } + + // Check if query param with transport exist + int qmarkPos = uri.indexOf('?'); + String hostAndPort = uri.substring(firstColonPos + 1); + if (qmarkPos != -1) { + if (TURN_PROTOCOLS.contains(protocol)) { + // Only Turn uses transport arg + String rawTransportType = uri.substring(qmarkPos); + hostAndPort = uri.substring(firstColonPos + 1, qmarkPos); + if (!TCP_TRANSPORT_SUFFIX.equals(rawTransportType) && !UDP_TRANSPORT_SUFFIX.equals(rawTransportType)) { + // If other argument rather than transport is specified, it is a wrong query for a STUN/TURN uri + throw new IllegalArgumentException("Wrong value specified in STUN/TURN uri: '" + + uri + "'. " + "Unique valid arguments after '?' are '" + + TCP_TRANSPORT_SUFFIX + "' or '" + UDP_TRANSPORT_SUFFIX); + } + } else { + throw new IllegalArgumentException("STUN uri can't have any '?' query param"); + } + } + + // Check if port is defined + int portColon = hostAndPort.indexOf(':'); + if (portColon != -1) { + String[] splittedHostAndPort = hostAndPort.split(":"); + if (splittedHostAndPort.length != 2) { + throw new IllegalArgumentException("Host or port are not correctly " + + "defined in STUN/TURN uri: '" + uri + "'"); + } + String host = splittedHostAndPort[0]; + String port = splittedHostAndPort[1]; + + // Check if host is defined. Valid Host (Domain or IP) will be done at server side + if (host == null || host.isEmpty()) { + throw new IllegalArgumentException("Host defined in '" + uri + "' is empty or null"); + } + + if (port == null || port.isEmpty()) { + throw new IllegalArgumentException("Port defined in '" + uri + "' is empty or null"); + } + try { + int parsedPort = Integer.parseInt(port); + if (parsedPort <= 0 || parsedPort > 65535) { + throw new IllegalArgumentException("The port defined in '" + uri + "' is not a valid port number"); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("The port defined in '" + uri + "' is not a number"); + } + } + } + } + +}