Move RecordingProperties#fromJson from openvidu-server to openvidu-java-client

pull/739/head
pabloFuente 2022-06-13 18:20:37 +02:00
parent 7e8b6adaad
commit 735d4a96bd
17 changed files with 403 additions and 445 deletions

View File

@ -17,6 +17,9 @@
package io.openvidu.java.client; package io.openvidu.java.client;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
@ -120,7 +123,9 @@ public class Recording {
} }
this.status = Recording.Status.valueOf(json.get("status").getAsString()); this.status = Recording.Status.valueOf(json.get("status").getAsString());
this.recordingProperties = RecordingProperties.fromJson(json); RecordingProperties.Builder builder = RecordingProperties
.fromJson(new Gson().fromJson(json.toString(), Map.class), null);
this.recordingProperties = builder.build();
} }
/** /**
@ -233,9 +238,9 @@ public class Recording {
* URL of the recording. You can access the file from there. It is * URL of the recording. You can access the file from there. It is
* <code>null</code> until recording reaches "ready" or "failed" status. If * <code>null</code> until recording reaches "ready" or "failed" status. If
* <a href="https://docs.openvidu.io/en/stable/reference-docs/openvidu-config/"> * <a href="https://docs.openvidu.io/en/stable/reference-docs/openvidu-config/">
* OpenVidu Server configuration * OpenVidu Server configuration </a> property
* </a> property <code>OPENVIDU_RECORDING_PUBLIC_ACCESS</code> is false, * <code>OPENVIDU_RECORDING_PUBLIC_ACCESS</code> is false, this path will be
* this path will be secured with OpenVidu credentials * secured with OpenVidu credentials
*/ */
public String getUrl() { public String getUrl() {
return url; return url;

View File

@ -17,9 +17,12 @@
package io.openvidu.java.client; package io.openvidu.java.client;
import java.util.Map;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.OutputMode; import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.utils.FormatChecker;
/** /**
* See * See
@ -28,6 +31,7 @@ import io.openvidu.java.client.Recording.OutputMode;
public class RecordingProperties { public class RecordingProperties {
public static class DefaultValues { public static class DefaultValues {
public static final String name = "";
public static final Boolean hasAudio = true; public static final Boolean hasAudio = true;
public static final Boolean hasVideo = true; public static final Boolean hasVideo = true;
public static final Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED; public static final Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED;
@ -35,14 +39,15 @@ public class RecordingProperties {
public static final String resolution = "1280x720"; public static final String resolution = "1280x720";
public static final Integer frameRate = 25; public static final Integer frameRate = 25;
public static final Long shmSize = 536870912L; public static final Long shmSize = 536870912L;
public static final String customLayout = "";
public static final Boolean ignoreFailedStreams = false; public static final Boolean ignoreFailedStreams = false;
} }
// For all // For all
private String name = ""; private String name = DefaultValues.name;
private Boolean hasAudio = true; private Boolean hasAudio = DefaultValues.hasAudio;
private Boolean hasVideo = true; private Boolean hasVideo = DefaultValues.hasVideo;
private Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED; private Recording.OutputMode outputMode = DefaultValues.outputMode;
// For COMPOSED/COMPOSED_QUICK_START + hasVideo // For COMPOSED/COMPOSED_QUICK_START + hasVideo
private RecordingLayout recordingLayout; private RecordingLayout recordingLayout;
private String resolution; private String resolution;
@ -60,15 +65,15 @@ public class RecordingProperties {
*/ */
public static class Builder { public static class Builder {
private String name = ""; private String name = DefaultValues.name;
private Boolean hasAudio = DefaultValues.hasAudio; private Boolean hasAudio = DefaultValues.hasAudio;
private Boolean hasVideo = DefaultValues.hasVideo; private Boolean hasVideo = DefaultValues.hasVideo;
private Recording.OutputMode outputMode = DefaultValues.outputMode; private Recording.OutputMode outputMode = DefaultValues.outputMode;
private RecordingLayout recordingLayout; private RecordingLayout recordingLayout = DefaultValues.recordingLayout;
private String resolution; private String resolution = DefaultValues.resolution;
private Integer frameRate; private Integer frameRate = DefaultValues.frameRate;
private Long shmSize; private Long shmSize = DefaultValues.shmSize;
private String customLayout; private String customLayout = DefaultValues.customLayout;
private Boolean ignoreFailedStreams = DefaultValues.ignoreFailedStreams; private Boolean ignoreFailedStreams = DefaultValues.ignoreFailedStreams;
private String mediaNode; private String mediaNode;
@ -202,9 +207,9 @@ public class RecordingProperties {
* to {@link io.openvidu.java.client.RecordingLayout#CUSTOM} you can call this * to {@link io.openvidu.java.client.RecordingLayout#CUSTOM} you can call this
* method to set the relative path to the specific custom layout you want to * method to set the relative path to the specific custom layout you want to
* use.<br> * use.<br>
* See <a href="https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"> * See <a href=
* Custom recording layouts * "https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts">
* </a> to learn more * Custom recording layouts </a> to learn more
*/ */
public RecordingProperties.Builder customLayout(String path) { public RecordingProperties.Builder customLayout(String path) {
this.customLayout = path; this.customLayout = path;
@ -234,13 +239,13 @@ public class RecordingProperties {
} }
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" style="display:
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * inline-block; background-color: rgb(0, 136, 170); color: white; font-weight:
* white; font-weight: bold; padding: 0px 5px; margin-right: 5px; border-radius: * bold; padding: 0px 5px; margin-right: 5px; border-radius: 3px; font-size:
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat, * 13px; line-height:21px; font-family: Montserrat, sans-serif">PRO</a> Call
* sans-serif">PRO</a> Call this method to force the recording to be hosted in * this method to force the recording to be hosted in the Media Node with
* the Media Node with identifier <code>mediaNodeId</code>. This property only * identifier <code>mediaNodeId</code>. This property only applies to
* applies to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START} * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true and is ignored * recordings with {@link RecordingProperties#hasVideo()} to true and is ignored
* for {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} * for {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL}
@ -257,7 +262,7 @@ public class RecordingProperties {
protected RecordingProperties(String name, Boolean hasAudio, Boolean hasVideo, Recording.OutputMode outputMode, protected RecordingProperties(String name, Boolean hasAudio, Boolean hasVideo, Recording.OutputMode outputMode,
RecordingLayout layout, String resolution, Integer frameRate, Long shmSize, String customLayout, RecordingLayout layout, String resolution, Integer frameRate, Long shmSize, String customLayout,
Boolean ignoreFailedStreams, String mediaNode) { Boolean ignoreFailedStreams, String mediaNode) {
this.name = name != null ? name : ""; this.name = name != null ? name : DefaultValues.name;
this.hasAudio = hasAudio != null ? hasAudio : DefaultValues.hasAudio; this.hasAudio = hasAudio != null ? hasAudio : DefaultValues.hasAudio;
this.hasVideo = hasVideo != null ? hasVideo : DefaultValues.hasVideo; this.hasVideo = hasVideo != null ? hasVideo : DefaultValues.hasVideo;
this.outputMode = outputMode != null ? outputMode : DefaultValues.outputMode; this.outputMode = outputMode != null ? outputMode : DefaultValues.outputMode;
@ -390,9 +395,9 @@ public class RecordingProperties {
* If {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} is * If {@link io.openvidu.java.client.RecordingProperties#recordingLayout()} is
* set to {@link io.openvidu.java.client.RecordingLayout#CUSTOM}, this property * set to {@link io.openvidu.java.client.RecordingLayout#CUSTOM}, this property
* defines the relative path to the specific custom layout you want to use.<br> * defines the relative path to the specific custom layout you want to use.<br>
* See <a href="https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts"> * See <a href=
* Custom recording layouts * "https://docs.openvidu.io/en/stable/advanced-features/recording#custom-recording-layouts">
* </a> to learn more * Custom recording layouts </a> to learn more
*/ */
public String customLayout() { public String customLayout() {
return this.customLayout; return this.customLayout;
@ -423,15 +428,15 @@ public class RecordingProperties {
} }
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" style="display:
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * inline-block; background-color: rgb(0, 136, 170); color: white; font-weight:
* white; font-weight: bold; padding: 0px 5px; margin-right: 5px; border-radius: * bold; padding: 0px 5px; margin-right: 5px; border-radius: 3px; font-size:
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat, * 13px; line-height:21px; font-family: Montserrat, sans-serif">PRO</a> The
* sans-serif">PRO</a> The Media Node where to host the recording. The default * Media Node where to host the recording. The default option if this property
* option if this property is not defined is the same Media Node hosting the * is not defined is the same Media Node hosting the Session to record. This
* Session to record. This property only applies to COMPOSED or * property only applies to COMPOSED or COMPOSED_QUICK_START recordings with
* COMPOSED_QUICK_START recordings with {@link RecordingProperties#hasVideo()} * {@link RecordingProperties#hasVideo()} to true and is ignored for INDIVIDUAL
* to true and is ignored for INDIVIDUAL recordings and audio-only recordings * recordings and audio-only recordings
*/ */
public String mediaNode() { public String mediaNode() {
return this.mediaNode; return this.mediaNode;
@ -442,7 +447,7 @@ public class RecordingProperties {
*/ */
public JsonObject toJson() { public JsonObject toJson() {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("name", name); json.addProperty("name", name != null ? name : DefaultValues.name);
json.addProperty("hasAudio", hasAudio != null ? hasAudio : DefaultValues.hasAudio); json.addProperty("hasAudio", hasAudio != null ? hasAudio : DefaultValues.hasAudio);
json.addProperty("hasVideo", hasVideo != null ? hasVideo : DefaultValues.hasVideo); json.addProperty("hasVideo", hasVideo != null ? hasVideo : DefaultValues.hasVideo);
json.addProperty("outputMode", outputMode != null ? outputMode.name() : DefaultValues.outputMode.name()); json.addProperty("outputMode", outputMode != null ? outputMode.name() : DefaultValues.outputMode.name());
@ -454,7 +459,7 @@ public class RecordingProperties {
json.addProperty("frameRate", frameRate != null ? frameRate : DefaultValues.frameRate); json.addProperty("frameRate", frameRate != null ? frameRate : DefaultValues.frameRate);
json.addProperty("shmSize", shmSize != null ? shmSize : DefaultValues.shmSize); json.addProperty("shmSize", shmSize != null ? shmSize : DefaultValues.shmSize);
if (RecordingLayout.CUSTOM.equals(recordingLayout)) { if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
json.addProperty("customLayout", customLayout != null ? customLayout : ""); json.addProperty("customLayout", customLayout != null ? customLayout : DefaultValues.customLayout);
} }
} }
if (OutputMode.INDIVIDUAL.equals(outputMode)) { if (OutputMode.INDIVIDUAL.equals(outputMode)) {
@ -467,69 +472,226 @@ public class RecordingProperties {
return json; return json;
} }
public static RecordingProperties.Builder fromJson(Map<String, ?> params, RecordingProperties defaultProps)
throws RuntimeException {
// Final properties being used
String nameFinal = null;
Boolean hasAudioFinal = null;
Boolean hasVideoFinal = null;
OutputMode outputModeFinal = null;
RecordingLayout recordingLayoutFinal = null;
String resolutionFinal = null;
Integer frameRateFinal = null;
Long shmSizeFinal = null;
String customLayoutFinal = null;
Boolean ignoreFailedStreamsFinal = null;
// Defaults properties
String nameDefault = null;
Boolean hasAudioDefault = null;
Boolean hasVideoDefault = null;
OutputMode outputModeDefault = null;
RecordingLayout recordingLayoutDefault = null;
String resolutionDefault = null;
Integer frameRateDefault = null;
Long shmSizeDefault = null;
String customLayoutDefault = null;
Boolean ignoreFailedStreamsDefault = null;
String mediaNodeDefault = null;
if (defaultProps != null) {
nameDefault = defaultProps.name();
hasAudioDefault = defaultProps.hasAudio();
hasVideoDefault = defaultProps.hasVideo();
outputModeDefault = defaultProps.outputMode();
recordingLayoutDefault = defaultProps.recordingLayout();
resolutionDefault = defaultProps.resolution();
frameRateDefault = defaultProps.frameRate();
shmSizeDefault = defaultProps.shmSize();
customLayoutDefault = defaultProps.customLayout();
ignoreFailedStreamsDefault = defaultProps.ignoreFailedStreams();
mediaNodeDefault = defaultProps.mediaNode();
}
nameDefault = nameDefault != null ? nameDefault : DefaultValues.name;
hasAudioDefault = hasAudioDefault != null ? hasAudioDefault : DefaultValues.hasAudio;
hasVideoDefault = hasVideoDefault != null ? hasVideoDefault : DefaultValues.hasVideo;
outputModeDefault = outputModeDefault != null ? outputModeDefault : DefaultValues.outputMode;
recordingLayoutDefault = recordingLayoutDefault != null ? recordingLayoutDefault
: DefaultValues.recordingLayout;
resolutionDefault = resolutionDefault != null ? resolutionDefault : DefaultValues.resolution;
frameRateDefault = frameRateDefault != null ? frameRateDefault : DefaultValues.frameRate;
shmSizeDefault = shmSizeDefault != null ? shmSizeDefault : DefaultValues.shmSize;
customLayoutDefault = customLayoutDefault != null ? customLayoutDefault : DefaultValues.customLayout;
ignoreFailedStreamsDefault = ignoreFailedStreamsDefault != null ? ignoreFailedStreamsDefault
: DefaultValues.ignoreFailedStreams;
// Provided properties through params
String nameParam;
Boolean hasAudioParam;
Boolean hasVideoParam;
String outputModeStringParam;
String recordingLayoutStringParam;
String resolutionParam;
Number frameRateParam;
Long shmSizeParam = null;
String customLayoutParam;
Boolean ignoreFailedStreamsParam;
try {
nameParam = (String) params.get("name");
hasAudioParam = (Boolean) params.get("hasAudio");
hasVideoParam = (Boolean) params.get("hasVideo");
outputModeStringParam = (String) params.get("outputMode");
recordingLayoutStringParam = (String) params.get("recordingLayout");
resolutionParam = (String) params.get("resolution");
frameRateParam = (Number) params.get("frameRate");
if (params.get("shmSize") != null) {
Number shmSize = (Number) params.get("shmSize");
shmSizeParam = shmSize.longValue();
}
customLayoutParam = (String) params.get("customLayout");
ignoreFailedStreamsParam = (Boolean) params.get("ignoreFailedStreams");
} catch (ClassCastException | NumberFormatException e) {
throw new IllegalArgumentException("Type error in some parameter: " + e.getMessage());
}
if (nameParam != null && !nameParam.isEmpty()) {
if (!FormatChecker.isValidRecordingName(nameParam)) {
throw new IllegalArgumentException(
"Parameter 'name' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-~]+");
}
nameFinal = nameParam;
} else {
nameFinal = nameDefault;
}
if (hasAudioParam != null) {
hasAudioFinal = hasAudioParam;
} else {
hasAudioFinal = hasAudioDefault;
}
if (hasVideoParam != null) {
hasVideoFinal = hasVideoParam;
} else {
hasVideoFinal = hasVideoDefault;
}
if (!hasAudioFinal && !hasVideoFinal) {
// Cannot start a recording with both "hasAudio" and "hasVideo" to false
throw new IllegalStateException(
"Cannot start a recording with both \"hasAudio\" and \"hasVideo\" set to false");
}
if (outputModeStringParam != null) {
try {
outputModeFinal = OutputMode.valueOf(outputModeStringParam);
// If param outputMode is COMPOSED when default is COMPOSED_QUICK_START,
// change outputMode to COMPOSED_QUICK_START (and vice versa)
if (defaultProps != null && OutputMode.COMPOSED_QUICK_START.equals(defaultProps.outputMode())
&& OutputMode.COMPOSED.equals(outputModeFinal)) {
outputModeFinal = OutputMode.COMPOSED_QUICK_START;
} else if (defaultProps != null && OutputMode.COMPOSED.equals(defaultProps.outputMode())
&& OutputMode.COMPOSED_QUICK_START.equals(outputModeFinal)) {
outputModeFinal = OutputMode.COMPOSED;
}
} catch (Exception e) {
throw new IllegalArgumentException("Type error in parameter 'outputMode'");
}
} else {
outputModeFinal = outputModeDefault;
}
if (IS_COMPOSED(outputModeFinal)) {
if (recordingLayoutStringParam != null) {
try {
recordingLayoutFinal = RecordingLayout.valueOf(recordingLayoutStringParam);
} catch (Exception e) {
throw new IllegalArgumentException("Type error in parameter 'recordingLayout'");
}
} else {
recordingLayoutFinal = recordingLayoutDefault;
}
if (resolutionParam != null) {
if (!FormatChecker.isAcceptableRecordingResolution(resolutionParam)) {
throw new IllegalStateException(
"Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height");
}
resolutionFinal = resolutionParam;
} else {
resolutionFinal = resolutionDefault;
}
if (frameRateParam != null) {
if (!FormatChecker.isAcceptableRecordingFrameRate(frameRateParam.intValue())) {
throw new IllegalStateException(
"Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height");
}
frameRateFinal = frameRateParam.intValue();
} else {
frameRateFinal = frameRateDefault;
}
if (shmSizeParam != null) {
if (!FormatChecker.isAcceptableRecordingShmSize(shmSizeParam)) {
throw new IllegalStateException("Wrong \"shmSize\" parameter. Must be 134217728 (128 MB) minimum");
}
shmSizeFinal = shmSizeParam;
} else {
shmSizeFinal = shmSizeDefault;
}
if (RecordingLayout.CUSTOM.equals(recordingLayoutFinal)) {
if (customLayoutParam != null) {
customLayoutFinal = customLayoutParam;
} else {
customLayoutFinal = customLayoutDefault;
}
}
} else if (OutputMode.INDIVIDUAL.equals(outputModeFinal)) {
if (ignoreFailedStreamsParam != null) {
ignoreFailedStreamsFinal = ignoreFailedStreamsParam;
} else {
ignoreFailedStreamsFinal = ignoreFailedStreamsDefault;
}
}
RecordingProperties.Builder builder = new RecordingProperties.Builder();
builder.name(nameFinal).hasAudio(hasAudioFinal).hasVideo(hasVideoFinal).outputMode(outputModeFinal);
if (IS_COMPOSED(outputModeFinal) && hasVideoFinal) {
builder.recordingLayout(recordingLayoutFinal);
builder.resolution(resolutionFinal);
builder.frameRate(frameRateFinal);
builder.shmSize(shmSizeFinal);
if (RecordingLayout.CUSTOM.equals(recordingLayoutFinal)) {
builder.customLayout(customLayoutFinal);
}
}
if (OutputMode.INDIVIDUAL.equals(outputModeFinal)) {
builder.ignoreFailedStreams(ignoreFailedStreamsFinal);
}
if (mediaNodeDefault == null) {
mediaNodeDefault = SessionProperties.getMediaNodeProperty(params);
}
if (mediaNodeDefault != null && !mediaNodeDefault.isEmpty()) {
builder.mediaNode = mediaNodeDefault;
}
return builder;
}
/** /**
* @hidden * @hidden
*/ */
public static RecordingProperties fromJson(JsonObject json) { public final static boolean IS_COMPOSED(OutputMode outputMode) {
return (OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode));
Boolean hasVideoAux = true;
Recording.OutputMode outputModeAux = null;
RecordingLayout recordingLayoutAux = null;
Builder builder = new RecordingProperties.Builder();
if (json.has("name")) {
builder.name(json.get("name").getAsString());
}
if (json.has("hasAudio")) {
builder.hasAudio(json.get("hasAudio").getAsBoolean());
}
if (json.has("hasVideo")) {
hasVideoAux = json.get("hasVideo").getAsBoolean();
builder.hasVideo(hasVideoAux);
}
if (json.has("outputMode")) {
outputModeAux = OutputMode.valueOf(json.get("outputMode").getAsString());
} else {
outputModeAux = DefaultValues.outputMode;
}
builder.outputMode(outputModeAux);
if ((OutputMode.COMPOSED.equals(outputModeAux) || OutputMode.COMPOSED_QUICK_START.equals(outputModeAux))
&& hasVideoAux) {
if (json.has("recordingLayout")) {
recordingLayoutAux = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
builder.recordingLayout(recordingLayoutAux);
}
if (json.has("resolution")) {
builder.resolution(json.get("resolution").getAsString());
}
if (json.has("frameRate")) {
builder.frameRate(json.get("frameRate").getAsInt());
}
if (json.has("shmSize")) {
builder.shmSize(json.get("shmSize").getAsLong());
}
if (RecordingLayout.CUSTOM.equals(recordingLayoutAux)) {
if (json.has("customLayout")) {
builder.customLayout(json.get("customLayout").getAsString());
}
}
}
if (json.has("ignoreFailedStreams") && OutputMode.INDIVIDUAL.equals(outputModeAux)) {
builder.ignoreFailedStreams(json.get("ignoreFailedStreams").getAsBoolean());
}
if (json.has("mediaNode")) {
String mediaNodeId = null;
if (json.get("mediaNode").isJsonObject()) {
mediaNodeId = json.get("mediaNode").getAsJsonObject().get("id").getAsString();
} else if (json.get("mediaNode").isJsonPrimitive()) {
mediaNodeId = json.get("mediaNode").getAsString();
}
if (mediaNodeId != null && !mediaNodeId.isEmpty()) {
builder.mediaNode(mediaNodeId);
}
}
return builder.build();
} }
} }

View File

@ -671,8 +671,7 @@ public class Session {
.recordingMode(properties.recordingMode()) .recordingMode(properties.recordingMode())
.defaultRecordingProperties(properties.defaultRecordingProperties()) .defaultRecordingProperties(properties.defaultRecordingProperties())
.customSessionId(properties.customSessionId()).mediaNode(properties.mediaNode()) .customSessionId(properties.customSessionId()).mediaNode(properties.mediaNode())
.forcedVideoCodec(forcedVideoCodec) .forcedVideoCodec(forcedVideoCodec).allowTranscoding(allowTranscoding).build();
.allowTranscoding(allowTranscoding).build();
this.properties = responseProperties; this.properties = responseProperties;
log.info("Session '{}' created", this.sessionId); log.info("Session '{}' created", this.sessionId);
@ -715,8 +714,10 @@ public class Session {
.mediaMode(MediaMode.valueOf(json.get("mediaMode").getAsString())) .mediaMode(MediaMode.valueOf(json.get("mediaMode").getAsString()))
.recordingMode(RecordingMode.valueOf(json.get("recordingMode").getAsString())); .recordingMode(RecordingMode.valueOf(json.get("recordingMode").getAsString()));
if (json.has("defaultRecordingProperties")) { if (json.has("defaultRecordingProperties")) {
builder.defaultRecordingProperties( String jsonString = json.get("defaultRecordingProperties").getAsJsonObject().toString();
RecordingProperties.fromJson(json.get("defaultRecordingProperties").getAsJsonObject())); RecordingProperties.Builder recBuilder = RecordingProperties
.fromJson(new Gson().fromJson(jsonString, Map.class), null);
builder.defaultRecordingProperties(recBuilder.build());
} }
if (json.has("customSessionId")) { if (json.has("customSessionId")) {
builder.customSessionId(json.get("customSessionId").getAsString()); builder.customSessionId(json.get("customSessionId").getAsString());

View File

@ -17,11 +17,14 @@
package io.openvidu.java.client; package io.openvidu.java.client;
import java.lang.reflect.Type;
import java.util.Map; import java.util.Map;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
/** /**
* See {@link io.openvidu.java.client.OpenVidu#createSession(SessionProperties)} * See {@link io.openvidu.java.client.OpenVidu#createSession(SessionProperties)}
@ -108,12 +111,12 @@ public class SessionProperties {
} }
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" style="display:
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * inline-block; background-color: rgb(0, 136, 170); color: white; font-weight:
* white; font-weight: bold; padding: 0px 5px; margin-right: 5px; border-radius: * bold; padding: 0px 5px; margin-right: 5px; border-radius: 3px; font-size:
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat, * 13px; line-height:21px; font-family: Montserrat, sans-serif">PRO</a> Call
* sans-serif">PRO</a> Call this method to force the session to be hosted in the * this method to force the session to be hosted in the Media Node with
* Media Node with identifier <code>mediaNodeId</code> * identifier <code>mediaNodeId</code>
*/ */
public SessionProperties.Builder mediaNode(String mediaNodeId) { public SessionProperties.Builder mediaNode(String mediaNodeId) {
this.mediaNode = mediaNodeId; this.mediaNode = mediaNodeId;
@ -121,14 +124,14 @@ public class SessionProperties {
} }
/** /**
* Define which video codec will be forcibly used for this session. * Define which video codec will be forcibly used for this session. This forces
* This forces all browsers/clients to use the same codec, which would * all browsers/clients to use the same codec, which would avoid transcoding in
* avoid transcoding in the media server (Kurento only). If * the media server (Kurento only). If <code>forcedVideoCodec</code> is set to
* <code>forcedVideoCodec</code> is set to NONE, no codec will be forced. * NONE, no codec will be forced.
* *
* If the browser/client is not compatible with the specified codec, and * If the browser/client is not compatible with the specified codec, and
* {@link #allowTranscoding(Boolean)} is <code>false</code>, an * {@link #allowTranscoding(Boolean)} is <code>false</code>, an exception will
* exception will occur. * occur.
* *
* If defined here, this parameter has prevalence over * If defined here, this parameter has prevalence over
* OPENVIDU_STREAMS_FORCED_VIDEO_CODEC. * OPENVIDU_STREAMS_FORCED_VIDEO_CODEC.
@ -141,11 +144,11 @@ public class SessionProperties {
} }
/** /**
* Actual video codec that will be forcibly used for this session. * Actual video codec that will be forcibly used for this session. This is the
* This is the same as <code>forcedVideoCodec</code>, except when its * same as <code>forcedVideoCodec</code>, except when its value is
* value is {@link VideoCodec#MEDIA_SERVER_PREFERRED}: in that case, * {@link VideoCodec#MEDIA_SERVER_PREFERRED}: in that case, OpenVidu Server will
* OpenVidu Server will fill this property with a resolved value, * fill this property with a resolved value, depending on what is the configured
* depending on what is the configured media server. * media server.
*/ */
public SessionProperties.Builder forcedVideoCodecResolved(VideoCodec forcedVideoCodec) { public SessionProperties.Builder forcedVideoCodecResolved(VideoCodec forcedVideoCodec) {
this.forcedVideoCodecResolved = forcedVideoCodec; this.forcedVideoCodecResolved = forcedVideoCodec;
@ -228,13 +231,13 @@ public class SessionProperties {
} }
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" style="display:
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * inline-block; background-color: rgb(0, 136, 170); color: white; font-weight:
* white; font-weight: bold; padding: 0px 5px; margin-right: 5px; border-radius: * bold; padding: 0px 5px; margin-right: 5px; border-radius: 3px; font-size:
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat, * 13px; line-height:21px; font-family: Montserrat, sans-serif">PRO</a> The
* sans-serif">PRO</a> The Media Node where to host the session. The default * Media Node where to host the session. The default option if this property is
* option if this property is not defined is the less loaded Media Node at the * not defined is the less loaded Media Node at the moment the first user joins
* moment the first user joins the session. * the session.
*/ */
public String mediaNode() { public String mediaNode() {
return this.mediaNode; return this.mediaNode;
@ -253,12 +256,12 @@ public class SessionProperties {
* Defines which video codec is being forced to be used in the browser/client. * Defines which video codec is being forced to be used in the browser/client.
* This is the resolved value, for actual usage in the server. * This is the resolved value, for actual usage in the server.
* *
* This is a server-only property, and as such, it doesn't need to be transmitted * This is a server-only property, and as such, it doesn't need to be
* over the wire between server and client. Thus it doesn't get serialized in * transmitted over the wire between server and client. Thus it doesn't get
* the `toJson()` method. * serialized in the `toJson()` method.
* *
* If more server-only properties start to appear here, maybe a good idea * If more server-only properties start to appear here, maybe a good idea would
* would be to refactor them all into a server-specific Properties class. * be to refactor them all into a server-specific Properties class.
* *
* @hidden * @hidden
*/ */
@ -377,8 +380,11 @@ public class SessionProperties {
} }
if (defaultRecordingPropertiesJson != null) { if (defaultRecordingPropertiesJson != null) {
try { try {
RecordingProperties defaultRecordingProperties = RecordingProperties
.fromJson(defaultRecordingPropertiesJson); String jsonString = defaultRecordingPropertiesJson.toString();
RecordingProperties.Builder recBuilder = RecordingProperties
.fromJson(new Gson().fromJson(jsonString, Map.class), null);
RecordingProperties defaultRecordingProperties = recBuilder.build();
builder = builder.defaultRecordingProperties(defaultRecordingProperties); builder = builder.defaultRecordingProperties(defaultRecordingProperties);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException( throw new IllegalArgumentException(
@ -408,8 +414,17 @@ public class SessionProperties {
JsonObject mediaNodeJson; JsonObject mediaNodeJson;
try { try {
mediaNodeJson = JsonParser.parseString(params.get("mediaNode").toString()).getAsJsonObject(); mediaNodeJson = JsonParser.parseString(params.get("mediaNode").toString()).getAsJsonObject();
} catch (Exception e) { } catch (JsonSyntaxException e) {
throw new IllegalArgumentException("Error in parameter 'mediaNode'. It is not a valid JSON object"); try {
Gson gson = new Gson();
Type gsonType = new TypeToken<Map>() {
}.getType();
String gsonString = gson.toJson(params.get("mediaNode"), gsonType);
mediaNodeJson = JsonParser.parseString(gsonString).getAsJsonObject();
} catch (Exception e2) {
throw new IllegalArgumentException("Error in parameter 'mediaNode'. It is not a valid JSON object");
}
} }
if (!mediaNodeJson.has("id")) { if (!mediaNodeJson.has("id")) {
throw new IllegalArgumentException("Error in parameter 'mediaNode'. Property 'id' not found"); throw new IllegalArgumentException("Error in parameter 'mediaNode'. Property 'id' not found");

View File

@ -0,0 +1,36 @@
package io.openvidu.java.client.utils;
/**
* @hidden
*/
public final class FormatChecker {
public static boolean isAcceptableRecordingResolution(String stringResolution) {
// Matches every string with format "AxB", being A and B any number not starting
// with 0 and 3 digits long or 4 digits long if they start with 1
return stringResolution.matches("^(?!(0))(([0-9]{3})|1([0-9]{3}))x(?!0)(([0-9]{3})|1([0-9]{3}))$");
}
public static boolean isAcceptableRecordingFrameRate(Integer frameRate) {
// Integer greater than 0 and below 120
return (frameRate > 0 && frameRate <= 120);
}
public static boolean isAcceptableRecordingShmSize(Long shmSize) {
// Long grater than 134217728 (128 MB)
return (shmSize >= 134217728L);
}
public static boolean isServerMetadataFormatCorrect(String metadata) {
return true;
}
public static boolean isValidCustomSessionId(String customSessionId) {
return customSessionId.matches("[a-zA-Z0-9_\\-]+");
}
public static boolean isValidRecordingName(String recodingName) {
return recodingName.matches("[a-zA-Z0-9_\\-~]+");
}
}

View File

@ -1,19 +1,24 @@
package io.openvidu.server.test.unit; package io.openvidu.java.client.test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Test; import io.openvidu.java.client.utils.FormatChecker;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import io.openvidu.server.utils.FormatChecker; public class FormatCheckerTest extends TestCase {
public class FormatCheckerTest { public FormatCheckerTest(String testName) {
super(testName);
}
@Test public static Test suite() {
void customSessionIdFormatTest() { return new TestSuite(FormatCheckerTest.class);
}
public void testCustomSessionIdFormat() {
List<String> invalidCustomSessionIds = Arrays.asList("", "session#", "session!", "session*", "'session", List<String> invalidCustomSessionIds = Arrays.asList("", "session#", "session!", "session*", "'session",
"\"session", "sess(ion", "sess_ion)", "session:session", ";session;", "session@session", "$", "\"session", "sess(ion", "sess_ion)", "session:session", ";session;", "session@session", "$",
@ -24,40 +29,35 @@ public class FormatCheckerTest {
"0session10", "-session", "session-", "-session-", "_session", "session_", "_session_", "_-session", "0session10", "-session", "session-", "-session-", "_session", "session_", "_session_", "_-session",
"session-_", "123_session-1"); "session-_", "123_session-1");
FormatChecker formatChecker = new FormatChecker();
for (String id : invalidCustomSessionIds) for (String id : invalidCustomSessionIds)
assertFalse(formatChecker.isValidCustomSessionId(id)); assertFalse(FormatChecker.isValidCustomSessionId(id));
for (String id : validCustomSessionIds) for (String id : validCustomSessionIds)
assertTrue(formatChecker.isValidCustomSessionId(id)); assertTrue(FormatChecker.isValidCustomSessionId(id));
} }
@Test public void testAcceptableRecordingResolution() {
void acceptableRecordingResolutionTest() {
List<String> invalidResolutions = Arrays.asList("", "a", "123", "true", "AXB", "AxB", "12x", "x12", "0920x1080", List<String> invalidResolutions = Arrays.asList("", "a", "123", "true", "AXB", "AxB", "12x", "x12", "0920x1080",
"1080x0720", "720x2000", "99x720", "1920X1080"); "1080x0720", "720x2000", "99x720", "1920X1080");
List<String> validResolutions = Arrays.asList("1920x1080", "1280x720", "100x1999"); List<String> validResolutions = Arrays.asList("1920x1080", "1280x720", "100x1999");
FormatChecker formatChecker = new FormatChecker();
for (String resolution : invalidResolutions) for (String resolution : invalidResolutions)
assertFalse(formatChecker.isAcceptableRecordingResolution(resolution)); assertFalse(FormatChecker.isAcceptableRecordingResolution(resolution));
for (String resolution : validResolutions) for (String resolution : validResolutions)
assertTrue(formatChecker.isAcceptableRecordingResolution(resolution)); assertTrue(FormatChecker.isAcceptableRecordingResolution(resolution));
} }
@Test public void testAcceptableRecordingFrameRate() {
void acceptableRecordingFrameRateTest() {
List<Integer> invalidFrameRates = Arrays.asList(-1, 0, 121, 9999); List<Integer> invalidFrameRates = Arrays.asList(-1, 0, 121, 9999);
List<Integer> validFramerates = Arrays.asList(1, 2, 30, 60, 119, 120); List<Integer> validFramerates = Arrays.asList(1, 2, 30, 60, 119, 120);
FormatChecker formatChecker = new FormatChecker();
for (int framerate : invalidFrameRates) for (int framerate : invalidFrameRates)
assertFalse(formatChecker.isAcceptableRecordingFrameRate(framerate)); assertFalse(FormatChecker.isAcceptableRecordingFrameRate(framerate));
for (int framerate : validFramerates) for (int framerate : validFramerates)
assertTrue(formatChecker.isAcceptableRecordingFrameRate(framerate)); assertTrue(FormatChecker.isAcceptableRecordingFrameRate(framerate));
} }
} }

View File

@ -21,9 +21,9 @@ import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.Status; import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.core.EndReason; import io.openvidu.server.core.EndReason;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.utils.RecordingUtils;
public class CDREventRecordingStatusChanged extends CDREventEnd { public class CDREventRecordingStatusChanged extends CDREventEnd {
@ -44,7 +44,7 @@ public class CDREventRecordingStatusChanged extends CDREventEnd {
json.addProperty("id", this.recording.getId()); json.addProperty("id", this.recording.getId());
json.addProperty("name", this.recording.getName()); json.addProperty("name", this.recording.getName());
json.addProperty("outputMode", this.recording.getOutputMode().name()); json.addProperty("outputMode", this.recording.getOutputMode().name());
if (RecordingUtils.IS_COMPOSED(this.recording.getOutputMode()) && this.recording.hasVideo()) { if (RecordingProperties.IS_COMPOSED(this.recording.getOutputMode()) && this.recording.hasVideo()) {
json.addProperty("resolution", this.recording.getResolution()); json.addProperty("resolution", this.recording.getResolution());
json.addProperty("frameRate", this.recording.getFrameRate()); json.addProperty("frameRate", this.recording.getFrameRate());
json.addProperty("recordingLayout", this.recording.getRecordingLayout().name()); json.addProperty("recordingLayout", this.recording.getRecordingLayout().name());

View File

@ -53,13 +53,13 @@ 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.utils.FormatChecker;
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;
import io.openvidu.server.kurento.endpoint.EndpointType; import io.openvidu.server.kurento.endpoint.EndpointType;
import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.kurento.kms.Kms;
import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.recording.service.RecordingManager;
import io.openvidu.server.utils.FormatChecker;
import io.openvidu.server.utils.GeoLocation; import io.openvidu.server.utils.GeoLocation;
import io.openvidu.server.utils.GeoLocationByIp; import io.openvidu.server.utils.GeoLocationByIp;
import io.openvidu.server.utils.MediaNodeManager; import io.openvidu.server.utils.MediaNodeManager;
@ -93,8 +93,6 @@ public abstract class SessionManager {
@Autowired @Autowired
protected GeoLocationByIp geoLocationByIp; protected GeoLocationByIp geoLocationByIp;
public FormatChecker formatChecker = new FormatChecker();
private UpdatableTimerTask sessionGarbageCollectorTimer; private UpdatableTimerTask sessionGarbageCollectorTimer;
final protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>(); final protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
@ -332,7 +330,7 @@ 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, List<IceServerProperties> customIceServers) 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");
} }

View File

@ -39,7 +39,6 @@ import com.google.gson.JsonObject;
import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.core.MediaServer; import io.openvidu.server.core.MediaServer;
import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.kurento.core.KurentoSession;
import io.openvidu.server.utils.RecordingUtils;
import io.openvidu.server.utils.UpdatableTimerTask; import io.openvidu.server.utils.UpdatableTimerTask;
/** /**
@ -192,14 +191,14 @@ public class Kms {
public synchronized void incrementActiveRecordings(String sessionId, String recordingId, public synchronized void incrementActiveRecordings(String sessionId, String recordingId,
RecordingProperties properties) { RecordingProperties properties) {
this.activeRecordings.put(recordingId, sessionId); this.activeRecordings.put(recordingId, sessionId);
if (RecordingUtils.IS_COMPOSED(properties.outputMode())) { if (RecordingProperties.IS_COMPOSED(properties.outputMode())) {
this.activeComposedRecordings.incrementAndGet(); this.activeComposedRecordings.incrementAndGet();
} }
} }
public synchronized void decrementActiveRecordings(String recordingId, RecordingProperties properties) { public synchronized void decrementActiveRecordings(String recordingId, RecordingProperties properties) {
this.activeRecordings.remove(recordingId); this.activeRecordings.remove(recordingId);
if (RecordingUtils.IS_COMPOSED(properties.outputMode())) { if (RecordingProperties.IS_COMPOSED(properties.outputMode())) {
this.activeComposedRecordings.decrementAndGet(); this.activeComposedRecordings.decrementAndGet();
} }
kmsManager.getMediaNodeManager().dropIdleMediaNode(this.id); kmsManager.getMediaNodeManager().dropIdleMediaNode(this.id);

View File

@ -23,7 +23,6 @@ import com.google.gson.JsonObject;
import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.utils.RecordingUtils;
public class Recording { public class Recording {
@ -77,7 +76,7 @@ public class Recording {
.valueOf(json.get("outputMode").getAsString()); .valueOf(json.get("outputMode").getAsString());
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString()) RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo); .outputMode(outputMode).hasAudio(hasAudio).hasVideo(hasVideo);
if (RecordingUtils.IS_COMPOSED(outputMode) && hasVideo) { if (RecordingProperties.IS_COMPOSED(outputMode) && hasVideo) {
if (json.has("resolution")) { if (json.has("resolution")) {
builder.resolution(json.get("resolution").getAsString()); builder.resolution(json.get("resolution").getAsString());
} }
@ -193,7 +192,8 @@ public class Recording {
json.addProperty("object", "recording"); json.addProperty("object", "recording");
json.addProperty("name", this.recordingProperties.name()); json.addProperty("name", this.recordingProperties.name());
json.addProperty("outputMode", this.getOutputMode().name()); json.addProperty("outputMode", this.getOutputMode().name());
if (RecordingUtils.IS_COMPOSED(this.recordingProperties.outputMode()) && this.recordingProperties.hasVideo()) { if (RecordingProperties.IS_COMPOSED(this.recordingProperties.outputMode())
&& this.recordingProperties.hasVideo()) {
json.addProperty("resolution", this.recordingProperties.resolution()); json.addProperty("resolution", this.recordingProperties.resolution());
json.addProperty("frameRate", this.recordingProperties.frameRate()); json.addProperty("frameRate", this.recordingProperties.frameRate());
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name()); json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());

View File

@ -80,7 +80,6 @@ import io.openvidu.server.utils.DockerManager;
import io.openvidu.server.utils.JsonUtils; import io.openvidu.server.utils.JsonUtils;
import io.openvidu.server.utils.LocalCustomFileManager; import io.openvidu.server.utils.LocalCustomFileManager;
import io.openvidu.server.utils.LocalDockerManager; import io.openvidu.server.utils.LocalDockerManager;
import io.openvidu.server.utils.RecordingUtils;
import io.openvidu.server.utils.RemoteOperationUtils; import io.openvidu.server.utils.RemoteOperationUtils;
public class RecordingManager { public class RecordingManager {
@ -443,7 +442,7 @@ public class RecordingManager {
this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile, this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile,
participant, new CountDownLatch(1)); participant, new CountDownLatch(1));
} else if (RecordingUtils.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) { } else if (RecordingProperties.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) {
// Connect this stream to existing Composite recorder // Connect this stream to existing Composite recorder
log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}", log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}",
session.getSessionId(), participant.getParticipantPublicId()); session.getSessionId(), participant.getParticipantPublicId());
@ -479,7 +478,7 @@ public class RecordingManager {
} catch (InterruptedException e) { } catch (InterruptedException e) {
log.error("Exception while waiting for state change", e); log.error("Exception while waiting for state change", e);
} }
} else if (RecordingUtils.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) { } else if (RecordingProperties.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) {
// Disconnect this stream from existing Composite recorder // Disconnect this stream from existing Composite recorder
log.info("Removing PublisherEndpoint from Composite in session {} for stream of participant {}", log.info("Removing PublisherEndpoint from Composite in session {} for stream of participant {}",
session.getSessionId(), streamId); session.getSessionId(), streamId);

View File

@ -38,7 +38,6 @@ import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.CommandExecutor;
import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.CustomFileManager;
import io.openvidu.server.utils.RecordingUtils;
public abstract class RecordingService { public abstract class RecordingService {
@ -160,7 +159,7 @@ public abstract class RecordingService {
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId) RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
.outputMode(properties.outputMode()).hasAudio(properties.hasAudio()).hasVideo(properties.hasVideo()) .outputMode(properties.outputMode()).hasAudio(properties.hasAudio()).hasVideo(properties.hasVideo())
.mediaNode(properties.mediaNode()); .mediaNode(properties.mediaNode());
if (RecordingUtils.IS_COMPOSED(properties.outputMode()) && properties.hasVideo()) { if (RecordingProperties.IS_COMPOSED(properties.outputMode()) && properties.hasVideo()) {
builder.resolution(properties.resolution()); builder.resolution(properties.resolution());
builder.frameRate(properties.frameRate()); builder.frameRate(properties.frameRate());
builder.recordingLayout(properties.recordingLayout()); builder.recordingLayout(properties.recordingLayout());

View File

@ -56,7 +56,6 @@ import io.openvidu.java.client.ConnectionType;
import io.openvidu.java.client.IceServerProperties; import io.openvidu.java.client.IceServerProperties;
import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.MediaMode;
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.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.java.client.SessionProperties; import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.VideoCodec; import io.openvidu.java.client.VideoCodec;
@ -70,7 +69,6 @@ import io.openvidu.server.core.Token;
import io.openvidu.server.kurento.core.KurentoMediaOptions; import io.openvidu.server.kurento.core.KurentoMediaOptions;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.service.RecordingManager; import io.openvidu.server.recording.service.RecordingManager;
import io.openvidu.server.utils.RecordingUtils;
import io.openvidu.server.utils.RestUtils; import io.openvidu.server.utils.RestUtils;
/** /**
@ -427,9 +425,9 @@ public class SessionRestController {
RecordingProperties recordingProperties; RecordingProperties recordingProperties;
try { try {
recordingProperties = getRecordingPropertiesFromParams(params, session).build(); recordingProperties = getRecordingPropertiesFromParams(params, session).build();
} catch (RuntimeException e) { } catch (IllegalStateException e) {
return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.UNPROCESSABLE_ENTITY); return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.UNPROCESSABLE_ENTITY);
} catch (Exception e) { } catch (RuntimeException e) {
return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.BAD_REQUEST); return this.generateErrorResponse(e.getMessage(), "/sessions", HttpStatus.BAD_REQUEST);
} }
@ -853,208 +851,10 @@ public class SessionRestController {
return builder; return builder;
} }
protected RecordingProperties.Builder getRecordingPropertiesFromParams(Map<?, ?> params, Session session) protected RecordingProperties.Builder getRecordingPropertiesFromParams(Map<String, ?> params, Session session)
throws Exception { throws RuntimeException {
RecordingProperties.Builder builder = RecordingProperties.fromJson(params,
// Final properties being used session.getSessionProperties().defaultRecordingProperties());
String nameFinal = null;
Boolean hasAudioFinal = null;
Boolean hasVideoFinal = null;
OutputMode outputModeFinal = null;
RecordingLayout recordingLayoutFinal = null;
String resolutionFinal = null;
Integer frameRateFinal = null;
Long shmSizeFinal = null;
String customLayoutFinal = null;
Boolean ignoreFailedStreamsFinal = null;
RecordingProperties defaultProps = session.getSessionProperties().defaultRecordingProperties();
// Default properties configured in Session
String nameDefault = defaultProps.name();
Boolean hasAudioDefault = defaultProps.hasAudio();
Boolean hasVideoDefault = defaultProps.hasVideo();
OutputMode outputModeDefault = defaultProps.outputMode();
RecordingLayout recordingLayoutDefault = defaultProps.recordingLayout();
String resolutionDefault = defaultProps.resolution();
Integer frameRateDefault = defaultProps.frameRate();
Long shmSizeDefault = defaultProps.shmSize();
String customLayoutDefault = defaultProps.customLayout();
Boolean ignoreFailedStreamsDefault = defaultProps.ignoreFailedStreams();
// Provided properties through params
String sessionIdParam;
String nameParam;
Boolean hasAudioParam;
Boolean hasVideoParam;
String outputModeStringParam;
String recordingLayoutStringParam;
String resolutionParam;
Integer frameRateParam;
Long shmSizeParam = null;
String customLayoutParam;
Boolean ignoreFailedStreamsParam;
try {
sessionIdParam = (String) params.get("session");
nameParam = (String) params.get("name");
hasAudioParam = (Boolean) params.get("hasAudio");
hasVideoParam = (Boolean) params.get("hasVideo");
outputModeStringParam = (String) params.get("outputMode");
recordingLayoutStringParam = (String) params.get("recordingLayout");
resolutionParam = (String) params.get("resolution");
frameRateParam = (Integer) params.get("frameRate");
if (params.get("shmSize") != null) {
shmSizeParam = Long.parseLong(params.get("shmSize").toString());
}
customLayoutParam = (String) params.get("customLayout");
ignoreFailedStreamsParam = (Boolean) params.get("ignoreFailedStreams");
} catch (ClassCastException | NumberFormatException e) {
throw new Exception("Type error in some parameter: " + e.getMessage());
}
if (sessionIdParam == null) {
// "session" parameter not found
throw new Exception("\"session\" parameter is mandatory");
}
if (nameParam != null && !nameParam.isEmpty()) {
if (!sessionManager.formatChecker.isValidRecordingName(nameParam)) {
throw new Exception("Parameter 'name' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-]+");
}
nameFinal = nameParam;
} else if (nameDefault != null) {
nameFinal = nameDefault;
} else {
nameFinal = "";
}
if (hasAudioParam != null) {
hasAudioFinal = hasAudioParam;
} else if (hasAudioDefault != null) {
hasAudioFinal = hasAudioDefault;
} else {
hasAudioFinal = RecordingProperties.DefaultValues.hasAudio;
}
if (hasVideoParam != null) {
hasVideoFinal = hasVideoParam;
} else if (hasAudioDefault != null) {
hasVideoFinal = hasVideoDefault;
} else {
hasVideoFinal = RecordingProperties.DefaultValues.hasVideo;
}
if (!hasAudioFinal && !hasVideoFinal) {
// Cannot start a recording with both "hasAudio" and "hasVideo" to false
throw new RuntimeException("Cannot start a recording with both \"hasAudio\" and \"hasVideo\" set to false");
}
if (outputModeStringParam != null) {
try {
outputModeFinal = OutputMode.valueOf(outputModeStringParam);
// If param outputMode is COMPOSED when default is COMPOSED_QUICK_START,
// change outputMode to COMPOSED_QUICK_START (and vice versa)
if (OutputMode.COMPOSED_QUICK_START.equals(outputModeDefault)
&& OutputMode.COMPOSED.equals(outputModeFinal)) {
outputModeFinal = OutputMode.COMPOSED_QUICK_START;
} else if (OutputMode.COMPOSED.equals(outputModeDefault)
&& OutputMode.COMPOSED_QUICK_START.equals(outputModeFinal)) {
outputModeFinal = OutputMode.COMPOSED;
}
} catch (Exception e) {
throw new Exception("Type error in parameter 'outputMode'");
}
} else if (outputModeDefault != null) {
outputModeFinal = outputModeDefault;
} else {
outputModeFinal = RecordingProperties.DefaultValues.outputMode;
}
if (RecordingUtils.IS_COMPOSED(outputModeFinal)) {
if (recordingLayoutStringParam != null) {
try {
recordingLayoutFinal = RecordingLayout.valueOf(recordingLayoutStringParam);
} catch (Exception e) {
throw new Exception("Type error in parameter 'recordingLayout'");
}
} else if (recordingLayoutDefault != null) {
recordingLayoutFinal = recordingLayoutDefault;
} else {
recordingLayoutFinal = RecordingProperties.DefaultValues.recordingLayout;
}
if (resolutionParam != null) {
if (!sessionManager.formatChecker.isAcceptableRecordingResolution(resolutionParam)) {
throw new RuntimeException(
"Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height");
}
resolutionFinal = resolutionParam;
} else if (resolutionDefault != null) {
resolutionFinal = resolutionDefault;
} else {
resolutionFinal = RecordingProperties.DefaultValues.resolution;
}
if (frameRateParam != null) {
if (!sessionManager.formatChecker.isAcceptableRecordingFrameRate(frameRateParam)) {
throw new RuntimeException(
"Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height");
}
frameRateFinal = frameRateParam;
} else if (frameRateDefault != null) {
frameRateFinal = frameRateDefault;
} else {
frameRateFinal = RecordingProperties.DefaultValues.frameRate;
}
if (shmSizeParam != null) {
if (!sessionManager.formatChecker.isAcceptableRecordingShmSize(shmSizeParam)) {
throw new RuntimeException("Wrong \"shmSize\" parameter. Must be 134217728 (128 MB) minimum");
}
shmSizeFinal = shmSizeParam;
} else if (shmSizeDefault != null) {
shmSizeFinal = shmSizeDefault;
} else {
shmSizeFinal = RecordingProperties.DefaultValues.shmSize;
}
if (RecordingLayout.CUSTOM.equals(recordingLayoutFinal)) {
if (customLayoutParam != null) {
customLayoutFinal = customLayoutParam;
} else if (customLayoutDefault != null) {
customLayoutFinal = customLayoutDefault;
} else {
customLayoutFinal = "";
}
}
} else if (OutputMode.INDIVIDUAL.equals(outputModeFinal)) {
if (ignoreFailedStreamsParam != null) {
ignoreFailedStreamsFinal = ignoreFailedStreamsParam;
} else if (ignoreFailedStreamsDefault != null) {
ignoreFailedStreamsFinal = ignoreFailedStreamsDefault;
} else {
ignoreFailedStreamsFinal = RecordingProperties.DefaultValues.ignoreFailedStreams;
}
}
RecordingProperties.Builder builder = new RecordingProperties.Builder();
builder.name(nameFinal).hasAudio(hasAudioFinal).hasVideo(hasVideoFinal).outputMode(outputModeFinal);
if (RecordingUtils.IS_COMPOSED(outputModeFinal) && hasVideoFinal) {
builder.recordingLayout(recordingLayoutFinal);
builder.resolution(resolutionFinal);
builder.frameRate(frameRateFinal);
builder.shmSize(shmSizeFinal);
if (RecordingLayout.CUSTOM.equals(recordingLayoutFinal)) {
builder.customLayout(customLayoutFinal);
}
}
if (OutputMode.INDIVIDUAL.equals(outputModeFinal)) {
builder.ignoreFailedStreams(ignoreFailedStreamsFinal);
}
return builder; return builder;
} }

View File

@ -47,6 +47,7 @@ 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.ConnectionProperties;
import io.openvidu.java.client.utils.FormatChecker;
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.core.EndReason; import io.openvidu.server.core.EndReason;
@ -277,7 +278,7 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
if (tokenObj != null) { if (tokenObj != null) {
String clientMetadata = getStringParam(request, ProtocolElements.JOINROOM_METADATA_PARAM); String clientMetadata = getStringParam(request, ProtocolElements.JOINROOM_METADATA_PARAM);
if (sessionManager.formatChecker.isServerMetadataFormatCorrect(clientMetadata)) { if (FormatChecker.isServerMetadataFormatCorrect(clientMetadata)) {
// While closing a session users can't join // While closing a session users can't join
if (session.closingLock.readLock().tryLock()) { if (session.closingLock.readLock().tryLock()) {

View File

@ -1,54 +0,0 @@
/*
* (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.
*
*/
package io.openvidu.server.utils;
public class FormatChecker {
public boolean isAcceptableRecordingResolution(String stringResolution) {
// Matches every string with format "AxB", being A and B any number not starting
// with 0 and 3 digits long or 4 digits long if they start with 1
return stringResolution.matches("^(?!(0))(([0-9]{3})|1([0-9]{3}))x(?!0)(([0-9]{3})|1([0-9]{3}))$");
}
public boolean isAcceptableRecordingFrameRate(Integer frameRate) {
// Integer greater than 0 and below 120
return (frameRate > 0 && frameRate <= 120);
}
public boolean isAcceptableRecordingShmSize(Long shmSize) {
// Long grater than 134217728 (128 MB)
return (shmSize >= 134217728L);
}
public boolean isServerMetadataFormatCorrect(String metadata) {
return true;
}
public boolean isValidCustomSessionId(String customSessionId) {
return isValidAlphanumeric(customSessionId);
}
public boolean isValidRecordingName(String recodingName) {
return isValidAlphanumeric(recodingName);
}
private boolean isValidAlphanumeric(String str) {
return str.matches("[a-zA-Z0-9_-]+");
}
}

View File

@ -1,15 +1,10 @@
package io.openvidu.server.utils; package io.openvidu.server.utils;
import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
public final class RecordingUtils { public final class RecordingUtils {
public final static boolean IS_COMPOSED(OutputMode outputMode) {
return (OutputMode.COMPOSED.equals(outputMode) || OutputMode.COMPOSED_QUICK_START.equals(outputMode));
}
public final static RecordingProperties RECORDING_PROPERTIES_WITH_MEDIA_NODE(Session session) { public final static RecordingProperties RECORDING_PROPERTIES_WITH_MEDIA_NODE(Session session) {
RecordingProperties recordingProperties = session.getSessionProperties().defaultRecordingProperties(); RecordingProperties recordingProperties = session.getSessionProperties().defaultRecordingProperties();
if (recordingProperties.mediaNode() == null) { if (recordingProperties.mediaNode() == null) {

View File

@ -3226,6 +3226,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST);
body = "{'session':'CUSTOM_SESSION_ID','name':999}"; body = "{'session':'CUSTOM_SESSION_ID','name':999}";
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST);
body = "{'session':'CUSTOM_SESSION_ID','name':'notalphanumeric@'}";
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST);
body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'NOT_EXISTS'}"; body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'NOT_EXISTS'}";
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST);
body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'NOT_EXISTS'}"; body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'NOT_EXISTS'}";
@ -3236,7 +3238,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_BAD_REQUEST);
// 422 // 422
body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':false,'hasVideo':false}"; body = "{'session':'CUSTOM_SESSION_ID','name':'NAME~','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':false,'hasVideo':false}";
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY);
body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':'1920x2000'}"; body = "{'session':'CUSTOM_SESSION_ID','name':'NAME','outputMode':'COMPOSED','recordingLayout':'BEST_FIT','customLayout':'CUSTOM_LAYOUT','hasAudio':true,'hasVideo':true,'resolution':'1920x2000'}";
restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY); restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", body, HttpStatus.SC_UNPROCESSABLE_ENTITY);
@ -3848,8 +3850,8 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
user.getDriver().findElement(By.id("session-settings-btn-0")).click(); user.getDriver().findElement(By.id("session-settings-btn-0")).click();
Thread.sleep(1000); Thread.sleep(1000);
String rareCharsName = "öæééEstoSi`+´çḈ€$"; String recordingName = "1234abcABC_-~";
user.getDriver().findElement(By.id("recording-name-field")).sendKeys(rareCharsName); user.getDriver().findElement(By.id("recording-name-field")).sendKeys(recordingName);
user.getDriver().findElement(By.id("recording-mode-select")).click(); user.getDriver().findElement(By.id("recording-mode-select")).click();
Thread.sleep(500); Thread.sleep(500);
user.getDriver().findElement(By.id("option-ALWAYS")).click(); user.getDriver().findElement(By.id("option-ALWAYS")).click();
@ -3888,7 +3890,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestappE2eTest {
Assert.assertEquals("Wrong recording startTime/timestamp in webhook event", Assert.assertEquals("Wrong recording startTime/timestamp in webhook event",
event.get("startTime").getAsLong(), event.get("timestamp").getAsLong()); event.get("startTime").getAsLong(), event.get("timestamp").getAsLong());
Assert.assertNull("Wrong recording reason in webhook event (should be null)", event.get("reason")); Assert.assertNull("Wrong recording reason in webhook event (should be null)", event.get("reason"));
Assert.assertEquals("Wrong recording name in webhook event", rareCharsName, Assert.assertEquals("Wrong recording name in webhook event", recordingName,
event.get("name").getAsString()); event.get("name").getAsString());
user.getDriver().findElement(By.id("add-user-btn")).click(); user.getDriver().findElement(By.id("add-user-btn")).click();