Merge pull request #698 from OpenVidu/feature/custom-ice-servers

Feature/custom ice servers
pull/699/head
Carlos Ruiz Ballesteros 2022-02-16 22:39:57 +01:00 committed by GitHub
commit a6df44699c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1277 additions and 57 deletions

View File

@ -1501,7 +1501,21 @@ export class Session extends EventDispatcher {
private processJoinRoomResponse(opts: LocalConnectionOptions) {
this.sessionId = opts.session;
if (opts.coturnIp != null && opts.coturnPort != null && opts.turnUsername != null && opts.turnCredential != null) {
if (opts.customIceServers != null && opts.customIceServers.length > 0) {
this.openvidu.iceServers = [];
for(const iceServer of opts.customIceServers) {
let rtcIceServer: RTCIceServer = {
urls: [ iceServer.url ]
}
logger.log("STUN/TURN server IP: " + iceServer.url);
if (iceServer.username != null && iceServer.credential != null) {
rtcIceServer.username = iceServer.username;
rtcIceServer.credential = iceServer.credential;
logger.log('TURN credentials [' + iceServer.username + ':' + iceServer.credential + ']');
}
this.openvidu.iceServers.push(rtcIceServer);
}
} else if (opts.coturnIp != null && opts.coturnPort != null && opts.turnUsername != null && opts.turnCredential != null) {
const turnUrl1 = 'turn:' + opts.coturnIp + ':' + opts.coturnPort;
this.openvidu.iceServers = [
{ urls: [turnUrl1], username: opts.turnUsername, credential: opts.turnCredential }

View File

@ -0,0 +1,21 @@
/*
* (C) Copyright 2017-2022 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
export interface IceServerProperties {
url: string;
username?: string;
credential?: string;
}

View File

@ -16,6 +16,7 @@
*/
import { RemoteConnectionOptions } from './RemoteConnectionOptions';
import { IceServerProperties } from './IceServerProperties';
export interface LocalConnectionOptions {
id: string;
@ -35,4 +36,5 @@ export interface LocalConnectionOptions {
mediaServer: string;
videoSimulcast: boolean;
life: number;
customIceServers?: IceServerProperties[]
}

View File

@ -165,6 +165,7 @@ public class ProtocolElements {
public static final String PARTICIPANTJOINED_ROLE_PARAM = "role";
public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp";
public static final String PARTICIPANTJOINED_COTURNPORT_PARAM = "coturnPort";
public static final String PARTICIPANTJOINED_CUSTOM_ICE_SERVERS = "customIceServers";
public static final String PARTICIPANTJOINED_TURNUSERNAME_PARAM = "turnUsername";
public static final String PARTICIPANTJOINED_TURNCREDENTIAL_PARAM = "turnCredential";

View File

@ -86,6 +86,11 @@
<version>${version.junit}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-validator</groupId>
<artifactId>commons-validator</artifactId>
<version>${version.commons-validator}</version>
</dependency>
</dependencies>
<profiles>

View File

@ -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()}
@ -190,6 +192,19 @@ public class Connection {
return this.connectionProperties.getNetworkCache();
}
/**
* Returns a list of custom ICE Servers configured for this connection.
* <br><br>
* See {@link io.openvidu.java.client.ConnectionProperties.Builder#addCustomIceServer(IceServerProperties)} for more
* information.
* <br><br>
* <strong>Only for
* {@link io.openvidu.java.client.ConnectionType#WEBRTC}</strong>
*/
public List<IceServerProperties> getCustomIceServers() {
return this.connectionProperties.getCustomIceServers();
}
/**
* Returns the token string associated to the Connection. This is the value that
* must be sent to the client-side to be consumed in OpenVidu Browser method
@ -322,6 +337,11 @@ public class Connection {
if (this.connectionProperties.getNetworkCache() != null) {
builder.networkCache(this.connectionProperties.getNetworkCache());
}
if (this.connectionProperties.getCustomIceServers() != null && !this.connectionProperties.getCustomIceServers().isEmpty()) {
for (IceServerProperties iceServerProperties: this.connectionProperties.getCustomIceServers()) {
builder.addCustomIceServer(iceServerProperties);
}
}
this.connectionProperties = builder.build();
}
@ -415,6 +435,24 @@ public class Connection {
? OpenViduRole.valueOf(json.get("role").getAsString())
: null;
List<IceServerProperties> 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("url") && !iceJsonObj.get("url").isJsonNull())
? iceJsonObj.get("url").getAsString()
: null;
String username = (iceJsonObj.has("username") && !iceJsonObj.get("username").isJsonNull())
? iceJsonObj.get("username").getAsString()
: null;
String credential = (iceJsonObj.has("credential") && !iceJsonObj.get("credential").isJsonNull())
? iceJsonObj.get("credential").getAsString()
: null;
customIceServers.add(new IceServerProperties.Builder().url(url).username(username).credential(credential).build());
});
}
// IPCAM
String rtspUri = (json.has("rtspUri") && !json.get("rtspUri").isJsonNull()) ? json.get("rtspUri").getAsString()
: null;
@ -428,8 +466,9 @@ public class Connection {
Integer networkCache = (json.has("networkCache") && !json.get("networkCache").isJsonNull())
? json.get("networkCache").getAsInt()
: null;
this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate,
onlyPlayWithSubscribers, networkCache);
onlyPlayWithSubscribers, networkCache, customIceServers);
return this;
}

View File

@ -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<IceServerProperties> customIceServers;
/**
*
* Builder for {@link io.openvidu.java.client.ConnectionProperties}
@ -36,18 +43,21 @@ public class ConnectionProperties {
// WEBRTC
private OpenViduRole role;
private KurentoOptions kurentoOptions;
private List<IceServerProperties> customIceServers = new ArrayList<>();
// IPCAM
private String rtspUri;
private Boolean adaptativeBitrate;
private Boolean onlyPlayWithSubscribers;
private Integer networkCache;
/**
* 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 +229,42 @@ public class ConnectionProperties {
this.networkCache = networkCache;
return this;
}
/**
* On certain type of networks, clients using default OpenVidu STUN/TURN server can not be reached it because
* firewall rules and network topologies at the client side. This method allows you to configure your
* own ICE Server for specific connections if you need it. This is usually not necessary, only it is usefull for
* OpenVidu users behind firewalls which allows traffic from/to specific ports which may need a custom
* ICE Server configuration
*
* Add an ICE Server if in your use case you need this connection to use your own ICE Server deployment.
* When the user uses this connection, it will use the specified ICE Servers defined here.
*
* The level of precedence for ICE Server configuration on every OpenVidu connection is:
* <ol>
* <li>Configured ICE Server using Openvidu.setAdvancedCofiguration() at openvidu-browser.</li>
* <li>Configured ICE server at
* {@link io.openvidu.java.client.ConnectionProperties#customIceServers ConnectionProperties.customIceServers}</li>
* <li>Configured ICE Server at global configuration parameter: OPENVIDU_WEBRTC_ICE_SERVERS</li>
* <li>Default deployed Coturn within OpenVidu deployment</li>
* </ol>
* <br>
* If no value is found at level 1, level 2 will be used, and so on until level 4.
* <br>
* This method is equivalent to level 2 of precedence.
* <br><br>
* <strong>Only for
* {@link io.openvidu.java.client.ConnectionType#WEBRTC}</strong>
*/
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<IceServerProperties> customIceServers) {
this.type = type;
this.data = data;
this.record = record;
@ -233,6 +274,7 @@ public class ConnectionProperties {
this.adaptativeBitrate = adaptativeBitrate;
this.onlyPlayWithSubscribers = onlyPlayWithSubscribers;
this.networkCache = networkCache;
this.customIceServers = customIceServers;
}
/**
@ -346,6 +388,19 @@ public class ConnectionProperties {
return this.networkCache;
}
/**
* Returns a list of custom ICE Servers configured for this connection.
* <br><br>
* See {@link io.openvidu.java.client.ConnectionProperties.Builder#addCustomIceServer(IceServerProperties)} for more
* information.
* <br><br>
* <strong>Only for
* {@link io.openvidu.java.client.ConnectionType#WEBRTC}</strong>
*/
public List<IceServerProperties> getCustomIceServers() {
return new ArrayList<>(this.customIceServers);
}
public JsonObject toJson(String sessionId) {
JsonObject json = new JsonObject();
json.addProperty("session", sessionId);
@ -376,6 +431,12 @@ public class ConnectionProperties {
} else {
json.add("kurentoOptions", JsonNull.INSTANCE);
}
JsonArray customIceServersJsonList = new JsonArray();
customIceServers.forEach((customIceServer) -> {
customIceServersJsonList.add(customIceServer.toJson());
});
json.add("customIceServers", customIceServersJsonList);
// IPCAM
if (getRtspUri() != null) {
json.addProperty("rtspUri", getRtspUri());

View File

@ -0,0 +1,286 @@
/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package io.openvidu.java.client;
import com.google.gson.JsonObject;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.InetAddressValidator;
import java.net.Inet6Address;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* See
* {@link io.openvidu.java.client.ConnectionProperties.Builder#addCustomIceServer(IceServerProperties)}
*/
public class IceServerProperties {
private String url;
private String username;
private String credential;
/**
* Returns the defined ICE Server url for this {@link IceServerProperties} object.
*/
public String getUrl() {
return url;
}
/**
* Returns the Username to be used for TURN connections at the defined {@link IceServerProperties#getUrl()}
* and {@link IceServerProperties#getCredential()} for this {@link IceServerProperties} object.
*/
public String getUsername() {
return username;
}
/**
* Returns the credential to be used for TURN connections at the defined {@link IceServerProperties#getUrl()}
* and {@link IceServerProperties#getUsername()} for this {@link IceServerProperties} object.
*/
public String getCredential() {
return credential;
}
private IceServerProperties(String url, String username, String credential) {
this.url = url;
this.username = username;
this.credential = credential;
}
/**
* @hidden
*/
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("url", getUrl());
if (getUsername() != null && !getUsername().isEmpty()) {
json.addProperty("username", getUsername());
}
if (getCredential() != null && !getCredential().isEmpty()) {
json.addProperty("credential", getCredential());
}
return json;
}
/**
* Builder for {@link IceServerProperties}
*/
public static class Builder {
private String url;
private String username;
private String credential;
/**
* Set the url for the ICE Server you want to use.
* It should follow a valid format:
* <ul>
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7065#section-3.1" target="_blank">https://datatracker.ietf.org/doc/html/rfc7065#section-3.1</a></li>
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7064#section-3.1" target="_blank">https://datatracker.ietf.org/doc/html/rfc7064#section-3.1</a></li>
* </ul>
*/
public IceServerProperties.Builder url(String url) {
this.url = url;
return this;
}
/**
* Set a username for the ICE Server you want to use.
* This parameter should be defined only for TURN, not for STUN ICE Servers.
*/
public IceServerProperties.Builder username(String userName) {
this.username = userName;
return this;
}
/**
* Set a credential for the ICE Server you want to use.
* This parameter should be defined only for TURN, not for STUN ICE Servers.
*/
public IceServerProperties.Builder credential(String credential) {
this.credential = credential;
return this;
}
/**
* Builder for {@link io.openvidu.java.client.RecordingProperties}
* @throws IllegalArgumentException if the defined properties does not follows
* common STUN/TURN RFCs:
* <ul>
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7065#section-3.1" target="_blank">https://datatracker.ietf.org/doc/html/rfc7065#section-3.1</a></li>
* <li><a href="https://datatracker.ietf.org/doc/html/rfc7064#section-3.1" target="_blank">https://datatracker.ietf.org/doc/html/rfc7064#section-3.1</a></li>
* </ul>
*/
public IceServerProperties build() throws IllegalArgumentException {
if (this.url == null) {
throw new IllegalArgumentException("External turn url cannot be null");
}
this.checkValidStunTurn(this.url);
if (this.url.startsWith("turn")) {
if ((this.username == null || this.credential == null)) {
throw new IllegalArgumentException("Credentials must be defined while using turn");
}
} else if (this.url.startsWith("stun")) {
if (this.username != null || this.credential != null) {
// 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);
}
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<String> TURN_PROTOCOLS = new HashSet<>(Arrays.asList(
"turn",
"turns"
));
final Set<String> 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(':');
// IPv6 are defined between brackets
int startIpv6Index = hostAndPort.indexOf('[');
int endIpv6Index = hostAndPort.indexOf(']');
if (startIpv6Index == -1 ^ endIpv6Index == -1) {
throw new IllegalArgumentException("Not closed bracket '[' or ']' in uri: " + uri);
}
if (portColon != -1) {
if (startIpv6Index == -1 && endIpv6Index == -1) {
// If Ipv4 and port defined
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
checkHostAndPort(uri, host, port);
} else {
// If portColon is found and Ipv6
String ipv6 = hostAndPort.substring(startIpv6Index + 1, endIpv6Index);
String auxPort = hostAndPort.substring(endIpv6Index + 1);
if (auxPort.startsWith(":")) {
if (auxPort.length() == 1) {
throw new IllegalArgumentException("Host or port are not correctly defined in STUN/TURN uri: " + uri);
}
// If port is defined
// Get port without colon and check host and port
String host = ipv6;
String port = auxPort.substring(1);
checkHostAndPort(uri, host, port);
} else if (auxPort.length() > 0) {
// If auxPort = 0, no port is defined
throw new IllegalArgumentException("Port is not specified correctly after IPv6 in uri: '" + uri + "'");
}
}
} else {
// If portColon not found, only host is defined
String host = hostAndPort;
checkHost(uri, host);
}
}
private void checkHost(String uri, String host) {
if (host == null || host.isEmpty()) {
throw new IllegalArgumentException("Host defined in '" + uri + "' is empty or null");
}
if (DomainValidator.getInstance().isValid(host)) {
return;
}
InetAddressValidator ipValidator = InetAddressValidator.getInstance();
if (ipValidator.isValid(host)) {
return;
}
try {
Inet6Address.getByName(host).getHostAddress();
return;
} catch (UnknownHostException e) {
throw new IllegalArgumentException("Is not a valid Internet Address (IP or Domain Name): '" + host + "'");
}
}
private void checkPort(String uri, String port) {
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 (0-65535)");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("The port defined in '" + uri + "' is not a number (0-65535)");
}
}
private void checkHostAndPort(String uri, String host, String port) {
this.checkHost(uri, host);
this.checkPort(uri, port);
}
}
}

View File

@ -18,6 +18,7 @@
import { Publisher } from './Publisher';
import { ConnectionProperties } from './ConnectionProperties';
import { OpenViduRole } from './OpenViduRole';
import { IceServerProperties } from './IceServerProperties';
/**
* See [[Session.connections]]
@ -138,6 +139,7 @@ export class Connection {
this.connectionProperties.adaptativeBitrate = json.adaptativeBitrate;
this.connectionProperties.onlyPlayWithSubscribers = json.onlyPlayWithSubscribers;
this.connectionProperties.networkCache = json.networkCache;
this.connectionProperties.customIceServers = json.customIceServers ?? []
} else {
this.connectionProperties = {
type: json.type,
@ -148,7 +150,8 @@ export class Connection {
rtspUri: json.rtspUri,
adaptativeBitrate: json.adaptativeBitrate,
onlyPlayWithSubscribers: json.onlyPlayWithSubscribers,
networkCache: json.networkCache
networkCache: json.networkCache,
customIceServers: json.customIceServers ?? []
}
}
this.role = json.role;
@ -224,6 +227,7 @@ export class Connection {
this.connectionProperties.adaptativeBitrate === other.connectionProperties.adaptativeBitrate &&
this.connectionProperties.onlyPlayWithSubscribers === other.connectionProperties.onlyPlayWithSubscribers &&
this.connectionProperties.networkCache === other.connectionProperties.networkCache &&
this.connectionProperties.customIceServers.length === other.connectionProperties.customIceServers.length &&
this.token === other.token &&
this.location === other.location &&
this.ip === other.ip &&
@ -238,6 +242,15 @@ export class Connection {
equals = (this.connectionProperties.kurentoOptions === other.connectionProperties.kurentoOptions);
}
}
if (equals) {
if (this.connectionProperties.customIceServers != null) {
// Order alphabetically Ice servers using url just to keep the same list order.
const simpleIceComparator = (a: IceServerProperties, b: IceServerProperties) => (a.url > b.url) ? 1 : -1
const sortedIceServers = this.connectionProperties.customIceServers.sort(simpleIceComparator);
const sortedOtherIceServers = other.connectionProperties.customIceServers.sort(simpleIceComparator);
equals = JSON.stringify(sortedIceServers) === JSON.stringify(sortedOtherIceServers);
}
}
if (equals) {
equals = JSON.stringify(this.subscribers.sort()) === JSON.stringify(other.subscribers.sort());
if (equals) {

View File

@ -15,6 +15,7 @@
*
*/
import { IceServerProperties } from './IceServerProperties';
import { ConnectionType } from './ConnectionType';
import { OpenViduRole } from './OpenViduRole';
@ -127,4 +128,30 @@ export interface ConnectionProperties {
*/
networkCache?: number;
/**
* On certain type of networks, clients using default OpenVidu STUN/TURN server can not be reached it because
* firewall rules and network topologies at the client side. This method allows you to configure your
* own ICE Server for specific connections if you need it. This is usually not necessary, only it is usefull for
* OpenVidu users behind firewalls which allows traffic from/to specific ports which may need a custom
* ICE Server configuration
*
* Add an ICE Server if in your use case you need this connection to use your own ICE Server deployment.
* When the user uses this connection, it will use the specified ICE Servers defined here.
*
* The level of precedence for ICE Server configuration on every OpenVidu connection is:
*
* 1. Configured ICE Server using Openvidu.setAdvancedCofiguration() at openvidu-browser.
* 2. Configured ICE server at [[ConnectionProperties.customIceServers]].
* 3. Configured ICE Server at global configuration parameter: `OPENVIDU_WEBRTC_ICE_SERVERS`.
* 4. Default deployed Coturn within OpenVidu deployment.
*
*
* If no value is found at level 1, level 2 will be used, and so on until level 4.
*
* This method is equivalent to level 2 of precedence.
*
* **Only for [[ConnectionType.WEBRTC]]**
*
*/
customIceServers?: IceServerProperties[];
}

View File

@ -0,0 +1,42 @@
/*
* (C) Copyright 2017-2020 OpenVidu (https://openvidu.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
export interface IceServerProperties {
/**
* Set the url for the ICE Server you want to use.
* It should follow a valid format:
*
* - [https://datatracker.ietf.org/doc/html/rfc7065#section-3.1](https://datatracker.ietf.org/doc/html/rfc7065#section-3.1)
* - [https://datatracker.ietf.org/doc/html/rfc7064#section-3.1](https://datatracker.ietf.org/doc/html/rfc7064#section-3.1)
*
*/
url: string;
/**
* Set a username for the ICE Server you want to use.
* This parameter should be defined only for TURN, not for STUN ICE Servers.
*/
username?: string;
/**
* Set a credential for the ICE Server you want to use.
* This parameter should be defined only for TURN, not for STUN ICE Servers.
*/
credential?: string;
}

View File

@ -28,6 +28,7 @@ import { RecordingMode } from './RecordingMode';
import { SessionProperties } from './SessionProperties';
import { TokenOptions } from './TokenOptions';
import { RecordingProperties } from 'RecordingProperties';
import { IceServerProperties } from 'IceServerProperties';
export class Session {
@ -150,7 +151,8 @@ export class Session {
rtspUri: (!!connectionProperties && !!connectionProperties.rtspUri) ? connectionProperties.rtspUri : null,
adaptativeBitrate: !!connectionProperties ? connectionProperties.adaptativeBitrate : null,
onlyPlayWithSubscribers: !!connectionProperties ? connectionProperties.onlyPlayWithSubscribers : null,
networkCache: (!!connectionProperties && (connectionProperties.networkCache != null)) ? connectionProperties.networkCache : null
networkCache: (!!connectionProperties && (connectionProperties.networkCache != null)) ? connectionProperties.networkCache : null,
customIceServers: (!!connectionProperties && (!!connectionProperties.customIceServers != null)) ? connectionProperties.customIceServers : null
});
axios.post(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection',
@ -560,7 +562,6 @@ export class Session {
// 1. Array to store fetched connections and later remove closed ones
const fetchedConnectionIds: string[] = [];
json.connections.content.forEach(jsonConnection => {
const connectionObj: Connection = new Connection(jsonConnection);
fetchedConnectionIds.push(connectionObj.connectionId);
let storedConnection = this.connections.find(c => c.connectionId === connectionObj.connectionId);
@ -583,6 +584,16 @@ export class Session {
// Order connections by time of creation
this.connections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0));
// Order Ice candidates in connection properties
this.connections.forEach(connection => {
if (connection.connectionProperties.customIceServers != null &&
connection.connectionProperties.customIceServers.length > 0) {
// Order alphabetically Ice servers using url just to keep the same list order.
const simpleIceComparator = (a: IceServerProperties, b: IceServerProperties) => (a.url > b.url) ? 1 : -1
connection.connectionProperties.customIceServers.sort(simpleIceComparator);
}
});
// Populate activeConnections array
this.updateActiveConnectionsArray();
return this;

View File

@ -12,4 +12,5 @@ export * from './Recording';
export * from './RecordingProperties';
export * from './Connection';
export * from './Publisher';
export * from './VideoCodec';
export * from './VideoCodec';
export * from './IceServerProperties';

View File

@ -38,6 +38,7 @@ import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import io.openvidu.java.client.IceServerProperties;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.DomainValidator;
@ -220,10 +221,12 @@ public class OpenviduConfig {
private MediaServer mediaServerInfo = MediaServer.kurento;
// Media properties
// Webrtc properties
private boolean webrtcSimulcast = false;
private List<IceServerProperties> webrtcIceServers;
// Plain config properties getters
public String getCoturnDatabaseDbname() {
@ -290,6 +293,10 @@ public class OpenviduConfig {
return this.webrtcSimulcast;
}
public List<IceServerProperties> getWebrtcIceServers() {
return webrtcIceServers;
}
public String getOpenViduRecordingPath() {
return this.openviduRecordingPath;
}
@ -619,6 +626,8 @@ public class OpenviduConfig {
checkCertificateType();
webrtcIceServers = loadWebrtcIceServers("OPENVIDU_WEBRTC_ICE_SERVERS");
}
private void checkCertificateType() {
@ -1147,4 +1156,46 @@ public class OpenviduConfig {
}
}
private List<IceServerProperties> loadWebrtcIceServers(String property) {
String rawIceServers = asOptionalString(property);
List<IceServerProperties> webrtcIceServers = new ArrayList<>();
if (rawIceServers == null || rawIceServers.isEmpty()) {
return webrtcIceServers;
}
List<String> arrayIceServers = asJsonStringsArray(property);
for (String iceServerString : arrayIceServers) {
try {
IceServerProperties iceServerProperties = readIceServer(property, iceServerString);
webrtcIceServers.add(iceServerProperties);
} catch (Exception e) {
addError(property, iceServerString + " is not a valid webrtc ice server: " + e.getMessage());
}
}
return webrtcIceServers;
}
private IceServerProperties readIceServer(String property, String iceServerString) {
String url = null, username = null, credential = null;
String[] iceServerPropList = iceServerString.split(",");
for (String iceServerProp: iceServerPropList) {
String[] iceServerPropEntry = iceServerProp.split("=");
if (iceServerPropEntry.length == 2) {
if (iceServerProp.startsWith("url=")) {
url = iceServerPropEntry[1];
} else if (iceServerProp.startsWith("username=")) {
username = iceServerPropEntry[1];
} else if (iceServerProp.startsWith("credential=")) {
credential = iceServerPropEntry[1];
} else {
addError(property, "Wrong parameter: " + iceServerProp);
}
} else {
addError(property, "Wrong parameter: " + iceServerProp);
}
}
IceServerProperties iceServerProperties = new IceServerProperties.Builder()
.url(url).username(username).credential(credential).build();
return iceServerProperties;
}
}

View File

@ -25,6 +25,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import io.openvidu.java.client.IceServerProperties;
import org.kurento.client.GenericMediaEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -186,6 +187,11 @@ public class SessionEventsHandler {
}
result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNIP_PARAM, openviduConfig.getCoturnIp());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNPORT_PARAM, openviduConfig.getCoturnPort());
List<IceServerProperties> customIceServers = participant.getToken().getCustomIceServers();
if (customIceServers!= null && !customIceServers.isEmpty()) {
result.add(ProtocolElements.PARTICIPANTJOINED_CUSTOM_ICE_SERVERS,
participant.getToken().getCustomIceServersAsJson());
}
if (participant.getToken().getTurnCredentials() != null) {
result.addProperty(ProtocolElements.PARTICIPANTJOINED_TURNUSERNAME_PARAM,
participant.getToken().getTurnCredentials().getUsername());

View File

@ -19,6 +19,7 @@ package io.openvidu.server.core;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
@ -51,6 +52,7 @@ import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.Recording;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.IceServerProperties;
import io.openvidu.server.cdr.CDREventRecordingStatusChanged;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService;
@ -329,13 +331,13 @@ public abstract class SessionManager {
}
public Token newToken(Session session, OpenViduRole role, String serverMetadata, boolean record,
KurentoOptions kurentoOptions) throws Exception {
KurentoOptions kurentoOptions, List<IceServerProperties> customIceServers) throws Exception {
if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) {
log.error("Data invalid format");
throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format");
}
Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), serverMetadata, record, role,
kurentoOptions);
kurentoOptions, customIceServers);
// Internal dev feature: allows customizing connectionId
if (serverMetadata.contains("openviduCustomConnectionId")) {

View File

@ -17,18 +17,18 @@
package io.openvidu.server.core;
import com.google.gson.JsonArray;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import io.openvidu.java.client.ConnectionProperties;
import io.openvidu.java.client.ConnectionType;
import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.server.core.Participant.ParticipantStatus;
import io.openvidu.server.coturn.TurnCredentials;
import java.util.List;
public class Token {
private String token;
@ -77,7 +77,8 @@ public class Token {
this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), newRecord,
connectionProperties.getRole(), connectionProperties.getKurentoOptions(),
connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(),
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache());
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(),
connectionProperties.getCustomIceServers());
}
public OpenViduRole getRole() {
@ -88,7 +89,8 @@ public class Token {
this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(),
connectionProperties.record(), newRole, connectionProperties.getKurentoOptions(),
connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(),
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache());
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(),
connectionProperties.getCustomIceServers());
}
public KurentoOptions getKurentoOptions() {
@ -118,6 +120,10 @@ public class Token {
public String getConnectionId() {
return connectionId;
}
public List<IceServerProperties> getCustomIceServers() {
return this.connectionProperties.getCustomIceServers();
}
public void setConnectionId(String connectionId) {
this.connectionId = connectionId;
@ -138,6 +144,16 @@ public class Token {
return json;
}
public JsonArray getCustomIceServersAsJson() {
JsonArray customIceServersJsonList = new JsonArray();
if (this.connectionProperties.getCustomIceServers() != null) {
this.connectionProperties.getCustomIceServers().forEach((customIceServer) -> {
customIceServersJsonList.add(customIceServer.toJson());
});
}
return customIceServersJsonList;
}
public JsonObject toJsonAsParticipant() {
JsonObject json = new JsonObject();
json.addProperty("id", this.getConnectionId());
@ -178,7 +194,7 @@ public class Token {
private void updateConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role,
KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers,
Integer networkCache) {
Integer networkCache, List<IceServerProperties> iceServerProperties) {
ConnectionProperties.Builder builder = new ConnectionProperties.Builder();
if (type != null) {
builder.type(type);
@ -207,6 +223,11 @@ public class Token {
if (networkCache != null) {
builder.networkCache(networkCache);
}
if (iceServerProperties != null) {
for (IceServerProperties customIceServer: iceServerProperties) {
builder.addCustomIceServer(customIceServer);
}
}
this.connectionProperties = builder.build();
}

View File

@ -24,12 +24,15 @@ import io.openvidu.java.client.ConnectionProperties;
import io.openvidu.java.client.ConnectionType;
import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.IceServerProperties;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduBuildInfo;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService;
import io.openvidu.server.coturn.TurnCredentials;
import java.util.List;
public class TokenGenerator {
@Autowired
@ -42,7 +45,7 @@ public class TokenGenerator {
protected OpenviduBuildInfo openviduBuildConfig;
public Token generateToken(String sessionId, String serverMetadata, boolean record, OpenViduRole role,
KurentoOptions kurentoOptions) throws Exception {
KurentoOptions kurentoOptions, List<IceServerProperties> customIceServers) throws Exception {
String token = OpenViduServer.wsUrl;
token += "?sessionId=" + sessionId;
token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase()
@ -51,8 +54,13 @@ public class TokenGenerator {
if (this.openviduConfig.isTurnadminAvailable()) {
turnCredentials = coturnCredentialsService.createUser();
}
ConnectionProperties connectionProperties = new ConnectionProperties.Builder().type(ConnectionType.WEBRTC)
.data(serverMetadata).record(record).role(role).kurentoOptions(kurentoOptions).build();
ConnectionProperties.Builder connectionPropertiesBuilder = new ConnectionProperties.Builder()
.type(ConnectionType.WEBRTC).data(serverMetadata).record(record).role(role)
.kurentoOptions(kurentoOptions);
for (IceServerProperties customIceServer: customIceServers) {
connectionPropertiesBuilder.addCustomIceServer(customIceServer);
}
ConnectionProperties connectionProperties = connectionPropertiesBuilder.build();
return new Token(token, sessionId, connectionProperties, turnCredentials);
}

View File

@ -21,11 +21,13 @@ import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -51,17 +53,7 @@ import com.google.gson.JsonParser;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.ConnectionProperties;
import io.openvidu.java.client.ConnectionType;
import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingMode;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.VideoCodec;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.IdentifierPrefixes;
@ -662,7 +654,7 @@ public class SessionRestController {
try {
Token token = sessionManager.newToken(session, connectionProperties.getRole(),
connectionProperties.getData(), connectionProperties.record(),
connectionProperties.getKurentoOptions());
connectionProperties.getKurentoOptions(), connectionProperties.getCustomIceServers());
return new ResponseEntity<>(token.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(),
HttpStatus.OK);
} catch (Exception e) {
@ -914,6 +906,41 @@ public class SessionRestController {
}
}
// Custom Ice Servers
JsonArray customIceServersJsonArray = null;
if (params.get("customIceServers") != null) {
try {
customIceServersJsonArray = new Gson().toJsonTree(params.get("customIceServers"), List.class)
.getAsJsonArray();
} catch (Exception e) {
throw new Exception("Error in parameter 'customIceServersJson'. It is not a valid JSON object");
}
}
if (customIceServersJsonArray != null) {
try {
for (int i = 0; i < customIceServersJsonArray.size(); i++) {
JsonObject customIceServerJson = customIceServersJsonArray.get(i).getAsJsonObject();
IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder();
iceServerPropertiesBuilder.url(customIceServerJson.get("url").getAsString());
if (customIceServerJson.has("username")) {
iceServerPropertiesBuilder.username(customIceServerJson.get("username").getAsString());
}
if (customIceServerJson.has("credential")) {
iceServerPropertiesBuilder.credential(customIceServerJson.get("credential").getAsString());
}
IceServerProperties iceServerProperties = iceServerPropertiesBuilder.build();
builder.addCustomIceServer(iceServerProperties);
}
} catch (Exception e) {
throw new Exception("Type error in some parameter of 'customIceServers': " + e.getMessage());
}
} else if(!openviduConfig.getWebrtcIceServers().isEmpty()){
// If not defined in connection, check if defined in openvidu config
for (IceServerProperties iceServerProperties: openviduConfig.getWebrtcIceServers()) {
builder.addCustomIceServer(iceServerProperties);
}
}
// Build WEBRTC options
builder.role(role).kurentoOptions(kurentoOptions);
@ -939,6 +966,8 @@ public class SessionRestController {
.onlyPlayWithSubscribers(onlyPlayWithSubscribers).networkCache(networkCache).build();
}
return builder;
}

View File

@ -0,0 +1,249 @@
package io.openvidu.server.test.unit;
import io.openvidu.java.client.IceServerProperties;
import org.junit.Test;
import org.junit.jupiter.api.DisplayName;
import static org.junit.jupiter.api.Assertions.*;
public class IceServerPropertiesTest {
@Test
@DisplayName("IceServerProperty exceptions tests")
public void iceServerPropertiesExceptionTest() {
// Wrong urls
notValidIceServerTest(
"wrongurl", null, null,
"Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'"
);
notValidIceServerTest(
"wrongurl", "anyuser", null,
"Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'"
);
notValidIceServerTest(
"wrongurl", null, "anypassword",
"Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'"
);
notValidIceServerTest(
"wrongurl", "anyuser", "anypassword",
"Not a valid TURN/STUN uri provided. No colons found in: 'wrongurl'"
);
// Wrong prefixes
notValidIceServerTest(
"turnss:wrongurl", null, null,
"The protocol 'turnss' is invalid. Only valid values are: [turn, turns] [stuns, stun]"
);
notValidIceServerTest(
"stunss:wrongurl", "anyuser", null,
"The protocol 'stunss' is invalid. Only valid values are: [turn, turns] [stuns, stun]"
);
notValidIceServerTest(
"anything:wrongurl", null, "anypassword",
"The protocol 'anything' is invalid. Only valid values are: [turn, turns] [stuns, stun]"
);
notValidIceServerTest(
":", null, null,
"The protocol '' is invalid. Only valid values are: [turn, turns] [stuns, stun]"
);
notValidIceServerTest(
"", null, null,
"Not a valid TURN/STUN uri provided. No colons found in: ''"
);
// Try invalid host and ports
notValidIceServerTest(
"stun:hostname.com:99a99", null, null,
"The port defined in 'stun:hostname.com:99a99' is not a number (0-65535)"
);
notValidIceServerTest(
"stun:hostname.com:-1", null, null,
"The port defined in 'stun:hostname.com:-1' is not a valid port number (0-65535)"
);
notValidIceServerTest(
"stun:hostname:port:more", null, null,
"Host or port are not correctly defined in STUN/TURN uri: 'stun:hostname:port:more'"
);
notValidIceServerTest(
"stun:hostname.com:port more", null, null,
"The port defined in 'stun:hostname.com:port more' is not a number (0-65535)"
);
notValidIceServerTest(
"stun:hostname.com:", null, null,
"Host or port are not correctly defined in STUN/TURN uri: 'stun:hostname.com:'"
);
notValidIceServerTest(
"stun:[1:2:3:4:5:6:7:8]junk:1000", null, null,
"Port is not specified correctly after IPv6 in uri: 'stun:[1:2:3:4:5:6:7:8]junk:1000'"
);
notValidIceServerTest(
"stun:[notvalid:]:1000", null, null,
"Is not a valid Internet Address (IP or Domain Name): 'notvalid:'"
);
notValidIceServerTest(
"stun::5555", null, null,
"Host defined in 'stun::5555' is empty or null"
);
notValidIceServerTest(
"stun:", null, null,
"Host defined in 'stun:' is empty or null"
);
// Illegal Uri tests according to RFC 3986 and RFC 7064 (URI schemes for STUN and TURN)
notValidIceServerTest(
"stun:/hostname.com", null, null,
"Is not a valid Internet Address (IP or Domain Name): '/hostname.com'"
);
notValidIceServerTest(
"stun:?hostname.com", null, null,
"STUN uri can't have any '?' query param"
);
notValidIceServerTest(
"stun:#hostname.com", null, null,
"Is not a valid Internet Address (IP or Domain Name): '#hostname.com'"
);
// illegal ?transport=xxx tests in turn uris
notValidIceServerTest(
"turn:hostname.com?transport=invalid", "anyuser", "anypassword",
"Wrong value specified in STUN/TURN uri: 'turn:hostname.com?transport=invalid'. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp"
);
notValidIceServerTest(
"turn:hostname.com?transport=", "anyuser", "anypassword",
"Wrong value specified in STUN/TURN uri: 'turn:hostname.com?transport='. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp"
);
notValidIceServerTest(
"turn:hostname.com?=", "anyuser", "anypassword",
"Wrong value specified in STUN/TURN uri: 'turn:hostname.com?='. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp"
);
notValidIceServerTest(
"turn:hostname.com?", "anyuser", "anypassword",
"Wrong value specified in STUN/TURN uri: 'turn:hostname.com?'. Unique valid arguments after '?' are '?transport=tcp' or '?transport=udp"
);
notValidIceServerTest(
"?", "anyuser", "anypassword",
"Not a valid TURN/STUN uri provided. No colons found in: '?'"
);
// Transport can not be defined in STUN
notValidIceServerTest(
"stun:hostname.com?transport=tcp", null, null,
"STUN uri can't have any '?' query param"
);
notValidIceServerTest(
"stun:hostname.com?transport=udp", null, null,
"STUN uri can't have any '?' query param"
);
// Stun can not have credentials defined
notValidIceServerTest(
"stun:hostname.com", "username", "credential",
"Credentials can not be defined while using stun."
);
notValidIceServerTest(
"stun:hostname.com", "username", "credential",
"Credentials can not be defined while using stun."
);
notValidIceServerTest(
"stun:hostname.com", "username", null,
"Credentials can not be defined while using stun."
);
notValidIceServerTest(
"stun:hostname.com", null, "credential",
"Credentials can not be defined while using stun."
);
// Turn must have credentials
notValidIceServerTest(
"turn:hostname.com", null, null,
"Credentials must be defined while using turn"
);
notValidIceServerTest(
"turn:hostname.com", "username", null,
"Credentials must be defined while using turn"
);
notValidIceServerTest(
"turn:hostname.com", null, "credential",
"Credentials must be defined while using turn"
);
}
@Test
@DisplayName("IceServerProperty exceptions tests")
public void iceServerPropertiesValidTest() {
// Stun and stuns
validIceServerTest("stun:hostname.com", null, null);
validIceServerTest("stuns:hostname.com", null, null);
// Turn and turns
validIceServerTest("turn:hostname.com", "anyuser", "credential");
validIceServerTest("turns:hostname.com", "anyuser", "credential");
// Test IPv4/IPv6/hostname and with/without port
validIceServerTest("stun:1.2.3.4:1234", null, null);
validIceServerTest("stun:[1:2:3:4:5:6:7:8]:4321", null, null);
validIceServerTest("stun:hostname.com:9999", null, null);
validIceServerTest("stun:1.2.3.4", null, null);
validIceServerTest("stun:[1:2:3:4:5:6:7:8]", null, null);
validIceServerTest("stuns:1.2.3.4:1234", null, null);
validIceServerTest("stuns:[1:2:3:4:5:6:7:8]:4321", null, null);
validIceServerTest("stuns:hostname.com:9999", null, null);
validIceServerTest("stuns:1.2.3.4", null, null);
validIceServerTest("stuns:[1:2:3:4:5:6:7:8]", null, null);
validIceServerTest("turn:1.2.3.4:1234", "anyuser", "credential");
validIceServerTest("turn:[1:2:3:4:5:6:7:8]:4321", "anyuser", "credential");
validIceServerTest("turn:hostname.com:9999", "anyuser", "credential");
validIceServerTest("turn:1.2.3.4", "anyuser", "credential");
validIceServerTest("turn:[1:2:3:4:5:6:7:8]", "anyuser", "credential");
validIceServerTest("turns:1.2.3.4:1234", "anyuser", "credential");
validIceServerTest("turns:[1:2:3:4:5:6:7:8]:4321", "anyuser", "credential");
validIceServerTest("turns:hostname.com:9999", "anyuser", "credential");
validIceServerTest("turns:1.2.3.4", "anyuser", "credential");
validIceServerTest("turns:[1:2:3:4:5:6:7:8]", "anyuser", "credential");
// Test valid ?transport=tcp or ?transport=udp
validIceServerTest("turn:hostname.com:1234?transport=tcp", "anyuser", "credential");
validIceServerTest("turn:hostname.com?transport=udp", "anyuser", "credential");
validIceServerTest("turn:1.2.3.4:1234?transport=tcp", "anyuser", "credential");
validIceServerTest("turn:1.2.3.4?transport=udp", "anyuser", "credential");
validIceServerTest("turn:[1:2:3:4:5:6:7:8]:4321?transport=udp", "anyuser", "credential");
validIceServerTest("turn:[1:2:3:4:5:6:7:8]?transport=udp", "anyuser", "credential");
}
private void validIceServerTest(String url, String username, String credential) {
assertDoesNotThrow(() -> {
IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder().url(url);
if (username != null) {
iceServerPropertiesBuilder.username(username);
}
if (credential != null) {
iceServerPropertiesBuilder.credential(credential);
}
IceServerProperties iceServerProperties = iceServerPropertiesBuilder.build();
assertEquals(url, iceServerProperties.getUrl());
if (username != null) {
assertEquals(username, iceServerProperties.getUsername());
}
if (credential != null) {
assertEquals(credential, iceServerProperties.getCredential());
}
});
}
private void notValidIceServerTest(String url, String username, String credential, String expectedMessage) {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
IceServerProperties.Builder iceServerPropertiesBuilder = new IceServerProperties.Builder().url(url);
if (username != null) {
iceServerPropertiesBuilder.username(username);
}
if (credential != null) {
iceServerPropertiesBuilder.credential(credential);
}
iceServerPropertiesBuilder.build();
});
String actualMessage = exception.getMessage();
assertEquals(actualMessage, expectedMessage);
}
}

View File

@ -68,9 +68,9 @@ public class OpenViduTestE2e {
protected static String MEDIA_SERVER_IMAGE = KURENTO_IMAGE + ":6.16.0";
final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultRecordingProperties':{'hasVideo':true,'frameRate':25,'hasAudio':true,'shmSize':536870912,'name':'','outputMode':'COMPOSED','resolution':'1280x720','recordingLayout':'BEST_FIT'},'customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false,'forcedVideoCodec':'STR','forcedVideoCodecResolved':'STR','allowTranscoding':false}";
final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'ip':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null}";
final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','ip':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[]}";
final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','ip':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[]}";
final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'ip':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null, 'customIceServers':[]}";
final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','ip':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[], 'customIceServers':[]}";
final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','ip':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[], 'customIceServers':[]}";
final protected String DEFAULT_JSON_TOKEN = "{'id':'STR','token':'STR','connectionId':'STR','createdAt':0,'session':'STR','role':'STR','data':'STR','kurentoOptions':{}}";
protected static String OPENVIDU_SECRET = "MY_SECRET";

View File

@ -35,6 +35,8 @@ import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import com.google.gson.*;
import io.openvidu.java.client.*;
import org.apache.http.HttpStatus;
import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll;
@ -49,35 +51,15 @@ import org.openqa.selenium.Dimension;
import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mashape.unirest.http.HttpMethod;
import io.appium.java_client.AppiumDriver;
import io.openvidu.java.client.Connection;
import io.openvidu.java.client.ConnectionProperties;
import io.openvidu.java.client.ConnectionType;
import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.OpenVidu;
import io.openvidu.java.client.OpenViduHttpException;
import io.openvidu.java.client.OpenViduJavaClientException;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.Publisher;
import io.openvidu.java.client.Recording;
import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingMode;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.java.client.Session;
import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.VideoCodec;
import io.openvidu.test.browsers.BrowserUser;
import io.openvidu.test.browsers.utils.BrowserNames;
import io.openvidu.test.browsers.utils.CustomHttpClient;
@ -3002,6 +2984,20 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
body = "{'type':'WEBRTC','role':'MODERATOR','data':true}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_BAD_REQUEST);
// 400 - Test some not valid customIceServers configured
body = "{'customIceServers': [{'url':'bad-ice-server'}]}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_BAD_REQUEST);
body = "{'customIceServers': [{'url':'turn:bad-ice-server'}]}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_BAD_REQUEST);
body = "{'customIceServers': [{'url':'bad-prefix:bad-ice-server'}]}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_BAD_REQUEST);
// 200
String kurentoOpts = "'kurentoOptions':{'videoMaxSendBandwidth':777,'allowedFilters':['GStreamerFilter']}";
body = "{'type':'WEBRTC','role':'MODERATOR','data':'SERVER_DATA'," + kurentoOpts + "}";
@ -3011,6 +3007,18 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
restClient.rest(HttpMethod.DELETE,
"/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + res.get("id").getAsString(),
HttpStatus.SC_NO_CONTENT);
// 200 - Test some good Ice Servers configured
String goodTurn = "{'url': 'turn:valid-domain.es', 'username': 'user', 'credential': 'pass'}";
String goodStun = "{'url': 'stun:valid-domain.es:1234'}";
body = "{ 'customIceServers': [" + goodTurn + "," + goodStun + "]}";
res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_OK, true, false, true,
mergeJson(DEFAULT_JSON_PENDING_CONNECTION, "{ 'customIceServers': [" + goodTurn + "," + goodStun+ "] }", new String[0]));
restClient.rest(HttpMethod.DELETE,
"/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + res.get("id").getAsString(),
HttpStatus.SC_NO_CONTENT);
// Default values
res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", "{}",
HttpStatus.SC_OK);
@ -4373,7 +4381,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
connectionJson.get("subscribers").getAsJsonArray().size() == 0);
Assert.assertTrue("Wrong token Connection property",
connectionJson.get("token").getAsString().contains(session.getSessionId()));
Assert.assertEquals("Wrong number of keys in connectionProperties", 9, connectionProperties.keySet().size());
Assert.assertEquals("Wrong number of keys in connectionProperties", 10, connectionProperties.keySet().size());
Assert.assertTrue("Wrong customIceServer property", connectionProperties.get("customIceServers").getAsJsonArray().size() == 0);
Assert.assertEquals("Wrong type property", ConnectionType.WEBRTC.name(),
connectionProperties.get("type").getAsString());
Assert.assertEquals("Wrong data property", "MY_SERVER_DATA", connectionProperties.get("data").getAsString());
@ -4489,7 +4498,216 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
Assert.assertFalse("Java fetch should be false", OV.fetch());
checkNodeFetchChanged(user, false, true);
checkNodeFetchChanged(user, false, false);
}
@Test
@DisplayName("Custom Ice Server connection tests from openvidu-java-client")
void customIceServerConnectionJavaClientTest() throws Exception {
customIceServerTest("openvidu-java-client");
}
@Test
@DisplayName("Custom Ice Server connection tests from openvidu-node-client")
void customIceServerConnectionNodeClientTest() throws Exception {
customIceServerTest("openvidu-node-client");
}
private void customIceServerTest(String client) throws Exception {
OpenViduTestappUser user = setupBrowserAndConnectToOpenViduTestapp("chrome");
// ------
// 1. Initialize connection with Custom Ice Servers from openvidu-java-client
// ------
Session session = OV.createSession();
String sessionId1 = session.getSessionId();
Assert.assertFalse("Java fetch should be false", OV.fetch());
Assert.assertFalse("Session fetch should be false", session.fetch());
user.getDriver().findElement(By.id("add-user-btn")).click();
WebElement sessionName1 = user.getDriver().findElement(By.id("session-name-input-0"));
sessionName1.clear();
sessionName1.sendKeys(session.getSessionId());
user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER));
user.getEventManager().waitUntilEventReaches("connectionCreated", 1);
user.getEventManager().waitUntilEventReaches("accessAllowed", 1);
user.getEventManager().waitUntilEventReaches("streamCreated", 1);
user.getEventManager().waitUntilEventReaches("streamPlaying", 1);
session.fetch();
Assert.assertEquals(session.getActiveConnections().size(), 1);
// Add second user with connection using custom ice servers
Connection connection1 = createConnWithCustomIceServer(session, user, client);
user.getDriver().findElement(By.id("add-user-btn")).click();
WebElement sessionName2 = user.getDriver().findElement(By.id("session-name-input-1"));
sessionName2.clear();
sessionName2.sendKeys(sessionId1);
user.getDriver().findElement(By.id("session-settings-btn-1")).click();
Thread.sleep(1000);
WebElement token1Input = user.getDriver().findElement(By.cssSelector("#custom-token-div input"));
token1Input.clear();
token1Input.sendKeys(connection1.getToken());
user.getDriver().findElement(By.id("save-btn")).click();
Thread.sleep(1000);
user.getDriver().findElements(By.className("join-btn"))
.stream().filter(el -> el.isEnabled()).forEach(el -> el.sendKeys(Keys.ENTER));
user.getEventManager().waitUntilEventReaches("connectionCreated", 4);
user.getEventManager().waitUntilEventReaches("accessAllowed", 2);
user.getEventManager().waitUntilEventReaches("streamCreated", 4);
user.getEventManager().waitUntilEventReaches("streamPlaying", 4);
// ------
// 2. Check that the IceServer is correctly setup on RTCPeerConnections created. This will ensure that the property
// is reached in WebRTC browser related objects
// ------
List<WebElement> iceConfiguredButtons = user.getDriver().findElements(By.className("ice-config-button-" + connection1.getConnectionId()));
for (WebElement iceConfigured : iceConfiguredButtons) {
iceConfigured.click();
Thread.sleep(1000);
for(int i = 0; i < connection1.getCustomIceServers().size(); i++) {
IceServerProperties customIceServer = connection1.getCustomIceServers().get(i);
String foundIceUrl = user.getDriver().findElement(By.id("ice-server-url-" + i)).getText();
String foundIceUsername = null, foundIceCred = null;
List<WebElement> foundIceUsernameWebElem = user.getDriver().findElements(By.id("ice-server-username-" + i));
List<WebElement> foundIceCredWebElem = user.getDriver().findElements(By.id("ice-server-credential-" + i));
if (foundIceUsernameWebElem.size() == 1) {
foundIceUsername = foundIceUsernameWebElem.get(0).getText();
}
if (foundIceCredWebElem.size() == 1) {
foundIceCred = foundIceCredWebElem.get(0).getText();
}
Assert.assertEquals(foundIceUrl, customIceServer.getUrl());
Assert.assertEquals(foundIceUsername, customIceServer.getUsername());
Assert.assertEquals(foundIceCred, customIceServer.getCredential());
}
user.getDriver().findElement(By.id("close-dialog-btn")).click();
Thread.sleep(1000);
}
// ------
// 3. Check that the data in openvidu-node-client is correctly fetched
// ------
user.getDriver().findElement(By.id("session-api-btn-0")).click();
Thread.sleep(1000);
checkNodeFetchChanged(user, false, true);
checkNodeFetchChanged(user, false, false);
checkNodeFetchChanged(user, true, false);
// ------
// 4. Check if Ice Servers are correctly received in openvidu-node-client
// ------
user.getDriver().findElement(By.id("close-dialog-btn")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.id("session-info-btn-0")).click();
// 4.1. Get all session data
JsonObject res = JsonParser
.parseString(user.getDriver().findElement(By.id("session-text-area")).getAttribute("value"))
.getAsJsonObject();
JsonArray connectionsJsonArray = res.get("connections").getAsJsonArray();
boolean foundConnection = false;
for (JsonElement connectionJson: connectionsJsonArray) {
// 4.2. Of all connections, get only the one created with openvidu-java-client with customIceServers added
// Check if connection is the one configured from java client
String connectionId = connectionJson.getAsJsonObject().get("connectionId").getAsString();
if (connectionId.equals(connection1.getConnectionId())) {
// 4.3. If connection with custom ice server is found, get the property with all customIceServers
foundConnection = true;
JsonArray customIceServersJsonArray = connectionJson.getAsJsonObject()
.get("connectionProperties").getAsJsonObject()
.get("customIceServers").getAsJsonArray();
for (IceServerProperties customIceServer: connection1.getCustomIceServers()) {
// 4.4 Compare both list, the one created with openvidu-java-client with the one received from openvidu-node-client
boolean foundIceServer = false;
Iterator<JsonElement> customIceServersJsonIterator = customIceServersJsonArray.iterator();
while(customIceServersJsonIterator.hasNext() && !foundIceServer) {
// 4.5 When the custom ICE server is found in the openvidu-node-client object, compare it with the
// openvidu-java-client object
JsonObject customIceJsonObject = customIceServersJsonIterator.next().getAsJsonObject();
String url = customIceJsonObject.get("url").getAsString();
if (url.equals(customIceServer.getUrl())) {
foundIceServer = true;
Assert.assertEquals(customIceServer.getUrl(), customIceJsonObject.get("url").getAsString());
if (customIceJsonObject.get("username") != null) {
Assert.assertEquals(customIceServer.getUsername(), customIceJsonObject.get("username").getAsString());
}
if (customIceJsonObject.get("credential") != null) {
Assert.assertEquals(customIceServer.getCredential(), customIceJsonObject.get("credential").getAsString());
}
}
}
// 4.6 Assert that the custom Ice Server was found on the openvidu-node-client connection object
Assert.assertTrue(foundIceServer);
}
}
}
// 4.7 Assert that the connection was found on the openvidu-node-client session object, to fail in case it was not registered
Assert.assertTrue(foundConnection);
}
private Connection createConnWithCustomIceServer(Session session, OpenViduTestappUser user, String fromClient)
throws OpenViduJavaClientException, OpenViduHttpException, InterruptedException {
if (fromClient.equals("openvidu-java-client")) {
IceServerProperties iceServerProperties1 = new IceServerProperties.Builder()
.url("turn:turn-server.com")
.username("usertest")
.credential("credtest")
.build();
IceServerProperties iceServerProperties2 = new IceServerProperties.Builder()
.url("stun:1.2.3.4:1234")
.build();
ConnectionProperties connectionProperties = new ConnectionProperties.Builder()
.addCustomIceServer(iceServerProperties1)
.addCustomIceServer(iceServerProperties2)
.build();
return session.createConnection(connectionProperties);
} else if (fromClient.equals("openvidu-node-client")) {
user.getDriver().findElement(By.id("session-api-btn-0")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.id("num-ice-servers-select")).click();
Thread.sleep(500);
user.getDriver().findElement(By.id("num-ice-servers-2")).click();
Thread.sleep(500);
WebElement iceUrl1 = user.getDriver().findElement(By.id("ice-server-url-0"));
WebElement iceUsername1 = user.getDriver().findElement(By.id("ice-server-username-0"));
WebElement iceCredential1 = user.getDriver().findElement(By.id("ice-server-credential-0"));
WebElement iceUrl2 = user.getDriver().findElement(By.id("ice-server-url-1"));
iceUrl1.clear();
iceUsername1.clear();
iceCredential1.clear();
iceUrl2.clear();
iceUrl1.sendKeys("turn:turn-server.com");
iceUsername1.sendKeys("usertest");
iceCredential1.sendKeys("credtest");
iceUrl2.sendKeys("stun:1.2.3.4:1234");
// Create connection
user.getDriver().findElement(By.id("crate-connection-api-btn")).click();
Thread.sleep(1000);
String responseAreaText = user.getDriver().findElement(By.id("api-response-text-area")).getAttribute("value");
user.getDriver().findElement(By.id("close-dialog-btn")).click();
String connectionCreatedPrefix = "Connection created: ";
Assert.assertTrue(responseAreaText.startsWith(connectionCreatedPrefix));
String connectionStringResponse = responseAreaText.split(connectionCreatedPrefix, 2)[1];
JsonObject connectionJsonResponse = JsonParser.parseString(connectionStringResponse).getAsJsonObject();
String connectionId = connectionJsonResponse.get("connectionId").getAsString();
Assert.assertTrue(session.fetch());
Assert.assertFalse(session.fetch());
Assert.assertFalse(OV.fetch());
for (Connection connection: session.getConnections()) {
if (connection.getConnectionId().equals(connectionId)) {
return connection;
}
}
return null;
} else {
return null;
}
}
@Test

View File

@ -33,6 +33,7 @@ import { OpenviduParamsService } from './services/openvidu-params.service';
import { TestFeedService } from './services/test-feed.service';
import { MuteSubscribersService } from './services/mute-subscribers.service';
import { SessionInfoDialogComponent } from "./components/dialogs/session-info-dialog/session-info-dialog.component";
import { ShowIceServerConfiguredDialog } from './components/dialogs/show-configured-ice/show-configured-ice.component';
@NgModule({
declarations: [
@ -53,6 +54,7 @@ import { SessionInfoDialogComponent } from "./components/dialogs/session-info-di
ScenarioPropertiesDialogComponent,
FilterDialogComponent,
ShowCodecDialogComponent,
ShowIceServerConfiguredDialog,
SessionInfoDialogComponent,
UsersTableComponent,
TableVideoComponent
@ -82,6 +84,7 @@ import { SessionInfoDialogComponent } from "./components/dialogs/session-info-di
ScenarioPropertiesDialogComponent,
FilterDialogComponent,
ShowCodecDialogComponent,
ShowIceServerConfiguredDialog,
SessionInfoDialogComponent
],
bootstrap: [AppComponent]

View File

@ -39,3 +39,12 @@ mat-dialog-content button {
top: 0;
right: 0;
}
#manual-turn-div {
background-color: #f7f7f7;
margin-top: 10px;
margin-bottom: 10px;
padding: 5px;
border: 1px solid #00000026;
border-radius: 3px;
}

View File

@ -30,6 +30,27 @@
<input matInput id="connection-data-field" placeholder="data" [(ngModel)]="connectionProperties.data">
</mat-form-field>
</div>
<div>
<mat-form-field class="inner-text-input" [style.fontSize.px]=14 style="width:33%">
<mat-label>Custom Ice Servers</mat-label>
<mat-select [(ngModel)]="numCustomIceServers" id="num-ice-servers-select" (selectionChange)="changedNumIceServers(numCustomIceServers)">
<mat-option *ngFor="let i of [0,1,2,3,4,5,6,7,8,9,10]" [value]="i">
<span [attr.id]="'num-ice-servers-' + i">{{ i }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<div id="manual-turn-div" *ngFor="let configuredIce of configuredCustomIceServers; let i = index">
<mat-form-field style="width: 100%">
<input matInput id="ice-server-url-{{i}}" placeholder="url" type="text" [(ngModel)]="configuredIce.url">
</mat-form-field>
<mat-form-field style="width: 48%; padding-right: 2px">
<input matInput id="ice-server-username-{{i}}" placeholder="username" type="text" [(ngModel)]="configuredIce.username">
</mat-form-field>
<mat-form-field style="width: 48%; padding-left: 2px">
<input matInput id="ice-server-credential-{{i}}" placeholder="credential" type="text" [(ngModel)]="configuredIce.credential">
</mat-form-field>
</div>
</div>
<div>
<button mat-button id="crate-connection-api-btn" (click)="createConnection()">Create connection</button>
<button mat-button id="update-connection-api-btn" (click)="updateConnection()" [disabled]="!connectionId">Update

View File

@ -23,6 +23,8 @@ export class SessionApiDialogComponent {
customLayout = '';
recPropertiesIcon = 'add_circle';
showRecProperties = false;
numCustomIceServers = 0;
configuredCustomIceServers = []
connectionProperties: ConnectionProperties = {
record: true,
@ -205,6 +207,7 @@ export class SessionApiDialogComponent {
createConnection() {
console.log('Creating connection');
this.connectionProperties.customIceServers = this.configuredCustomIceServers;
this.session.createConnection(this.connectionProperties)
.then(connection => {
this.response = 'Connection created: ' + JSON.stringify(connection);
@ -238,4 +241,23 @@ export class SessionApiDialogComponent {
this.recPropertiesIcon = this.showRecProperties ? 'remove_circle' : 'add_circle';
}
changedNumIceServers(numIceServers: number) {
// Save Previous Ice Servers
let previousIceServers = [];
for (let i = 0; i < this.configuredCustomIceServers.length; i++) {
previousIceServers.push(this.configuredCustomIceServers[i]);
}
// Fill empty ice servers
this.configuredCustomIceServers = []
for(let i = 1; i <= numIceServers; i++) {
this.configuredCustomIceServers.push({});
}
// Add previous items
for(let i = 0; i < previousIceServers.length && i < this.configuredCustomIceServers.length; i++) {
this.configuredCustomIceServers[0] = previousIceServers[0];
}
}
}

View File

@ -0,0 +1,37 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
@Component({
selector: 'app-ice-configured-dialog',
template: `
<div id="app-ice-configured-dialog-container">
<ul id="ice-server-list">
<div class="ice-server" *ngFor="let iceServer of iceServerList; index as i">
<li id="ice-server-{{i}}">
<p>
ICE Server URL: <span id="ice-server-url-{{i}}">{{iceServer.urls}}</span> -
Username: <span *ngIf="iceServer.username" id="ice-server-username-{{i}}">{{iceServer.username}}</span> -
Credential: <span *ngIf="iceServer.credential" id="ice-server-credential-{{i}}">{{iceServer.credential}}</span>
</p>
</li>
</div>
<button mat-button id="close-dialog-btn" [mat-dialog-close]="{}">CLOSE</button>
</ul>
</div>
`,
styles: [`
#app-ice-configured-dialog-container {
text-align: center
}
`]
})
export class ShowIceServerConfiguredDialog {
iceServerList: RTCIceServer[]
constructor(public dialogRef: MatDialogRef<ShowIceServerConfiguredDialog>,
@Inject(MAT_DIALOG_DATA) public data) {
this.iceServerList = data.iceServerList;
}
}

View File

@ -11,6 +11,11 @@
<button class="video-btn stats-button bottom-left-rounded" title="Peer Connection Stats" (click)="showCodecUsed()">
<mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button>
<button *ngIf="OV.session.connection.connectionId"
class="video-btn bottom-left-rounded ice-config-button-{{OV.session.connection.connectionId}}"
title="Ice Server configuration" (click)="getConfiguredIceServer()">
<mat-icon aria-label="Ice Server configuration" class="mat-icon material-icons" role="img" aria-hidden="true">storage</mat-icon>
</button>
</div>
<div class="bottom-div">
<button class="video-btn pub-btn" title="Publish/Unpublish" (click)="pubUnpub()">
@ -53,6 +58,11 @@
<button class="video-btn stats-button bottom-left-rounded" title="Peer Connection Stats" (click)="showCodecUsed()">
<mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button>
<button *ngIf="OV.session.connection.connectionId"
class="video-btn bottom-left-rounded ice-config-button-{{OV.session.connection.connectionId}}"
title="Ice Server configuration" (click)="getConfiguredIceServer()">
<mat-icon aria-label="Ice Server configuration" class="mat-icon material-icons" role="img" aria-hidden="true">storage</mat-icon>
</button>
</div>
<div class="bottom-div">
<button class="video-btn sub-btn" title="Subscribe/Unsubscribe" (click)="subUnsub()">

View File

@ -23,6 +23,7 @@ import { LocalRecordingDialogComponent } from '../dialogs/local-recording-dialog
import { ExtensionDialogComponent } from '../dialogs/extension-dialog/extension-dialog.component';
import { FilterDialogComponent } from '../dialogs/filter-dialog/filter-dialog.component';
import { OpenViduEvent } from '../openvidu-instance/openvidu-instance.component';
import { ShowIceServerConfiguredDialog } from '../dialogs/show-configured-ice/show-configured-ice.component';
@Component({
selector: 'app-video',
@ -813,6 +814,16 @@ export class VideoComponent implements OnInit, OnDestroy {
});
}
getConfiguredIceServer() {
let iceServerList: RTCIceServer[] = this.streamManager.stream.getWebRtcPeer().pc.getConfiguration().iceServers;
this.dialog.open(ShowIceServerConfiguredDialog, {
data: {
iceServerList: iceServerList
},
width: '450px'
})
}
async showCodecUsed() {
let stats = await this.streamManager.stream.getWebRtcPeer().pc.getStats();
let codecIdIndex = null;