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) { private processJoinRoomResponse(opts: LocalConnectionOptions) {
this.sessionId = opts.session; 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; const turnUrl1 = 'turn:' + opts.coturnIp + ':' + opts.coturnPort;
this.openvidu.iceServers = [ this.openvidu.iceServers = [
{ urls: [turnUrl1], username: opts.turnUsername, credential: opts.turnCredential } { 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 { RemoteConnectionOptions } from './RemoteConnectionOptions';
import { IceServerProperties } from './IceServerProperties';
export interface LocalConnectionOptions { export interface LocalConnectionOptions {
id: string; id: string;
@ -35,4 +36,5 @@ export interface LocalConnectionOptions {
mediaServer: string; mediaServer: string;
videoSimulcast: boolean; videoSimulcast: boolean;
life: number; 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_ROLE_PARAM = "role";
public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp"; public static final String PARTICIPANTJOINED_COTURNIP_PARAM = "coturnIp";
public static final String PARTICIPANTJOINED_COTURNPORT_PARAM = "coturnPort"; 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_TURNUSERNAME_PARAM = "turnUsername";
public static final String PARTICIPANTJOINED_TURNCREDENTIAL_PARAM = "turnCredential"; public static final String PARTICIPANTJOINED_TURNCREDENTIAL_PARAM = "turnCredential";

View File

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

View File

@ -25,7 +25,9 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/** /**
* See {@link io.openvidu.java.client.Session#getConnections()} * See {@link io.openvidu.java.client.Session#getConnections()}
@ -190,6 +192,19 @@ public class Connection {
return this.connectionProperties.getNetworkCache(); 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 * 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 * 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) { if (this.connectionProperties.getNetworkCache() != null) {
builder.networkCache(this.connectionProperties.getNetworkCache()); 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(); this.connectionProperties = builder.build();
} }
@ -415,6 +435,24 @@ public class Connection {
? OpenViduRole.valueOf(json.get("role").getAsString()) ? OpenViduRole.valueOf(json.get("role").getAsString())
: null; : 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 // IPCAM
String rtspUri = (json.has("rtspUri") && !json.get("rtspUri").isJsonNull()) ? json.get("rtspUri").getAsString() String rtspUri = (json.has("rtspUri") && !json.get("rtspUri").isJsonNull()) ? json.get("rtspUri").getAsString()
: null; : null;
@ -428,8 +466,9 @@ public class Connection {
Integer networkCache = (json.has("networkCache") && !json.get("networkCache").isJsonNull()) Integer networkCache = (json.has("networkCache") && !json.get("networkCache").isJsonNull())
? json.get("networkCache").getAsInt() ? json.get("networkCache").getAsInt()
: null; : null;
this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate, this.connectionProperties = new ConnectionProperties(type, data, record, role, null, rtspUri, adaptativeBitrate,
onlyPlayWithSubscribers, networkCache); onlyPlayWithSubscribers, networkCache, customIceServers);
return this; return this;
} }

View File

@ -1,8 +1,12 @@
package io.openvidu.java.client; package io.openvidu.java.client;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull; import com.google.gson.JsonNull;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
/** /**
* See * See
* {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)} * {@link io.openvidu.java.client.Session#createConnection(ConnectionProperties)}
@ -22,6 +26,9 @@ public class ConnectionProperties {
private Boolean onlyPlayWithSubscribers; private Boolean onlyPlayWithSubscribers;
private Integer networkCache; private Integer networkCache;
// External Turn Service
private List<IceServerProperties> customIceServers;
/** /**
* *
* Builder for {@link io.openvidu.java.client.ConnectionProperties} * Builder for {@link io.openvidu.java.client.ConnectionProperties}
@ -36,18 +43,21 @@ public class ConnectionProperties {
// WEBRTC // WEBRTC
private OpenViduRole role; private OpenViduRole role;
private KurentoOptions kurentoOptions; private KurentoOptions kurentoOptions;
private List<IceServerProperties> customIceServers = new ArrayList<>();
// IPCAM // IPCAM
private String rtspUri; private String rtspUri;
private Boolean adaptativeBitrate; private Boolean adaptativeBitrate;
private Boolean onlyPlayWithSubscribers; private Boolean onlyPlayWithSubscribers;
private Integer networkCache; private Integer networkCache;
/** /**
* Builder for {@link io.openvidu.java.client.ConnectionProperties}. * Builder for {@link io.openvidu.java.client.ConnectionProperties}.
*/ */
public ConnectionProperties build() { public ConnectionProperties build() {
return new ConnectionProperties(this.type, this.data, this.record, this.role, this.kurentoOptions, 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; this.networkCache = networkCache;
return this; 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, ConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role,
KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers,
Integer networkCache) { Integer networkCache, List<IceServerProperties> customIceServers) {
this.type = type; this.type = type;
this.data = data; this.data = data;
this.record = record; this.record = record;
@ -233,6 +274,7 @@ public class ConnectionProperties {
this.adaptativeBitrate = adaptativeBitrate; this.adaptativeBitrate = adaptativeBitrate;
this.onlyPlayWithSubscribers = onlyPlayWithSubscribers; this.onlyPlayWithSubscribers = onlyPlayWithSubscribers;
this.networkCache = networkCache; this.networkCache = networkCache;
this.customIceServers = customIceServers;
} }
/** /**
@ -346,6 +388,19 @@ public class ConnectionProperties {
return this.networkCache; 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) { public JsonObject toJson(String sessionId) {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("session", sessionId); json.addProperty("session", sessionId);
@ -376,6 +431,12 @@ public class ConnectionProperties {
} else { } else {
json.add("kurentoOptions", JsonNull.INSTANCE); json.add("kurentoOptions", JsonNull.INSTANCE);
} }
JsonArray customIceServersJsonList = new JsonArray();
customIceServers.forEach((customIceServer) -> {
customIceServersJsonList.add(customIceServer.toJson());
});
json.add("customIceServers", customIceServersJsonList);
// IPCAM // IPCAM
if (getRtspUri() != null) { if (getRtspUri() != null) {
json.addProperty("rtspUri", getRtspUri()); 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 { Publisher } from './Publisher';
import { ConnectionProperties } from './ConnectionProperties'; import { ConnectionProperties } from './ConnectionProperties';
import { OpenViduRole } from './OpenViduRole'; import { OpenViduRole } from './OpenViduRole';
import { IceServerProperties } from './IceServerProperties';
/** /**
* See [[Session.connections]] * See [[Session.connections]]
@ -138,6 +139,7 @@ export class Connection {
this.connectionProperties.adaptativeBitrate = json.adaptativeBitrate; this.connectionProperties.adaptativeBitrate = json.adaptativeBitrate;
this.connectionProperties.onlyPlayWithSubscribers = json.onlyPlayWithSubscribers; this.connectionProperties.onlyPlayWithSubscribers = json.onlyPlayWithSubscribers;
this.connectionProperties.networkCache = json.networkCache; this.connectionProperties.networkCache = json.networkCache;
this.connectionProperties.customIceServers = json.customIceServers ?? []
} else { } else {
this.connectionProperties = { this.connectionProperties = {
type: json.type, type: json.type,
@ -148,7 +150,8 @@ export class Connection {
rtspUri: json.rtspUri, rtspUri: json.rtspUri,
adaptativeBitrate: json.adaptativeBitrate, adaptativeBitrate: json.adaptativeBitrate,
onlyPlayWithSubscribers: json.onlyPlayWithSubscribers, onlyPlayWithSubscribers: json.onlyPlayWithSubscribers,
networkCache: json.networkCache networkCache: json.networkCache,
customIceServers: json.customIceServers ?? []
} }
} }
this.role = json.role; this.role = json.role;
@ -224,6 +227,7 @@ export class Connection {
this.connectionProperties.adaptativeBitrate === other.connectionProperties.adaptativeBitrate && this.connectionProperties.adaptativeBitrate === other.connectionProperties.adaptativeBitrate &&
this.connectionProperties.onlyPlayWithSubscribers === other.connectionProperties.onlyPlayWithSubscribers && this.connectionProperties.onlyPlayWithSubscribers === other.connectionProperties.onlyPlayWithSubscribers &&
this.connectionProperties.networkCache === other.connectionProperties.networkCache && this.connectionProperties.networkCache === other.connectionProperties.networkCache &&
this.connectionProperties.customIceServers.length === other.connectionProperties.customIceServers.length &&
this.token === other.token && this.token === other.token &&
this.location === other.location && this.location === other.location &&
this.ip === other.ip && this.ip === other.ip &&
@ -238,6 +242,15 @@ export class Connection {
equals = (this.connectionProperties.kurentoOptions === other.connectionProperties.kurentoOptions); 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) { if (equals) {
equals = JSON.stringify(this.subscribers.sort()) === JSON.stringify(other.subscribers.sort()); equals = JSON.stringify(this.subscribers.sort()) === JSON.stringify(other.subscribers.sort());
if (equals) { if (equals) {

View File

@ -15,6 +15,7 @@
* *
*/ */
import { IceServerProperties } from './IceServerProperties';
import { ConnectionType } from './ConnectionType'; import { ConnectionType } from './ConnectionType';
import { OpenViduRole } from './OpenViduRole'; import { OpenViduRole } from './OpenViduRole';
@ -127,4 +128,30 @@ export interface ConnectionProperties {
*/ */
networkCache?: number; 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 { SessionProperties } from './SessionProperties';
import { TokenOptions } from './TokenOptions'; import { TokenOptions } from './TokenOptions';
import { RecordingProperties } from 'RecordingProperties'; import { RecordingProperties } from 'RecordingProperties';
import { IceServerProperties } from 'IceServerProperties';
export class Session { export class Session {
@ -150,7 +151,8 @@ export class Session {
rtspUri: (!!connectionProperties && !!connectionProperties.rtspUri) ? connectionProperties.rtspUri : null, rtspUri: (!!connectionProperties && !!connectionProperties.rtspUri) ? connectionProperties.rtspUri : null,
adaptativeBitrate: !!connectionProperties ? connectionProperties.adaptativeBitrate : null, adaptativeBitrate: !!connectionProperties ? connectionProperties.adaptativeBitrate : null,
onlyPlayWithSubscribers: !!connectionProperties ? connectionProperties.onlyPlayWithSubscribers : 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( axios.post(
this.ov.host + OpenVidu.API_SESSIONS + '/' + this.sessionId + '/connection', 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 // 1. Array to store fetched connections and later remove closed ones
const fetchedConnectionIds: string[] = []; const fetchedConnectionIds: string[] = [];
json.connections.content.forEach(jsonConnection => { json.connections.content.forEach(jsonConnection => {
const connectionObj: Connection = new Connection(jsonConnection); const connectionObj: Connection = new Connection(jsonConnection);
fetchedConnectionIds.push(connectionObj.connectionId); fetchedConnectionIds.push(connectionObj.connectionId);
let storedConnection = this.connections.find(c => c.connectionId === 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 // Order connections by time of creation
this.connections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0)); 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 // Populate activeConnections array
this.updateActiveConnectionsArray(); this.updateActiveConnectionsArray();
return this; return this;

View File

@ -13,3 +13,4 @@ export * from './RecordingProperties';
export * from './Connection'; export * from './Connection';
export * from './Publisher'; 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 javax.annotation.PostConstruct;
import io.openvidu.java.client.IceServerProperties;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.validator.routines.DomainValidator; import org.apache.commons.validator.routines.DomainValidator;
@ -220,10 +221,12 @@ public class OpenviduConfig {
private MediaServer mediaServerInfo = MediaServer.kurento; private MediaServer mediaServerInfo = MediaServer.kurento;
// Media properties // Webrtc properties
private boolean webrtcSimulcast = false; private boolean webrtcSimulcast = false;
private List<IceServerProperties> webrtcIceServers;
// Plain config properties getters // Plain config properties getters
public String getCoturnDatabaseDbname() { public String getCoturnDatabaseDbname() {
@ -290,6 +293,10 @@ public class OpenviduConfig {
return this.webrtcSimulcast; return this.webrtcSimulcast;
} }
public List<IceServerProperties> getWebrtcIceServers() {
return webrtcIceServers;
}
public String getOpenViduRecordingPath() { public String getOpenViduRecordingPath() {
return this.openviduRecordingPath; return this.openviduRecordingPath;
} }
@ -619,6 +626,8 @@ public class OpenviduConfig {
checkCertificateType(); checkCertificateType();
webrtcIceServers = loadWebrtcIceServers("OPENVIDU_WEBRTC_ICE_SERVERS");
} }
private void checkCertificateType() { 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.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.openvidu.java.client.IceServerProperties;
import org.kurento.client.GenericMediaEvent; import org.kurento.client.GenericMediaEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -186,6 +187,11 @@ public class SessionEventsHandler {
} }
result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNIP_PARAM, openviduConfig.getCoturnIp()); result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNIP_PARAM, openviduConfig.getCoturnIp());
result.addProperty(ProtocolElements.PARTICIPANTJOINED_COTURNPORT_PARAM, openviduConfig.getCoturnPort()); 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) { if (participant.getToken().getTurnCredentials() != null) {
result.addProperty(ProtocolElements.PARTICIPANTJOINED_TURNUSERNAME_PARAM, result.addProperty(ProtocolElements.PARTICIPANTJOINED_TURNUSERNAME_PARAM,
participant.getToken().getTurnCredentials().getUsername()); participant.getToken().getTurnCredentials().getUsername());

View File

@ -19,6 +19,7 @@ package io.openvidu.server.core;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Iterator; import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; 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.OpenViduRole;
import io.openvidu.java.client.Recording; import io.openvidu.java.client.Recording;
import io.openvidu.java.client.SessionProperties; import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.IceServerProperties;
import io.openvidu.server.cdr.CDREventRecordingStatusChanged; import io.openvidu.server.cdr.CDREventRecordingStatusChanged;
import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService; 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, 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)) { if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) {
log.error("Data invalid format"); log.error("Data invalid format");
throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format"); throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format");
} }
Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), serverMetadata, record, role, Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), serverMetadata, record, role,
kurentoOptions); kurentoOptions, customIceServers);
// Internal dev feature: allows customizing connectionId // Internal dev feature: allows customizing connectionId
if (serverMetadata.contains("openviduCustomConnectionId")) { if (serverMetadata.contains("openviduCustomConnectionId")) {

View File

@ -17,18 +17,18 @@
package io.openvidu.server.core; package io.openvidu.server.core;
import com.google.gson.JsonArray;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import com.google.gson.JsonNull; import com.google.gson.JsonNull;
import com.google.gson.JsonObject; 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.core.Participant.ParticipantStatus;
import io.openvidu.server.coturn.TurnCredentials; import io.openvidu.server.coturn.TurnCredentials;
import java.util.List;
public class Token { public class Token {
private String token; private String token;
@ -77,7 +77,8 @@ public class Token {
this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), newRecord, this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), newRecord,
connectionProperties.getRole(), connectionProperties.getKurentoOptions(), connectionProperties.getRole(), connectionProperties.getKurentoOptions(),
connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(),
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(),
connectionProperties.getCustomIceServers());
} }
public OpenViduRole getRole() { public OpenViduRole getRole() {
@ -88,7 +89,8 @@ public class Token {
this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(), this.updateConnectionProperties(connectionProperties.getType(), connectionProperties.getData(),
connectionProperties.record(), newRole, connectionProperties.getKurentoOptions(), connectionProperties.record(), newRole, connectionProperties.getKurentoOptions(),
connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(), connectionProperties.getRtspUri(), connectionProperties.adaptativeBitrate(),
connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache()); connectionProperties.onlyPlayWithSubscribers(), connectionProperties.getNetworkCache(),
connectionProperties.getCustomIceServers());
} }
public KurentoOptions getKurentoOptions() { public KurentoOptions getKurentoOptions() {
@ -119,6 +121,10 @@ public class Token {
return connectionId; return connectionId;
} }
public List<IceServerProperties> getCustomIceServers() {
return this.connectionProperties.getCustomIceServers();
}
public void setConnectionId(String connectionId) { public void setConnectionId(String connectionId) {
this.connectionId = connectionId; this.connectionId = connectionId;
} }
@ -138,6 +144,16 @@ public class Token {
return json; 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() { public JsonObject toJsonAsParticipant() {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("id", this.getConnectionId()); json.addProperty("id", this.getConnectionId());
@ -178,7 +194,7 @@ public class Token {
private void updateConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role, private void updateConnectionProperties(ConnectionType type, String data, Boolean record, OpenViduRole role,
KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers, KurentoOptions kurentoOptions, String rtspUri, Boolean adaptativeBitrate, Boolean onlyPlayWithSubscribers,
Integer networkCache) { Integer networkCache, List<IceServerProperties> iceServerProperties) {
ConnectionProperties.Builder builder = new ConnectionProperties.Builder(); ConnectionProperties.Builder builder = new ConnectionProperties.Builder();
if (type != null) { if (type != null) {
builder.type(type); builder.type(type);
@ -207,6 +223,11 @@ public class Token {
if (networkCache != null) { if (networkCache != null) {
builder.networkCache(networkCache); builder.networkCache(networkCache);
} }
if (iceServerProperties != null) {
for (IceServerProperties customIceServer: iceServerProperties) {
builder.addCustomIceServer(customIceServer);
}
}
this.connectionProperties = builder.build(); 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.ConnectionType;
import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.IceServerProperties;
import io.openvidu.server.OpenViduServer; import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduBuildInfo; import io.openvidu.server.config.OpenviduBuildInfo;
import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService; import io.openvidu.server.coturn.CoturnCredentialsService;
import io.openvidu.server.coturn.TurnCredentials; import io.openvidu.server.coturn.TurnCredentials;
import java.util.List;
public class TokenGenerator { public class TokenGenerator {
@Autowired @Autowired
@ -42,7 +45,7 @@ public class TokenGenerator {
protected OpenviduBuildInfo openviduBuildConfig; protected OpenviduBuildInfo openviduBuildConfig;
public Token generateToken(String sessionId, String serverMetadata, boolean record, OpenViduRole role, 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; String token = OpenViduServer.wsUrl;
token += "?sessionId=" + sessionId; token += "?sessionId=" + sessionId;
token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase() token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase()
@ -51,8 +54,13 @@ public class TokenGenerator {
if (this.openviduConfig.isTurnadminAvailable()) { if (this.openviduConfig.isTurnadminAvailable()) {
turnCredentials = coturnCredentialsService.createUser(); turnCredentials = coturnCredentialsService.createUser();
} }
ConnectionProperties connectionProperties = new ConnectionProperties.Builder().type(ConnectionType.WEBRTC) ConnectionProperties.Builder connectionPropertiesBuilder = new ConnectionProperties.Builder()
.data(serverMetadata).record(record).role(role).kurentoOptions(kurentoOptions).build(); .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); return new Token(token, sessionId, connectionProperties, turnCredentials);
} }

View File

@ -21,11 +21,13 @@ import java.net.MalformedURLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import io.openvidu.java.client.*;
import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -51,17 +53,7 @@ import com.google.gson.JsonParser;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements; 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.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.config.OpenviduConfig;
import io.openvidu.server.core.EndReason; import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.IdentifierPrefixes;
@ -662,7 +654,7 @@ public class SessionRestController {
try { try {
Token token = sessionManager.newToken(session, connectionProperties.getRole(), Token token = sessionManager.newToken(session, connectionProperties.getRole(),
connectionProperties.getData(), connectionProperties.record(), connectionProperties.getData(), connectionProperties.record(),
connectionProperties.getKurentoOptions()); connectionProperties.getKurentoOptions(), connectionProperties.getCustomIceServers());
return new ResponseEntity<>(token.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(), return new ResponseEntity<>(token.toJsonAsParticipant().toString(), RestUtils.getResponseHeaders(),
HttpStatus.OK); HttpStatus.OK);
} catch (Exception e) { } 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 // Build WEBRTC options
builder.role(role).kurentoOptions(kurentoOptions); builder.role(role).kurentoOptions(kurentoOptions);
@ -939,6 +966,8 @@ public class SessionRestController {
.onlyPlayWithSubscribers(onlyPlayWithSubscribers).networkCache(networkCache).build(); .onlyPlayWithSubscribers(onlyPlayWithSubscribers).networkCache(networkCache).build();
} }
return builder; 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"; 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_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_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':[]}"; 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':[]}"; 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':{}}"; 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"; 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.function.BiFunction;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.google.gson.*;
import io.openvidu.java.client.*;
import org.apache.http.HttpStatus; import org.apache.http.HttpStatus;
import org.junit.Assert; import org.junit.Assert;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
@ -49,35 +51,15 @@ import org.openqa.selenium.Dimension;
import org.openqa.selenium.Keys; import org.openqa.selenium.Keys;
import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.ExpectedConditions;
import org.springframework.test.context.junit.jupiter.SpringExtension; 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 com.mashape.unirest.http.HttpMethod;
import io.appium.java_client.AppiumDriver; 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.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.BrowserUser;
import io.openvidu.test.browsers.utils.BrowserNames; import io.openvidu.test.browsers.utils.BrowserNames;
import io.openvidu.test.browsers.utils.CustomHttpClient; import io.openvidu.test.browsers.utils.CustomHttpClient;
@ -3002,6 +2984,20 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
body = "{'type':'WEBRTC','role':'MODERATOR','data':true}"; body = "{'type':'WEBRTC','role':'MODERATOR','data':true}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body, restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", body,
HttpStatus.SC_BAD_REQUEST); 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 // 200
String kurentoOpts = "'kurentoOptions':{'videoMaxSendBandwidth':777,'allowedFilters':['GStreamerFilter']}"; String kurentoOpts = "'kurentoOptions':{'videoMaxSendBandwidth':777,'allowedFilters':['GStreamerFilter']}";
body = "{'type':'WEBRTC','role':'MODERATOR','data':'SERVER_DATA'," + kurentoOpts + "}"; body = "{'type':'WEBRTC','role':'MODERATOR','data':'SERVER_DATA'," + kurentoOpts + "}";
@ -3011,6 +3007,18 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
restClient.rest(HttpMethod.DELETE, restClient.rest(HttpMethod.DELETE,
"/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + res.get("id").getAsString(), "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + res.get("id").getAsString(),
HttpStatus.SC_NO_CONTENT); 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 // Default values
res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", "{}", res = restClient.rest(HttpMethod.POST, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection", "{}",
HttpStatus.SC_OK); HttpStatus.SC_OK);
@ -4373,7 +4381,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
connectionJson.get("subscribers").getAsJsonArray().size() == 0); connectionJson.get("subscribers").getAsJsonArray().size() == 0);
Assert.assertTrue("Wrong token Connection property", Assert.assertTrue("Wrong token Connection property",
connectionJson.get("token").getAsString().contains(session.getSessionId())); 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(), Assert.assertEquals("Wrong type property", ConnectionType.WEBRTC.name(),
connectionProperties.get("type").getAsString()); connectionProperties.get("type").getAsString());
Assert.assertEquals("Wrong data property", "MY_SERVER_DATA", connectionProperties.get("data").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()); Assert.assertFalse("Java fetch should be false", OV.fetch());
checkNodeFetchChanged(user, false, true); checkNodeFetchChanged(user, false, true);
checkNodeFetchChanged(user, false, false); 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); 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 @Test

View File

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

View File

@ -39,3 +39,12 @@ mat-dialog-content button {
top: 0; top: 0;
right: 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"> <input matInput id="connection-data-field" placeholder="data" [(ngModel)]="connectionProperties.data">
</mat-form-field> </mat-form-field>
</div> </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> <div>
<button mat-button id="crate-connection-api-btn" (click)="createConnection()">Create connection</button> <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 <button mat-button id="update-connection-api-btn" (click)="updateConnection()" [disabled]="!connectionId">Update

View File

@ -23,6 +23,8 @@ export class SessionApiDialogComponent {
customLayout = ''; customLayout = '';
recPropertiesIcon = 'add_circle'; recPropertiesIcon = 'add_circle';
showRecProperties = false; showRecProperties = false;
numCustomIceServers = 0;
configuredCustomIceServers = []
connectionProperties: ConnectionProperties = { connectionProperties: ConnectionProperties = {
record: true, record: true,
@ -205,6 +207,7 @@ export class SessionApiDialogComponent {
createConnection() { createConnection() {
console.log('Creating connection'); console.log('Creating connection');
this.connectionProperties.customIceServers = this.configuredCustomIceServers;
this.session.createConnection(this.connectionProperties) this.session.createConnection(this.connectionProperties)
.then(connection => { .then(connection => {
this.response = 'Connection created: ' + JSON.stringify(connection); this.response = 'Connection created: ' + JSON.stringify(connection);
@ -238,4 +241,23 @@ export class SessionApiDialogComponent {
this.recPropertiesIcon = this.showRecProperties ? 'remove_circle' : 'add_circle'; 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()"> <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> <mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button> </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>
<div class="bottom-div"> <div class="bottom-div">
<button class="video-btn pub-btn" title="Publish/Unpublish" (click)="pubUnpub()"> <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()"> <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> <mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button> </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>
<div class="bottom-div"> <div class="bottom-div">
<button class="video-btn sub-btn" title="Subscribe/Unsubscribe" (click)="subUnsub()"> <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 { ExtensionDialogComponent } from '../dialogs/extension-dialog/extension-dialog.component';
import { FilterDialogComponent } from '../dialogs/filter-dialog/filter-dialog.component'; import { FilterDialogComponent } from '../dialogs/filter-dialog/filter-dialog.component';
import { OpenViduEvent } from '../openvidu-instance/openvidu-instance.component'; import { OpenViduEvent } from '../openvidu-instance/openvidu-instance.component';
import { ShowIceServerConfiguredDialog } from '../dialogs/show-configured-ice/show-configured-ice.component';
@Component({ @Component({
selector: 'app-video', 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() { async showCodecUsed() {
let stats = await this.streamManager.stream.getWebRtcPeer().pc.getStats(); let stats = await this.streamManager.stream.getWebRtcPeer().pc.getStats();
let codecIdIndex = null; let codecIdIndex = null;