openvidu-server: individual stream recording

pull/203/head
pabloFuente 2019-01-18 11:58:12 +01:00
parent 902470e7a4
commit 6d392d7e4a
24 changed files with 1522 additions and 548 deletions

View File

@ -23,7 +23,6 @@ import java.net.URL;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.ws.rs.ProcessingException;
import org.kurento.jsonrpc.JsonUtils;
import org.kurento.jsonrpc.internal.server.config.JsonRpcConfiguration;
@ -57,7 +56,7 @@ import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.core.KurentoSessionEventsHandler;
import io.openvidu.server.kurento.core.KurentoSessionManager;
import io.openvidu.server.kurento.kms.FixedOneKmsManager;
import io.openvidu.server.recording.ComposedRecordingService;
import io.openvidu.server.recording.service.RecordingManager;
import io.openvidu.server.rpc.RpcHandler;
import io.openvidu.server.rpc.RpcNotificationService;
import io.openvidu.server.utils.CommandExecutor;
@ -147,8 +146,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
@Bean
@ConditionalOnMissingBean
public ComposedRecordingService composedRecordingService() {
return new ComposedRecordingService();
public RecordingManager recordingManager() {
return new RecordingManager();
}
@Bean
@ -233,57 +232,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
OpenViduServer.wsUrl = OpenViduServer.wsUrl.substring(0, OpenViduServer.wsUrl.length() - 1);
}
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
if (recordingModuleEnabled) {
ComposedRecordingService recordingService = composedRecordingService();
recordingService.setRecordingVersion(openviduConf.getOpenViduRecordingVersion());
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
+ openviduConf.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
boolean imageExists = false;
try {
imageExists = recordingService.recordingImageExistsLocally();
} catch (ProcessingException exception) {
String message = "Exception connecting to Docker daemon: ";
if ("docker".equals(openviduConf.getSpringProfile())) {
final String NEW_LINE = System.getProperty("line.separator");
message += "make sure you include the following flags in your \"docker run\" command:" +
NEW_LINE + " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" +
NEW_LINE + " -e MY_UID=$(id -u $USER)" +
NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock" +
NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" +
NEW_LINE;
} else {
message += "you need Docker installed in this machine to enable OpenVidu recording service";
}
log.error(message);
throw new RuntimeException(message);
}
if (imageExists) {
log.info("Docker image already exists locally");
} else {
Thread t = new Thread(() -> {
boolean keep = true;
log.info("Downloading ");
while (keep) {
System.out.print(".");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
keep = false;
log.info("\nDownload complete");
}
}
});
t.start();
recordingService.downloadRecordingImage();
t.interrupt();
t.join();
log.info("Docker image available");
}
recordingService.initRecordingPath();
if (this.openviduConfig().isRecordingModuleEnabled()) {
this.recordingManager().initializeRecordingManager();
}
httpUrl = openviduConf.getFinalUrl();

View File

@ -17,9 +17,7 @@
package io.openvidu.server.cdr;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@ -32,6 +30,7 @@ import io.openvidu.server.core.MediaOptions;
import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.service.RecordingManager;
/**
* CDR logger to register all information of a Session.
@ -69,6 +68,7 @@ import io.openvidu.server.recording.Recording;
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "openviduServerStopped"
*
* [OPTIONAL_PROPERTIES]:
* - receivingFrom: only if connection = "INBOUND"
@ -91,9 +91,6 @@ public class CallDetailRecord {
private Map<String, Set<CDREventWebrtcConnection>> subscriptions = new ConcurrentHashMap<>();
private Map<String, CDREventRecording> recordings = new ConcurrentHashMap<>();
private final List<String> lastParticipantLeftReasons = Arrays.asList(
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
public CallDetailRecord(CDRLogger logger) {
this.logger = logger;
}
@ -108,7 +105,7 @@ public class CallDetailRecord {
public void recordSessionDestroyed(String sessionId, String reason) {
CDREvent e = this.sessions.remove(sessionId);
if (openviduConfig.isCdrEnabled())
this.logger.log(new CDREventSession(e, this.finalReason(reason)));
this.logger.log(new CDREventSession(e, RecordingManager.finalReason(reason)));
}
public void recordParticipantJoined(Participant participant, String sessionId) {
@ -124,7 +121,8 @@ public class CallDetailRecord {
this.logger.log(new CDREventParticipant(e, reason));
}
public void recordNewPublisher(Participant participant, String sessionId, MediaOptions mediaOptions, Long timestamp) {
public void recordNewPublisher(Participant participant, String sessionId, MediaOptions mediaOptions,
Long timestamp) {
CDREventWebrtcConnection publisher = new CDREventWebrtcConnection(sessionId,
participant.getParticipantPublicId(), mediaOptions, null, timestamp);
this.publications.put(participant.getParticipantPublicId(), publisher);
@ -181,15 +179,7 @@ public class CallDetailRecord {
public void recordRecordingStopped(String sessionId, Recording recording, String reason) {
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
if (openviduConfig.isCdrEnabled())
this.logger.log(new CDREventRecording(recordingStartedEvent, this.finalReason(reason)));
}
private String finalReason(String reason) {
if (lastParticipantLeftReasons.contains(reason)) {
return "lastParticipantLeft";
} else {
return reason;
}
this.logger.log(new CDREventRecording(recordingStartedEvent, RecordingManager.finalReason(reason)));
}
}

View File

@ -24,7 +24,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

View File

@ -143,8 +143,7 @@ public class SessionEventsHandler {
// Metadata associated to new participant
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM,
participant.getParticipantPublicId());
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM,
participant.getCreatedAt());
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getCreatedAt());
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM,
participant.getFullMetadata());
@ -404,12 +403,17 @@ public class SessionEventsHandler {
evictedParticipant.getParticipantPublicId());
params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason);
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
for (Participant p : participants) {
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
// Do not send a message when evicting RECORDER participant
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
}
for (Participant p : participants) {
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
}
}
}
public void sendRecordingStartedNotification(Session session, Recording recording) {
@ -550,6 +554,9 @@ public class SessionEventsHandler {
private Set<Participant> filterParticipantsByRole(ParticipantRole[] roles, Set<Participant> participants) {
return participants.stream().filter(part -> {
if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(part.getParticipantPublicId())) {
return false;
}
boolean isRole = false;
for (ParticipantRole role : roles) {
isRole = role.equals(part.getToken().getRole());

View File

@ -44,7 +44,7 @@ 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.recording.ComposedRecordingService;
import io.openvidu.server.recording.service.RecordingManager;
public abstract class SessionManager {
@ -54,7 +54,7 @@ public abstract class SessionManager {
protected SessionEventsHandler sessionEventsHandler;
@Autowired
protected ComposedRecordingService recordingService;
protected RecordingManager recordingManager;
@Autowired
protected CallDetailRecord CDR;
@ -440,19 +440,21 @@ public abstract class SessionManager {
this.closeSessionAndEmptyCollections(session, reason);
if (recordingService.sessionIsBeingRecorded(session.getSessionId())) {
recordingService.stopRecording(session, null, reason);
}
return participants;
}
public void closeSessionAndEmptyCollections(Session session, String reason) {
protected void closeSessionAndEmptyCollections(Session session, String reason) {
if (openviduConfig.isRecordingModuleEnabled()
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason));
}
if (session.close(reason)) {
sessionEventsHandler.onSessionClosed(session.getSessionId(), reason);
}
sessions.remove(session.getSessionId());
sessions.remove(session.getSessionId());
sessionProperties.remove(session.getSessionId());
sessionCreationTime.remove(session.getSessionId());
sessionidParticipantpublicidParticipant.remove(session.getSessionId());

View File

@ -17,8 +17,6 @@
package io.openvidu.server.kurento;
import io.openvidu.server.kurento.KurentoClientSessionInfo;
/**
* Implementation of the session info interface, contains a participant's
* private id and the session's id.

View File

@ -53,19 +53,21 @@ import io.openvidu.server.core.MediaOptions;
import io.openvidu.server.core.Participant;
import io.openvidu.server.kurento.TrackType;
import io.openvidu.server.kurento.endpoint.KmsEvent;
import io.openvidu.server.kurento.endpoint.KmsMediaEvent;
import io.openvidu.server.kurento.endpoint.MediaEndpoint;
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
import io.openvidu.server.kurento.endpoint.SdpType;
import io.openvidu.server.kurento.endpoint.SubscriberEndpoint;
import io.openvidu.server.recording.service.RecordingManager;
public class KurentoParticipant extends Participant {
private static final Logger log = LoggerFactory.getLogger(KurentoParticipant.class);
private OpenviduConfig openviduConfig;
private InfoHandler infoHandler;
private CallDetailRecord CDR;
private OpenviduConfig openviduConfig;
private RecordingManager recordingManager;
private boolean webParticipant = true;
@ -79,11 +81,15 @@ public class KurentoParticipant extends Participant {
private final ConcurrentMap<String, SubscriberEndpoint> subscribers = new ConcurrentHashMap<String, SubscriberEndpoint>();
public KurentoParticipant(Participant participant, KurentoSession kurentoSession, MediaPipeline pipeline,
InfoHandler infoHandler, CallDetailRecord CDR, OpenviduConfig openviduConfig) {
InfoHandler infoHandler, CallDetailRecord CDR, OpenviduConfig openviduConfig,
RecordingManager recordingManager) {
super(participant.getParticipantPrivateId(), participant.getParticipantPublicId(), participant.getToken(),
participant.getClientMetadata(), participant.getLocation(), participant.getPlatform(),
participant.getCreatedAt());
this.infoHandler = infoHandler;
this.CDR = CDR;
this.openviduConfig = openviduConfig;
this.recordingManager = recordingManager;
this.session = kurentoSession;
this.pipeline = pipeline;
this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline,
@ -94,8 +100,6 @@ public class KurentoParticipant extends Participant {
getNewOrExistingSubscriber(other.getParticipantPublicId());
}
}
this.infoHandler = infoHandler;
this.CDR = CDR;
}
public void createPublishingEndpoint(MediaOptions mediaOptions) {
@ -233,6 +237,11 @@ public class KurentoParticipant extends Participant {
log.info("PARTICIPANT {}: Is now publishing video in room {}", this.getParticipantPublicId(),
this.session.getSessionId());
if (this.openviduConfig.isRecordingModuleEnabled()
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
}
CDR.recordNewPublisher(this, this.session.getSessionId(), this.publisher.getMediaOptions(),
this.publisher.createdAt());
@ -425,9 +434,15 @@ public class KurentoParticipant extends Participant {
private void releasePublisherEndpoint(String reason) {
if (publisher != null && publisher.getEndpoint() != null) {
// Store streamId from publisher's map
// Remove streamId from publisher's map
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
if (this.openviduConfig.isRecordingModuleEnabled()
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
this.getPublisherStreamId());
}
publisher.unregisterErrorListeners();
for (MediaElement el : publisher.getMediaElements()) {
releaseElement(getParticipantPublicId(), el);
@ -507,53 +522,6 @@ public class KurentoParticipant extends Participant {
* " | TIMESTAMP: " + System.currentTimeMillis(); log.debug(msg);
* this.infoHandler.sendInfo(msg); });
*
* endpoint.getWebEndpoint().addMediaFlowInStateChangeListener((event) -> {
* String msg1 = " Media flow in state change (" +
* endpoint.getEndpoint().getName() + ") -> " + "STATE: " +
* event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " +
* event.getPadName() + " | MEDIATYPE: " + event.getMediaType() +
* " | TIMESTAMP: " + System.currentTimeMillis();
*
* endpoint.flowInMedia.put(event.getSource().getName() + "/" +
* event.getMediaType(), event.getSource());
*
* String msg2;
*
* if (endpoint.flowInMedia.values().size() != 2) { msg2 =
* " THERE ARE LESS FLOW IN MEDIA'S THAN EXPECTED IN " +
* endpoint.getEndpoint().getName() + " (" +
* endpoint.flowInMedia.values().size() + ")"; } else { msg2 =
* " NUMBER OF FLOW IN MEDIA'S IS NOW CORRECT IN " +
* endpoint.getEndpoint().getName() + " (" +
* endpoint.flowInMedia.values().size() + ")"; }
*
* log.debug(msg1); log.debug(msg2); this.infoHandler.sendInfo(msg1);
* this.infoHandler.sendInfo(msg2); });
*
* endpoint.getWebEndpoint().addMediaFlowOutStateChangeListener((event) -> {
* String msg1 = " Media flow out state change (" +
* endpoint.getEndpoint().getName() + ") -> " + "STATE: " +
* event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " +
* event.getPadName() + " | MEDIATYPE: " + event.getMediaType() +
* " | TIMESTAMP: " + System.currentTimeMillis();
*
* endpoint.flowOutMedia. @SuppressWarnings("unchecked")
* put(event.getSource().getName() + "/" + event.getMediaType(),
* event.getSource());
*
* String msg2;
*
* if (endpoint.flowOutMedia.values().size() != 2) { msg2 =
* " THERE ARE LESS FLOW OUT MEDIA'S THAN EXPECTED IN " +
* endpoint.getEndpoint().getName() + " (" +
* endpoint.flowOutMedia.values().size() + ")"; } else { msg2 =
* " NUMBER OF FLOW OUT MEDIA'S IS NOW CORRECT IN " +
* endpoint.getEndpoint().getName() + " (" +
* endpoint.flowOutMedia.values().size() + ")"; }
*
* log.debug(msg1); log.debug(msg2); this.infoHandler.sendInfo(msg1);
* this.infoHandler.sendInfo(msg2); });
*
* endpoint.getWebEndpoint().addMediaSessionStartedListener((event) -> { String
* msg = " Media session started (" +
* endpoint.getEndpoint().getName() + ") | TIMESTAMP: " +
@ -568,15 +536,8 @@ public class KurentoParticipant extends Participant {
*
* endpoint.getWebEndpoint().addMediaStateChangedListener((event) -> { String
* msg = " Media state changed (" +
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() +
* " to " + event.getNewState(); log.debug(msg); this.infoHandler.sendInfo(msg);
* });
*
* endpoint.getWebEndpoint().addConnectionStateChangedListener((event) -> {
* String msg = " Connection state changed (" +
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() +
* " to " + event.getNewState() + " | TIMESTAMP: " + System.currentTimeMillis();
* log.debug(msg); this.infoHandler.sendInfo(msg); });
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() + " to " +
* event.getNewState(); log.debug(msg); this.infoHandler.sendInfo(msg); });
*
* endpoint.getWebEndpoint().addIceCandidateFoundListener((event) -> { String
* msg = " ICE CANDIDATE FOUND (" +
@ -584,38 +545,13 @@ public class KurentoParticipant extends Participant {
* event.getCandidate().getCandidate() + " | TIMESTAMP: " +
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
* });
*
* endpoint.getWebEndpoint().addIceComponentStateChangeListener((event) -> {
* String msg = " ICE COMPONENT STATE CHANGE (" +
* endpoint.getEndpoint().getName() + "): for component " +
* event.getComponentId() + " - STATE: " + event.getState() + " | TIMESTAMP: " +
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
* });
*
* endpoint.getWebEndpoint().addIceGatheringDoneListener((event) -> { String msg
* = " ICE GATHERING DONE! (" +
* endpoint.getEndpoint().getName() + ")" + " | TIMESTAMP: " +
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
* });
*/
endpoint.getWebEndpoint().addMediaFlowInStateChangeListener(event -> {
String msg1 = "Media flow in state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
/*
* endpoint.flowInMedia.put(event.getSource().getName(), event.getMediaType());
* if (endpoint.getPublisher().getMediaOptions().hasAudio() &&
* endpoint.getPublisher().getMediaOptions().hasVideo() &&
* endpoint.flowInMedia.values().size() == 2) {
*/
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
/*
* } else if (endpoint.flowInMedia.values().size() == 1) {
* endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt())); }
*/
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
log.info(msg1);
this.infoHandler.sendInfo(msg1);
});
@ -624,19 +560,7 @@ public class KurentoParticipant extends Participant {
String msg1 = "Media flow out state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
/*
* endpoint.flowOutMedia.put(event.getSource().getName(), event.getMediaType());
* if (endpoint.getPublisher().getMediaOptions().hasAudio() &&
* endpoint.getPublisher().getMediaOptions().hasVideo() &&
* endpoint.flowOutMedia.values().size() == 2) {
*/
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
/*
* } else if (endpoint.flowOutMedia.values().size() == 1) {
* endpoint.kmsEvents.add(new KmsEvent(event)); }
*/
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
log.info(msg1);
this.infoHandler.sendInfo(msg1);
});
@ -661,7 +585,7 @@ public class KurentoParticipant extends Participant {
});
endpoint.getEndpoint().addMediaTranscodingStateChangeListener(event -> {
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
});
endpoint.getWebEndpoint().addIceComponentStateChangeListener(event -> {

View File

@ -47,6 +47,7 @@ import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session;
import io.openvidu.server.recording.service.RecordingManager;
/**
* @author Pablo Fuente (pablofuenteperez@gmail.com)
@ -57,6 +58,7 @@ public class KurentoSession implements Session {
public static final int ASYNC_LATCH_TIMEOUT = 30;
private OpenviduConfig openviduConfig;
private RecordingManager recordingManager;
private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>();
private String sessionId;
@ -85,7 +87,8 @@ public class KurentoSession implements Session {
public KurentoSession(String sessionId, Long startTime, SessionProperties sessionProperties,
KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler,
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig) {
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig,
RecordingManager recordingManager) {
this.sessionId = sessionId;
this.sessionProperties = sessionProperties;
this.kurentoClient = kurentoClient;
@ -93,6 +96,7 @@ public class KurentoSession implements Session {
this.kurentoSessionHandler = kurentoSessionHandler;
this.CDR = CDR;
this.openviduConfig = openviduConfig;
this.recordingManager = recordingManager;
this.startTime = startTime;
log.debug("New SESSION instance with id '{}'", sessionId);
}
@ -113,7 +117,7 @@ public class KurentoSession implements Session {
createPipeline();
KurentoParticipant kurentoParticipant = new KurentoParticipant(participant, this, getPipeline(),
kurentoSessionHandler.getInfoHandler(), this.CDR, this.openviduConfig);
kurentoSessionHandler.getInfoHandler(), this.CDR, this.openviduConfig, this.recordingManager);
participants.put(participant.getParticipantPrivateId(), kurentoParticipant);
filterStates.forEach((filterId, state) -> {

View File

@ -170,25 +170,37 @@ public class KurentoSessionManager extends SessionManager {
reason);
if (remainingParticipants.isEmpty()) {
log.info("No more participants in session '{}', removing it and closing it", sessionId);
this.closeSessionAndEmptyCollections(session, reason);
showTokens();
if (openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)) {
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
// a Publisher starts before timeout)
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
sessionId);
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
} else {
log.info("No more participants in session '{}', removing it and closing it", sessionId);
this.closeSessionAndEmptyCollections(session, reason);
showTokens();
}
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
// Immediately stop recording when last real participant left if
// RecordingMode.ALWAYS
log.info("Last participant left. Stopping recording for session {}", sessionId);
recordingService.stopRecording(session, null, reason);
recordingManager.stopRecording(session, null, reason);
evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null,
null, "EVICT_RECORDER");
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
// a Publisher starts before timeout)
log.info("Last participant left. Starting countdown for stopping recording of session {}", sessionId);
recordingService.initAutomaticRecordingStopThread(session.getSessionId());
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
sessionId);
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
}
}
@ -277,20 +289,21 @@ public class KurentoSessionManager extends SessionManager {
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
&& session.getActivePublishers() == 0) {
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
&& !recordingService.sessionIsBeingRecorded(session.getSessionId())) {
&& !recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
// Insecure session recording
new Thread(() -> {
recordingService.startRecording(session,
recordingManager.startRecording(session,
new RecordingProperties.Builder().name("")
.outputMode(io.openvidu.java.client.Recording.OutputMode.COMPOSED)
.recordingLayout(session.getSessionProperties().defaultRecordingLayout())
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
}).start();
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())
&& recordingService.sessionIsBeingRecorded(session.getSessionId())) {
&& recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
// Abort automatic recording stop (user published before timeout)
log.info("Participant {} published before timeout finished. Aborting automatic recording stop",
participant.getParticipantPublicId());
boolean stopAborted = recordingService.abortAutomaticRecordingStopThread(session.getSessionId());
boolean stopAborted = recordingManager.abortAutomaticRecordingStopThread(session.getSessionId());
if (stopAborted) {
log.info("Automatic recording stopped succesfully aborted");
} else {
@ -491,7 +504,7 @@ public class KurentoSessionManager extends SessionManager {
this.kurentoClient = kcProvider.getKurentoClient(kcSessionInfo);
session = new KurentoSession(sessionId, this.sessionCreationTime.get(sessionId), sessionProperties,
kurentoClient, kurentoSessionEventsHandler, kcProvider.destroyWhenUnused(), this.CDR,
this.openviduConfig);
this.openviduConfig, this.recordingManager);
KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session);
if (oldSession != null) {

View File

@ -1,3 +1,20 @@
/*
* (C) Copyright 2017-2018 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.kurento.endpoint;
import org.kurento.client.MediaEvent;

View File

@ -0,0 +1,32 @@
/*
* (C) Copyright 2017-2018 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.kurento.endpoint;
import org.kurento.client.MediaEvent;
import org.kurento.client.MediaType;
public class KmsMediaEvent extends KmsEvent {
MediaType mediaType;
public KmsMediaEvent(MediaEvent event, MediaType mediaType, long createdAt) {
super(event, createdAt);
this.mediaType = mediaType;
}
}

View File

@ -19,9 +19,7 @@ package io.openvidu.server.kurento.endpoint;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
@ -32,7 +30,6 @@ import org.kurento.client.IceCandidate;
import org.kurento.client.ListenerSubscription;
import org.kurento.client.MediaElement;
import org.kurento.client.MediaPipeline;
import org.kurento.client.MediaType;
import org.kurento.client.OnIceCandidateEvent;
import org.kurento.client.RtpEndpoint;
import org.kurento.client.SdpEndpoint;
@ -85,9 +82,6 @@ public abstract class MediaEndpoint {
private final List<IceCandidate> receivedCandidateList = new LinkedList<IceCandidate>();
private LinkedList<IceCandidate> candidates = new LinkedList<IceCandidate>();
public Map<String, MediaType> flowInMedia = new ConcurrentHashMap<>();
public Map<String, MediaType> flowOutMedia = new ConcurrentHashMap<>();
public String selectedLocalIceCandidate;
public String selectedRemoteIceCandidate;
public Queue<KmsEvent> kmsEvents = new ConcurrentLinkedQueue<>();

View File

@ -18,19 +18,42 @@
package io.openvidu.server.kurento.kms;
import org.kurento.client.KurentoClient;
import io.openvidu.server.kurento.kms.Kms;
import io.openvidu.server.kurento.kms.KmsManager;
import org.kurento.client.KurentoConnectionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FixedOneKmsManager extends KmsManager {
public FixedOneKmsManager(String kmsWsUri) {
this(kmsWsUri, 1);
}
private static final Logger log = LoggerFactory.getLogger(FixedOneKmsManager.class);
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
for (int i = 0; i < numKmss; i++) {
this.addKms(new Kms(KurentoClient.create(kmsWsUri), kmsWsUri));
}
}
public FixedOneKmsManager(String kmsWsUri) {
this(kmsWsUri, 1);
}
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
for (int i = 0; i < numKmss; i++) {
this.addKms(new Kms(KurentoClient.create(kmsWsUri, new KurentoConnectionListener() {
@Override
public void reconnected(boolean isReconnected) {
log.warn("Kurento Client reconnected ({}) to KMS with uri {}", isReconnected, kmsWsUri);
}
@Override
public void disconnected() {
log.warn("Kurento Client disconnected from KMS with uri {}", kmsWsUri);
}
@Override
public void connectionFailed() {
log.warn("Kurento Client failed connecting to KMS with uri {}", kmsWsUri);
}
@Override
public void connected() {
log.warn("Kurento Client is now connected to KMS with uri {}", kmsWsUri);
}
}), kmsWsUri));
}
}
}

View File

@ -0,0 +1,131 @@
/*
* (C) Copyright 2017-2018 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.recording;
import org.kurento.client.RecorderEndpoint;
import com.google.gson.JsonObject;
public class RecorderEndpointWrapper {
RecorderEndpoint recorder;
String connectionId;
String recordingId;
String streamId;
String clientData;
String serverData;
boolean hasAudio;
boolean hasVideo;
String typeOfVideo;
long startTime;
long endTime;
long size;
public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId,
String clientData, String serverData, boolean hasAudio, boolean hasVideo, String typeOfVideo) {
this.recorder = recorder;
this.connectionId = connectionId;
this.recordingId = recordingId;
this.streamId = streamId;
this.clientData = clientData;
this.serverData = serverData;
this.hasAudio = hasAudio;
this.hasVideo = hasVideo;
this.typeOfVideo = typeOfVideo;
}
public RecorderEndpoint getRecorder() {
return recorder;
}
public String getConnectionId() {
return connectionId;
}
public String getRecordingId() {
return recordingId;
}
public String getStreamId() {
return streamId;
}
public String getClientData() {
return clientData;
}
public String getServerData() {
return serverData;
}
public long getStartTime() {
return startTime;
}
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public long getEndTime() {
return endTime;
}
public void setEndTime(long endTime) {
this.endTime = endTime;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public boolean hasAudio() {
return hasAudio;
}
public boolean hasVideo() {
return hasVideo;
}
public String getTypeOfVideo() {
return typeOfVideo;
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
json.addProperty("connectionId", this.connectionId);
json.addProperty("streamId", this.streamId);
json.addProperty("clientData", this.clientData);
json.addProperty("serverData", this.serverData);
json.addProperty("startTime", this.startTime);
json.addProperty("endTime", this.endTime);
json.addProperty("duration", this.endTime - this.startTime);
json.addProperty("size", this.size);
json.addProperty("hasAudio", this.hasAudio);
json.addProperty("hasVideo", this.hasVideo);
if (this.hasVideo) {
json.addProperty("typeOfVideo", this.typeOfVideo);
}
return json;
}
}

View File

@ -24,16 +24,7 @@ import io.openvidu.java.client.RecordingProperties;
public class Recording {
public enum Status {
starting, // The recording is starting (cannot be stopped)
started, // The recording has started and is going on
stopped, // The recording has finished OK
available, // The recording is available for downloading. This status is reached for all
// stopped recordings if property 'openvidu.recording.public-access' is true
failed; // The recording has failed
}
private Recording.Status status;
private io.openvidu.java.client.Recording.Status status;
private String id;
private String sessionId;
@ -49,7 +40,7 @@ public class Recording {
this.sessionId = sessionId;
this.createdAt = System.currentTimeMillis();
this.id = id;
this.status = Status.started;
this.status = io.openvidu.java.client.Recording.Status.started;
this.recordingProperties = recordingProperties;
}
@ -70,16 +61,27 @@ public class Recording {
}
this.hasAudio = json.get("hasAudio").getAsBoolean();
this.hasVideo = json.get("hasVideo").getAsBoolean();
this.status = Status.valueOf(json.get("status").getAsString());
this.recordingProperties = new RecordingProperties.Builder().name(json.get("name").getAsString())
.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString())).build();
this.status = io.openvidu.java.client.Recording.Status.valueOf(json.get("status").getAsString());
io.openvidu.java.client.Recording.OutputMode outputMode = io.openvidu.java.client.Recording.OutputMode
.valueOf(json.get("outputMode").getAsString());
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
.outputMode(outputMode);
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode)) {
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
builder.recordingLayout(recordingLayout);
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
builder.customLayout(json.get("customLayout").getAsString());
}
}
this.recordingProperties = builder.build();
}
public Status getStatus() {
public io.openvidu.java.client.Recording.Status getStatus() {
return status;
}
public void setStatus(Status status) {
public void setStatus(io.openvidu.java.client.Recording.Status status) {
this.status = status;
}
@ -95,6 +97,10 @@ public class Recording {
return this.recordingProperties.name();
}
public io.openvidu.java.client.Recording.OutputMode getOutputMode() {
return this.recordingProperties.outputMode();
}
public RecordingLayout getRecordingLayout() {
return this.recordingProperties.recordingLayout();
}
@ -103,6 +109,10 @@ public class Recording {
return this.recordingProperties.customLayout();
}
public RecordingProperties getRecordingProperties() {
return this.recordingProperties;
}
public String getSessionId() {
return sessionId;
}
@ -163,9 +173,12 @@ public class Recording {
JsonObject json = new JsonObject();
json.addProperty("id", this.id);
json.addProperty("name", this.recordingProperties.name());
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
json.addProperty("customLayout", this.recordingProperties.customLayout());
json.addProperty("outputMode", this.getOutputMode().name());
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recordingProperties.outputMode())) {
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
json.addProperty("customLayout", this.recordingProperties.customLayout());
}
}
json.addProperty("sessionId", this.sessionId);
json.addProperty("createdAt", this.createdAt);

View File

@ -17,6 +17,7 @@
package io.openvidu.server.recording;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
@ -38,35 +39,35 @@ public class RecordingInfoUtils {
private JsonObject videoStream;
private JsonObject audioStream;
public RecordingInfoUtils(String fullVideoPath) throws FileNotFoundException, IOException, OpenViduException {
private String infoFilePath;
public RecordingInfoUtils(String infoFilePath) throws FileNotFoundException, IOException, OpenViduException {
this.infoFilePath = infoFilePath;
this.parser = new JsonParser();
try {
this.json = parser.parse(new FileReader(fullVideoPath)).getAsJsonObject();
this.json = parser.parse(new FileReader(infoFilePath)).getAsJsonObject();
} catch (JsonIOException | JsonSyntaxException e) {
// Recording metadata from ffprobe is not a JSON: video file is corrupted
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is corrupted");
}
if (this.json.size() == 0) {
// Recording metadata from ffprobe is an emtpy JSON
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty");
}
this.jsonFormat = json.get("format").getAsJsonObject();
JsonArray streams = json.get("streams").getAsJsonArray();
for (int i = 0; i < streams.size(); i++) {
JsonObject stream = streams.get(i).getAsJsonObject();
if ("video".equals(stream.get("codec_type").toString())) {
if ("video".equals(stream.get("codec_type").getAsString())) {
this.videoStream = stream;
} else if ("audio".equals(stream.get("codec_type").toString())) {
} else if ("audio".equals(stream.get("codec_type").getAsString())) {
this.audioStream = stream;
}
}
}
public double getDurationInSeconds() {
@ -124,4 +125,8 @@ public class RecordingInfoUtils {
return audioStream.get("codec_long_name").toString();
}
public boolean deleteFilePath() {
return new File(this.infoFilePath).delete();
}
}

View File

@ -15,35 +15,22 @@
*
*/
package io.openvidu.server.recording;
package io.openvidu.server.recording.service;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.ProcessingException;
import org.apache.commons.io.FilenameUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
@ -60,8 +47,6 @@ import com.github.dockerjava.core.DockerClientBuilder;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.command.ExecStartResultCallback;
import com.github.dockerjava.core.command.PullImageResultCallback;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
@ -70,58 +55,42 @@ import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.OpenViduServer;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionEventsHandler;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingInfoUtils;
import io.openvidu.server.utils.CommandExecutor;
@Service
public class ComposedRecordingService {
public class ComposedRecordingService extends RecordingService {
private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
@Autowired
private OpenviduConfig openviduConfig;
@Autowired
private SessionEventsHandler sessionHandler;
private Map<String, String> containers = new ConcurrentHashMap<>();
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
private Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
private final Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
Runtime.getRuntime().availableProcessors());
private final String IMAGE_NAME = "openvidu/openvidu-recording";
private String IMAGE_TAG;
private final String RECORDING_ENTITY_FILE = ".recording.";
private DockerClient dockerClient;
public ComposedRecordingService() {
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
super(recordingManager, openviduConfig);
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
this.dockerClient = DockerClientBuilder.getInstance(config).build();
}
public Recording startRecording(Session session, RecordingProperties properties) {
@Override
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
List<String> envs = new ArrayList<>();
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
session.getSessionId().length());
String recordingId = this.getFreeRecordingId(session.getSessionId(), shortSessionId);
if (properties.name() == null || properties.name().isEmpty()) {
// No name provided for the recording file
properties = new RecordingProperties.Builder().name(recordingId)
.recordingLayout(properties.recordingLayout()).customLayout(properties.customLayout()).build();
}
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
properties);
properties = updatePropertiesAndRecordingId.properties;
String recordingId = updatePropertiesAndRecordingId.recordingId;
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
this.sessionsRecordings.put(session.getSessionId(), recording);
this.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
this.startingRecordings.put(recording.getId(), recording);
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
this.recordingManager.startingRecordings.put(recording.getId(), recording);
String uid = null;
try {
@ -133,7 +102,7 @@ public class ComposedRecordingService {
e.printStackTrace();
}
String layoutUrl = this.getLayoutUrl(recording, shortSessionId);
String layoutUrl = this.getLayoutUrl(recording, this.getShortSessionId(session));
envs.add("URL=" + layoutUrl);
envs.add("RESOLUTION=1920x1080");
@ -147,37 +116,38 @@ public class ComposedRecordingService {
log.info(recording.toJson().toString());
log.info("Recorder connecting to url {}", layoutUrl);
String containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
String containerId;
try {
containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
} catch (Exception e) {
this.cleanRecordingMapsAndReturnContainerId(recording);
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
"Couldn't initialize recording container. Error: " + e.getMessage());
}
this.waitForVideoFileNotEmpty(properties.name());
this.waitForVideoFileNotEmpty(recording);
this.sessionsContainers.put(session.getSessionId(), containerId);
recording.setStatus(Recording.Status.started);
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
this.startedRecordings.put(recording.getId(), recording);
this.startingRecordings.remove(recording.getId());
this.recordingManager.startedRecordings.put(recording.getId(), recording);
this.recordingManager.startingRecordings.remove(recording.getId());
return recording;
}
public Recording stopRecording(Session session, String recordingId, String reason) {
Recording recording;
String containerId;
@Override
public Recording stopRecording(Session session, Recording recording, String reason) {
String containerId = cleanRecordingMapsAndReturnContainerId(recording);
final String recordingId = recording.getId();
if (session == null) {
log.warn(
"Existing recording {} does not have an active session associated. This usually means the recording"
+ " layout did not join a recorded participant or the recording has been automatically"
+ " stopped after last user left and timeout passed",
recordingId);
recording = this.startedRecordings.remove(recordingId);
containerId = this.sessionsContainers.remove(recording.getSessionId());
this.sessionsRecordings.remove(recording.getSessionId());
} else {
recording = this.sessionsRecordings.remove(session.getSessionId());
containerId = this.sessionsContainers.remove(session.getSessionId());
this.startedRecordings.remove(recording.getId());
recording.getId());
}
if (containerId == null) {
@ -205,8 +175,9 @@ public class ComposedRecordingService {
containerClosed = true;
log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux,
session.getSessionId());
log.warn("Deleting unusable files for recording {}", recording.getId());
if (HttpStatus.NO_CONTENT.equals(this.deleteRecordingFromHost(recording.getId(), true))) {
log.warn("Deleting unusable files for recording {}", recordingId);
if (HttpStatus.NO_CONTENT
.equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) {
log.warn("Files properly deleted");
}
}
@ -234,12 +205,12 @@ public class ComposedRecordingService {
try {
stopped = latch.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
recording.setStatus(Recording.Status.failed);
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process has been unexpectedly interrupted"));
}
if (!stopped) {
recording.setStatus(Recording.Status.failed);
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process couldn't finish in 60 seconds"));
}
@ -250,28 +221,25 @@ public class ComposedRecordingService {
// Update recording attributes reading from video report file
try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + ".info");
this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + recordingId + ".info");
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
recording.setStatus(Recording.Status.available);
} else {
recording.setStatus(Recording.Status.stopped);
}
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
recording.setDuration(infoUtils.getDurationInSeconds());
recording.setSize(infoUtils.getSizeInBytes());
recording.setHasAudio(infoUtils.hasAudio());
recording.setHasVideo(infoUtils.hasVideo());
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
}
infoUtils.deleteFilePath();
recording = this.recordingManager.updateRecordingUrl(recording);
} catch (IOException e) {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
"There was an error generating the metadata report file for the recording");
}
if (session != null) {
this.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
}
}
return recording;
@ -306,19 +274,7 @@ public class ComposedRecordingService {
}
}
public boolean sessionIsBeingRecorded(String sessionId) {
return (this.sessionsRecordings.get(sessionId) != null);
}
public Recording getStartedRecording(String recordingId) {
return this.startedRecordings.get(recordingId);
}
public Recording getStartingRecording(String recordingId) {
return this.startingRecordings.get(recordingId);
}
private String runRecordingContainer(List<String> envs, String containerName) {
private String runRecordingContainer(List<String> envs, String containerName) throws Exception {
Volume volume1 = new Volume("/recordings");
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
@ -334,7 +290,10 @@ public class ComposedRecordingService {
log.error(
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
containerName);
return null;
throw e;
} catch (NotFoundException e) {
log.error("Docker image {} couldn't be found in docker host", IMAGE_NAME + ":" + IMAGE_TAG);
throw e;
}
}
@ -358,159 +317,13 @@ public class ComposedRecordingService {
return imageExists;
}
public Collection<Recording> getAllRecordings() {
return this.getAllRecordingsFromHost();
}
public Collection<Recording> getStartingRecordings() {
return this.startingRecordings.values();
}
public Collection<Recording> getStartedRecordings() {
return this.startedRecordings.values();
}
public Collection<Recording> getFinishedRecordings() {
return this.getAllRecordingsFromHost().stream()
.filter(recording -> (recording.getStatus().equals(Recording.Status.stopped)
|| recording.getStatus().equals(Recording.Status.available)))
.collect(Collectors.toSet());
}
public File initRecordingPath() throws OpenViduException {
try {
Path path = Files.createDirectories(Paths.get(this.openviduConfig.getOpenViduRecordingPath()));
if (!Files.isWritable(path)) {
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath()
+ "' is not valid. Reason: OpenVidu Server process needs write permissions");
}
log.info("Recording path: {}", this.openviduConfig.getOpenViduRecordingPath());
return path.toFile();
} catch (IOException e) {
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath() + "' is not valid. Reason: "
+ e.getClass().getName());
}
}
private Recording getRecordingFromHost(String recordingId) {
log.info(this.openviduConfig.getOpenViduRecordingPath() + RECORDING_ENTITY_FILE + recordingId);
File file = new File(this.openviduConfig.getOpenViduRecordingPath() + RECORDING_ENTITY_FILE + recordingId);
log.info("File exists: " + file.exists());
return this.getRecordingFromEntityFile(file);
}
private Set<Recording> getAllRecordingsFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
if (files == null) {
files = initRecordingPath().listFiles();
}
Set<Recording> recordingEntities = new HashSet<>();
for (int i = 0; i < files.length; i++) {
Recording recording = this.getRecordingFromEntityFile(files[i]);
if (recording != null) {
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
if (Recording.Status.stopped.equals(recording.getStatus())) {
recording.setStatus(Recording.Status.available);
recording.setUrl(
this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
}
}
recordingEntities.add(recording);
}
}
return recordingEntities;
}
private Set<String> getRecordingIdsFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
if (files == null) {
files = initRecordingPath().listFiles();
}
Set<String> fileNamesNoExtension = new HashSet<>();
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && !files[i].getName().startsWith(RECORDING_ENTITY_FILE)) {
fileNamesNoExtension.add(FilenameUtils.removeExtension(files[i].getName()));
}
}
return fileNamesNoExtension;
}
public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {
if (!force && (this.startedRecordings.containsKey(recordingId)
|| this.startingRecordings.containsKey(recordingId))) {
// Cannot delete an active recording
return HttpStatus.CONFLICT;
}
Recording recording = getRecordingFromHost(recordingId);
if (recording == null) {
return HttpStatus.NOT_FOUND;
}
String name = getRecordingFromHost(recordingId).getName();
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && isFileFromRecording(files[i], recordingId, name)) {
files[i].delete();
}
}
return HttpStatus.NO_CONTENT;
}
private Recording getRecordingFromEntityFile(File file) {
if (file.isFile() && file.getName().startsWith(RECORDING_ENTITY_FILE)) {
JsonObject json = null;
try {
json = new JsonParser().parse(new FileReader(file)).getAsJsonObject();
} catch (IOException e) {
return null;
}
return new Recording(json);
}
return null;
}
private boolean isFileFromRecording(File file, String recordingId, String recordingName) {
return (((recordingId + ".info").equals(file.getName()))
|| ((RECORDING_ENTITY_FILE + recordingId).equals(file.getName()))
|| (recordingName + ".mp4").equals(file.getName()) || (recordingId + ".jpg").equals(file.getName()));
}
private String getFreeRecordingId(String sessionId, String shortSessionId) {
Set<String> recordingIds = this.getRecordingIdsFromHost();
String recordingId = shortSessionId;
boolean isPresent = recordingIds.contains(recordingId);
int i = 1;
while (isPresent) {
recordingId = shortSessionId + "-" + i;
i++;
isPresent = recordingIds.contains(recordingId);
}
return recordingId;
}
private void waitForVideoFileNotEmpty(String videoName) {
private void waitForVideoFileNotEmpty(Recording recording) {
boolean isPresent = false;
while (!isPresent) {
try {
Thread.sleep(150);
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + videoName + ".mp4");
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
+ recording.getName() + ".mp4");
isPresent = ((f.isFile()) && (f.length() > 0));
} catch (InterruptedException e) {
e.printStackTrace();
@ -547,23 +360,14 @@ public class ComposedRecordingService {
return finalUrl;
}
public void setRecordingVersion(String version) {
private String cleanRecordingMapsAndReturnContainerId(Recording recording) {
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
this.recordingManager.startedRecordings.remove(recording.getId());
return this.sessionsContainers.remove(recording.getSessionId());
}
public void setRecordingContainerVersion(String version) {
this.IMAGE_TAG = version;
}
public void initAutomaticRecordingStopThread(String sessionId) {
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
this.stopRecording(null, recordingId, "lastParticipantLeft");
this.automaticRecordingStopThreads.remove(sessionId);
}, 2, TimeUnit.MINUTES);
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
}
public boolean abortAutomaticRecordingStopThread(String sessionId) {
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(sessionId);
return future.cancel(false);
}
}

View File

@ -0,0 +1,441 @@
/*
* (C) Copyright 2017-2018 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.recording.service;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.ws.rs.ProcessingException;
import org.kurento.client.MediaProfileSpecType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionEventsHandler;
import io.openvidu.server.recording.Recording;
public class RecordingManager {
private static final Logger log = LoggerFactory.getLogger(RecordingManager.class);
RecordingService recordingService;
private ComposedRecordingService composedRecordingService;
private SingleStreamRecordingService singleStreamRecordingService;
@Autowired
protected SessionEventsHandler sessionHandler;
@Autowired
protected OpenviduConfig openviduConfig;
protected Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
protected Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
protected Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
private final Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
Runtime.getRuntime().availableProcessors());
static final String RECORDING_ENTITY_FILE = ".recording.";
private static final List<String> LAST_PARTICIPANT_LEFT_REASONS = Arrays.asList(
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
public SessionEventsHandler getSessionEventsHandler() {
return this.sessionHandler;
}
public void initializeRecordingManager() {
this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, openviduConfig);
ComposedRecordingService recServiceAux = this.composedRecordingService;
recServiceAux.setRecordingContainerVersion(openviduConfig.getOpenViduRecordingVersion());
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
boolean imageExists = false;
try {
imageExists = recServiceAux.recordingImageExistsLocally();
} catch (ProcessingException exception) {
String message = "Exception connecting to Docker daemon: ";
if ("docker".equals(openviduConfig.getSpringProfile())) {
final String NEW_LINE = System.getProperty("line.separator");
message += "make sure you include the following flags in your \"docker run\" command:" + NEW_LINE
+ " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE
+ " -e MY_UID=$(id -u $USER)" + NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock"
+ NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE;
} else {
message += "you need Docker installed in this machine to enable OpenVidu recording service";
}
log.error(message);
throw new RuntimeException(message);
}
if (imageExists) {
log.info("Docker image already exists locally");
} else {
Thread t = new Thread(() -> {
boolean keep = true;
log.info("Downloading ");
while (keep) {
System.out.print(".");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
keep = false;
log.info("\nDownload complete");
}
}
});
t.start();
recServiceAux.downloadRecordingImage();
t.interrupt();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("Docker image available");
}
this.initRecordingPath();
this.recordingService = recServiceAux;
}
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
Recording recording = null;
try {
switch (properties.outputMode()) {
case COMPOSED:
recording = this.composedRecordingService.startRecording(session, properties);
break;
case INDIVIDUAL:
recording = this.singleStreamRecordingService.startRecording(session, properties);
break;
}
} catch (OpenViduException e) {
throw e;
}
if (session.getActivePublishers() == 0) {
// Init automatic recording stop if there are now publishers when starting
// recording
this.initAutomaticRecordingStopThread(session.getSessionId());
}
return recording;
}
public Recording stopRecording(Session session, String recordingId, String reason) {
Recording recording;
if (session == null) {
recording = this.startedRecordings.remove(recordingId);
} else {
recording = this.sessionsRecordings.remove(session.getSessionId());
}
switch (recording.getOutputMode()) {
case COMPOSED:
recording = this.composedRecordingService.stopRecording(session, recording, reason);
break;
case INDIVIDUAL:
recording = this.singleStreamRecordingService.stopRecording(session, recording, reason);
break;
}
return recording;
}
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
Participant participant) {
Recording recording = this.sessionsRecordings.get(session.getSessionId());
if (recording == null) {
log.error("Cannot start recording of new stream {}. Session {} is not being recorded",
participant.getPublisherStreamId(), session.getSessionId());
}
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
final CountDownLatch startedCountDown = new CountDownLatch(1);
this.singleStreamRecordingService.startOneIndividualStreamRecording(session, recordingId, profile,
participant, startedCountDown);
}
}
public void stopOneIndividualStreamRecording(String sessionId, String streamId) {
Recording recording = this.sessionsRecordings.get(sessionId);
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
this.singleStreamRecordingService.stopOneIndividualStreamRecording(sessionId, streamId, stoppedCountDown);
try {
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
sessionId);
}
} catch (InterruptedException e) {
log.error("Exception while waiting for state change", e);
}
}
}
public boolean sessionIsBeingRecorded(String sessionId) {
return (this.sessionsRecordings.get(sessionId) != null);
}
public boolean sessionIsBeingRecordedIndividual(String sessionId) {
Recording rec = this.sessionsRecordings.get(sessionId);
return (rec != null && io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(rec.getOutputMode()));
}
public boolean sessionIsBeingRecordedComposed(String sessionId) {
Recording rec = this.sessionsRecordings.get(sessionId);
return (rec != null && io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(rec.getOutputMode()));
}
public Recording getStartedRecording(String recordingId) {
return this.startedRecordings.get(recordingId);
}
public Recording getStartingRecording(String recordingId) {
return this.startingRecordings.get(recordingId);
}
public Collection<Recording> getFinishedRecordings() {
return this.getAllRecordingsFromHost().stream()
.filter(recording -> (recording.getStatus().equals(io.openvidu.java.client.Recording.Status.stopped)
|| recording.getStatus().equals(io.openvidu.java.client.Recording.Status.available)))
.collect(Collectors.toSet());
}
public Recording getRecording(String recordingId) {
return this.getRecordingFromHost(recordingId);
}
public Collection<Recording> getAllRecordings() {
return this.getAllRecordingsFromHost();
}
public String getFreeRecordingId(String sessionId, String shortSessionId) {
Set<String> recordingIds = this.getRecordingIdsFromHost();
String recordingId = shortSessionId;
boolean isPresent = recordingIds.contains(recordingId);
int i = 1;
while (isPresent) {
recordingId = shortSessionId + "-" + i;
i++;
isPresent = recordingIds.contains(recordingId);
}
return recordingId;
}
public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {
if (!force && (this.startedRecordings.containsKey(recordingId)
|| this.startingRecordings.containsKey(recordingId))) {
// Cannot delete an active recording
return HttpStatus.CONFLICT;
}
Recording recording = getRecordingFromHost(recordingId);
if (recording == null) {
return HttpStatus.NOT_FOUND;
}
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory() && files[i].getName().equals(recordingId)) {
// Correct folder. Delete all content and the folder itself
File[] allContents = files[i].listFiles();
if (allContents != null) {
for (File file : allContents) {
file.delete();
}
}
files[i].delete();
break;
}
}
return HttpStatus.NO_CONTENT;
}
public Recording getRecordingFromEntityFile(File file) {
if (file.isFile() && file.getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
JsonObject json = null;
try {
json = new JsonParser().parse(new FileReader(file)).getAsJsonObject();
} catch (IOException e) {
return null;
}
return new Recording(json);
}
return null;
}
public void initAutomaticRecordingStopThread(String sessionId) {
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
this.stopRecording(null, recordingId, "lastParticipantLeft");
this.automaticRecordingStopThreads.remove(sessionId);
}, 2, TimeUnit.MINUTES);
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
}
public boolean abortAutomaticRecordingStopThread(String sessionId) {
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(sessionId);
if (future != null) {
return future.cancel(false);
} else {
return true;
}
}
public Recording updateRecordingUrl(Recording recording) {
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
if (io.openvidu.java.client.Recording.Status.stopped.equals(recording.getStatus())) {
String extension;
switch (recording.getOutputMode()) {
case COMPOSED:
extension = "mp4";
break;
case INDIVIDUAL:
extension = "zip";
break;
default:
extension = "mp4";
}
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/"
+ recording.getName() + "." + extension);
recording.setStatus(io.openvidu.java.client.Recording.Status.available);
}
}
return recording;
}
private Recording getRecordingFromHost(String recordingId) {
log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
File file = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
log.info("File exists: " + file.exists());
Recording recording = this.getRecordingFromEntityFile(file);
if (recording != null) {
this.updateRecordingUrl(recording);
}
return recording;
}
private Set<Recording> getAllRecordingsFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
Set<Recording> recordingEntities = new HashSet<>();
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
File[] innerFiles = files[i].listFiles();
for (int j = 0; j < innerFiles.length; j++) {
Recording recording = this.getRecordingFromEntityFile(innerFiles[j]);
if (recording != null) {
this.updateRecordingUrl(recording);
recordingEntities.add(recording);
}
}
}
}
return recordingEntities;
}
private Set<String> getRecordingIdsFromHost() {
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
Set<String> fileNamesNoExtension = new HashSet<>();
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
File[] innerFiles = files[i].listFiles();
for (int j = 0; j < innerFiles.length; j++) {
if (innerFiles[j].isFile()
&& innerFiles[j].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
fileNamesNoExtension
.add(innerFiles[j].getName().replaceFirst(RecordingManager.RECORDING_ENTITY_FILE, ""));
break;
}
}
}
}
return fileNamesNoExtension;
}
private File initRecordingPath() throws OpenViduException {
try {
Path path = Files.createDirectories(Paths.get(this.openviduConfig.getOpenViduRecordingPath()));
if (!Files.isWritable(path)) {
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath()
+ "' is not valid. Reason: OpenVidu Server process needs write permissions");
}
log.info("Recording path: {}", this.openviduConfig.getOpenViduRecordingPath());
return path.toFile();
} catch (IOException e) {
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath() + "' is not valid. Reason: "
+ e.getClass().getName());
}
}
public static String finalReason(String reason) {
if (RecordingManager.LAST_PARTICIPANT_LEFT_REASONS.contains(reason)) {
return "lastParticipantLeft";
} else {
return reason;
}
}
}

View File

@ -0,0 +1,85 @@
/*
* (C) Copyright 2017-2018 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.recording.service;
import io.openvidu.client.OpenViduException;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Session;
import io.openvidu.server.recording.Recording;
public abstract class RecordingService {
protected OpenviduConfig openviduConfig;
protected RecordingManager recordingManager;
RecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
this.recordingManager = recordingManager;
this.openviduConfig = openviduConfig;
}
public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException;
public abstract Recording stopRecording(Session session, Recording recording, String reason);
protected RecordingProperties setFinalRecordingName(Session session, RecordingProperties properties) {
// TODO Auto-generated method stub
return null;
}
protected PropertiesRecordingId setFinalRecordingNameAndGetFreeRecordingId(Session session,
RecordingProperties properties) {
String recordingId = this.recordingManager.getFreeRecordingId(session.getSessionId(),
this.getShortSessionId(session));
if (properties.name() == null || properties.name().isEmpty()) {
// No name provided for the recording file. Use recordingId
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
.outputMode(properties.outputMode());
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(properties.outputMode())) {
builder.recordingLayout(properties.recordingLayout());
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
builder.customLayout(properties.customLayout());
}
}
properties = builder.build();
}
return new PropertiesRecordingId(properties, recordingId);
}
protected String getShortSessionId(Session session) {
return session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
session.getSessionId().length());
}
/**
* Simple wrapper for returning update RecordingProperties and a free
* recordingId when starting a new recording
*/
protected class PropertiesRecordingId {
RecordingProperties properties;
String recordingId;
PropertiesRecordingId(RecordingProperties properties, String recordingId) {
this.properties = properties;
this.recordingId = recordingId;
}
}
}

View File

@ -0,0 +1,456 @@
/*
* (C) Copyright 2017-2018 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.recording.service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FilenameUtils;
import org.kurento.client.ErrorEvent;
import org.kurento.client.EventListener;
import org.kurento.client.MediaPipeline;
import org.kurento.client.MediaProfileSpecType;
import org.kurento.client.MediaType;
import org.kurento.client.RecorderEndpoint;
import org.kurento.client.RecordingEvent;
import org.kurento.client.StoppedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session;
import io.openvidu.server.kurento.core.KurentoParticipant;
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
import io.openvidu.server.recording.RecorderEndpointWrapper;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.utils.CustomFileWriter;
public class SingleStreamRecordingService extends RecordingService {
private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class);
private Map<String, Map<String, RecorderEndpointWrapper>> recorders = new ConcurrentHashMap<>();
private CustomFileWriter fileWriter = new CustomFileWriter();
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
public SingleStreamRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
super(recordingManager, openviduConfig);
}
@Override
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
properties);
properties = updatePropertiesAndRecordingId.properties;
String recordingId = updatePropertiesAndRecordingId.recordingId;
recorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
final CountDownLatch recordingStartedCountdown = new CountDownLatch(session.getActivePublishers());
for (Participant p : session.getParticipants()) {
if (p.isStreaming()) {
MediaProfileSpecType profile = null;
try {
profile = generateMediaProfile(properties, p);
} catch (OpenViduException e) {
log.error(
"Cannot start single stream recorder for stream {} in session {}: {}. Skipping to next stream being published",
p.getPublisherStreamId(), session.getSessionId(), e.getMessage());
continue;
}
this.startOneIndividualStreamRecording(session, recordingId, profile, p, recordingStartedCountdown);
}
}
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
this.recordingManager.startingRecordings.put(recording.getId(), recording);
try {
if (!recordingStartedCountdown.await(5, TimeUnit.SECONDS)) {
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId());
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
this.recordingManager.startingRecordings.remove(recording.getId());
this.stopRecording(session, recording, null);
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
"Couldn't initialize some RecorderEndpoint");
}
} catch (InterruptedException e) {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
log.error("Exception while waiting for state change", e);
}
if (session.getActivePublishers() == 0) {
// Recording started for a session with some user connected but no publishers
// Must create recording root folder for storing metadata archive
this.fileWriter.createFolder(this.openviduConfig.getOpenViduRecordingPath() + recording.getId());
}
this.generateRecordingMetadataFile(recording);
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
this.recordingManager.startingRecordings.remove(recording.getId());
this.recordingManager.startedRecordings.put(recording.getId(), recording);
this.recordingManager.getSessionEventsHandler().sendRecordingStartedNotification(session, recording);
return recording;
}
@Override
public Recording stopRecording(Session session, Recording recording, String reason) {
final int numberOfActiveRecorders = recorders.get(recording.getSessionId()).size();
final CountDownLatch stoppedCountDown = new CountDownLatch(numberOfActiveRecorders);
for (RecorderEndpointWrapper wrapper : recorders.get(recording.getSessionId()).values()) {
this.stopOneIndividualStreamRecording(recording.getSessionId(), wrapper.getStreamId(), stoppedCountDown);
}
try {
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
log.error("Error waiting for some recorder endpoint to stop in session {}", recording.getSessionId());
}
} catch (InterruptedException e) {
log.error("Exception while waiting for state change", e);
}
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
this.recordingManager.startedRecordings.remove(recording.getId());
this.recorders.remove(recording.getSessionId());
recording = this.sealMetadataFiles(recording);
recording = this.recordingManager.updateRecordingUrl(recording);
if (reason != null && session != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
}
return recording;
}
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
Participant participant, CountDownLatch globalStartLatch) {
log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(),
session.getSessionId());
if (recordingId == null) {
// Stream is being recorded because is a new publisher in an ongoing recorded
// session. If recordingId is defined is because Stream is being recorded from
// "startRecording" method
Recording recording = this.recordingManager.sessionsRecordings.get(session.getSessionId());
recordingId = recording.getId();
try {
profile = generateMediaProfile(recording.getRecordingProperties(), participant);
} catch (OpenViduException e) {
log.error("Cannot start single stream recorder for stream {} in session {}: {}",
participant.getPublisherStreamId(), session.getSessionId(), e.getMessage());
return;
}
}
KurentoParticipant kurentoParticipant = (KurentoParticipant) participant;
MediaPipeline pipeline = kurentoParticipant.getPublisher().getPipeline();
RecorderEndpoint recorder = new RecorderEndpoint.Builder(pipeline,
"file://" + this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
+ participant.getPublisherStreamId() + ".webm").withMediaProfile(profile).build();
recorder.addRecordingListener(new EventListener<RecordingEvent>() {
@Override
public void onEvent(RecordingEvent event) {
recorders.get(session.getSessionId()).get(participant.getPublisherStreamId())
.setStartTime(System.currentTimeMillis());
log.info("Recording started event for stream {}", participant.getPublisherStreamId());
globalStartLatch.countDown();
}
});
recorder.addErrorListener(new EventListener<ErrorEvent>() {
@Override
public void onEvent(ErrorEvent event) {
log.error(event.getErrorCode() + " " + event.getDescription());
}
});
connectAccordingToProfile(kurentoParticipant.getPublisher(), recorder, profile);
RecorderEndpointWrapper wrapper = new RecorderEndpointWrapper(recorder, participant.getParticipantPublicId(),
recordingId, participant.getPublisherStreamId(), participant.getClientMetadata(),
participant.getServerMetadata(), kurentoParticipant.getPublisher().getMediaOptions().hasAudio(),
kurentoParticipant.getPublisher().getMediaOptions().hasVideo(),
kurentoParticipant.getPublisher().getMediaOptions().getTypeOfVideo());
recorders.get(session.getSessionId()).put(participant.getPublisherStreamId(), wrapper);
wrapper.getRecorder().record();
}
public void stopOneIndividualStreamRecording(String sessionId, String streamId, CountDownLatch globalStopLatch) {
log.info("Stopping single stream recorder for stream {} in session {}", streamId, sessionId);
RecorderEndpointWrapper wrapper = this.recorders.get(sessionId).remove(streamId);
if (wrapper != null) {
wrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
@Override
public void onEvent(StoppedEvent event) {
wrapper.setEndTime(System.currentTimeMillis());
generateIndividualMetadataFile(wrapper);
log.info("Recording stopped event for stream {}", streamId);
globalStopLatch.countDown();
}
});
wrapper.getRecorder().stop();
} else {
log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId);
}
}
private MediaProfileSpecType generateMediaProfile(RecordingProperties properties, Participant participant)
throws OpenViduException {
KurentoParticipant kParticipant = (KurentoParticipant) participant;
MediaProfileSpecType profile = null;
boolean streamHasAudio = kParticipant.getPublisher().getMediaOptions().hasAudio();
boolean streamHasVideo = kParticipant.getPublisher().getMediaOptions().hasVideo();
boolean propertiesHasAudio = properties.hasAudio();
boolean propertiesHasVideo = properties.hasVideo();
if (streamHasAudio) {
if (streamHasVideo) {
// Stream has both audio and video tracks
if (propertiesHasAudio) {
if (propertiesHasVideo) {
profile = MediaProfileSpecType.WEBM;
} else {
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
}
} else if (propertiesHasVideo) {
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
} else {
// ERROR: RecordingProperties set to not record audio nor video
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
"RecordingProperties set to \"hasVideo(false)\" and \"hasAudio(false)\"");
}
} else {
// Stream has audio track only
if (propertiesHasAudio) {
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
} else {
// ERROR: RecordingProperties set to video only but there's no video track
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
"RecordingProperties set to \"hasAudio(false)\" but stream is audio-only");
}
}
} else if (streamHasVideo) {
// Stream has video track only
if (propertiesHasVideo) {
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
} else {
// ERROR: RecordingProperties set to audio only but there's no audio track
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
"RecordingProperties set to \"hasVideo(false)\" but stream is video-only");
}
} else {
// ERROR: Stream has no track at all
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
"Stream has no track at all. Cannot be recorded");
}
return profile;
}
private void connectAccordingToProfile(PublisherEndpoint publisherEndpoint, RecorderEndpoint recorder,
MediaProfileSpecType profile) {
switch (profile) {
case WEBM:
publisherEndpoint.connect(recorder, MediaType.AUDIO);
publisherEndpoint.connect(recorder, MediaType.VIDEO);
break;
case WEBM_AUDIO_ONLY:
publisherEndpoint.connect(recorder, MediaType.AUDIO);
break;
case WEBM_VIDEO_ONLY:
publisherEndpoint.connect(recorder, MediaType.VIDEO);
break;
default:
throw new UnsupportedOperationException("Unsupported profile when single stream recording: " + profile);
}
}
private void generateRecordingMetadataFile(Recording recording) {
String filePath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
+ RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
String text = recording.toJson().toString();
this.fileWriter.createAndWriteFile(filePath, text);
}
private void generateIndividualMetadataFile(RecorderEndpointWrapper wrapper) {
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/";
File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm");
wrapper.setSize(videoFile.length());
String metadataFilePath = filesPath + INDIVIDUAL_STREAM_METADATA_FILE + wrapper.getStreamId();
String metadataFileContent = wrapper.toJson().toString();
this.fileWriter.createAndWriteFile(metadataFilePath, metadataFileContent);
}
private Recording sealMetadataFiles(Recording recording) {
// Must update recording "status" (to stopped), "duration" (min startTime of all
// individual recordings) and "size" (sum of all individual recordings size)
String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
String syncFilePath = folderPath + recording.getId() + ".json";
recording = this.recordingManager.getRecordingFromEntityFile(new File(metadataFilePath));
long minStartTime = Long.MAX_VALUE;
long maxEndTime = 0;
long accumulatedSize = 0;
File folder = new File(folderPath);
File[] files = folder.listFiles();
Reader reader = null;
Gson gson = new Gson();
// Sync metadata json object to store in "RECORDING_ID.json"
JsonObject json = new JsonObject();
json.addProperty("createdAt", recording.getCreatedAt());
json.addProperty("id", recording.getId());
json.addProperty("name", recording.getName());
json.addProperty("sessionId", recording.getSessionId());
JsonArray jsonArrayFiles = new JsonArray();
for (int i = 0; i < files.length; i++) {
if (files[i].isFile() && files[i].getName().startsWith(INDIVIDUAL_STREAM_METADATA_FILE)) {
try {
reader = new FileReader(files[i].getAbsolutePath());
} catch (FileNotFoundException e) {
log.error("Error reading file {}. Error: {}", files[i].getAbsolutePath(), e.getMessage());
}
RecorderEndpointWrapper wr = gson.fromJson(reader, RecorderEndpointWrapper.class);
minStartTime = Math.min(minStartTime, wr.getStartTime());
maxEndTime = Math.max(maxEndTime, wr.getEndTime());
accumulatedSize += wr.getSize();
JsonObject jsonFile = new JsonObject();
jsonFile.addProperty("connectionId", wr.getConnectionId());
jsonFile.addProperty("streamId", wr.getStreamId());
jsonFile.addProperty("size", wr.getSize());
jsonFile.addProperty("clientData", wr.getClientData());
jsonFile.addProperty("serverData", wr.getServerData());
jsonFile.addProperty("hasAudio", wr.hasAudio());
jsonFile.addProperty("hasVideo", wr.hasVideo());
if (wr.hasVideo()) {
jsonFile.addProperty("typeOfVideo", wr.getTypeOfVideo());
}
jsonFile.addProperty("startTimeOffset", wr.getStartTime() - recording.getCreatedAt());
jsonFile.addProperty("endTimeOffset", wr.getEndTime() - recording.getCreatedAt());
jsonArrayFiles.add(jsonFile);
}
}
json.add("files", jsonArrayFiles);
long duration = (maxEndTime - minStartTime) / 1000;
recording.setSize(accumulatedSize); // Size in bytes
recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString());
this.fileWriter.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json));
this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip");
return recording;
}
private void generateZipFileAndCleanFolder(String folder, String fileName) {
FileOutputStream fos = null;
ZipOutputStream zipOut = null;
final File[] files = new File(folder).listFiles();
try {
fos = new FileOutputStream(folder + fileName);
zipOut = new ZipOutputStream(fos);
for (int i = 0; i < files.length; i++) {
String fileExtension = FilenameUtils.getExtension(files[i].getName());
if (files[i].isFile() && (fileExtension.equals("json") || fileExtension.equals("webm"))) {
// Zip video files and json sync metadata file
FileInputStream fis = new FileInputStream(files[i]);
ZipEntry zipEntry = new ZipEntry(files[i].getName());
zipOut.putNextEntry(zipEntry);
byte[] bytes = new byte[1024];
int length;
while ((length = fis.read(bytes)) >= 0) {
zipOut.write(bytes, 0, length);
}
fis.close();
}
if (!files[i].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
// Clean inspected file if it is not
files[i].delete();
}
}
} catch (IOException e) {
log.error("Error generating ZIP file {}. Error: {}", folder + fileName, e.getMessage());
} finally {
try {
zipOut.close();
fos.close();
} catch (IOException e) {
log.error("Error closing FileOutputStream or ZipOutputStream. Error: {}", e.getMessage());
e.printStackTrace();
}
}
}
}

View File

@ -15,7 +15,7 @@
*
*/
package io.openvidu.server.recording;
package io.openvidu.server.recording.service;
import java.io.Closeable;
import java.io.IOException;

View File

@ -19,7 +19,6 @@ package io.openvidu.server.rest;
import java.util.Collection;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
@ -52,8 +51,8 @@ import io.openvidu.server.core.ParticipantRole;
import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionManager;
import io.openvidu.server.kurento.core.KurentoTokenOptions;
import io.openvidu.server.recording.ComposedRecordingService;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.service.RecordingManager;
/**
*
@ -68,7 +67,7 @@ public class SessionRestController {
private SessionManager sessionManager;
@Autowired
private ComposedRecordingService recordingService;
private RecordingManager recordingManager;
@Autowired
private OpenviduConfig openviduConfig;
@ -148,7 +147,7 @@ public class SessionRestController {
Session session = this.sessionManager.getSession(sessionId);
if (session != null) {
JsonObject response = (webRtcStats == true) ? session.withStatsToJson() : session.toJson();
response.addProperty("recording", this.recordingService.sessionIsBeingRecorded(sessionId));
response.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(sessionId));
return new ResponseEntity<>(response.toString(), getResponseHeaders(), HttpStatus.OK);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@ -163,7 +162,7 @@ public class SessionRestController {
JsonArray jsonArray = new JsonArray();
sessions.forEach(s -> {
JsonObject sessionJson = (webRtcStats == true) ? s.withStatsToJson() : s.toJson();
sessionJson.addProperty("recording", this.recordingService.sessionIsBeingRecorded(s.getSessionId()));
sessionJson.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(s.getSessionId()));
jsonArray.add(sessionJson);
});
json.addProperty("numberOfElements", sessions.size());
@ -298,6 +297,7 @@ public class SessionRestController {
String sessionId = (String) params.get("session");
String name = (String) params.get("name");
String outputModeString = (String) params.get("outputMode");
String recordingLayoutString = (String) params.get("recordingLayout");
String customLayout = (String) params.get("customLayout");
@ -322,28 +322,49 @@ public class SessionRestController {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
if (!(session.getSessionProperties().mediaMode().equals(MediaMode.ROUTED))
|| this.recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|| this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
// Session is not in ROUTED MediMode or it is already being recorded
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
RecordingLayout recordingLayout;
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
// "recordingLayout" parameter not defined. Use global layout from
// SessionProperties (it is always configured as it has RecordingLayout.BEST_FIT
// as default value)
recordingLayout = session.getSessionProperties().defaultRecordingLayout();
} else {
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
io.openvidu.java.client.Recording.OutputMode outputMode;
try {
outputMode = io.openvidu.java.client.Recording.OutputMode.valueOf(outputModeString);
} catch (Exception e) {
outputMode = io.openvidu.java.client.Recording.OutputMode.COMPOSED;
}
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(name).outputMode(outputMode);
if (outputMode.equals(io.openvidu.java.client.Recording.OutputMode.COMPOSED)) {
RecordingLayout recordingLayout;
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
// "recordingLayout" parameter not defined. Use global layout from
// SessionProperties (it is always configured as it has RecordingLayout.BEST_FIT
// as default value)
recordingLayout = session.getSessionProperties().defaultRecordingLayout();
} else {
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
}
builder.recordingLayout(recordingLayout);
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
customLayout = (customLayout == null || customLayout.isEmpty())
? session.getSessionProperties().defaultCustomLayout()
: customLayout;
builder.customLayout(customLayout);
}
builder.build();
}
customLayout = (customLayout == null || customLayout.isEmpty())
? session.getSessionProperties().defaultCustomLayout()
: customLayout;
Recording startedRecording = this.recordingService.startRecording(session, new RecordingProperties.Builder()
.name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
try {
Recording startedRecording = this.recordingManager.startRecording(session, builder.build());
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
} catch (OpenViduException e) {
return new ResponseEntity<>("Error starting recording: " + e.getMessage(), getResponseHeaders(),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST)
@ -354,24 +375,24 @@ public class SessionRestController {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
Recording recording = recordingService.getStartedRecording(recordingId);
Recording recording = recordingManager.getStartedRecording(recordingId);
if (recording == null) {
if (recordingService.getStartingRecording(recordingId) != null) {
if (recordingManager.getStartingRecording(recordingId) != null) {
// Recording is still starting
return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
}
// Recording does not exist
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) {
if (!this.recordingManager.sessionIsBeingRecorded(recording.getSessionId())) {
// Session is not being recorded
return new ResponseEntity<>(HttpStatus.CONFLICT);
}
Session session = sessionManager.getSession(recording.getSessionId());
Recording stoppedRecording = this.recordingService.stopRecording(session, recording.getId(),
Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(),
"recordingStoppedByServer");
if (session != null) {
@ -386,27 +407,26 @@ public class SessionRestController {
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET)
public ResponseEntity<?> getRecording(@PathVariable("recordingId") String recordingId) {
try {
Recording recording = this.recordingService.getAllRecordings().stream()
.filter(rec -> rec.getId().equals(recordingId)).findFirst().get();
if (Recording.Status.started.equals(recording.getStatus())
&& recordingService.getStartingRecording(recording.getId()) != null) {
recording.setStatus(Recording.Status.starting);
Recording recording = this.recordingManager.getRecording(recordingId);
if (io.openvidu.java.client.Recording.Status.started.equals(recording.getStatus())
&& recordingManager.getStartingRecording(recording.getId()) != null) {
recording.setStatus(io.openvidu.java.client.Recording.Status.starting);
}
return new ResponseEntity<>(recording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
} catch (NoSuchElementException e) {
} catch (Exception e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
}
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
public ResponseEntity<?> getAllRecordings() {
Collection<Recording> recordings = this.recordingService.getAllRecordings();
Collection<Recording> recordings = this.recordingManager.getAllRecordings();
JsonObject json = new JsonObject();
JsonArray jsonArray = new JsonArray();
recordings.forEach(rec -> {
if (Recording.Status.started.equals(rec.getStatus())
&& recordingService.getStartingRecording(rec.getId()) != null) {
rec.setStatus(Recording.Status.starting);
if (io.openvidu.java.client.Recording.Status.started.equals(rec.getStatus())
&& recordingManager.getStartingRecording(rec.getId()) != null) {
rec.setStatus(io.openvidu.java.client.Recording.Status.starting);
}
jsonArray.add(rec.toJson());
});
@ -417,7 +437,7 @@ public class SessionRestController {
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE)
public ResponseEntity<?> deleteRecording(@PathVariable("recordingId") String recordingId) {
return new ResponseEntity<>(this.recordingService.deleteRecordingFromHost(recordingId, false));
return new ResponseEntity<>(this.recordingManager.deleteRecordingFromHost(recordingId, false));
}
private ResponseEntity<String> generateErrorResponse(String errorMessage, String path, HttpStatus status) {

View File

@ -0,0 +1,63 @@
/*
* (C) Copyright 2017-2018 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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CustomFileWriter {
private static final Logger log = LoggerFactory.getLogger(CustomFileWriter.class);
public void createAndWriteFile(String filePath, String text) {
try {
this.writeAndCloseOnOutputStreamWriter(new FileOutputStream(filePath), text);
} catch (IOException e) {
log.error("Couldn't create file {}. Error: ", filePath, e.getMessage());
}
}
public void overwriteFile(String filePath, String text) {
try {
this.writeAndCloseOnOutputStreamWriter(new FileOutputStream(filePath, false), text);
} catch (IOException e) {
log.error("Couldn't overwrite file {}. Error: ", filePath, e.getMessage());
}
}
public boolean createFolder(String path) {
return new File(path).mkdir();
}
private void writeAndCloseOnOutputStreamWriter(FileOutputStream fos, String text) throws IOException {
OutputStreamWriter osw = null;
try {
osw = new OutputStreamWriter(fos);
osw.write(text);
} finally {
osw.close();
fos.close();
}
}
}

View File

@ -1033,7 +1033,7 @@ public class OpenViduTestAppE2eTest {
@Test
@DisplayName("Remote record")
void remoteRecordTest() throws Exception {
void remoteComposedRecordTest() throws Exception {
setupBrowser("chrome");
log.info("Remote record");
@ -1131,13 +1131,11 @@ public class OpenViduTestAppE2eTest {
user.getEventManager().waitUntilEventReaches("recordingStopped", 1);
String recordingsPath = "/opt/openvidu/recordings/";
File file1 = new File(recordingsPath + sessionName + ".mp4");
File file2 = new File(recordingsPath + ".recording." + sessionName);
File file3 = new File(recordingsPath + sessionName + ".info");
File file1 = new File(recordingsPath + sessionName + "/" + sessionName + ".mp4");
File file2 = new File(recordingsPath + sessionName + "/" + ".recording." + sessionName);
Assert.assertTrue(file1.exists() || file1.length() > 0);
Assert.assertTrue(file2.exists() || file2.length() > 0);
Assert.assertTrue(file3.exists() || file3.length() > 0);
Assert.assertTrue(
this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName)));
@ -1159,7 +1157,6 @@ public class OpenViduTestAppE2eTest {
Assert.assertFalse(file1.exists());
Assert.assertFalse(file2.exists());
Assert.assertFalse(file3.exists());
user.getDriver().findElement(By.id("close-dialog-btn")).click();
@ -1167,6 +1164,12 @@ public class OpenViduTestAppE2eTest {
}
@Test
@DisplayName("Remote record")
void remoteIndividualRecordTest() throws Exception {
}
@Test
@DisplayName("REST API: Fetch all, fetch one, force disconnect, force unpublish, close session")
void restApiFetchForce() throws Exception {