Merge pull request #533 from OpenVidu/other

Merge from master
pull/534/head^2
Pablo Fuente Pérez 2020-09-07 13:55:35 +02:00 committed by GitHub
commit fd6eb2017f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 198 additions and 130 deletions

View File

@ -25,11 +25,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse; import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpDelete;
@ -40,6 +35,12 @@ import org.apache.http.util.EntityUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
public class Session { public class Session {
private static final Logger log = LoggerFactory.getLogger(Session.class); private static final Logger log = LoggerFactory.getLogger(Session.class);
@ -120,8 +121,22 @@ public class Session {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("session", this.sessionId); json.addProperty("session", this.sessionId);
json.addProperty("role", tokenOptions.getRole().name());
if (tokenOptions.getData() != null) {
json.addProperty("data", tokenOptions.getData()); json.addProperty("data", tokenOptions.getData());
} else {
json.add("data", JsonNull.INSTANCE);
}
if (tokenOptions.getRole() != null) {
json.addProperty("role", tokenOptions.getRole().name());
} else {
json.add("role", JsonNull.INSTANCE);
}
if (tokenOptions.record() != null) {
json.addProperty("record", tokenOptions.record());
} else {
json.add("record", JsonNull.INSTANCE);
}
if (tokenOptions.getKurentoOptions() != null) { if (tokenOptions.getKurentoOptions() != null) {
JsonObject kurentoOptions = new JsonObject(); JsonObject kurentoOptions = new JsonObject();
if (tokenOptions.getKurentoOptions().getVideoMaxRecvBandwidth() != null) { if (tokenOptions.getKurentoOptions().getVideoMaxRecvBandwidth() != null) {

View File

@ -24,6 +24,7 @@ public class TokenOptions {
private String data; private String data;
private OpenViduRole role; private OpenViduRole role;
private Boolean record;
private KurentoOptions kurentoOptions; private KurentoOptions kurentoOptions;
/** /**
@ -33,15 +34,16 @@ public class TokenOptions {
*/ */
public static class Builder { public static class Builder {
private String data = ""; private String data;
private OpenViduRole role = OpenViduRole.PUBLISHER; private OpenViduRole role;
private Boolean record;
private KurentoOptions kurentoOptions; private KurentoOptions kurentoOptions;
/** /**
* Builder for {@link io.openvidu.java.client.TokenOptions} * Builder for {@link io.openvidu.java.client.TokenOptions}
*/ */
public TokenOptions build() { public TokenOptions build() {
return new TokenOptions(this.data, this.role, this.kurentoOptions); return new TokenOptions(this.data, this.role, this.record, this.kurentoOptions);
} }
/** /**
@ -79,6 +81,17 @@ public class TokenOptions {
return this; return this;
} }
/**
* Call this method to flag the streams published by the participant
* owning this token to be recorded or not. This only affects <a href=
* "https://docs.openvidu.io/en/stable/advanced-features/recording#selecting-streams-to-be-recorded"
* target="_blank">INDIVIDUAL recording</a>. If not set by default will be true
*/
public Builder record(boolean record) {
this.record = record;
return this;
}
/** /**
* Call this method to set a {@link io.openvidu.java.client.KurentoOptions} * Call this method to set a {@link io.openvidu.java.client.KurentoOptions}
* object for this token * object for this token
@ -90,9 +103,10 @@ public class TokenOptions {
} }
private TokenOptions(String data, OpenViduRole role, KurentoOptions kurentoOptions) { private TokenOptions(String data, OpenViduRole role, Boolean record, KurentoOptions kurentoOptions) {
this.data = data; this.data = data;
this.role = role; this.role = role;
this.record = record;
this.kurentoOptions = kurentoOptions; this.kurentoOptions = kurentoOptions;
} }
@ -110,6 +124,16 @@ public class TokenOptions {
return this.role; return this.role;
} }
/**
* Whether the streams published by the participant owning this token will be
* recorded or not. This only affects <a href=
* "https://docs.openvidu.io/en/stable/advanced-features/recording#selecting-streams-to-be-recorded"
* target="_blank">INDIVIDUAL recording</a>
*/
public Boolean record() {
return this.record;
}
/** /**
* Returns the Kurento options assigned to this token * Returns the Kurento options assigned to this token
*/ */

View File

@ -105,9 +105,10 @@ export class Session {
const data = JSON.stringify({ const data = JSON.stringify({
session: this.sessionId, session: this.sessionId,
role: (!!tokenOptions && !!tokenOptions.role) ? tokenOptions.role : OpenViduRole.PUBLISHER, data: (!!tokenOptions && !!tokenOptions.data) ? tokenOptions.data : null,
data: (!!tokenOptions && !!tokenOptions.data) ? tokenOptions.data : '', role: (!!tokenOptions && !!tokenOptions.role) ? tokenOptions.role : null,
kurentoOptions: (!!tokenOptions && !!tokenOptions.kurentoOptions) ? tokenOptions.kurentoOptions : {}, record: !!tokenOptions ? tokenOptions.record : null,
kurentoOptions: (!!tokenOptions && !!tokenOptions.kurentoOptions) ? tokenOptions.kurentoOptions : null
}); });
axios.post( axios.post(

View File

@ -32,9 +32,18 @@ export interface TokenOptions {
/** /**
* The role assigned to this token * The role assigned to this token
*
* @default PUBLISHER
*/ */
role?: OpenViduRole; role?: OpenViduRole;
/**
* Whether to record the streams published by the participant owning this token or not. This only affects [INDIVIDUAL recording](/en/stable/advanced-features/recording#selecting-streams-to-be-recorded)
*
* @default true
*/
record?: boolean;
/** /**
* **WARNING**: experimental option. This interface may change in the near future * **WARNING**: experimental option. This interface may change in the near future
* *

View File

@ -47,7 +47,7 @@ fi
touch xvfb.log touch xvfb.log
chmod 777 xvfb.log chmod 777 xvfb.log
xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required $DEBUG_CHROME_FLAGS $URL &> xvfb.log & xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --disable-dev-shm-usage --autoplay-policy=no-user-gesture-required $DEBUG_CHROME_FLAGS $URL &> xvfb.log &
touch stop touch stop
chmod 777 /recordings chmod 777 /recordings

View File

@ -30,7 +30,7 @@ if [[ -z "${COMPOSED_QUICK_START_ACTION}" ]]; then
touch xvfb.log touch xvfb.log
chmod 777 xvfb.log chmod 777 xvfb.log
xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --autoplay-policy=no-user-gesture-required --enable-logging --v=1 $DEBUG_CHROME_FLAGS $URL &> xvfb.log & xvfb-run --auto-servernum --server-args="-ac -screen 0 ${RESOLUTION}x24 -noreset" google-chrome --kiosk --start-maximized --test-type --no-sandbox --disable-infobars --disable-gpu --disable-popup-blocking --window-size=$WIDTH,$HEIGHT --window-position=0,0 --no-first-run --ignore-certificate-errors --disable-dev-shm-usage --autoplay-policy=no-user-gesture-required --enable-logging --v=1 $DEBUG_CHROME_FLAGS $URL &> xvfb.log &
chmod 777 /recordings chmod 777 /recordings
until pids=$(pidof Xvfb) until pids=$(pidof Xvfb)

View File

@ -50,7 +50,6 @@ import io.openvidu.server.config.OpenviduConfig.Error;
import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionEventsHandler;
import io.openvidu.server.core.SessionManager; import io.openvidu.server.core.SessionManager;
import io.openvidu.server.core.TokenGenerator; import io.openvidu.server.core.TokenGenerator;
import io.openvidu.server.core.TokenGeneratorDefault;
import io.openvidu.server.coturn.CoturnCredentialsService; import io.openvidu.server.coturn.CoturnCredentialsService;
import io.openvidu.server.coturn.CoturnCredentialsServiceFactory; import io.openvidu.server.coturn.CoturnCredentialsServiceFactory;
import io.openvidu.server.kurento.core.KurentoParticipantEndpointConfig; import io.openvidu.server.kurento.core.KurentoParticipantEndpointConfig;
@ -157,7 +156,7 @@ public class OpenViduServer implements JsonRpcConfigurer {
@ConditionalOnMissingBean @ConditionalOnMissingBean
@DependsOn("openviduConfig") @DependsOn("openviduConfig")
public TokenGenerator tokenGenerator() { public TokenGenerator tokenGenerator() {
return new TokenGeneratorDefault(); return new TokenGenerator();
} }
@Bean @Bean

View File

@ -60,6 +60,7 @@ public class Session implements SessionInterface {
protected volatile boolean closed = false; protected volatile boolean closed = false;
protected AtomicInteger activePublishers = new AtomicInteger(0); protected AtomicInteger activePublishers = new AtomicInteger(0);
protected AtomicInteger activeIndividualRecordedPublishers = new AtomicInteger(0);
/** /**
* This lock protects the following operations with read lock: [REST API](POST * This lock protects the following operations with read lock: [REST API](POST
@ -145,12 +146,22 @@ public class Session implements SessionInterface {
return activePublishers.get(); return activePublishers.get();
} }
public void registerPublisher() { public int getActiveIndividualRecordedPublishers() {
this.activePublishers.incrementAndGet(); return activeIndividualRecordedPublishers.get();
} }
public void deregisterPublisher() { public void registerPublisher(Participant participant) {
this.activePublishers.incrementAndGet();
if (participant.getToken().record()) {
activeIndividualRecordedPublishers.incrementAndGet();
}
}
public void deregisterPublisher(Participant participant) {
this.activePublishers.decrementAndGet(); this.activePublishers.decrementAndGet();
if (participant.getToken().record()) {
activeIndividualRecordedPublishers.decrementAndGet();
}
} }
public void storeToken(Token token) { public void storeToken(Token token) {

View File

@ -297,13 +297,13 @@ public abstract class SessionManager {
return sessionNotActive; return sessionNotActive;
} }
public String newToken(Session session, OpenViduRole role, String serverMetadata, public String newToken(Session session, OpenViduRole role, String serverMetadata, boolean record,
KurentoTokenOptions kurentoTokenOptions) throws Exception { KurentoTokenOptions kurentoTokenOptions) 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(), role, serverMetadata, Token tokenObj = tokenGenerator.generateToken(session.getSessionId(), role, serverMetadata, record,
kurentoTokenOptions); kurentoTokenOptions);
session.storeToken(tokenObj); session.storeToken(tokenObj);
session.showTokens("Token created"); session.showTokens("Token created");
@ -312,7 +312,8 @@ public abstract class SessionManager {
public Token newTokenForInsecureUser(Session session, String token, String serverMetadata) throws Exception { public Token newTokenForInsecureUser(Session session, String token, String serverMetadata) throws Exception {
Token tokenObj = new Token(token, OpenViduRole.PUBLISHER, serverMetadata != null ? serverMetadata : "", Token tokenObj = new Token(token, OpenViduRole.PUBLISHER, serverMetadata != null ? serverMetadata : "",
this.openviduConfig.isTurnadminAvailable() ? this.coturnCredentialsService.createUser() : null, null); this.openviduConfig.isTurnadminAvailable() ? this.coturnCredentialsService.createUser() : null, true,
null);
session.storeToken(tokenObj); session.storeToken(tokenObj);
session.showTokens("Token created for insecure user"); session.showTokens("Token created for insecure user");
return tokenObj; return tokenObj;

View File

@ -27,6 +27,7 @@ public class Token {
private OpenViduRole role; private OpenViduRole role;
private String serverMetadata = ""; private String serverMetadata = "";
private TurnCredentials turnCredentials; private TurnCredentials turnCredentials;
private boolean record = true;
private KurentoTokenOptions kurentoTokenOptions; private KurentoTokenOptions kurentoTokenOptions;
@ -35,11 +36,12 @@ public class Token {
} }
public Token(String token, OpenViduRole role, String serverMetadata, TurnCredentials turnCredentials, public Token(String token, OpenViduRole role, String serverMetadata, TurnCredentials turnCredentials,
KurentoTokenOptions kurentoTokenOptions) { boolean record, KurentoTokenOptions kurentoTokenOptions) {
this.token = token; this.token = token;
this.role = role; this.role = role;
this.serverMetadata = serverMetadata; this.serverMetadata = serverMetadata;
this.turnCredentials = turnCredentials; this.turnCredentials = turnCredentials;
this.record = record;
this.kurentoTokenOptions = kurentoTokenOptions; this.kurentoTokenOptions = kurentoTokenOptions;
} }
@ -63,6 +65,10 @@ public class Token {
return turnCredentials; return turnCredentials;
} }
public boolean record() {
return record;
}
public KurentoTokenOptions getKurentoTokenOptions() { public KurentoTokenOptions getKurentoTokenOptions() {
return kurentoTokenOptions; return kurentoTokenOptions;
} }

View File

@ -17,12 +17,45 @@
package io.openvidu.server.core; package io.openvidu.server.core;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.OpenViduRole;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduBuildInfo;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService;
import io.openvidu.server.coturn.TurnCredentials;
import io.openvidu.server.kurento.core.KurentoTokenOptions; import io.openvidu.server.kurento.core.KurentoTokenOptions;
public interface TokenGenerator { public class TokenGenerator {
public Token generateToken(String sessionId, OpenViduRole role, String serverMetadata, @Autowired
KurentoTokenOptions kurentoTokenOptions) throws Exception; private CoturnCredentialsService coturnCredentialsService;
@Autowired
protected OpenviduConfig openviduConfig;
@Autowired
protected OpenviduBuildInfo openviduBuildConfig;
public Token generateToken(String sessionId, OpenViduRole role, String serverMetadata, boolean record,
KurentoTokenOptions kurentoTokenOptions) throws Exception {
String token = OpenViduServer.wsUrl;
token += "?sessionId=" + sessionId;
token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase()
+ RandomStringUtils.randomAlphanumeric(15);
token += "&role=" + role.name();
token += "&version=" + openviduBuildConfig.getOpenViduServerVersion();
TurnCredentials turnCredentials = null;
if (this.openviduConfig.isTurnadminAvailable()) {
turnCredentials = coturnCredentialsService.createUser();
if (turnCredentials != null) {
token += "&coturnIp=" + openviduConfig.getCoturnIp();
token += "&turnUsername=" + turnCredentials.getUsername();
token += "&turnCredential=" + turnCredentials.getCredential();
}
}
return new Token(token, role, serverMetadata, turnCredentials, record, kurentoTokenOptions);
}
} }

View File

@ -1,62 +0,0 @@
/*
* (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.server.core;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import io.openvidu.java.client.OpenViduRole;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduBuildInfo;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.coturn.CoturnCredentialsService;
import io.openvidu.server.coturn.TurnCredentials;
import io.openvidu.server.kurento.core.KurentoTokenOptions;
public class TokenGeneratorDefault implements TokenGenerator {
@Autowired
private CoturnCredentialsService coturnCredentialsService;
@Autowired
protected OpenviduConfig openviduConfig;
@Autowired
protected OpenviduBuildInfo openviduBuildConfig;
@Override
public Token generateToken(String sessionId, OpenViduRole role, String serverMetadata,
KurentoTokenOptions kurentoTokenOptions) throws Exception {
String token = OpenViduServer.wsUrl;
token += "?sessionId=" + sessionId;
token += "&token=" + IdentifierPrefixes.TOKEN_ID + RandomStringUtils.randomAlphabetic(1).toUpperCase()
+ RandomStringUtils.randomAlphanumeric(15);
token += "&role=" + role.name();
token += "&version=" + openviduBuildConfig.getOpenViduServerVersion();
TurnCredentials turnCredentials = null;
if (this.openviduConfig.isTurnadminAvailable()) {
turnCredentials = coturnCredentialsService.createUser();
if (turnCredentials != null) {
token += "&coturnIp=" + openviduConfig.getCoturnIp();
token += "&turnUsername=" + turnCredentials.getUsername();
token += "&turnCredential=" + turnCredentials.getCredential();
}
}
return new Token(token, role, serverMetadata, turnCredentials, kurentoTokenOptions);
}
}

View File

@ -181,7 +181,7 @@ public class KurentoParticipant extends Participant {
this.session.getSessionId()); this.session.getSessionId());
if (this.openviduConfig.isRecordingModuleEnabled() if (this.openviduConfig.isRecordingModuleEnabled()
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { && this.recordingManager.sessionIsBeingRecorded(session.getSessionId()) && this.token.record()) {
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this); this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
} }
@ -504,7 +504,7 @@ public class KurentoParticipant extends Participant {
} }
releaseElement(getParticipantPublicId(), publisher.getEndpoint()); releaseElement(getParticipantPublicId(), publisher.getEndpoint());
this.streaming = false; this.streaming = false;
this.session.deregisterPublisher(); this.session.deregisterPublisher(this);
endpointConfig.getCdr().stopPublisher(this.getParticipantPublicId(), publisher.getStreamId(), reason); endpointConfig.getCdr().stopPublisher(this.getParticipantPublicId(), publisher.getStreamId(), reason);
publisher = null; publisher = null;

View File

@ -88,7 +88,7 @@ public class KurentoSession extends Session {
} }
public void newPublisher(Participant participant) { public void newPublisher(Participant participant) {
registerPublisher(); registerPublisher(participant);
log.debug("SESSION {}: Virtually subscribed other participants {} to new publisher {}", sessionId, log.debug("SESSION {}: Virtually subscribed other participants {} to new publisher {}", sessionId,
participants.values(), participant.getParticipantPublicId()); participants.values(), participant.getParticipantPublicId());
} }

View File

@ -97,7 +97,7 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService
log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}", log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}",
recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(),
RecordingManager.finalReason(reason)); RecordingManager.finalReason(reason));
log.info("Container for session {} still being ready for new recordings", session.getSessionId()); log.info("Container for session {} still being ready for new recordings", recording.getSessionId());
String containerId = this.sessionsContainers.get(recording.getSessionId()); String containerId = this.sessionsContainers.get(recording.getSessionId());
@ -119,10 +119,7 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService
recording = updateRecordingAttributes(recording); recording = updateRecordingAttributes(recording);
final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"; this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), getMetadataFilePath(recording));
final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
metadataFilePath);
cleanRecordingMaps(recording); cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis(); final long timestamp = System.currentTimeMillis();

View File

@ -103,7 +103,6 @@ public class ComposedRecordingService extends RecordingService {
@Override @Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) { public Recording stopRecording(Session session, Recording recording, EndReason reason) {
recording = this.sealRecordingMetadataFileAsStopped(recording);
if (recording.hasVideo()) { if (recording.hasVideo()) {
return this.stopRecordingWithVideo(session, recording, reason); return this.stopRecordingWithVideo(session, recording, reason);
} else { } else {
@ -297,10 +296,8 @@ public class ComposedRecordingService extends RecordingService {
stopAndRemoveRecordingContainer(recording, containerId, 30); stopAndRemoveRecordingContainer(recording, containerId, 30);
recording = updateRecordingAttributes(recording); recording = updateRecordingAttributes(recording);
final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
metadataFilePath); getMetadataFilePath(recording));
cleanRecordingMaps(recording); cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis(); final long timestamp = System.currentTimeMillis();

View File

@ -279,8 +279,7 @@ public class RecordingManager {
this.sessionHandler.sendRecordingStartedNotification(session, recording); this.sessionHandler.sendRecordingStartedNotification(session, recording);
} }
if (session.getActivePublishers() == 0) { if (session.getActivePublishers() == 0) {
// Init automatic recording stop if there are now publishers when starting // Init automatic recording stop if no publishers when starting the recording
// recording
log.info("No publisher in session {}. Starting {} seconds countdown for stopping recording", log.info("No publisher in session {}. Starting {} seconds countdown for stopping recording",
session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout()); session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout());
this.initAutomaticRecordingStopThread(session); this.initAutomaticRecordingStopThread(session);
@ -310,6 +309,8 @@ public class RecordingManager {
recording = this.sessionsRecordings.get(session.getSessionId()); recording = this.sessionsRecordings.get(session.getSessionId());
} }
recording = ((RecordingService) singleStreamRecordingService).sealRecordingMetadataFileAsStopped(recording);
final long timestamp = System.currentTimeMillis(); final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, Status.stopped); this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, Status.stopped);
cdr.recordRecordingStopped(recording, reason, timestamp); cdr.recordRecordingStopped(recording, reason, timestamp);

View File

@ -175,6 +175,10 @@ public abstract class RecordingService {
protected OpenViduException failStartRecording(Session session, Recording recording, String errorMessage) { protected OpenViduException failStartRecording(Session session, Recording recording, String errorMessage) {
log.error("Recording start failed for session {}: {}", session.getSessionId(), errorMessage); log.error("Recording start failed for session {}: {}", session.getSessionId(), errorMessage);
recording.setStatus(io.openvidu.java.client.Recording.Status.failed); recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
getMetadataFilePath(recording));
this.recordingManager.startingRecordings.remove(recording.getId()); this.recordingManager.startingRecordings.remove(recording.getId());
this.recordingManager.sessionsRecordingsStarting.remove(session.getSessionId()); this.recordingManager.sessionsRecordingsStarting.remove(session.getSessionId());
this.stopRecording(session, recording, null); this.stopRecording(session, recording, null);
@ -186,6 +190,11 @@ public abstract class RecordingService {
this.recordingManager.startedRecordings.remove(recording.getId()); this.recordingManager.startedRecordings.remove(recording.getId());
} }
protected String getMetadataFilePath(Recording recording) {
final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
return folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
}
/** /**
* Simple wrapper for returning update RecordingProperties and a free * Simple wrapper for returning update RecordingProperties and a free
* recordingId when starting a new recording * recordingId when starting a new recording

View File

@ -98,11 +98,11 @@ public class SingleStreamRecordingService extends RecordingService {
activeRecorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>()); activeRecorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
storedRecorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>()); storedRecorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
final int activePublishers = session.getActivePublishers(); int activePublishersToRecord = session.getActiveIndividualRecordedPublishers();
final CountDownLatch recordingStartedCountdown = new CountDownLatch(activePublishers); final CountDownLatch recordingStartedCountdown = new CountDownLatch(activePublishersToRecord);
for (Participant p : session.getParticipants()) { for (Participant p : session.getParticipants()) {
if (p.isStreaming()) { if (p.isStreaming() && p.getToken().record()) {
MediaProfileSpecType profile = null; MediaProfileSpecType profile = null;
try { try {
@ -139,7 +139,6 @@ public class SingleStreamRecordingService extends RecordingService {
@Override @Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) { public Recording stopRecording(Session session, Recording recording, EndReason reason) {
recording = this.sealRecordingMetadataFileAsStopped(recording);
return this.stopRecording(session, recording, reason, 0); return this.stopRecording(session, recording, reason, 0);
} }

View File

@ -149,7 +149,7 @@ public class SessionRestController {
if (customSessionId != null && !customSessionId.isEmpty()) { if (customSessionId != null && !customSessionId.isEmpty()) {
if (!sessionManager.formatChecker.isValidCustomSessionId(customSessionId)) { if (!sessionManager.formatChecker.isValidCustomSessionId(customSessionId)) {
return this.generateErrorResponse( return this.generateErrorResponse(
"Parameter \"customSessionId\" is wrong. Must be an alphanumeric string", "Parameter 'customSessionId' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-]",
"/api/sessions", HttpStatus.BAD_REQUEST); "/api/sessions", HttpStatus.BAD_REQUEST);
} }
builder = builder.customSessionId(customSessionId); builder = builder.customSessionId(customSessionId);
@ -345,10 +345,12 @@ public class SessionRestController {
String sessionId; String sessionId;
String roleString; String roleString;
String metadata; String metadata;
Boolean record;
try { try {
sessionId = (String) params.get("session"); sessionId = (String) params.get("session");
roleString = (String) params.get("role"); roleString = (String) params.get("role");
metadata = (String) params.get("data"); metadata = (String) params.get("data");
record = (Boolean) params.get("record");
} catch (ClassCastException e) { } catch (ClassCastException e) {
return this.generateErrorResponse("Type error in some parameter", "/api/tokens", HttpStatus.BAD_REQUEST); return this.generateErrorResponse("Type error in some parameter", "/api/tokens", HttpStatus.BAD_REQUEST);
} }
@ -398,17 +400,19 @@ public class SessionRestController {
} }
metadata = (metadata != null) ? metadata : ""; metadata = (metadata != null) ? metadata : "";
record = (record != null) ? record : true;
// While closing a session tokens can't be generated // While closing a session tokens can't be generated
if (session.closingLock.readLock().tryLock()) { if (session.closingLock.readLock().tryLock()) {
try { try {
String token = sessionManager.newToken(session, role, metadata, kurentoTokenOptions); String token = sessionManager.newToken(session, role, metadata, record, kurentoTokenOptions);
JsonObject responseJson = new JsonObject(); JsonObject responseJson = new JsonObject();
responseJson.addProperty("id", token); responseJson.addProperty("id", token);
responseJson.addProperty("session", sessionId); responseJson.addProperty("session", sessionId);
responseJson.addProperty("role", role.toString()); responseJson.addProperty("role", role.toString());
responseJson.addProperty("data", metadata); responseJson.addProperty("data", metadata);
responseJson.addProperty("record", record);
responseJson.addProperty("token", token); responseJson.addProperty("token", token);
if (kurentoOptions != null) { if (kurentoOptions != null) {
@ -500,28 +504,36 @@ public class SessionRestController {
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
if (name != null && !name.isEmpty()) {
if (!sessionManager.formatChecker.isValidRecordingName(name)) {
return this.generateErrorResponse(
"Parameter 'name' is wrong. Must be an alphanumeric string [a-zA-Z0-9_-]", "/api/sessions",
HttpStatus.BAD_REQUEST);
}
}
OutputMode finalOutputMode = OutputMode.COMPOSED; OutputMode finalOutputMode = OutputMode.COMPOSED;
RecordingLayout recordingLayout = null; RecordingLayout recordingLayout = null;
if (outputModeString != null && !outputModeString.isEmpty()) { if (outputModeString != null && !outputModeString.isEmpty()) {
try { try {
finalOutputMode = OutputMode.valueOf(outputModeString); finalOutputMode = OutputMode.valueOf(outputModeString);
} catch (Exception e) { } catch (Exception e) {
return this.generateErrorResponse("Type error in some parameter", "/api/recordings/start", return this.generateErrorResponse("Type error in parameter 'outputMode'", "/api/recordings/start",
HttpStatus.BAD_REQUEST); HttpStatus.BAD_REQUEST);
} }
} }
if (RecordingUtils.IS_COMPOSED(finalOutputMode)) { if (RecordingUtils.IS_COMPOSED(finalOutputMode)) {
if (resolution != null && !sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) { if (resolution != null && !sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) {
return this.generateErrorResponse( return this.generateErrorResponse(
"Wrong \"resolution\" parameter. Acceptable values from 100 to 1999 for both width and height", "Wrong 'resolution' parameter. Acceptable values from 100 to 1999 for both width and height",
"/api/recordings/start", HttpStatus.UNPROCESSABLE_ENTITY); "/api/recordings/start", HttpStatus.UNPROCESSABLE_ENTITY);
} }
if (recordingLayoutString != null && !recordingLayoutString.isEmpty()) { if (recordingLayoutString != null && !recordingLayoutString.isEmpty()) {
try { try {
recordingLayout = RecordingLayout.valueOf(recordingLayoutString); recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
} catch (Exception e) { } catch (Exception e) {
return this.generateErrorResponse("Type error in some parameter", "/api/recordings/start", return this.generateErrorResponse("Type error in parameter 'recordingLayout'",
HttpStatus.BAD_REQUEST); "/api/recordings/start", HttpStatus.BAD_REQUEST);
} }
} }
} }

View File

@ -30,8 +30,15 @@ public class FormatChecker {
} }
public boolean isValidCustomSessionId(String customSessionId) { public boolean isValidCustomSessionId(String customSessionId) {
// Alphanumeric string return isValidAlphanumeric(customSessionId);
return customSessionId.matches("[a-zA-Z0-9_-]+"); }
public boolean isValidRecordingName(String recodingName) {
return isValidAlphanumeric(recodingName);
}
private boolean isValidAlphanumeric(String str) {
return str.matches("[a-zA-Z0-9_-]+");
} }
} }

View File

@ -115,7 +115,7 @@ public class SessionGarbageCollectorIntegrationTest {
} }
private void joinParticipant(String sessionId, String token) { private void joinParticipant(String sessionId, String token) {
Token t = new Token(token, OpenViduRole.PUBLISHER, "SERVER_METADATA", null, null); Token t = new Token(token, OpenViduRole.PUBLISHER, "SERVER_METADATA", null, true, null);
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
String participantPrivateId = "PARTICIPANT_PRIVATE_ID_" + uuid; String participantPrivateId = "PARTICIPANT_PRIVATE_ID_" + uuid;
String finalUserId = "FINAL_USER_ID_" + uuid; String finalUserId = "FINAL_USER_ID_" + uuid;

View File

@ -934,9 +934,13 @@ public class OpenViduTestAppE2eTest {
user.getEventManager().waitUntilEventReaches("streamCreated", 2); user.getEventManager().waitUntilEventReaches("streamCreated", 2);
user.getEventManager().waitUntilEventReaches("streamPlaying", 2); user.getEventManager().waitUntilEventReaches("streamPlaying", 2);
// Give some time for the screen sharing warning to stop resizing the viewport
Thread.sleep(3000);
// Unpublish video // Unpublish video
final CountDownLatch latch1 = new CountDownLatch(2); final CountDownLatch latch1 = new CountDownLatch(2);
user.getEventManager().on("streamPropertyChanged", (event) -> { user.getEventManager().on("streamPropertyChanged", (event) -> {
System.out.println(event.toString());
threadAssertions.add("videoActive".equals(event.get("changedProperty").getAsString())); threadAssertions.add("videoActive".equals(event.get("changedProperty").getAsString()));
threadAssertions.add(!event.get("newValue").getAsBoolean()); threadAssertions.add(!event.get("newValue").getAsBoolean());
latch1.countDown(); latch1.countDown();
@ -960,6 +964,7 @@ public class OpenViduTestAppE2eTest {
// Unpublish audio // Unpublish audio
final CountDownLatch latch2 = new CountDownLatch(2); final CountDownLatch latch2 = new CountDownLatch(2);
user.getEventManager().on("streamPropertyChanged", (event) -> { user.getEventManager().on("streamPropertyChanged", (event) -> {
System.out.println(event.toString());
threadAssertions.add("audioActive".equals(event.get("changedProperty").getAsString())); threadAssertions.add("audioActive".equals(event.get("changedProperty").getAsString()));
threadAssertions.add(!event.get("newValue").getAsBoolean()); threadAssertions.add(!event.get("newValue").getAsBoolean());
latch2.countDown(); latch2.countDown();
@ -992,9 +997,11 @@ public class OpenViduTestAppE2eTest {
+ "}"; + "}";
System.out.println("Publisher dimensions: " + event.get("newValue").getAsJsonObject().toString()); System.out.println("Publisher dimensions: " + event.get("newValue").getAsJsonObject().toString());
System.out.println("Real dimensions of viewport: " + expectedDimensions); System.out.println("Real dimensions of viewport: " + expectedDimensions);
threadAssertions.add("videoDimensions".equals(event.get("changedProperty").getAsString())); if ("videoDimensions".equals(event.get("changedProperty").getAsString())) {
threadAssertions.add(expectedDimensions.equals(event.get("newValue").getAsJsonObject().toString())); if (expectedDimensions.equals(event.get("newValue").getAsJsonObject().toString())) {
latch3.countDown(); latch3.countDown();
}
}
}); });
user.getDriver().manage().window().setSize(new Dimension(newWidth, newHeight)); user.getDriver().manage().window().setSize(new Dimension(newWidth, newHeight));
@ -1009,7 +1016,7 @@ public class OpenViduTestAppE2eTest {
user.getEventManager().waitUntilEventReaches("streamPropertyChanged", 6); user.getEventManager().waitUntilEventReaches("streamPropertyChanged", 6);
if (!latch3.await(5000, TimeUnit.MILLISECONDS)) { if (!latch3.await(6000, TimeUnit.MILLISECONDS)) {
gracefullyLeaveParticipants(2); gracefullyLeaveParticipants(2);
fail(); fail();
return; return;
@ -1018,11 +1025,6 @@ public class OpenViduTestAppE2eTest {
System.out.println(getBase64Screenshot(user)); System.out.println(getBase64Screenshot(user));
user.getEventManager().off("streamPropertyChanged"); user.getEventManager().off("streamPropertyChanged");
log.info("Thread assertions: {}", threadAssertions.toString());
for (Iterator<Boolean> iter = threadAssertions.iterator(); iter.hasNext();) {
Assert.assertTrue("Some Event property was wrong", iter.next());
iter.remove();
}
gracefullyLeaveParticipants(2); gracefullyLeaveParticipants(2);
} }
@ -1347,8 +1349,15 @@ public class OpenViduTestAppE2eTest {
final String sessionName = "TestSession"; final String sessionName = "TestSession";
final String recordingName = "CUSTOM_NAME"; final String recordingName = "CUSTOM_NAME";
user.getDriver().findElement(By.id("auto-join-checkbox")).click(); // Connect 2 users. One of them not recorded
user.getDriver().findElement(By.id("one2one-btn")).click(); user.getDriver().findElement(By.id("one2one-btn")).click();
user.getDriver().findElement(By.id("session-settings-btn-0")).click();
Thread.sleep(1000);
user.getDriver().findElement(By.id("record-checkbox")).click();
user.getDriver().findElement(By.id("save-btn")).click();
Thread.sleep(1000);
user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER));
user.getEventManager().waitUntilEventReaches("connectionCreated", 4); user.getEventManager().waitUntilEventReaches("connectionCreated", 4);
user.getEventManager().waitUntilEventReaches("accessAllowed", 2); user.getEventManager().waitUntilEventReaches("accessAllowed", 2);
@ -1416,7 +1425,7 @@ public class OpenViduTestAppE2eTest {
String recPath = recordingsPath + sessionName + "/"; String recPath = recordingsPath + sessionName + "/";
Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName); Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName);
this.checkIndividualRecording(recPath, recording, 2, "opus", "vp8", true); this.checkIndividualRecording(recPath, recording, 1, "opus", "vp8", true);
// Try to get the stopped recording // Try to get the stopped recording
user.getDriver().findElement(By.id("get-recording-btn")).click(); user.getDriver().findElement(By.id("get-recording-btn")).click();

View File

@ -16,7 +16,7 @@
"core-js": "3.4.7", "core-js": "3.4.7",
"hammerjs": "2.0.8", "hammerjs": "2.0.8",
"openvidu-browser": "2.15.0", "openvidu-browser": "2.15.0",
"openvidu-node-client": "2.11.0", "openvidu-node-client": "2.15.0",
"rxjs": "6.5.3", "rxjs": "6.5.3",
"zone.js": "0.10.2" "zone.js": "0.10.2"
}, },