diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index ee2d8cbe..f1acf638 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -61,8 +61,12 @@ import io.openvidu.server.kurento.kms.FixedOneKmsManager; import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.kurento.kms.LoadManager; import io.openvidu.server.recording.DummyRecordingDownloader; +import io.openvidu.server.recording.DummyRecordingUploader; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.recording.service.RecordingManager; +import io.openvidu.server.recording.service.RecordingManagerUtils; +import io.openvidu.server.recording.service.RecordingManagerUtilsLocalStorage; import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.rpc.RpcNotificationService; import io.openvidu.server.utils.CommandExecutor; @@ -184,6 +188,20 @@ public class OpenViduServer implements JsonRpcConfigurer { return new KurentoParticipantEndpointConfig(); } + @Bean + @ConditionalOnMissingBean + @DependsOn({ "openviduConfig", "recordingManager" }) + public RecordingManagerUtils recordingManagerUtils(OpenviduConfig openviduConfig, + RecordingManager recordingManager) { + return new RecordingManagerUtilsLocalStorage(openviduConfig, recordingManager); + } + + @Bean + @ConditionalOnMissingBean + public RecordingUploader recordingUpload() { + return new DummyRecordingUploader(); + } + @Bean @ConditionalOnMissingBean public RecordingDownloader recordingDownload() { diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java new file mode 100644 index 00000000..828e10c1 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/DummyRecordingUploader.java @@ -0,0 +1,20 @@ +package io.openvidu.server.recording; + +public class DummyRecordingUploader implements RecordingUploader { + + @Override + public void uploadRecording(Recording recording, Runnable successCallback, Runnable errorCallback) { + // Just immediately run success callback function + successCallback.run(); + } + + @Override + public void storeAsUploadingRecording(String recording) { + } + + @Override + public boolean isBeingUploaded(String recordingId) { + return false; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java new file mode 100644 index 00000000..e88b8719 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingUploader.java @@ -0,0 +1,11 @@ +package io.openvidu.server.recording; + +public interface RecordingUploader { + + void uploadRecording(Recording recording, Runnable successCallback, Runnable errorCallback); + + void storeAsUploadingRecording(String recording); + + boolean isBeingUploaded(String recordingId); + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java index c5325740..c0b8d86c 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java @@ -1,7 +1,14 @@ package io.openvidu.server.recording.service; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; + import io.openvidu.client.OpenViduException; import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.cdr.CallDetailRecord; @@ -10,204 +17,213 @@ import io.openvidu.server.core.EndReason; import io.openvidu.server.core.Session; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.QuarantineKiller; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.List; public class ComposedQuickStartRecordingService extends ComposedRecordingService { - private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); + private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); - public ComposedQuickStartRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); - } + public ComposedQuickStartRecordingService(RecordingManager recordingManager, + RecordingDownloader recordingDownloader, RecordingUploader recordingUploader, OpenviduConfig openviduConfig, + CallDetailRecord cdr, QuarantineKiller quarantineKiller) { + super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller); + } - public void stopRecordingContainer(Session session, EndReason reason) { - log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", - session.getSessionId(), RecordingManager.finalReason(reason)); + public void stopRecordingContainer(Session session, EndReason reason) { + log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", session.getSessionId(), + RecordingManager.finalReason(reason)); - String containerId = this.sessionsContainers.get(session.getSessionId()); + String containerId = this.sessionsContainers.get(session.getSessionId()); - if (containerId != null) { - try { - dockerManager.removeDockerContainer(containerId, true); - } catch (Exception e) { - log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", session.getSessionId()); - } + if (containerId != null) { + try { + dockerManager.removeDockerContainer(containerId, true); + } catch (Exception e) { + log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", + session.getSessionId()); + } - containers.remove(containerId); - sessionsContainers.remove(session.getSessionId()); - } + containers.remove(containerId); + sessionsContainers.remove(session.getSessionId()); + } - } + } - @Override - protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties) - throws OpenViduException { + @Override + protected Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties) + throws OpenViduException { - log.info("Starting COMPOSED_QUICK_START ({}) recording {} of session {}", - properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId()); + log.info("Starting COMPOSED_QUICK_START ({}) recording {} of session {}", + properties.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId()); - List envs = new ArrayList<>(); + List envs = new ArrayList<>(); - envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); - envs.add("RESOLUTION=" + properties.resolution()); - envs.add("ONLY_VIDEO=" + !properties.hasAudio()); - envs.add("FRAMERATE=30"); - envs.add("VIDEO_ID=" + recording.getId()); - envs.add("VIDEO_NAME=" + properties.name()); - envs.add("VIDEO_FORMAT=mp4"); - envs.add("RECORDING_JSON='" + recording.toJson().toString() + "'"); + envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); + envs.add("RESOLUTION=" + properties.resolution()); + envs.add("ONLY_VIDEO=" + !properties.hasAudio()); + envs.add("FRAMERATE=30"); + envs.add("VIDEO_ID=" + recording.getId()); + envs.add("VIDEO_NAME=" + properties.name()); + envs.add("VIDEO_FORMAT=mp4"); + envs.add("RECORDING_JSON='" + recording.toJson().toString() + "'"); - String containerId = this.sessionsContainers.get(session.getSessionId()); - try { - String recordExecCommand = ""; - for(int i = 0; i < envs.size(); i++) { - if (i > 0) { - recordExecCommand += "&& "; - } - recordExecCommand += "export " + envs.get(i) + " "; - } - recordExecCommand += "&& ./composed_quick_start.sh --start-recording > /var/log/ffmpeg.log 2>&1 &"; - dockerManager.runCommandInContainer(containerId, recordExecCommand, 0); - } catch (Exception e) { - this.cleanRecordingMaps(recording); - throw this.failStartRecording(session, recording, - "Couldn't initialize recording container. Error: " + e.getMessage()); - } + String containerId = this.sessionsContainers.get(session.getSessionId()); + try { + String recordExecCommand = ""; + for (int i = 0; i < envs.size(); i++) { + if (i > 0) { + recordExecCommand += "&& "; + } + recordExecCommand += "export " + envs.get(i) + " "; + } + recordExecCommand += "&& ./composed_quick_start.sh --start-recording > /var/log/ffmpeg.log 2>&1 &"; + dockerManager.runCommandInContainer(containerId, recordExecCommand, 0); + } catch (Exception e) { + this.cleanRecordingMaps(recording); + throw this.failStartRecording(session, recording, + "Couldn't initialize recording container. Error: " + e.getMessage()); + } - this.sessionsContainers.put(session.getSessionId(), containerId); + this.sessionsContainers.put(session.getSessionId(), containerId); - try { - this.waitForVideoFileNotEmpty(recording); - } catch (OpenViduException e) { - this.cleanRecordingMaps(recording); - throw this.failStartRecording(session, recording, - "Couldn't initialize recording container. Error: " + e.getMessage()); - } + try { + this.waitForVideoFileNotEmpty(recording); + } catch (OpenViduException e) { + this.cleanRecordingMaps(recording); + throw this.failStartRecording(session, recording, + "Couldn't initialize recording container. Error: " + e.getMessage()); + } - return recording; - } + return recording; + } - @Override - protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { - log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}", - recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), - RecordingManager.finalReason(reason)); - log.info("Container for session {} still being ready for new recordings", recording.getSessionId()); + @Override + protected Recording stopRecordingWithVideo(Session session, Recording recording, EndReason reason) { + log.info("Stopping COMPOSED_QUICK_START ({}) recording {} of session {}. Reason: {}", + recording.hasAudio() ? "video + audio" : "audio-only", recording.getId(), recording.getSessionId(), + RecordingManager.finalReason(reason)); + log.info("Container for session {} still being ready for new recordings", recording.getSessionId()); - String containerId = this.sessionsContainers.get(recording.getSessionId()); + String containerId = this.sessionsContainers.get(recording.getSessionId()); - if (session == null) { - log.warn( - "Existing recording {} does not have an active session associated. This usually means a custom recording" - + " layout did not join a recorded participant or the recording has been automatically" - + " stopped after last user left and timeout passed", - recording.getId()); - } + if (session == null) { + log.warn( + "Existing recording {} does not have an active session associated. This usually means a custom recording" + + " layout did not join a recorded participant or the recording has been automatically" + + " stopped after last user left and timeout passed", + recording.getId()); + } - try { - dockerManager.runCommandInContainer(containerId, "./composed_quick_start.sh --stop-recording", 10); - } catch (InterruptedException e1) { - cleanRecordingMaps(recording); - log.error("Error stopping recording for session id: {}", session.getSessionId()); - e1.printStackTrace(); - } + try { + dockerManager.runCommandInContainer(containerId, "./composed_quick_start.sh --stop-recording", 10); + } catch (InterruptedException e1) { + cleanRecordingMaps(recording); + log.error("Error stopping recording for session id: {}", session.getSessionId()); + e1.printStackTrace(); + } - recording = updateRecordingAttributes(recording); + recording = updateRecordingAttributes(recording); - this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), getMetadataFilePath(recording)); - cleanRecordingMaps(recording); + this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), + getMetadataFilePath(recording)); + cleanRecordingMaps(recording); - final long timestamp = System.currentTimeMillis(); - this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); + if (session != null && reason != null) { + this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); + } - if (session != null && reason != null) { - this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); - } + final Recording[] finalRecordingArray = new Recording[1]; + finalRecordingArray[0] = recording; + this.uploadRecording(finalRecordingArray[0], reason); - // Decrement active recordings - // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); + // Decrement active recordings + // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); - return recording; - } + return recording; + } - public void runComposedQuickStartContainer(Session session) { - // Start recording container if output mode=COMPOSED_QUICK_START - Session recorderSession = session; - io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties().defaultOutputMode(); - if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) - && sessionsContainers.get(recorderSession.getSessionId()) == null) { - // Retry to run if container is launched for the same session quickly after close it - int secondsToRetry = 10; - int secondsBetweenRetries = 1; - int seconds = 0; - boolean launched = false; - while (!launched && seconds < secondsToRetry) { - try { - log.info("Launching COMPOSED_QUICK_START recording container for session: {}", recorderSession.getSessionId()); - runContainer(recorderSession, new RecordingProperties.Builder().name("") - .outputMode(recorderSession.getSessionProperties().defaultOutputMode()) - .recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout()) - .customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build()); - log.info("COMPOSED_QUICK_START recording container launched for session: {}", recorderSession.getSessionId()); - launched = true; - } catch (Exception e) { - log.warn("Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", recorderSession.getSessionId(), secondsBetweenRetries); - try { - Thread.sleep(secondsBetweenRetries * 1000); - } catch (InterruptedException e2) {} - seconds++; - } finally { - if (seconds == secondsToRetry && !launched) { - log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", recorderSession.getSessionId()); - } - } - } - } - } + public void runComposedQuickStartContainer(Session session) { + // Start recording container if output mode=COMPOSED_QUICK_START + Session recorderSession = session; + io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties() + .defaultOutputMode(); + if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) + && sessionsContainers.get(recorderSession.getSessionId()) == null) { + // Retry to run if container is launched for the same session quickly after + // close it + int secondsToRetry = 10; + int secondsBetweenRetries = 1; + int seconds = 0; + boolean launched = false; + while (!launched && seconds < secondsToRetry) { + try { + log.info("Launching COMPOSED_QUICK_START recording container for session: {}", + recorderSession.getSessionId()); + runContainer(recorderSession, new RecordingProperties.Builder().name("") + .outputMode(recorderSession.getSessionProperties().defaultOutputMode()) + .recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout()) + .customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build()); + log.info("COMPOSED_QUICK_START recording container launched for session: {}", + recorderSession.getSessionId()); + launched = true; + } catch (Exception e) { + log.warn( + "Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", + recorderSession.getSessionId(), secondsBetweenRetries); + try { + Thread.sleep(secondsBetweenRetries * 1000); + } catch (InterruptedException e2) { + } + seconds++; + } finally { + if (seconds == secondsToRetry && !launched) { + log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", + recorderSession.getSessionId()); + } + } + } + } + } - private String runContainer(Session session, RecordingProperties properties) throws Exception { - log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId()); + private String runContainer(Session session, RecordingProperties properties) throws Exception { + log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId()); - Recording recording = new Recording(session.getSessionId(), null, properties); - String layoutUrl = this.getLayoutUrl(recording); + Recording recording = new Recording(session.getSessionId(), null, properties); + String layoutUrl = this.getLayoutUrl(recording); - List envs = new ArrayList<>(); - envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); - envs.add("RECORDING_TYPE=COMPOSED_QUICK_START"); - envs.add("RESOLUTION=" + properties.resolution()); - envs.add("URL=" + layoutUrl); + List envs = new ArrayList<>(); + envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); + envs.add("RECORDING_TYPE=COMPOSED_QUICK_START"); + envs.add("RESOLUTION=" + properties.resolution()); + envs.add("URL=" + layoutUrl); - log.info("Recorder connecting to url {}", layoutUrl); + log.info("Recorder connecting to url {}", layoutUrl); - String containerId = null; - try { - final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG; - final String containerName = "recording_" + session.getSessionId(); - Volume volume1 = new Volume("/recordings"); - List volumes = new ArrayList<>(); - volumes.add(volume1); - Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); - List binds = new ArrayList<>(); - binds.add(bind1); - containerId = dockerManager.runContainer(container, containerName, null, volumes, binds, "host", envs, null, - properties.shmSize(), false, null); - containers.put(containerId, containerName); - this.sessionsContainers.put(session.getSessionId(), containerId); - } catch (Exception e) { - if (containerId != null) { - dockerManager.removeDockerContainer(containerId, true); - containers.remove(containerId); - sessionsContainers.remove(session.getSessionId()); - } - log.error("Error while launchig container for COMPOSED_QUICK_START: ({})", e.getMessage()); - throw e; - } - return containerId; - } + String containerId = null; + try { + final String container = RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG; + final String containerName = "recording_" + session.getSessionId(); + Volume volume1 = new Volume("/recordings"); + List volumes = new ArrayList<>(); + volumes.add(volume1); + Bind bind1 = new Bind(openviduConfig.getOpenViduRecordingPath(), volume1); + List binds = new ArrayList<>(); + binds.add(bind1); + containerId = dockerManager.runContainer(container, containerName, null, volumes, binds, "host", envs, null, + properties.shmSize(), false, null); + containers.put(containerId, containerName); + this.sessionsContainers.put(session.getSessionId(), containerId); + } catch (Exception e) { + if (containerId != null) { + dockerManager.removeDockerContainer(containerId, true); + containers.remove(containerId); + sessionsContainers.remove(session.getSessionId()); + } + log.error("Error while launchig container for COMPOSED_QUICK_START: ({})", e.getMessage()); + throw e; + } + return containerId; + } } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java index 449c2a63..0ddf3581 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java @@ -56,6 +56,7 @@ import io.openvidu.server.recording.CompositeWrapper; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingInfoUtils; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.QuarantineKiller; @@ -70,8 +71,9 @@ public class ComposedRecordingService extends RecordingService { protected DockerManager dockerManager; public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); + RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr, + QuarantineKiller quarantineKiller) { + super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller); this.dockerManager = new DockerManager(); } @@ -278,7 +280,9 @@ public class ComposedRecordingService extends RecordingService { log.warn("Deleting unusable files for recording {}", recordingId); if (HttpStatus.NO_CONTENT .equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) { - log.warn("Files properly deleted"); + log.warn("Files properly deleted for recording {}", recordingId); + } else { + log.warn("No files found for recording {}", recordingId); } } } @@ -300,13 +304,15 @@ public class ComposedRecordingService extends RecordingService { getMetadataFilePath(recording)); cleanRecordingMaps(recording); - final long timestamp = System.currentTimeMillis(); - this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); - if (session != null && reason != null) { this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); } + // Upload if necessary + final Recording[] finalRecordingArray = new Recording[1]; + finalRecordingArray[0] = recording; + this.uploadRecording(finalRecordingArray[0], reason); + // Decrement active recordings // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); } @@ -354,6 +360,7 @@ public class ComposedRecordingService extends RecordingService { finalRecordingArray[0] = recording; try { this.recordingDownloader.downloadRecording(finalRecordingArray[0], null, () -> { + String filesPath = this.openviduConfig.getOpenViduRecordingPath() + finalRecordingArray[0].getId() + "/"; File videoFile = new File(filesPath + finalRecordingArray[0].getName() + ".webm"); @@ -364,16 +371,15 @@ public class ComposedRecordingService extends RecordingService { finalDuration, filesPath + RecordingManager.RECORDING_ENTITY_FILE + finalRecordingArray[0].getId()); - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp, - finalRecordingArray[0].getStatus()); - // Decrement active recordings once it is downloaded ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); // Now we can drop Media Node if waiting-idle-to-terminate this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); + // Upload if necessary + this.uploadRecording(finalRecordingArray[0], reason); + }); } catch (IOException e) { log.error("Error while downloading recording {}: {}", finalRecordingArray[0].getName(), e.getMessage()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java index 57d41d1b..9d4dc50b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java @@ -72,6 +72,7 @@ import io.openvidu.server.kurento.kms.Kms; import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.JsonUtils; @@ -93,9 +94,15 @@ public class RecordingManager { @Autowired private SessionManager sessionManager; + @Autowired + protected RecordingManagerUtils recordingManagerUtils; + @Autowired private RecordingDownloader recordingDownloader; + @Autowired + private RecordingUploader recordingUploader; + @Autowired protected OpenviduConfig openviduConfig; @@ -119,7 +126,7 @@ public class RecordingManager { private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor( Runtime.getRuntime().availableProcessors()); - static final String RECORDING_ENTITY_FILE = ".recording."; + public static final String RECORDING_ENTITY_FILE = ".recording."; public static final String IMAGE_NAME = "openvidu/openvidu-recording"; static String IMAGE_TAG; @@ -159,12 +166,12 @@ public class RecordingManager { RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion(); this.dockerManager = new DockerManager(); - this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr, - quarantineKiller); - this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader, + this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller); - this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig, - cdr, quarantineKiller); + this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader, + recordingUploader, openviduConfig, cdr, quarantineKiller); + this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, + recordingUploader, openviduConfig, cdr, quarantineKiller); log.info("Recording module required: Downloading openvidu/openvidu-recording:" + openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)"); @@ -432,64 +439,105 @@ public class RecordingManager { } public Collection getFinishedRecordings() { - return this.getAllRecordingsFromHost().stream().filter(recording -> recording.getStatus().equals(Status.ready)) - .collect(Collectors.toSet()); + return recordingManagerUtils.getAllRecordingsFromStorage().stream() + .filter(recording -> recording.getStatus().equals(Status.ready)).collect(Collectors.toSet()); } public Recording getRecording(String recordingId) { - return this.getRecordingFromHost(recordingId); + return recordingManagerUtils.getRecordingFromStorage(recordingId); } public Collection getAllRecordings() { - return this.getAllRecordingsFromHost(); + return recordingManagerUtils.getAllRecordingsFromStorage(); } public String getFreeRecordingId(String sessionId) { - Set recordingIds = this.getRecordingIdsFromHost(); - String recordingId = sessionId; - boolean isPresent = recordingIds.contains(recordingId); - int i = 1; - while (isPresent) { - recordingId = sessionId + "-" + i; - i++; - isPresent = recordingIds.contains(recordingId); - } - return recordingId; + return recordingManagerUtils.getFreeRecordingId(sessionId); } 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; + if (this.startedRecordings.containsKey(recordingId) || this.startingRecordings.containsKey(recordingId)) { + if (!force) { + // Cannot delete an active recording + return HttpStatus.CONFLICT; + } } - Recording recording = getRecordingFromHost(recordingId); + Recording recording = recordingManagerUtils.getRecordingFromStorage(recordingId); if (recording == null) { return HttpStatus.NOT_FOUND; } if (Status.stopped.equals(recording.getStatus())) { - // Recording is being downloaded from remote host - log.warn("Cancelling ongoing download process of recording {}", recording.getId()); + // Recording is being downloaded from remote host or being uploaded + log.warn("Recording {} status is \"stopped\". Cancelling possible ongoing download process", recording.getId()); this.recordingDownloader.cancelDownload(recording.getId()); } - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); + return recordingManagerUtils.deleteRecordingFromStorage(recordingId); + } + + public Set getAllRecordingIdsFromLocalStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + + Set 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; + } + + public HttpStatus deleteRecordingFromLocalStorage(String recordingId) { + File folder = new File(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 it try { FileUtils.deleteDirectory(files[i]); + return HttpStatus.NO_CONTENT; } catch (IOException e) { log.error("Couldn't delete folder {}", files[i].getAbsolutePath()); + return HttpStatus.INTERNAL_SERVER_ERROR; } - break; } } + return HttpStatus.NOT_FOUND; + } - return HttpStatus.NO_CONTENT; + public File getRecordingEntityFileFromLocalStorage(String recordingId) { + String metadataFilePath = openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + + RecordingManager.RECORDING_ENTITY_FILE + recordingId; + return new File(metadataFilePath); + } + + public Set getAllRecordingsFromLocalStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + Set 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 = getRecordingFromEntityFile(innerFiles[j]); + if (recording != null) { + recordingEntities.add(recording); + } + } + } + } + return recordingEntities; } public Recording getRecordingFromEntityFile(File file) { @@ -501,28 +549,27 @@ public class RecordingManager { log.error("Error reading recording entity file {}: {}", file.getAbsolutePath(), (e.getMessage())); return null; } - Recording recording = new Recording(json); - if (Status.ready.equals(recording.getStatus()) || Status.failed.equals(recording.getStatus())) { - recording.setUrl(getRecordingUrl(recording)); - } - return recording; + return getRecordingFromJson(json); } return null; } - public String getRecordingUrl(Recording recording) { - return openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/" + recording.getName() + "." - + this.getExtensionFromRecording(recording); + public Recording getRecordingFromJson(JsonObject json) { + Recording recording = new Recording(json); + if (Status.ready.equals(recording.getStatus()) + && composedQuickStartRecordingService.isBeingUploaded(recording)) { + // Recording has finished but is being uploaded + recording.setStatus(Status.stopped); + recording.setUrl(null); + } else if (Status.ready.equals(recording.getStatus()) || Status.failed.equals(recording.getStatus())) { + // Recording has been completely processed and must include URL + recording.setUrl(recordingManagerUtils.getRecordingUrl(recording)); + } + return recording; } - private String getExtensionFromRecording(Recording recording) { - if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { - return "zip"; - } else if (recording.hasVideo()) { - return "mp4"; - } else { - return "webm"; - } + public String getRecordingUrl(Recording recording) { + return recordingManagerUtils.getRecordingUrl(recording); } public void initAutomaticRecordingStopThread(final Session session) { @@ -623,56 +670,6 @@ public class RecordingManager { } } - 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); - return recording; - } - - private Set getAllRecordingsFromHost() { - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); - File[] files = folder.listFiles(); - - Set 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) { - recordingEntities.add(recording); - } - } - } - } - return recordingEntities; - } - - private Set getRecordingIdsFromHost() { - File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); - File[] files = folder.listFiles(); - - Set 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 void checkRecordingPaths(String openviduRecordingPath, String openviduRecordingCustomLayout) throws OpenViduException { log.info("Initializing recording paths"); diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java new file mode 100644 index 00000000..b2eaa7cb --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtils.java @@ -0,0 +1,61 @@ +package io.openvidu.server.recording.service; + +import java.util.Set; + +import org.springframework.http.HttpStatus; + +import io.openvidu.java.client.Recording.OutputMode; +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.recording.Recording; +import io.openvidu.server.utils.JsonUtils; + +public abstract class RecordingManagerUtils { + + protected OpenviduConfig openviduConfig; + protected RecordingManager recordingManager; + + protected JsonUtils jsonUtils = new JsonUtils(); + + public RecordingManagerUtils(OpenviduConfig openviduConfig, RecordingManager recordingManager) { + this.openviduConfig = openviduConfig; + this.recordingManager = recordingManager; + } + + public abstract Recording getRecordingFromStorage(String recordingId); + + public abstract Set getAllRecordingsFromStorage(); + + public abstract HttpStatus deleteRecordingFromStorage(String recordingId); + + protected abstract String getRecordingUrl(Recording recording); + + protected abstract Set getAllRecordingIdsFromStorage(); + + protected String getExtensionFromRecording(Recording recording) { + if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { + return "zip"; + } else if (recording.hasVideo()) { + return "mp4"; + } else { + return "webm"; + } + } + + public String getFreeRecordingId(String sessionId) { + Set recordingIds = getAllRecordingIdsFromStorage(); + return getNextAvailableRecordingId(sessionId, recordingIds); + } + + private String getNextAvailableRecordingId(String baseRecordingId, Set existingRecordingIds) { + String recordingId = baseRecordingId; + boolean isPresent = existingRecordingIds.contains(recordingId); + int i = 1; + while (isPresent) { + recordingId = baseRecordingId + "-" + i; + i++; + isPresent = existingRecordingIds.contains(recordingId); + } + return recordingId; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java new file mode 100644 index 00000000..e69f10e2 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManagerUtilsLocalStorage.java @@ -0,0 +1,62 @@ +package io.openvidu.server.recording.service; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.http.HttpStatus; + +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.recording.Recording; + +public class RecordingManagerUtilsLocalStorage extends RecordingManagerUtils { + + public RecordingManagerUtilsLocalStorage(OpenviduConfig openviduConfig, RecordingManager recordingManager) { + super(openviduConfig, recordingManager); + } + + @Override + public Recording getRecordingFromStorage(String recordingId) { + File file = recordingManager.getRecordingEntityFileFromLocalStorage(recordingId); + return recordingManager.getRecordingFromEntityFile(file); + } + + @Override + public Set getAllRecordingsFromStorage() { + return recordingManager.getAllRecordingsFromLocalStorage(); + } + + @Override + public HttpStatus deleteRecordingFromStorage(String recordingId) { + return recordingManager.deleteRecordingFromLocalStorage(recordingId); + } + + @Override + public String getRecordingUrl(Recording recording) { + return openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/" + recording.getName() + "." + + this.getExtensionFromRecording(recording); + } + + @Override + protected Set getAllRecordingIdsFromStorage() { + File folder = new File(openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + + Set 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; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java index 385899e3..f47aba83 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingService.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; +import io.openvidu.java.client.Recording.Status; import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.cdr.CallDetailRecord; @@ -32,6 +33,7 @@ import io.openvidu.server.core.EndReason; import io.openvidu.server.core.Session; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.QuarantineKiller; @@ -44,14 +46,17 @@ public abstract class RecordingService { protected OpenviduConfig openviduConfig; protected RecordingManager recordingManager; protected RecordingDownloader recordingDownloader; + protected RecordingUploader recordingUploader; protected CallDetailRecord cdr; protected QuarantineKiller quarantineKiller; protected CustomFileManager fileWriter = new CustomFileManager(); RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { + RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr, + QuarantineKiller quarantineKiller) { this.recordingManager = recordingManager; this.recordingDownloader = recordingDownloader; + this.recordingUploader = recordingUploader; this.openviduConfig = openviduConfig; this.cdr = cdr; this.quarantineKiller = quarantineKiller; @@ -105,9 +110,14 @@ public abstract class RecordingService { */ protected Recording sealRecordingMetadataFileAsReady(Recording recording, long size, double duration, String metadataFilePath) { - io.openvidu.java.client.Recording.Status status = io.openvidu.java.client.Recording.Status.failed - .equals(recording.getStatus()) ? io.openvidu.java.client.Recording.Status.failed - : io.openvidu.java.client.Recording.Status.ready; + Status status = Status.failed.equals(recording.getStatus()) ? Status.failed : Status.ready; + + if (Status.ready.equals(status)) { + // Prevent uploading recordings from being retrieved from REST API with "ready" + // status. This will force their status back to "stopped" on GET until upload + // process has finished + storeAsUploadingRecording(recording); + } // Status is now failed or ready. Url property must be defined recording.setUrl(recordingManager.getRecordingUrl(recording)); @@ -195,6 +205,25 @@ public abstract class RecordingService { return folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); } + protected void uploadRecording(final Recording recording, EndReason reason) { + recordingUploader.uploadRecording(recording, () -> { + final long timestamp = System.currentTimeMillis(); + cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus()); + }, () -> { + final long timestamp = System.currentTimeMillis(); + cdr.recordRecordingStatusChanged(recording, reason, timestamp, + io.openvidu.java.client.Recording.Status.failed); + }); + } + + protected void storeAsUploadingRecording(Recording recording) { + recordingUploader.storeAsUploadingRecording(recording.getId()); + } + + protected boolean isBeingUploaded(Recording recording) { + return recordingUploader.isBeingUploaded(recording.getId()); + } + /** * Simple wrapper for returning update RecordingProperties and a free * recordingId when starting a new recording diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java index 404ca521..27c7e61f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/SingleStreamRecordingService.java @@ -64,6 +64,7 @@ import io.openvidu.server.kurento.endpoint.PublisherEndpoint; import io.openvidu.server.recording.RecorderEndpointWrapper; import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; +import io.openvidu.server.recording.RecordingUploader; import io.openvidu.server.utils.QuarantineKiller; public class SingleStreamRecordingService extends RecordingService { @@ -76,8 +77,9 @@ public class SingleStreamRecordingService extends RecordingService { private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream."; public SingleStreamRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, - OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { - super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); + RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr, + QuarantineKiller quarantineKiller) { + super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller); } @Override @@ -179,10 +181,6 @@ public class SingleStreamRecordingService extends RecordingService { } finalRecordingArray[0] = this.sealMetadataFiles(finalRecordingArray[0]); - final long timestamp = System.currentTimeMillis(); - cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp, - finalRecordingArray[0].getStatus()); - cleanRecordingWrappers(finalRecordingArray[0].getSessionId()); // Decrement active recordings once it is downloaded @@ -191,6 +189,9 @@ public class SingleStreamRecordingService extends RecordingService { // Now we can drop Media Node if waiting-idle-to-terminate this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); + // Upload if necessary + this.uploadRecording(finalRecordingArray[0], reason); + }); } catch (IOException e) { log.error("Error while downloading recording {}", finalRecordingArray[0].getName()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java b/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java index 7e0468a3..24242d3b 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/JsonUtils.java @@ -20,6 +20,7 @@ package io.openvidu.server.utils; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.io.Reader; import java.util.Map.Entry; import org.kurento.jsonrpc.Props; @@ -56,13 +57,15 @@ public class JsonUtils { public JsonElement fromFileToJsonElement(String filePath) throws IOException, FileNotFoundException, JsonParseException, IllegalStateException { + return fromReaderToJsonElement(new FileReader(filePath)); + } + + public JsonObject fromReaderToJsonObject(Reader reader) throws IOException { + return this.fromReaderToJsonElement(reader).getAsJsonObject(); + } + + public JsonElement fromReaderToJsonElement(Reader reader) throws IOException { JsonElement json = null; - FileReader reader = null; - try { - reader = new FileReader(filePath); - } catch (FileNotFoundException e) { - throw e; - } try { json = JsonParser.parseReader(reader); } catch (JsonParseException | IllegalStateException exception) {