diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java index 24173bd5..9e402202 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java @@ -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(); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index 7d4cc7bf..d4214b43 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -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 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 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) { + public void closeSessionAndEmptyCollections(Session session, EndReason reason, boolean stopRecording) { - 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)) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java index 5d9cdb5f..76f35817 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java @@ -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); 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 a991e738..cfb5b3c9 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 @@ -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,95 +250,61 @@ 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 - new Thread(() -> { - log.warn("Session closed while starting recording container"); - boolean containerClosed = false; - String containerIdAux; - int i = 0; - final int timeout = 30; - while (!containerClosed && (i < timeout)) { - containerIdAux = this.sessionsContainers.remove(session.getSessionId()); - if (containerIdAux == null) { - try { - log.warn("Waiting for container to be launched..."); - i++; - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } else { - log.warn("Removing container {} for closed session {}...", containerIdAux, - session.getSessionId()); - dockerManager.removeDockerContainer(containerIdAux, true); - containers.remove(containerId); - containerClosed = true; - 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))) { - log.warn("Files properly deleted"); + // 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; + String containerIdAux; + int i = 0; + final int timeout = 30; + while (!containerClosed && (i < timeout)) { + containerIdAux = this.sessionsContainers.remove(session.getSessionId()); + if (containerIdAux == null) { + try { + log.warn("Waiting for container to be launched..."); + i++; + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } else { + log.warn("Removing container {} for closed session {}...", containerIdAux, + session.getSessionId()); + dockerManager.removeDockerContainer(containerIdAux, true); + containers.remove(containerId); + containerClosed = true; + 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))) { + log.warn("Files properly deleted"); + } } } - } - if (i == timeout) { - log.error("Container did not launched in {} seconds", timeout / 2); - return; - } - }).start(); - + 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(); + // Decrement active recordings + // ((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; 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 dabb5502..6f46c657 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 @@ -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 -> { - 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) - log.info("Closing session {} after automatic stop of recording {}", session.getSessionId(), - recordingId); - sessionManager.closeSessionAndEmptyCollections(session, EndReason.automaticStop); - sessionManager.showTokens(); + 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.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, 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.stopRecording(session, recordingId, EndReason.automaticStop); + // This code shouldn't be reachable + log.warn("Recording {} was already automatically stopped by a previous thread", recordingId); } - } 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 - log.warn("Recording {} was already automatically stopped by a previous thread", recordingId); - } + }, this.openviduConfig.getOpenviduRecordingAutostopTimeout(), TimeUnit.SECONDS); - }, 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; diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java index 37c06607..fefdbaa5 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java @@ -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); }