openvidu-server: recording upload refactoring

pull/546/head
pabloFuente 2020-09-17 17:47:26 +02:00
parent 693e2e6583
commit 01894918b7
11 changed files with 511 additions and 287 deletions

View File

@ -61,8 +61,12 @@ import io.openvidu.server.kurento.kms.FixedOneKmsManager;
import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.kurento.kms.KmsManager;
import io.openvidu.server.kurento.kms.LoadManager; import io.openvidu.server.kurento.kms.LoadManager;
import io.openvidu.server.recording.DummyRecordingDownloader; import io.openvidu.server.recording.DummyRecordingDownloader;
import io.openvidu.server.recording.DummyRecordingUploader;
import io.openvidu.server.recording.RecordingDownloader; 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.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.RpcHandler;
import io.openvidu.server.rpc.RpcNotificationService; import io.openvidu.server.rpc.RpcNotificationService;
import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.CommandExecutor;
@ -184,6 +188,20 @@ public class OpenViduServer implements JsonRpcConfigurer {
return new KurentoParticipantEndpointConfig(); 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 @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public RecordingDownloader recordingDownload() { public RecordingDownloader recordingDownload() {

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -1,7 +1,14 @@
package io.openvidu.server.recording.service; 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.Bind;
import com.github.dockerjava.api.model.Volume; import com.github.dockerjava.api.model.Volume;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord; import io.openvidu.server.cdr.CallDetailRecord;
@ -10,24 +17,22 @@ import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Session; import io.openvidu.server.core.Session;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.QuarantineKiller; 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 { 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) { public ComposedQuickStartRecordingService(RecordingManager recordingManager,
super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); RecordingDownloader recordingDownloader, RecordingUploader recordingUploader, OpenviduConfig openviduConfig,
CallDetailRecord cdr, QuarantineKiller quarantineKiller) {
super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller);
} }
public void stopRecordingContainer(Session session, EndReason reason) { public void stopRecordingContainer(Session session, EndReason reason) {
log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", log.info("Stopping COMPOSED_QUICK_START of session {}. Reason: {}", session.getSessionId(),
session.getSessionId(), RecordingManager.finalReason(reason)); RecordingManager.finalReason(reason));
String containerId = this.sessionsContainers.get(session.getSessionId()); String containerId = this.sessionsContainers.get(session.getSessionId());
@ -35,7 +40,8 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService
try { try {
dockerManager.removeDockerContainer(containerId, true); dockerManager.removeDockerContainer(containerId, true);
} catch (Exception e) { } catch (Exception e) {
log.error("Can't remove COMPOSED_QUICK_START recording container from session {}", session.getSessionId()); log.error("Can't remove COMPOSED_QUICK_START recording container from session {}",
session.getSessionId());
} }
containers.remove(containerId); containers.remove(containerId);
@ -119,16 +125,18 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService
recording = updateRecordingAttributes(recording); recording = updateRecordingAttributes(recording);
this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(), getMetadataFilePath(recording)); this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
getMetadataFilePath(recording));
cleanRecordingMaps(recording); cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus());
if (session != null && reason != null) { if (session != null && reason != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
} }
final Recording[] finalRecordingArray = new Recording[1];
finalRecordingArray[0] = recording;
this.uploadRecording(finalRecordingArray[0], reason);
// Decrement active recordings // Decrement active recordings
// ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
@ -138,32 +146,40 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService
public void runComposedQuickStartContainer(Session session) { public void runComposedQuickStartContainer(Session session) {
// Start recording container if output mode=COMPOSED_QUICK_START // Start recording container if output mode=COMPOSED_QUICK_START
Session recorderSession = session; Session recorderSession = session;
io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties().defaultOutputMode(); io.openvidu.java.client.Recording.OutputMode defaultOutputMode = recorderSession.getSessionProperties()
.defaultOutputMode();
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode) if (io.openvidu.java.client.Recording.OutputMode.COMPOSED_QUICK_START.equals(defaultOutputMode)
&& sessionsContainers.get(recorderSession.getSessionId()) == null) { && sessionsContainers.get(recorderSession.getSessionId()) == null) {
// Retry to run if container is launched for the same session quickly after close it // Retry to run if container is launched for the same session quickly after
// close it
int secondsToRetry = 10; int secondsToRetry = 10;
int secondsBetweenRetries = 1; int secondsBetweenRetries = 1;
int seconds = 0; int seconds = 0;
boolean launched = false; boolean launched = false;
while (!launched && seconds < secondsToRetry) { while (!launched && seconds < secondsToRetry) {
try { try {
log.info("Launching COMPOSED_QUICK_START recording container for session: {}", recorderSession.getSessionId()); log.info("Launching COMPOSED_QUICK_START recording container for session: {}",
recorderSession.getSessionId());
runContainer(recorderSession, new RecordingProperties.Builder().name("") runContainer(recorderSession, new RecordingProperties.Builder().name("")
.outputMode(recorderSession.getSessionProperties().defaultOutputMode()) .outputMode(recorderSession.getSessionProperties().defaultOutputMode())
.recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout()) .recordingLayout(recorderSession.getSessionProperties().defaultRecordingLayout())
.customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build()); .customLayout(recorderSession.getSessionProperties().defaultCustomLayout()).build());
log.info("COMPOSED_QUICK_START recording container launched for session: {}", recorderSession.getSessionId()); log.info("COMPOSED_QUICK_START recording container launched for session: {}",
recorderSession.getSessionId());
launched = true; launched = true;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds", recorderSession.getSessionId(), secondsBetweenRetries); log.warn(
"Failed to launch COMPOSED_QUICK_START recording container for session {}. Trying again in {} seconds",
recorderSession.getSessionId(), secondsBetweenRetries);
try { try {
Thread.sleep(secondsBetweenRetries * 1000); Thread.sleep(secondsBetweenRetries * 1000);
} catch (InterruptedException e2) {} } catch (InterruptedException e2) {
}
seconds++; seconds++;
} finally { } finally {
if (seconds == secondsToRetry && !launched) { if (seconds == secondsToRetry && !launched) {
log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}", recorderSession.getSessionId()); log.error("Error launchaing COMPOSED_QUICK_ªSTART recording container for session {}",
recorderSession.getSessionId());
} }
} }
} }

View File

@ -56,6 +56,7 @@ import io.openvidu.server.recording.CompositeWrapper;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingInfoUtils; import io.openvidu.server.recording.RecordingInfoUtils;
import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.DockerManager;
import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.QuarantineKiller;
@ -70,8 +71,9 @@ public class ComposedRecordingService extends RecordingService {
protected DockerManager dockerManager; protected DockerManager dockerManager;
public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, public ComposedRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr,
super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); QuarantineKiller quarantineKiller) {
super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller);
this.dockerManager = new DockerManager(); this.dockerManager = new DockerManager();
} }
@ -278,7 +280,9 @@ public class ComposedRecordingService extends RecordingService {
log.warn("Deleting unusable files for recording {}", recordingId); log.warn("Deleting unusable files for recording {}", recordingId);
if (HttpStatus.NO_CONTENT if (HttpStatus.NO_CONTENT
.equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) { .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)); getMetadataFilePath(recording));
cleanRecordingMaps(recording); cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus());
if (session != null && reason != null) { if (session != null && reason != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason); 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 // Decrement active recordings
// ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); // ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
} }
@ -354,6 +360,7 @@ public class ComposedRecordingService extends RecordingService {
finalRecordingArray[0] = recording; finalRecordingArray[0] = recording;
try { try {
this.recordingDownloader.downloadRecording(finalRecordingArray[0], null, () -> { this.recordingDownloader.downloadRecording(finalRecordingArray[0], null, () -> {
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + finalRecordingArray[0].getId() String filesPath = this.openviduConfig.getOpenViduRecordingPath() + finalRecordingArray[0].getId()
+ "/"; + "/";
File videoFile = new File(filesPath + finalRecordingArray[0].getName() + ".webm"); File videoFile = new File(filesPath + finalRecordingArray[0].getName() + ".webm");
@ -364,16 +371,15 @@ public class ComposedRecordingService extends RecordingService {
finalDuration, finalDuration,
filesPath + RecordingManager.RECORDING_ENTITY_FILE + finalRecordingArray[0].getId()); 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 // Decrement active recordings once it is downloaded
((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet(); ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
// Now we can drop Media Node if waiting-idle-to-terminate // Now we can drop Media Node if waiting-idle-to-terminate
this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); this.quarantineKiller.dropMediaNode(session.getMediaNodeId());
// Upload if necessary
this.uploadRecording(finalRecordingArray[0], reason);
}); });
} catch (IOException e) { } catch (IOException e) {
log.error("Error while downloading recording {}: {}", finalRecordingArray[0].getName(), e.getMessage()); log.error("Error while downloading recording {}: {}", finalRecordingArray[0].getName(), e.getMessage());

View File

@ -72,6 +72,7 @@ import io.openvidu.server.kurento.kms.Kms;
import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.kurento.kms.KmsManager;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.CustomFileManager;
import io.openvidu.server.utils.DockerManager; import io.openvidu.server.utils.DockerManager;
import io.openvidu.server.utils.JsonUtils; import io.openvidu.server.utils.JsonUtils;
@ -93,9 +94,15 @@ public class RecordingManager {
@Autowired @Autowired
private SessionManager sessionManager; private SessionManager sessionManager;
@Autowired
protected RecordingManagerUtils recordingManagerUtils;
@Autowired @Autowired
private RecordingDownloader recordingDownloader; private RecordingDownloader recordingDownloader;
@Autowired
private RecordingUploader recordingUploader;
@Autowired @Autowired
protected OpenviduConfig openviduConfig; protected OpenviduConfig openviduConfig;
@ -119,7 +126,7 @@ public class RecordingManager {
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor( private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
Runtime.getRuntime().availableProcessors()); 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"; public static final String IMAGE_NAME = "openvidu/openvidu-recording";
static String IMAGE_TAG; static String IMAGE_TAG;
@ -159,12 +166,12 @@ public class RecordingManager {
RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion(); RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion();
this.dockerManager = new DockerManager(); this.dockerManager = new DockerManager();
this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, openviduConfig, cdr, this.composedRecordingService = new ComposedRecordingService(this, recordingDownloader, recordingUploader,
quarantineKiller);
this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader,
openviduConfig, cdr, quarantineKiller); openviduConfig, cdr, quarantineKiller);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader, openviduConfig, this.composedQuickStartRecordingService = new ComposedQuickStartRecordingService(this, recordingDownloader,
cdr, quarantineKiller); recordingUploader, openviduConfig, cdr, quarantineKiller);
this.singleStreamRecordingService = new SingleStreamRecordingService(this, recordingDownloader,
recordingUploader, openviduConfig, cdr, quarantineKiller);
log.info("Recording module required: Downloading openvidu/openvidu-recording:" log.info("Recording module required: Downloading openvidu/openvidu-recording:"
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)"); + openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)");
@ -432,64 +439,105 @@ public class RecordingManager {
} }
public Collection<Recording> getFinishedRecordings() { public Collection<Recording> getFinishedRecordings() {
return this.getAllRecordingsFromHost().stream().filter(recording -> recording.getStatus().equals(Status.ready)) return recordingManagerUtils.getAllRecordingsFromStorage().stream()
.collect(Collectors.toSet()); .filter(recording -> recording.getStatus().equals(Status.ready)).collect(Collectors.toSet());
} }
public Recording getRecording(String recordingId) { public Recording getRecording(String recordingId) {
return this.getRecordingFromHost(recordingId); return recordingManagerUtils.getRecordingFromStorage(recordingId);
} }
public Collection<Recording> getAllRecordings() { public Collection<Recording> getAllRecordings() {
return this.getAllRecordingsFromHost(); return recordingManagerUtils.getAllRecordingsFromStorage();
} }
public String getFreeRecordingId(String sessionId) { public String getFreeRecordingId(String sessionId) {
Set<String> recordingIds = this.getRecordingIdsFromHost(); return recordingManagerUtils.getFreeRecordingId(sessionId);
String recordingId = sessionId;
boolean isPresent = recordingIds.contains(recordingId);
int i = 1;
while (isPresent) {
recordingId = sessionId + "-" + i;
i++;
isPresent = recordingIds.contains(recordingId);
}
return recordingId;
} }
public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) { public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {
if (!force && (this.startedRecordings.containsKey(recordingId) if (this.startedRecordings.containsKey(recordingId) || this.startingRecordings.containsKey(recordingId)) {
|| this.startingRecordings.containsKey(recordingId))) { if (!force) {
// Cannot delete an active recording // Cannot delete an active recording
return HttpStatus.CONFLICT; return HttpStatus.CONFLICT;
} }
}
Recording recording = getRecordingFromHost(recordingId); Recording recording = recordingManagerUtils.getRecordingFromStorage(recordingId);
if (recording == null) { if (recording == null) {
return HttpStatus.NOT_FOUND; return HttpStatus.NOT_FOUND;
} }
if (Status.stopped.equals(recording.getStatus())) { if (Status.stopped.equals(recording.getStatus())) {
// Recording is being downloaded from remote host // Recording is being downloaded from remote host or being uploaded
log.warn("Cancelling ongoing download process of recording {}", recording.getId()); log.warn("Recording {} status is \"stopped\". Cancelling possible ongoing download process", recording.getId());
this.recordingDownloader.cancelDownload(recording.getId()); this.recordingDownloader.cancelDownload(recording.getId());
} }
File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); return recordingManagerUtils.deleteRecordingFromStorage(recordingId);
}
public Set<String> getAllRecordingIdsFromLocalStorage() {
File folder = new File(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;
}
public HttpStatus deleteRecordingFromLocalStorage(String recordingId) {
File folder = new File(openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles(); File[] files = folder.listFiles();
for (int i = 0; i < files.length; i++) { for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory() && files[i].getName().equals(recordingId)) { if (files[i].isDirectory() && files[i].getName().equals(recordingId)) {
// Correct folder. Delete it // Correct folder. Delete it
try { try {
FileUtils.deleteDirectory(files[i]); FileUtils.deleteDirectory(files[i]);
return HttpStatus.NO_CONTENT;
} catch (IOException e) { } catch (IOException e) {
log.error("Couldn't delete folder {}", files[i].getAbsolutePath()); 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<Recording> getAllRecordingsFromLocalStorage() {
File folder = new File(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 = getRecordingFromEntityFile(innerFiles[j]);
if (recording != null) {
recordingEntities.add(recording);
}
}
}
}
return recordingEntities;
} }
public Recording getRecordingFromEntityFile(File file) { public Recording getRecordingFromEntityFile(File file) {
@ -501,28 +549,27 @@ public class RecordingManager {
log.error("Error reading recording entity file {}: {}", file.getAbsolutePath(), (e.getMessage())); log.error("Error reading recording entity file {}: {}", file.getAbsolutePath(), (e.getMessage()));
return null; return null;
} }
Recording recording = new Recording(json); return getRecordingFromJson(json);
if (Status.ready.equals(recording.getStatus()) || Status.failed.equals(recording.getStatus())) {
recording.setUrl(getRecordingUrl(recording));
}
return recording;
} }
return null; return null;
} }
public String getRecordingUrl(Recording recording) { public Recording getRecordingFromJson(JsonObject json) {
return openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/" + recording.getName() + "." Recording recording = new Recording(json);
+ this.getExtensionFromRecording(recording); 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) { public String getRecordingUrl(Recording recording) {
if (OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) { return recordingManagerUtils.getRecordingUrl(recording);
return "zip";
} else if (recording.hasVideo()) {
return "mp4";
} else {
return "webm";
}
} }
public void initAutomaticRecordingStopThread(final Session session) { 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<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) {
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 void checkRecordingPaths(String openviduRecordingPath, String openviduRecordingCustomLayout) private void checkRecordingPaths(String openviduRecordingPath, String openviduRecordingCustomLayout)
throws OpenViduException { throws OpenViduException {
log.info("Initializing recording paths"); log.info("Initializing recording paths");

View File

@ -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<Recording> getAllRecordingsFromStorage();
public abstract HttpStatus deleteRecordingFromStorage(String recordingId);
protected abstract String getRecordingUrl(Recording recording);
protected abstract Set<String> 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<String> recordingIds = getAllRecordingIdsFromStorage();
return getNextAvailableRecordingId(sessionId, recordingIds);
}
private String getNextAvailableRecordingId(String baseRecordingId, Set<String> existingRecordingIds) {
String recordingId = baseRecordingId;
boolean isPresent = existingRecordingIds.contains(recordingId);
int i = 1;
while (isPresent) {
recordingId = baseRecordingId + "-" + i;
i++;
isPresent = existingRecordingIds.contains(recordingId);
}
return recordingId;
}
}

View File

@ -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<Recording> 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<String> getAllRecordingIdsFromStorage() {
File folder = new File(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;
}
}

View File

@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory;
import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.OpenViduException.Code;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingLayout;
import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord; 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.core.Session;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.CommandExecutor; import io.openvidu.server.utils.CommandExecutor;
import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.CustomFileManager;
import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.QuarantineKiller;
@ -44,14 +46,17 @@ public abstract class RecordingService {
protected OpenviduConfig openviduConfig; protected OpenviduConfig openviduConfig;
protected RecordingManager recordingManager; protected RecordingManager recordingManager;
protected RecordingDownloader recordingDownloader; protected RecordingDownloader recordingDownloader;
protected RecordingUploader recordingUploader;
protected CallDetailRecord cdr; protected CallDetailRecord cdr;
protected QuarantineKiller quarantineKiller; protected QuarantineKiller quarantineKiller;
protected CustomFileManager fileWriter = new CustomFileManager(); protected CustomFileManager fileWriter = new CustomFileManager();
RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, RecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr,
QuarantineKiller quarantineKiller) {
this.recordingManager = recordingManager; this.recordingManager = recordingManager;
this.recordingDownloader = recordingDownloader; this.recordingDownloader = recordingDownloader;
this.recordingUploader = recordingUploader;
this.openviduConfig = openviduConfig; this.openviduConfig = openviduConfig;
this.cdr = cdr; this.cdr = cdr;
this.quarantineKiller = quarantineKiller; this.quarantineKiller = quarantineKiller;
@ -105,9 +110,14 @@ public abstract class RecordingService {
*/ */
protected Recording sealRecordingMetadataFileAsReady(Recording recording, long size, double duration, protected Recording sealRecordingMetadataFileAsReady(Recording recording, long size, double duration,
String metadataFilePath) { String metadataFilePath) {
io.openvidu.java.client.Recording.Status status = io.openvidu.java.client.Recording.Status.failed Status status = Status.failed.equals(recording.getStatus()) ? Status.failed : Status.ready;
.equals(recording.getStatus()) ? io.openvidu.java.client.Recording.Status.failed
: io.openvidu.java.client.Recording.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 // Status is now failed or ready. Url property must be defined
recording.setUrl(recordingManager.getRecordingUrl(recording)); recording.setUrl(recordingManager.getRecordingUrl(recording));
@ -195,6 +205,25 @@ public abstract class RecordingService {
return folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId(); 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 * Simple wrapper for returning update RecordingProperties and a free
* recordingId when starting a new recording * recordingId when starting a new recording

View File

@ -64,6 +64,7 @@ import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
import io.openvidu.server.recording.RecorderEndpointWrapper; import io.openvidu.server.recording.RecorderEndpointWrapper;
import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.Recording;
import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingDownloader;
import io.openvidu.server.recording.RecordingUploader;
import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.QuarantineKiller;
public class SingleStreamRecordingService extends RecordingService { public class SingleStreamRecordingService extends RecordingService {
@ -76,8 +77,9 @@ public class SingleStreamRecordingService extends RecordingService {
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream."; private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
public SingleStreamRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader, public SingleStreamRecordingService(RecordingManager recordingManager, RecordingDownloader recordingDownloader,
OpenviduConfig openviduConfig, CallDetailRecord cdr, QuarantineKiller quarantineKiller) { RecordingUploader recordingUploader, OpenviduConfig openviduConfig, CallDetailRecord cdr,
super(recordingManager, recordingDownloader, openviduConfig, cdr, quarantineKiller); QuarantineKiller quarantineKiller) {
super(recordingManager, recordingDownloader, recordingUploader, openviduConfig, cdr, quarantineKiller);
} }
@Override @Override
@ -179,10 +181,6 @@ public class SingleStreamRecordingService extends RecordingService {
} }
finalRecordingArray[0] = this.sealMetadataFiles(finalRecordingArray[0]); finalRecordingArray[0] = this.sealMetadataFiles(finalRecordingArray[0]);
final long timestamp = System.currentTimeMillis();
cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp,
finalRecordingArray[0].getStatus());
cleanRecordingWrappers(finalRecordingArray[0].getSessionId()); cleanRecordingWrappers(finalRecordingArray[0].getSessionId());
// Decrement active recordings once it is downloaded // 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 // Now we can drop Media Node if waiting-idle-to-terminate
this.quarantineKiller.dropMediaNode(session.getMediaNodeId()); this.quarantineKiller.dropMediaNode(session.getMediaNodeId());
// Upload if necessary
this.uploadRecording(finalRecordingArray[0], reason);
}); });
} catch (IOException e) { } catch (IOException e) {
log.error("Error while downloading recording {}", finalRecordingArray[0].getName()); log.error("Error while downloading recording {}", finalRecordingArray[0].getName());

View File

@ -20,6 +20,7 @@ package io.openvidu.server.utils;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.util.Map.Entry; import java.util.Map.Entry;
import org.kurento.jsonrpc.Props; import org.kurento.jsonrpc.Props;
@ -56,13 +57,15 @@ public class JsonUtils {
public JsonElement fromFileToJsonElement(String filePath) public JsonElement fromFileToJsonElement(String filePath)
throws IOException, FileNotFoundException, JsonParseException, IllegalStateException { throws IOException, FileNotFoundException, JsonParseException, IllegalStateException {
JsonElement json = null; return fromReaderToJsonElement(new FileReader(filePath));
FileReader reader = null;
try {
reader = new FileReader(filePath);
} catch (FileNotFoundException e) {
throw e;
} }
public JsonObject fromReaderToJsonObject(Reader reader) throws IOException {
return this.fromReaderToJsonElement(reader).getAsJsonObject();
}
public JsonElement fromReaderToJsonElement(Reader reader) throws IOException {
JsonElement json = null;
try { try {
json = JsonParser.parseReader(reader); json = JsonParser.parseReader(reader);
} catch (JsonParseException | IllegalStateException exception) { } catch (JsonParseException | IllegalStateException exception) {