io.openvidu.server.recording.service.RecordingManager.java Source code

Java tutorial

Introduction

Here is the source code for io.openvidu.server.recording.service.RecordingManager.java

Source

/*
 * (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package io.openvidu.server.recording.service;

import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import org.apache.commons.io.FileUtils;
import org.kurento.client.ErrorEvent;
import org.kurento.client.EventListener;
import org.kurento.client.MediaPipeline;
import org.kurento.client.MediaProfileSpecType;
import org.kurento.client.RecorderEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import io.openvidu.client.OpenViduException;
import io.openvidu.client.OpenViduException.Code;
import io.openvidu.client.internal.ProtocolElements;
import io.openvidu.java.client.RecordingProperties;
import io.openvidu.server.config.OpenviduConfig;
import io.openvidu.server.core.EndReason;
import io.openvidu.server.core.Participant;
import io.openvidu.server.core.Session;
import io.openvidu.server.core.SessionEventsHandler;
import io.openvidu.server.core.SessionManager;
import io.openvidu.server.kurento.KurentoClientProvider;
import io.openvidu.server.kurento.KurentoClientSessionInfo;
import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo;
import io.openvidu.server.recording.Recording;
import io.openvidu.server.utils.CustomFileManager;
import io.openvidu.server.utils.DockerManager;

@Service
public class RecordingManager {

    private static final Logger log = LoggerFactory.getLogger(RecordingManager.class);

    RecordingService recordingService;
    private ComposedRecordingService composedRecordingService;
    private SingleStreamRecordingService singleStreamRecordingService;
    private DockerManager dockerManager;

    @Autowired
    protected SessionEventsHandler sessionHandler;

    @Autowired
    private SessionManager sessionManager;

    @Autowired
    protected OpenviduConfig openviduConfig;

    @Autowired
    private KurentoClientProvider kcProvider;

    protected Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
    protected Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
    protected Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
    private final Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();

    private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
            Runtime.getRuntime().availableProcessors());

    static final String RECORDING_ENTITY_FILE = ".recording.";
    public static final String IMAGE_NAME = "openvidu/openvidu-recording";
    static String IMAGE_TAG;

    private static final List<EndReason> LAST_PARTICIPANT_LEFT_REASONS = Arrays
            .asList(new EndReason[] { EndReason.disconnect, EndReason.forceDisconnectByUser,
                    EndReason.forceDisconnectByServer, EndReason.networkDisconnect });

    public SessionEventsHandler getSessionEventsHandler() {
        return this.sessionHandler;
    }

    public SessionManager getSessionManager() {
        return this.sessionManager;
    }

    public void initializeRecordingManager() throws OpenViduException {

        RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion();

        this.dockerManager = new DockerManager();
        this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
        this.singleStreamRecordingService = new SingleStreamRecordingService(this, openviduConfig);

        log.info("Recording module required: Downloading openvidu/openvidu-recording:"
                + openviduConfig.getOpenViduRecordingVersion() + " Docker image (350MB aprox)");

        this.checkRecordingRequirements(this.openviduConfig.getOpenViduRecordingPath(),
                this.openviduConfig.getOpenviduRecordingCustomLayout());

        if (dockerManager.dockerImageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) {
            log.info("Docker image already exists locally");
        } else {
            Thread t = new Thread(() -> {
                boolean keep = true;
                log.info("Downloading ");
                while (keep) {
                    System.out.print(".");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        keep = false;
                        log.info("\nDownload complete");
                    }
                }
            });
            t.start();
            try {
                dockerManager.downloadDockerImage(IMAGE_NAME + ":" + IMAGE_TAG, 600);
            } catch (Exception e) {
                log.error("Error downloading docker image {}:{}", IMAGE_NAME, IMAGE_TAG);
            }
            t.interrupt();
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("Docker image available");
        }

        // Clean any stranded openvidu/openvidu-recording container on startup
        dockerManager.cleanStrandedContainers(RecordingManager.IMAGE_NAME);
    }

    public void checkRecordingRequirements(String openviduRecordingPath, String openviduRecordingCustomLayout)
            throws OpenViduException {
        if (dockerManager == null) {
            this.dockerManager = new DockerManager();
        }
        dockerManager.checkDockerEnabled(openviduConfig.getSpringProfile());
        this.checkRecordingPaths(openviduRecordingPath, openviduRecordingCustomLayout);
    }

    public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
        Recording recording = null;
        try {
            switch (properties.outputMode()) {
            case COMPOSED:
                recording = this.composedRecordingService.startRecording(session, properties);
                break;
            case INDIVIDUAL:
                recording = this.singleStreamRecordingService.startRecording(session, properties);
                break;
            }
        } catch (OpenViduException e) {
            throw e;
        }
        if (session.getActivePublishers() == 0) {
            // Init automatic recording stop if there are now publishers when starting
            // recording
            log.info("No publisher in session {}. Starting {} seconds countdown for stopping recording",
                    session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout());
            this.initAutomaticRecordingStopThread(session);
        }
        return recording;
    }

    public Recording stopRecording(Session session, String recordingId, EndReason reason) {
        Recording recording;
        if (session == null) {
            recording = this.startedRecordings.get(recordingId);
        } else {
            recording = this.sessionsRecordings.get(session.getSessionId());
        }
        switch (recording.getOutputMode()) {
        case COMPOSED:
            recording = this.composedRecordingService.stopRecording(session, recording, reason);
            break;
        case INDIVIDUAL:
            recording = this.singleStreamRecordingService.stopRecording(session, recording, reason);
            break;
        }
        this.abortAutomaticRecordingStopThread(session);
        return recording;
    }

    public Recording forceStopRecording(Session session, EndReason reason) {
        Recording recording;
        recording = this.sessionsRecordings.get(session.getSessionId());
        switch (recording.getOutputMode()) {
        case COMPOSED:
            recording = this.composedRecordingService.stopRecording(session, recording, reason, true);
            break;
        case INDIVIDUAL:
            recording = this.singleStreamRecordingService.stopRecording(session, recording, reason, true);
            break;
        }
        this.abortAutomaticRecordingStopThread(session);
        return recording;
    }

    public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
            Participant participant) {
        Recording recording = this.sessionsRecordings.get(session.getSessionId());
        if (recording == null) {
            log.error("Cannot start recording of new stream {}. Session {} is not being recorded",
                    participant.getPublisherStreamId(), session.getSessionId());
        }
        if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
            // Start new RecorderEndpoint for this stream
            log.info("Starting new RecorderEndpoint in session {} for new stream of participant {}",
                    session.getSessionId(), participant.getParticipantPublicId());
            final CountDownLatch startedCountDown = new CountDownLatch(1);
            this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(session, recordingId,
                    profile, participant, startedCountDown);
        } else if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(recording.getOutputMode())
                && !recording.hasVideo()) {
            // Connect this stream to existing Composite recorder
            log.info(
                    "Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}",
                    session.getSessionId(), participant.getParticipantPublicId());
            this.composedRecordingService.joinPublisherEndpointToComposite(session, recordingId, participant);
        }
    }

    public void stopOneIndividualStreamRecording(String sessionId, String streamId, boolean forceAfterKmsRestart) {
        Recording recording = this.sessionsRecordings.get(sessionId);
        if (recording == null) {
            log.error("Cannot stop recording of existing stream {}. Session {} is not being recorded", streamId,
                    sessionId);
        }
        if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
            // Stop specific RecorderEndpoint for this stream
            log.info("Stopping RecorderEndpoint in session {} for stream of participant {}", sessionId, streamId);
            final CountDownLatch stoppedCountDown = new CountDownLatch(1);
            this.singleStreamRecordingService.stopRecorderEndpointOfPublisherEndpoint(sessionId, streamId,
                    stoppedCountDown, forceAfterKmsRestart);
            try {
                if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
                    log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
                            sessionId);
                }
            } catch (InterruptedException e) {
                log.error("Exception while waiting for state change", e);
            }
        } else if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(recording.getOutputMode())
                && !recording.hasVideo()) {
            // Disconnect this stream from existing Composite recorder
            log.info("Removing PublisherEndpoint from Composite in session {} for stream of participant {}",
                    sessionId, streamId);
            this.composedRecordingService.removePublisherEndpointFromComposite(sessionId, streamId);
        }
    }

    public boolean sessionIsBeingRecorded(String sessionId) {
        return (this.sessionsRecordings.get(sessionId) != null);
    }

    public Recording getStartedRecording(String recordingId) {
        return this.startedRecordings.get(recordingId);
    }

    public Recording getStartingRecording(String recordingId) {
        return this.startingRecordings.get(recordingId);
    }

    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)))
                .collect(Collectors.toSet());
    }

    public Recording getRecording(String recordingId) {
        return this.getRecordingFromHost(recordingId);
    }

    public Collection<Recording> getAllRecordings() {
        return this.getAllRecordingsFromHost();
    }

    public String getFreeRecordingId(String sessionId, String shortSessionId) {
        Set<String> recordingIds = this.getRecordingIdsFromHost();
        String recordingId = shortSessionId;
        boolean isPresent = recordingIds.contains(recordingId);
        int i = 1;

        while (isPresent) {
            recordingId = shortSessionId + "-" + i;
            i++;
            isPresent = recordingIds.contains(recordingId);
        }

        return recordingId;
    }

    public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {

        if (!force && (this.startedRecordings.containsKey(recordingId)
                || this.startingRecordings.containsKey(recordingId))) {
            // Cannot delete an active recording
            return HttpStatus.CONFLICT;
        }

        Recording recording = getRecordingFromHost(recordingId);
        if (recording == null) {
            return HttpStatus.NOT_FOUND;
        }

        File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
        File[] files = folder.listFiles();
        for (int i = 0; i < files.length; i++) {
            if (files[i].isDirectory() && files[i].getName().equals(recordingId)) {
                // Correct folder. Delete it
                try {
                    FileUtils.deleteDirectory(files[i]);
                } catch (IOException e) {
                    log.error("Couldn't delete folder {}", files[i].getAbsolutePath());
                }
                break;
            }
        }

        return HttpStatus.NO_CONTENT;
    }

    public Recording getRecordingFromEntityFile(File file) {
        if (file.isFile() && file.getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
            JsonObject json = null;
            try {
                json = new JsonParser().parse(new FileReader(file)).getAsJsonObject();
            } catch (IOException e) {
                return null;
            }
            return new Recording(json);
        }
        return null;
    }

    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());

            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();
                } else {
                    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
                log.warn("Recording {} was already automatically stopped by a previous thread", recordingId);
            }

        }, this.openviduConfig.getOpenviduRecordingAutostopTimeout(), TimeUnit.SECONDS);
        this.automaticRecordingStopThreads.putIfAbsent(session.getSessionId(), future);
    }

    public boolean abortAutomaticRecordingStopThread(Session session) {
        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)) {
                // 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, EndReason.automaticStop);
                sessionManager.showTokens();
            }
            return cancelled;
        } else {
            return true;
        }
    }

    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);
        File file = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
                + RecordingManager.RECORDING_ENTITY_FILE + recordingId);
        log.info("File exists: " + file.exists());
        Recording recording = this.getRecordingFromEntityFile(file);
        if (recording != null) {
            this.updateRecordingUrl(recording);
        }
        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) {
                        this.updateRecordingUrl(recording);
                        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)
            throws OpenViduException {
        log.info("Initializing recording paths");

        Path recordingPath = null;
        try {
            recordingPath = Files.createDirectories(Paths.get(openviduRecordingPath));
        } catch (IOException e) {
            String errorMessage = "The recording path \"" + openviduRecordingPath
                    + "\" is not valid. Reason: OpenVidu Server cannot find path \"" + openviduRecordingPath
                    + "\" and doesn't have permissions to create it";
            log.error(errorMessage);
            throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, errorMessage);
        }

        // Check OpenVidu Server write permissions in recording path
        if (!Files.isWritable(recordingPath)) {
            String errorMessage = "The recording path \"" + openviduRecordingPath
                    + "\" is not valid. Reason: OpenVidu Server needs write permissions. Try running command \"sudo chmod 777 "
                    + openviduRecordingPath + "\"";
            log.error(errorMessage);
            throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, errorMessage);
        } else {
            log.info("OpenVidu Server has write permissions on recording path: {}", openviduRecordingPath);
        }

        final String testFolderPath = openviduRecordingPath + "/TEST_RECORDING_PATH_" + System.currentTimeMillis();
        final String testFilePath = testFolderPath + "/TEST_RECORDING_PATH.webm";

        // Check Kurento Media Server write permissions in recording path
        KurentoClientSessionInfo kcSessionInfo = new OpenViduKurentoClientSessionInfo("TEST_RECORDING_PATH",
                "TEST_RECORDING_PATH");
        MediaPipeline pipeline = this.kcProvider.getKurentoClient(kcSessionInfo).createMediaPipeline();
        RecorderEndpoint recorder = new RecorderEndpoint.Builder(pipeline, "file://" + testFilePath).build();

        final AtomicBoolean kurentoRecorderError = new AtomicBoolean(false);

        recorder.addErrorListener(new EventListener<ErrorEvent>() {
            @Override
            public void onEvent(ErrorEvent event) {
                if (event.getErrorCode() == 6) {
                    // KMS write permissions error
                    kurentoRecorderError.compareAndSet(false, true);
                }
            }
        });

        recorder.record();

        try {
            // Give the error event some time to trigger if necessary
            Thread.sleep(500);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        if (kurentoRecorderError.get()) {
            String errorMessage = "The recording path \"" + openviduRecordingPath
                    + "\" is not valid. Reason: Kurento Media Server needs write permissions. Try running command \"sudo chmod 777 "
                    + openviduRecordingPath + "\"";
            log.error(errorMessage);
            throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, errorMessage);
        }

        recorder.stop();
        recorder.release();
        pipeline.release();

        log.info("Kurento Media Server has write permissions on recording path: {}", openviduRecordingPath);

        try {
            new CustomFileManager().deleteFolder(testFolderPath);
            log.info("OpenVidu Server has write permissions over files created by Kurento Media Server");
        } catch (IOException e) {
            String errorMessage = "The recording path \"" + openviduRecordingPath
                    + "\" is not valid. Reason: OpenVidu Server does not have write permissions over files created by Kurento Media Server. "
                    + "Try running Kurento Media Server as user \"" + System.getProperty("user.name")
                    + "\" or run OpenVidu Server as superuser";
            log.error(errorMessage);
            log.error(
                    "Be aware that a folder \"{}\" was created and should be manually deleted (\"sudo rm -rf {}\")",
                    testFolderPath, testFolderPath);
            throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, errorMessage);
        }

        if (openviduConfig.openviduRecordingCustomLayoutChanged(openviduRecordingCustomLayout)) {
            // Property openvidu.recording.custom-layout changed
            File dir = new File(openviduRecordingCustomLayout);
            if (dir.exists()) {
                if (!dir.isDirectory()) {
                    String errorMessage = "The custom layouts path \"" + openviduRecordingCustomLayout
                            + "\" is not valid. Reason: path already exists but it is not a directory";
                    log.error(errorMessage);
                    throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, errorMessage);
                } else {
                    if (dir.listFiles() == null) {
                        String errorMessage = "The custom layouts path \"" + openviduRecordingCustomLayout
                                + "\" is not valid. Reason: OpenVidu Server needs read permissions. Try running command \"sudo chmod 755 "
                                + openviduRecordingCustomLayout + "\"";
                        log.error(errorMessage);
                        throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, errorMessage);
                    } else {
                        log.info("OpenVidu Server has read permissions on custom layout path: {}",
                                openviduRecordingCustomLayout);
                        log.info("Custom layouts path successfully initialized at {}",
                                openviduRecordingCustomLayout);
                    }
                }
            } else {
                try {
                    Files.createDirectories(dir.toPath());
                    log.warn(
                            "OpenVidu custom layouts path (system property 'openvidu.recording.custom-layout') has been created, being folder {}. "
                                    + "It is an empty folder, so no custom layout is currently present",
                            dir.getAbsolutePath());
                } catch (IOException e) {
                    String errorMessage = "The custom layouts path \"" + openviduRecordingCustomLayout
                            + "\" is not valid. Reason: OpenVidu Server cannot find path \""
                            + openviduRecordingCustomLayout + "\" and doesn't have permissions to create it";
                    log.error(errorMessage);
                    throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, errorMessage);
                }
            }
        }

        log.info("Recording path successfully initialized at {}", openviduRecordingPath);
    }

    public static EndReason finalReason(EndReason reason) {
        if (RecordingManager.LAST_PARTICIPANT_LEFT_REASONS.contains(reason)) {
            return EndReason.lastParticipantLeft;
        } else {
            return reason;
        }
    }

}