openvidu-server: recording stop refactoring

pull/391/head
pabloFuente 2020-01-23 18:45:35 +01:00
parent 6c1a168aca
commit 4fcb6839d0
6 changed files with 156 additions and 129 deletions

View File

@ -102,6 +102,11 @@ public class Session implements SessionInterface {
return null;
}
public boolean onlyRecorderParticipant() {
return this.participants.size() == 1 && ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
.equals(this.participants.values().iterator().next().getParticipantPublicId());
}
public int getActivePublishers() {
return activePublishers.get();
}

View File

@ -461,19 +461,16 @@ public abstract class SessionManager {
* was forcibly closed.
*
* @param sessionId identifier of the session
* @return
* @return set of {@link Participant} POJOS representing the session's
* participants
* @throws OpenViduException in case the session doesn't exist or has been
* already closed
*/
public Set<Participant> closeSession(String sessionId, EndReason reason) {
public void closeSession(String sessionId, EndReason reason) {
Session session = sessions.get(sessionId);
if (session == null) {
throw new OpenViduException(Code.ROOM_NOT_FOUND_ERROR_CODE, "Session '" + sessionId + "' not found");
}
if (session.isClosed()) {
this.closeSessionAndEmptyCollections(session, reason);
this.cleanCollections(sessionId);
throw new OpenViduException(Code.ROOM_CLOSED_ERROR_CODE, "Session '" + sessionId + "' already closed");
}
Set<Participant> participants = getParticipants(sessionId);
@ -489,21 +486,27 @@ public abstract class SessionManager {
}
if (!sessionClosedByLastParticipant) {
// This code should never be executed, as last evicted participant must trigger
// session close
this.closeSessionAndEmptyCollections(session, reason);
// This code should only be executed when there were no participants connected
// to the session. That is: if the session was in the automatic recording stop
// timeout with INDIVIDUAL recording (no docker participant connected)
this.closeSessionAndEmptyCollections(session, reason, true);
}
}
return participants;
}
public void closeSessionAndEmptyCollections(Session session, EndReason reason, boolean stopRecording) {
public void closeSessionAndEmptyCollections(Session session, EndReason reason) {
if (openviduConfig.isRecordingModuleEnabled()
if (openviduConfig.isRecordingModuleEnabled() && stopRecording
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason));
}
if (EndReason.automaticStop.equals(reason) && !session.getParticipants().isEmpty()
&& !session.onlyRecorderParticipant()) {
log.warn(
"Some user connected to the session between automatic recording stop and session close up. Canceling session close up");
return;
}
final String mediaNodeId = session.getMediaNodeId();
if (session.close(reason)) {

View File

@ -214,7 +214,7 @@ public class KurentoSessionManager extends SessionManager {
recordingManager.initAutomaticRecordingStopThread(session);
} else {
log.info("No more participants in session '{}', removing it and closing it", sessionId);
this.closeSessionAndEmptyCollections(session, reason);
this.closeSessionAndEmptyCollections(session, reason, true);
sessionClosedByLastParticipant = true;
showTokens();
}
@ -223,7 +223,7 @@ public class KurentoSessionManager extends SessionManager {
&& this.recordingManager.sessionIsBeingRecorded(sessionId)
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
// Start countdown
// RECORDER participant is the last one standing. Start countdown
log.info("Last participant left. Starting {} seconds countdown for stopping recording of session {}",
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
recordingManager.initAutomaticRecordingStopThread(session);

View File

@ -97,7 +97,7 @@ public class ComposedRecordingService extends RecordingService {
}
// Increment active recordings
((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet();
// ((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet();
return recording;
}
@ -225,6 +225,9 @@ public class ComposedRecordingService extends RecordingService {
this.generateRecordingMetadataFile(recording);
// Increment active recordings
((KurentoSession) session).getKms().getActiveRecordings().incrementAndGet();
return recording;
}
@ -235,7 +238,6 @@ public class ComposedRecordingService extends RecordingService {
RecordingManager.finalReason(reason));
String containerId = this.sessionsContainers.remove(recording.getSessionId());
this.cleanRecordingMaps(recording);
final String recordingId = recording.getId();
@ -248,9 +250,11 @@ public class ComposedRecordingService extends RecordingService {
}
if (containerId == null) {
if (this.recordingManager.startingRecordings.containsKey(recordingId)) {
// Session was closed while recording container was initializing
// Wait until containerId is available and force its stop and deletion
final Recording recordingAux = recording;
new Thread(() -> {
log.warn("Session closed while starting recording container");
boolean containerClosed = false;
@ -273,8 +277,8 @@ public class ComposedRecordingService extends RecordingService {
dockerManager.removeDockerContainer(containerIdAux, true);
containers.remove(containerId);
containerClosed = true;
log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux,
session.getSessionId());
log.warn("Container {} for closed session {} succesfully stopped and removed",
containerIdAux, session.getSessionId());
log.warn("Deleting unusable files for recording {}", recordingId);
if (HttpStatus.NO_CONTENT
.equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) {
@ -282,61 +286,25 @@ public class ComposedRecordingService extends RecordingService {
}
}
}
cleanRecordingMaps(recordingAux);
if (i == timeout) {
log.error("Container did not launched in {} seconds", timeout / 2);
return;
}
// Decrement active recordings
// ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
}).start();
}
} else {
// Gracefully stop ffmpeg process
try {
dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
stopAndRemoveRecordingContainer(recording, containerId, 30);
recording = updateRecordingAttributes(recording);
// Wait for the container to be gracefully self-stopped
final int timeOfWait = 30;
try {
dockerManager.waitForContainerStopped(containerId, timeOfWait);
} catch (Exception e) {
failRecordingCompletion(recording, containerId,
new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process couldn't finish in " + timeOfWait + " seconds"));
}
// Remove container
dockerManager.removeDockerContainer(containerId, false);
containers.remove(containerId);
// Update recording attributes reading from video report file
try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + recordingId + ".info");
if (!infoUtils.hasVideo()) {
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.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();
} 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: " + e.getMessage());
}
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
recording = this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
filesPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId());
final String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
final String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
this.sealRecordingMetadataFileAsReady(recording, recording.getSize(), recording.getDuration(),
metadataFilePath);
cleanRecordingMaps(recording);
final long timestamp = System.currentTimeMillis();
this.cdr.recordRecordingStatusChanged(recording, reason, timestamp, recording.getStatus());
@ -344,10 +312,10 @@ public class ComposedRecordingService extends RecordingService {
if (session != null && reason != null) {
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
}
}
// Decrement active recordings
((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
// ((KurentoSession) session).getKms().getActiveRecordings().decrementAndGet();
}
return recording;
}
@ -425,6 +393,53 @@ public class ComposedRecordingService extends RecordingService {
return finalRecordingArray[0];
}
private void stopAndRemoveRecordingContainer(Recording recording, String containerId, int secondsOfWait) {
// Gracefully stop ffmpeg process
try {
dockerManager.runCommandInContainer(containerId, "echo 'q' > stop", 0);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
// Wait for the container to be gracefully self-stopped
final int timeOfWait = 30;
try {
dockerManager.waitForContainerStopped(containerId, timeOfWait);
} catch (Exception e) {
failRecordingCompletion(recording, containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
"The recording completion process couldn't finish in " + timeOfWait + " seconds"));
}
// Remove container
dockerManager.removeDockerContainer(containerId, false);
containers.remove(containerId);
}
private Recording updateRecordingAttributes(Recording recording) {
try {
RecordingInfoUtils infoUtils = new RecordingInfoUtils(this.openviduConfig.getOpenViduRecordingPath()
+ recording.getId() + "/" + recording.getId() + ".info");
if (!infoUtils.hasVideo()) {
log.error("COMPOSED recording {} with hasVideo=true has not video track", recording.getId());
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
} else {
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();
return 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: " + e.getMessage());
}
}
private void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException {
boolean isPresent = false;
int i = 1;

View File

@ -474,48 +474,50 @@ public class RecordingManager {
public void initAutomaticRecordingStopThread(final Session session) {
final String recordingId = this.sessionsRecordings.get(session.getSessionId()).getId();
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
log.info("Stopping recording {} after {} seconds wait (no publisher published before timeout)", recordingId,
this.openviduConfig.getOpenviduRecordingAutostopTimeout());
this.automaticRecordingStopThreads.computeIfAbsent(session.getSessionId(), f -> {
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
log.info("Stopping recording {} after {} seconds wait (no publisher published before timeout)",
recordingId, this.openviduConfig.getOpenviduRecordingAutostopTimeout());
if (this.automaticRecordingStopThreads.remove(session.getSessionId()) != null) {
if (session.getParticipants().size() == 0 || (session.getParticipants().size() == 1
&& session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID) != null)) {
// Close session if there are no participants connected (except for RECORDER).
// This code won't be executed only when some user reconnects to the session
// but never publishing (publishers automatically abort this thread)
if (session.getParticipants().size() == 0 || session.onlyRecorderParticipant()) {
// Close session if there are no participants connected (RECORDER does not
// count) and publishing
log.info("Closing session {} after automatic stop of recording {}", session.getSessionId(),
recordingId);
sessionManager.closeSessionAndEmptyCollections(session, EndReason.automaticStop);
sessionManager.closeSessionAndEmptyCollections(session, EndReason.automaticStop, true);
sessionManager.showTokens();
} else {
// There are users connected, but no one is publishing
log.info(
"Automatic stopping recording {}. There are users connected to session {}, but no one is publishing",
recordingId, session.getSessionId());
this.stopRecording(session, recordingId, EndReason.automaticStop);
}
} else {
// This code is reachable if there already was an automatic stop of a recording
// caused by not user publishing within timeout after recording started, and a
// new automatic stop thread was started by last user leaving the session
// This code shouldn't be reachable
log.warn("Recording {} was already automatically stopped by a previous thread", recordingId);
}
}, this.openviduConfig.getOpenviduRecordingAutostopTimeout(), TimeUnit.SECONDS);
this.automaticRecordingStopThreads.putIfAbsent(session.getSessionId(), future);
return future;
});
}
public boolean abortAutomaticRecordingStopThread(Session session, EndReason reason) {
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(session.getSessionId());
if (future != null) {
boolean cancelled = future.cancel(false);
if (session.getParticipants().size() == 0 || (session.getParticipants().size() == 1
&& session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID) != null)) {
if (session.getParticipants().size() == 0 || session.onlyRecorderParticipant()) {
// Close session if there are no participants connected (except for RECORDER).
// This code will only be executed if recording is manually stopped during the
// automatic stop timeout, so the session must be also closed
log.info(
"Ongoing recording of session {} was explicetly stopped within timeout for automatic recording stop. Closing session",
session.getSessionId());
sessionManager.closeSessionAndEmptyCollections(session, reason);
sessionManager.closeSessionAndEmptyCollections(session, reason, false);
sessionManager.showTokens();
}
return cancelled;

View File

@ -223,7 +223,8 @@ public class SessionRestController {
Session sessionNotActive = this.sessionManager.getSessionNotActive(sessionId);
if (sessionNotActive != null) {
this.sessionManager.closeSessionAndEmptyCollections(sessionNotActive, EndReason.sessionClosedByServer);
this.sessionManager.closeSessionAndEmptyCollections(sessionNotActive, EndReason.sessionClosedByServer,
true);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
} else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
@ -541,7 +542,8 @@ public class SessionRestController {
session.recordingManuallyStopped.set(true);
if (session != null && OutputMode.COMPOSED.equals(recording.getOutputMode()) && recording.hasVideo()) {
if (session != null && !session.isClosed() && OutputMode.COMPOSED.equals(recording.getOutputMode())
&& recording.hasVideo()) {
sessionManager.evictParticipant(
session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null, null, null);
}