openvidu-server: recording status refactoring

pull/375/head
pabloFuente 2019-06-27 14:04:02 +02:00
parent a528e1da8e
commit 690900a45b
14 changed files with 132 additions and 128 deletions

View File

@ -450,8 +450,9 @@ public class OpenVidu {
/**
* Deletes a recording. The recording must have status
* {@link io.openvidu.java.client.Recording.Status#stopped} or
* {@link io.openvidu.java.client.Recording.Status#available}
* {@link io.openvidu.java.client.Recording.Status#stopped},
* {@link io.openvidu.java.client.Recording.Status#ready} or
* {@link io.openvidu.java.client.Recording.Status#failed}
*
* @param recordingId The id property of the recording you want to delete
*

View File

@ -30,7 +30,8 @@ public class Recording {
public enum Status {
/**
* The recording is starting (cannot be stopped)
* The recording is starting (cannot be stopped). Some recording may not go
* through this status and directly reach "started" status
*/
starting,
@ -40,27 +41,21 @@ public class Recording {
started,
/**
* The recording has finished OK
* The recording has stopped and is being processed. At some point it will reach
* "ready" status
*/
stopped,
/**
* The recording has stopped but is being processed. This status will change to
* stopped/available or failed
* The recording has finished OK and is available for download through OpenVidu
* Server recordings endpoint:
* https://YOUR_OPENVIDUSERVER_IP/recordings/{RECORDING_ID}/{RECORDING_NAME}.{EXTENSION}
*/
processing,
ready,
/**
* The recording is available for downloading. This status is reached for all
* stopped recordings if
* <a href="https://openvidu.io/docs/reference-docs/openvidu-server-params/"
* target="_blank">OpenVidu Server configuration</a> property
* <code>openvidu.recording.public-access</code> is true
*/
available,
/**
* The recording has failed
* The recording has failed. This status may be reached from "starting",
* "started" and "stopped" status
*/
failed;
}

View File

@ -359,7 +359,7 @@ export class OpenVidu {
}
/**
* Deletes a [[Recording]]. The recording must have status `stopped` or `available`
* Deletes a [[Recording]]. The recording must have status `stopped`, `ready` or `failed`
*
* @param recordingId
*

View File

@ -101,7 +101,8 @@ export namespace Recording {
export enum Status {
/**
* The recording is starting (cannot be stopped)
* The recording is starting (cannot be stopped). Some recording may not go
* through this status and directly reach "started" status
*/
starting = 'starting',
@ -111,25 +112,21 @@ export namespace Recording {
started = 'started',
/**
* The recording has finished OK
*/
* The recording has stopped and is being processed. At some point it will reach
* "ready" status
*/
stopped = 'stopped',
/**
* The recording has stopped but is being processed. This status will change to
* stopped/available or failed when processing ends
*/
processing = 'processing',
/**
* The recording is available for downloading. This status is reached for all
* stopped recordings if [OpenVidu Server configuration](https://openvidu.io/docs/reference-docs/openvidu-server-params/)
* property `openvidu.recording.public-access` is true
* The recording has finished OK and is available for download through OpenVidu
* Server recordings endpoint:
* https://YOUR_OPENVIDUSERVER_IP/recordings/{RECORDING_ID}/{RECORDING_NAME}.{EXTENSION}
*/
available = 'available',
ready = 'ready',
/**
* The recording has failed
* The recording has failed. This status may be reached from "starting",
* "started" and "stopped" status
*/
failed = 'failed'
}

View File

@ -28,8 +28,8 @@ public class CDREventRecording extends CDREventEnd {
protected Recording recording;
// recordingStarted
public CDREventRecording(String sessionId, Recording recording) {
super(CDREventName.recordingStarted, sessionId, recording.getCreatedAt());
public CDREventRecording(Recording recording) {
super(CDREventName.recordingStarted, recording.getSessionId(), recording.getCreatedAt());
this.recording = recording;
}

View File

@ -20,33 +20,41 @@ package io.openvidu.server.cdr;
import com.google.gson.JsonObject;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingLayout;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.recording.Recording;
public class CDREventRecordingStatus extends CDREventRecording {
public class CDREventRecordingStatus extends CDREventEnd {
private Recording recording;
private Status status;
public CDREventRecordingStatus(String sessionId, Recording recording, Status status) {
super(sessionId, recording);
this.eventName = CDREventName.recordingStatusChanged;
public CDREventRecordingStatus(Recording recording, Long startTime, EndReason reason, Long timestamp,
Status status) {
super(CDREventName.recordingStatusChanged, recording.getSessionId(), startTime, reason, timestamp);
this.recording = recording;
this.status = status;
}
public CDREventRecordingStatus(CDREventRecording recordingStartedEvent, Recording recording, EndReason finalReason,
long timestamp, Status status) {
super(recordingStartedEvent, recording, finalReason, timestamp);
this.eventName = CDREventName.recordingStatusChanged;
this.status = status;
}
public Status getStatus() {
return status;
}
@Override
public JsonObject toJson() {
JsonObject json = super.toJson();
json.addProperty("id", this.recording.getId());
json.addProperty("name", this.recording.getName());
json.addProperty("outputMode", this.recording.getOutputMode().name());
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recording.getOutputMode())
&& this.recording.hasVideo()) {
json.addProperty("resolution", this.recording.getResolution());
json.addProperty("recordingLayout", this.recording.getRecordingLayout().name());
if (RecordingLayout.CUSTOM.equals(this.recording.getRecordingLayout())
&& this.recording.getCustomLayout() != null && !this.recording.getCustomLayout().isEmpty()) {
json.addProperty("customLayout", this.recording.getCustomLayout());
}
}
json.addProperty("hasAudio", this.recording.hasAudio());
json.addProperty("hasVideo", this.recording.hasVideo());
json.addProperty("size", this.recording.getSize());
json.addProperty("duration", this.recording.getDuration());
json.addProperty("status", this.status.name());
return json;
}

View File

@ -198,34 +198,25 @@ public class CallDetailRecord {
}
}
public void recordRecordingStarted(String sessionId, Recording recording) {
CDREventRecording recordingStartedEvent = new CDREventRecording(sessionId, recording);
public void recordRecordingStarted(Recording recording) {
CDREventRecording recordingStartedEvent = new CDREventRecording(recording);
this.recordings.putIfAbsent(recording.getId(), recordingStartedEvent);
this.log(new CDREventRecording(sessionId, recording));
this.recordRecordingStatusChanged(sessionId, recording, Status.started);
this.log(recordingStartedEvent);
}
public void recordRecordingStopped(String sessionId, Recording recording, EndReason reason) {
public void recordRecordingStopped(Recording recording, EndReason reason, long timestamp) {
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
final long timestamp = System.currentTimeMillis();
CDREventRecording recordingStoppedEvent = new CDREventRecording(recordingStartedEvent, recording,
RecordingManager.finalReason(reason), timestamp);
this.log(recordingStoppedEvent);
this.recordRecordingStatusChanged(recordingStartedEvent, recording, RecordingManager.finalReason(reason),
timestamp, Status.stopped);
// Summary: update ended recording
sessionManager.getAccumulatedRecordings(sessionId).add(recordingStoppedEvent);
sessionManager.getAccumulatedRecordings(recording.getSessionId()).add(recordingStoppedEvent);
}
public void recordRecordingStatusChanged(String sessionId, Recording recording, Status status) {
this.log(new CDREventRecordingStatus(sessionId, recording, status));
}
public void recordRecordingStatusChanged(CDREventRecording recordingStartedEvent, Recording recording,
EndReason finalReason, long timestamp, Status status) {
this.log(new CDREventRecordingStatus(recordingStartedEvent, recording, finalReason, timestamp, status));
public void recordRecordingStatusChanged(Recording recording, EndReason finalReason, long timestamp,
Status status) {
this.log(new CDREventRecordingStatus(recording, recording.getCreatedAt(), finalReason, timestamp, status));
}
private void log(CDREvent event) {

View File

@ -13,4 +13,8 @@ public class DummyRecordingDownloader implements RecordingDownloader {
return;
}
@Override
public void cancelDownload(String recordingId) {
}
}

View File

@ -25,4 +25,6 @@ public interface RecordingDownloader {
public void downloadRecording(Recording recording, Collection<String> streamIds, Runnable callback)
throws IOException;
public void cancelDownload(String recordingId);
}

View File

@ -94,10 +94,10 @@ public class ComposedRecordingService extends RecordingService {
@Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) {
recording = this.sealRecordingMetadataFileAsStopped(recording);
if (recording.hasVideo()) {
return this.stopRecordingWithVideo(session, recording, reason);
} else {
recording = this.sealRecordingMetadataFileAsProcessing(recording);
return this.stopRecordingAudioOnly(session, recording, reason, 0);
}
}
@ -240,7 +240,7 @@ public class ComposedRecordingService extends RecordingService {
if (containerId == null) {
// Session was closed while recording container was initializing
// Wait until containerId is available and force its stop and removal
// Wait until containerId is available and force its stop and deletion
new Thread(() -> {
log.warn("Session closed while starting recording container");
boolean containerClosed = false;
@ -311,25 +311,23 @@ public class ComposedRecordingService extends RecordingService {
log.error("COMPOSED recording {} with hasVideo=true has not video track", recordingId);
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
} else {
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
recording.setStatus(io.openvidu.java.client.Recording.Status.ready);
recording.setDuration(infoUtils.getDurationInSeconds());
recording.setSize(infoUtils.getSizeInBytes());
recording.setResolution(infoUtils.videoWidth() + "x" + infoUtils.videoHeight());
recording.setHasAudio(infoUtils.hasAudio());
recording.setHasVideo(infoUtils.hasVideo());
}
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");
}
this.cdr.recordRecordingStopped(recording.getSessionId(), recording, reason);
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStopped(recording, reason, timestamp);
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus());
if (session != null && reason != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
@ -383,9 +381,13 @@ public class ComposedRecordingService extends RecordingService {
long finalSize = videoFile.length();
double finalDuration = (double) compositeWrapper.getDuration() / 1000;
this.updateFilePermissions(filesPath);
finalRecordingArray[0] = this.sealRecordingMetadataFileAsStopped(recording, finalSize, finalDuration,
finalRecordingArray[0] = this.sealRecordingMetadataFileAsReady(recording, finalSize, finalDuration,
filesPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId());
cdr.recordRecordingStopped(finalRecordingArray[0].getSessionId(), finalRecordingArray[0], reason);
final long timestamp = System.currentTimeMillis();
cdr.recordRecordingStopped(finalRecordingArray[0], reason, timestamp);
cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp,
finalRecordingArray[0].getStatus());
});
} catch (IOException e) {
log.error("Error while downloading recording {}: {}", recording.getName(), e.getMessage());

View File

@ -55,6 +55,7 @@ import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.Recording.OutputMode;
import io.openvidu.java.client.Recording.Status;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.cdr.CallDetailRecord;
import io.openvidu.server.config.OpenviduConfig;
@ -187,7 +188,9 @@ public class RecordingManager {
}
this.updateRecordingManagerCollections(session, recording);
this.cdr.recordRecordingStarted(session.getSessionId(), recording);
this.cdr.recordRecordingStarted(recording);
this.cdr.recordRecordingStatusChanged(recording, null, recording.getCreatedAt(),
io.openvidu.java.client.Recording.Status.started);
if (!(OutputMode.COMPOSED.equals(properties.outputMode()) && properties.hasVideo())) {
// Directly send recording started notification for all cases except for
@ -211,6 +214,10 @@ public class RecordingManager {
} else {
recording = this.sessionsRecordings.get(session.getSessionId());
}
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, Status.stopped);
switch (recording.getOutputMode()) {
case COMPOSED:
recording = this.composedRecordingService.stopRecording(session, recording, reason);
@ -312,8 +319,7 @@ public class RecordingManager {
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)))
.filter(recording -> recording.getStatus().equals(io.openvidu.java.client.Recording.Status.ready))
.collect(Collectors.toSet());
}
@ -352,6 +358,11 @@ public class RecordingManager {
if (recording == null) {
return HttpStatus.NOT_FOUND;
}
if (io.openvidu.java.client.Recording.Status.stopped.equals(recording.getStatus())) {
// Recording is being downloaded from remote host
log.warn("Cancelling ongoing download process of recording {}", recording.getId());
this.recordingDownloader.cancelDownload(recording.getId());
}
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
File[] files = folder.listFiles();
@ -443,30 +454,6 @@ public class RecordingManager {
}
}
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 = recording.hasVideo() ? "mp4" : "webm";
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);
@ -474,9 +461,6 @@ public class RecordingManager {
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
log.info("File exists: " + file.exists());
Recording recording = this.getRecordingFromEntityFile(file);
if (recording != null) {
this.updateRecordingUrl(recording);
}
return recording;
}
@ -491,7 +475,6 @@ public class RecordingManager {
for (int j = 0; j < innerFiles.length; j++) {
Recording recording = this.getRecordingFromEntityFile(innerFiles[j]);
if (recording != null) {
this.updateRecordingUrl(recording);
recordingEntities.add(recording);
}
}

View File

@ -24,7 +24,6 @@ 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;
@ -82,31 +81,29 @@ public abstract class RecordingService {
}
/**
* Update and overwrites metadata recording file to set it in "processing"
* status. Recording size and duration will remain as 0
* Update and overwrites metadata recording file to set it in "stopped" status.
* Recording size and duration will remain as 0
*
* @return updated Recording object
*/
protected Recording sealRecordingMetadataFileAsProcessing(Recording recording) {
protected Recording sealRecordingMetadataFileAsStopped(Recording recording) {
final String entityFile = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
+ RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
Recording rec = this.sealRecordingMetadataFile(recording, 0, 0,
io.openvidu.java.client.Recording.Status.processing, entityFile);
this.cdr.recordRecordingStatusChanged(recording.getSessionId(), recording, Status.processing);
return rec;
return this.sealRecordingMetadataFile(recording, 0, 0, io.openvidu.java.client.Recording.Status.stopped,
entityFile);
}
/**
* Update and overwrites metadata recording file to set it in "stopped" (or
* Update and overwrites metadata recording file to set it in "ready" (or
* "failed") status
*
* @return updated Recording object
*/
protected Recording sealRecordingMetadataFileAsStopped(Recording recording, long size, double duration,
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.stopped;
: io.openvidu.java.client.Recording.Status.ready;
final String entityFile = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
+ RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
@ -119,7 +116,6 @@ public abstract class RecordingService {
recording.setSize(size); // Size in bytes
recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds
this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString());
recording = this.recordingManager.updateRecordingUrl(recording);
log.info("Sealed recording metadata file at {} with status [{}]", metadataFilePath, status.name());

View File

@ -131,7 +131,7 @@ public class SingleStreamRecordingService extends RecordingService {
@Override
public Recording stopRecording(Session session, Recording recording, EndReason reason) {
recording = this.sealRecordingMetadataFileAsProcessing(recording);
recording = this.sealRecordingMetadataFileAsStopped(recording);
return this.stopRecording(session, recording, reason, 0);
}
@ -171,7 +171,11 @@ public class SingleStreamRecordingService extends RecordingService {
}
}
finalRecordingArray[0] = this.sealMetadataFiles(recording);
cdr.recordRecordingStopped(finalRecordingArray[0].getSessionId(), finalRecordingArray[0], reason);
final long timestamp = System.currentTimeMillis();
cdr.recordRecordingStopped(finalRecordingArray[0], reason, timestamp);
cdr.recordRecordingStatusChanged(finalRecordingArray[0], reason, timestamp,
finalRecordingArray[0].getStatus());
});
} catch (IOException e) {
log.error("Error while downloading recording {}", recording.getName());
@ -427,7 +431,7 @@ public class SingleStreamRecordingService extends RecordingService {
double duration = (double) (maxEndTime - minStartTime) / 1000;
duration = duration > 0 ? duration : 0;
recording = this.sealRecordingMetadataFileAsStopped(recording, accumulatedSize, duration, metadataFilePath);
recording = this.sealRecordingMetadataFileAsReady(recording, accumulatedSize, duration, metadataFilePath);
return recording;
}

View File

@ -2209,8 +2209,8 @@ public class OpenViduTestAppE2eTest {
Assert.assertTrue("Wrong recording size. Excepected > 0 and was " + recording.getSize(),
recording.getSize() > 0);
Assert.assertNull("Wrong recording url. Expected not null and was null", recording.getUrl());
Assert.assertEquals("Wrong recording status. Expected stopped and was " + recording.getStatus().name(),
Recording.Status.stopped, recording.getStatus());
Assert.assertEquals("Wrong recording status. Expected ready and was " + recording.getStatus().name(),
Recording.Status.ready, recording.getStatus());
Assert.assertFalse("Session shouldn't be being recorded", session.isBeingRecorded());
Assert.assertFalse("OpenVidu.fetch() should return false", OV.fetch());
@ -2261,7 +2261,7 @@ public class OpenViduTestAppE2eTest {
Assert.assertTrue("Wrong recording duration", recording2.getDuration() > 0);
Assert.assertTrue("Wrong recording size", recording2.getSize() > 0);
Assert.assertNull("Wrong recording url", recording2.getUrl());
Assert.assertEquals("Wrong recording status", Recording.Status.stopped, recording2.getStatus());
Assert.assertEquals("Wrong recording status", Recording.Status.ready, recording2.getStatus());
Assert.assertFalse("Session shouldn't be being recorded", session.isBeingRecorded());
Assert.assertFalse("Session.fetch() should return false", session.fetch());
@ -2773,6 +2773,9 @@ public class OpenViduTestAppE2eTest {
event.get("outputMode").getAsString());
Assert.assertEquals("Wrong recording outputMode in webhook event", 0, event.get("size").getAsLong());
Assert.assertEquals("Wrong recording outputMode in webhook event", 0, event.get("duration").getAsLong());
Assert.assertEquals("Wrong recording startTime/timestamp in webhook event",
event.get("startTime").getAsLong(), event.get("timestamp").getAsLong());
Assert.assertNull("Wrong recording reason in webhook event (should be null)", event.get("reason"));
user.getDriver().findElement(By.id("add-user-btn")).click();
user.getDriver().findElement(By.cssSelector("#openvidu-instance-1 .join-btn")).click();
@ -2796,16 +2799,34 @@ public class OpenViduTestAppE2eTest {
CustomWebhook.waitForEvent("participantLeft", 2);
event = CustomWebhook.waitForEvent("recordingStatusChanged", 2);
Assert.assertEquals("Wrong recording status in webhook event", "processing",
OV.fetch();
List<Recording> recs = OV.listRecordings();
Assert.assertEquals("Wrong number of recording entities", 1, recs.size());
Recording rec = recs.get(0);
Assert.assertEquals("Wrong recording status in webhook event", "stopped",
event.get("status").getAsString());
Assert.assertEquals("Wrong recording outputMode in webhook event", 0, event.get("size").getAsLong());
Assert.assertEquals("Wrong recording outputMode in webhook event", 0, event.get("duration").getAsLong());
Assert.assertEquals("Wrong recording reason in webhook event", "sessionClosedByServer",
event.get("reason").getAsString());
Assert.assertEquals("Wrong recording reason in webhook event", rec.getCreatedAt(),
event.get("startTime").getAsLong());
event = CustomWebhook.waitForEvent("recordingStatusChanged", 2);
Assert.assertEquals("Wrong recording status in webhook event", "stopped",
event.get("status").getAsString());
OV.fetch();
recs = OV.listRecordings();
Assert.assertEquals("Wrong number of recording entities", 1, recs.size());
rec = recs.get(0);
Assert.assertEquals("Wrong recording status in webhook event", "ready", event.get("status").getAsString());
Assert.assertTrue("Wrong recording outputMode in webhook event", event.get("size").getAsLong() > 0);
Assert.assertTrue("Wrong recording outputMode in webhook event", event.get("duration").getAsLong() > 0);
Assert.assertEquals("Wrong recording reason in webhook event", "sessionClosedByServer",
event.get("reason").getAsString());
Assert.assertEquals("Wrong recording reason in webhook event", rec.getCreatedAt(),
event.get("startTime").getAsLong());
CustomWebhook.waitForEvent("sessionDestroyed", 2);