diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index 78bbea0b..c21e0e00 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -48,7 +48,7 @@ import io.openvidu.server.kurento.KurentoClientProvider; import io.openvidu.server.kurento.core.KurentoSessionEventsHandler; import io.openvidu.server.kurento.core.KurentoSessionManager; import io.openvidu.server.kurento.kms.FixedOneKmsManager; -import io.openvidu.server.recording.RecordingService; +import io.openvidu.server.recording.ComposedRecordingService; import io.openvidu.server.rest.NgrokRestController; import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.rpc.RpcNotificationService; @@ -150,6 +150,7 @@ public class OpenViduServer implements JsonRpcConfigurer { NEW_LINE; System.out.println(str); OpenViduServer.publicUrl = ngrok.getNgrokServerUrl().replaceFirst("https://", "wss://"); + openviduConf.setFinalUrl(ngrok.getNgrokServerUrl()); } catch (Exception e) { System.err.println("Ngrok URL was configured, but there was an error connecting to ngrok: " @@ -161,6 +162,7 @@ public class OpenViduServer implements JsonRpcConfigurer { case "docker": try { OpenViduServer.publicUrl = "wss://" + getContainerIp() + ":" + openviduConf.getServerPort(); + openviduConf.setFinalUrl("https://" + getContainerIp() + ":" + openviduConf.getServerPort()); } catch (Exception e) { System.err.println("Docker container IP was configured, but there was an error obtaining IP: " + e.getClass().getName() + " " + e.getMessage()); @@ -170,6 +172,9 @@ public class OpenViduServer implements JsonRpcConfigurer { case "local": break; + + case "docker-local": + break; default: @@ -178,6 +183,9 @@ public class OpenViduServer implements JsonRpcConfigurer { type = "custom"; OpenViduServer.publicUrl = publicUrl.replaceFirst("https://", "wss://"); + OpenViduServer.publicUrl = publicUrl.replaceFirst("http://", "wss://"); + openviduConf.setFinalUrl(url.toString()); + if (!OpenViduServer.publicUrl.startsWith("wss://")) { OpenViduServer.publicUrl = "wss://" + OpenViduServer.publicUrl; } @@ -191,26 +199,34 @@ public class OpenViduServer implements JsonRpcConfigurer { if (OpenViduServer.publicUrl == null) { type = "local"; OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort(); + openviduConf.setFinalUrl("https://localhost:" + openviduConf.getServerPort()); } - + if (OpenViduServer.publicUrl.endsWith("/")) { OpenViduServer.publicUrl = OpenViduServer.publicUrl.substring(0, OpenViduServer.publicUrl.length() - 1); } boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled(); if (recordingModuleEnabled) { - RecordingService recordingService = context.getBean(RecordingService.class); - System.out.println("Recording module required: Downloading openvidu/openvidu-recording Docker image (800 MB aprox)"); - + ComposedRecordingService recordingService = context.getBean(ComposedRecordingService.class); + System.out.println( + "Recording module required: Downloading openvidu/openvidu-recording Docker image (800 MB aprox)"); + boolean imageExists = false; - try { - imageExists = recordingService.recordingImageExistsLocally(); - } catch (ProcessingException exception) { - log.error("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service"); - throw new RuntimeException("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service"); - } - - if (imageExists) { + try { + imageExists = recordingService.recordingImageExistsLocally(); + } catch (ProcessingException exception) { + String message = "Exception connecting to Docker daemon: "; + if ("docker-local".equals(openviduConf.getOpenViduPublicUrl())) { + message += "make sure you include flag \"-v /var/run/docker.sock:/var/run/docker.sock\" in \"docker run\" command"; + } else { + message += "you need Docker installed in this machine to enable OpenVidu recorder service"; + } + log.error(message); + throw new RuntimeException(message); + } + + if (imageExists) { System.out.println("Docker image already exists locally"); } else { Thread t = new Thread(() -> { diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java index d184fed7..d6fc14d7 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Component; public class OpenviduConfig { @Value("${openvidu.publicurl}") - private String openviduPublicUrl; // local, ngrok, docker, [FINAL_URL] + private String openviduPublicUrl; // local, docker-local, ngrok, docker, [FINAL_URL] @Value("${server.port}") private String serverPort; @@ -27,6 +27,8 @@ public class OpenviduConfig { @Value("${openvidu.recording.free-access}") boolean openviduRecordingFreeAccess; + private String finalUrl; + public String getOpenViduPublicUrl() { return this.openviduPublicUrl; } @@ -59,4 +61,16 @@ public class OpenviduConfig { return this.openviduRecordingFreeAccess; } + public void setOpenViduRecordingPath(String recordingPath) { + this.openviduRecordingPath = recordingPath; + } + + public String getFinalUrl() { + return finalUrl; + } + + public void setFinalUrl(String finalUrl) { + this.finalUrl = finalUrl.endsWith("/") ? (finalUrl) : (finalUrl + "/"); + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java index d7882135..488439b6 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/SecurityConfig.java @@ -21,6 +21,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { .authorizeRequests() .antMatchers(HttpMethod.POST, "/api/sessions").authenticated() .antMatchers(HttpMethod.POST, "/api/tokens").authenticated() + .antMatchers(HttpMethod.POST, "/api/recordings/start").authenticated() + .antMatchers(HttpMethod.POST, "/api/recordings/stop").authenticated() + .antMatchers(HttpMethod.GET, "/api/recordings").authenticated() + .antMatchers(HttpMethod.GET, "/api/recordings/**").authenticated() + .antMatchers(HttpMethod.DELETE, "/api/recordings/**").authenticated() .antMatchers("/").authenticated(); if (openviduConf.getOpenViduRecordingFreeAccess()) { diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/ServletCustomizer.java b/openvidu-server/src/main/java/io/openvidu/server/config/ServletCustomizer.java new file mode 100644 index 00000000..15e4f89d --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/config/ServletCustomizer.java @@ -0,0 +1,17 @@ +package io.openvidu.server.config; + +import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; +import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; +import org.springframework.boot.context.embedded.MimeMappings; +import org.springframework.stereotype.Component; + +@Component +public class ServletCustomizer implements EmbeddedServletContainerCustomizer { + + @Override + public void customize(ConfigurableEmbeddedServletContainer container) { + MimeMappings mappings = new MimeMappings(MimeMappings.DEFAULT); + mappings.add("mkv","video/x-matroska"); + container.setMimeMappings(mappings); + } +} \ No newline at end of file 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 c4f803c1..204784bd 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 @@ -24,4 +24,6 @@ public interface Session { Participant getParticipantByPublicId(String participantPublicId); + int getActivePublishers(); + } 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 7db72d00..824d846f 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 @@ -1,7 +1,5 @@ package io.openvidu.server.core; -import java.math.BigInteger; -import java.security.SecureRandom; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -10,6 +8,7 @@ import java.util.stream.Collectors; import javax.annotation.PreDestroy; +import org.apache.commons.lang3.RandomStringUtils; import org.kurento.jsonrpc.message.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +59,15 @@ public abstract class SessionManager { */ public void evictParticipant(String participantPrivateId) throws OpenViduException { } + + /** + * Returns a Session given its id + * + * @return Session + */ + public Session getSession(String sessionId) { + return sessions.get(sessionId); + } /** * Returns all currently active (opened) sessions. @@ -141,7 +149,7 @@ public abstract class SessionManager { public String newSessionId(SessionProperties sessionProperties) { String sessionId = OpenViduServer.publicUrl; - sessionId += "/" + new BigInteger(130, new SecureRandom()).toString(32); + sessionId += "/" + RandomStringUtils.randomAlphanumeric(16).toLowerCase(); this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>()); this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>()); @@ -155,7 +163,7 @@ public abstract class SessionManager { if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null && this.sessionidTokenTokenobj.get(sessionId) != null) { if (isMetadataFormatCorrect(serverMetadata)) { - String token = new BigInteger(130, new SecureRandom()).toString(32); + String token = RandomStringUtils.randomAlphanumeric(16).toLowerCase(); this.sessionidTokenTokenobj.get(sessionId).put(token, new Token(token, role, serverMetadata)); showTokens(); return token; @@ -226,12 +234,12 @@ public abstract class SessionManager { public Participant newParticipant(String sessionId, String participantPrivatetId, Token token, String clientMetadata) { if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) { - String participantPublicId = new BigInteger(130, new SecureRandom()).toString(32); + String participantPublicId = RandomStringUtils.randomAlphanumeric(16).toLowerCase(); ConcurrentHashMap participantpublicidParticipant = this.sessionidParticipantpublicidParticipant .get(sessionId); while (participantpublicidParticipant.containsKey(participantPublicId)) { // Avoid random 'participantpublicid' collisions - participantPublicId = new BigInteger(130, new SecureRandom()).toString(32); + participantPublicId = RandomStringUtils.randomAlphanumeric(16).toLowerCase(); } Participant p = new Participant(participantPrivatetId, participantPublicId, token, clientMetadata); this.sessionidParticipantpublicidParticipant.get(sessionId).put(participantPublicId, p); diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java index 1da900c8..e0dc7e61 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java @@ -219,7 +219,8 @@ public class KurentoSession implements Session { other.cancelReceivingMedia(participant.getParticipantPublicId()); } } - + + @Override public int getActivePublishers() { return activePublishers.get(); } 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 aa52b142..9a215fa3 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 @@ -18,6 +18,7 @@ import com.google.gson.JsonSyntaxException; import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ArchiveLayout; import io.openvidu.java.client.ArchiveMode; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.SessionProperties; @@ -26,7 +27,7 @@ import io.openvidu.server.kurento.KurentoClientProvider; import io.openvidu.server.kurento.KurentoClientSessionInfo; import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo; import io.openvidu.server.kurento.endpoint.SdpType; -import io.openvidu.server.recording.RecordingService; +import io.openvidu.server.recording.ComposedRecordingService; import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.MediaOptions; @@ -44,7 +45,7 @@ public class KurentoSessionManager extends SessionManager { private KurentoSessionEventsHandler sessionHandler; @Autowired - private RecordingService recordingService; + private ComposedRecordingService recordingService; @Autowired OpenviduConfig openviduConfig; @@ -62,7 +63,7 @@ public class KurentoSessionManager extends SessionManager { SessionProperties properties = sessionProperties.get(sessionId); if (properties == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) { properties = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED) - .archiveMode(ArchiveMode.ALWAYS).build(); + .archiveMode(ArchiveMode.ALWAYS).archiveLayout(ArchiveLayout.BEST_FIT).build(); } createSession(kcSessionInfo, properties); } @@ -94,7 +95,7 @@ public class KurentoSessionManager extends SessionManager { @Override public void leaveRoom(Participant participant, Integer transactionId) { log.debug("Request [LEAVE_ROOM] ({})", participant.getParticipantPublicId()); - + KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoSession session = kParticipant.getSession(); String sessionId = session.getSessionId(); @@ -147,16 +148,15 @@ public class KurentoSessionManager extends SessionManager { showTokens(); log.warn("Session '{}' removed and closed", sessionId); - } - if ( - remainingParticipants.size() == 1 && - openviduConfig.isRecordingModuleEnabled() && - MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) && - ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID.equals(remainingParticipants.iterator().next().getParticipantPublicId()) - ) { - log.info("Last participant left. Stopping recording for session {}", sessionId); - evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId()); - recordingService.stopRecording(session); + } + if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled() + && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) + && ArchiveMode.ALWAYS.equals(session.getSessionProperties().archiveMode()) + && ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID + .equals(remainingParticipants.iterator().next().getParticipantPublicId())) { + log.info("Last participant left. Stopping recording for session {}", sessionId); + evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId()); + recordingService.stopRecording(session); } sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/ComposedRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/ComposedRecordingService.java new file mode 100644 index 00000000..72d078c4 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/ComposedRecordingService.java @@ -0,0 +1,402 @@ +package io.openvidu.server.recording; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +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.TimeUnit; +import java.util.stream.Collectors; + +import javax.ws.rs.ProcessingException; + +import org.apache.commons.io.FilenameUtils; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +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.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.command.CreateContainerResponse; +import com.github.dockerjava.api.command.ExecCreateCmdResponse; +import com.github.dockerjava.api.exception.ConflictException; +import com.github.dockerjava.api.exception.DockerClientException; +import com.github.dockerjava.api.exception.InternalServerErrorException; +import com.github.dockerjava.api.exception.NotFoundException; +import com.github.dockerjava.api.model.Bind; +import com.github.dockerjava.api.model.Volume; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientBuilder; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.command.ExecStartResultCallback; +import com.github.dockerjava.core.command.PullImageResultCallback; + +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.CommandExecutor; +import io.openvidu.server.OpenViduServer; +import io.openvidu.server.config.OpenviduConfig; +import io.openvidu.server.core.Session; + +@Service +public class ComposedRecordingService { + + private Logger log = LoggerFactory.getLogger(ComposedRecordingService.class); + + @Autowired + OpenviduConfig openviduConfig; + + private Map containers = new ConcurrentHashMap<>(); + private Map sessionsContainers = new ConcurrentHashMap<>(); + private Map startingRecordings = new ConcurrentHashMap<>(); + private Map startedRecordings = new ConcurrentHashMap<>(); + private Map sessionsRecordings = new ConcurrentHashMap<>(); + + private final String IMAGE_NAME = "openvidu/openvidu-recording"; + private final String RECORDING_ENTITY_FILE = ".recording."; + + private DockerClient dockerClient; + + public ComposedRecordingService() { + DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); + this.dockerClient = DockerClientBuilder.getInstance(config).build(); + } + + public Recording startRecording(Session session) { + List envs = new ArrayList<>(); + String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1, + session.getSessionId().length()); + String videoId = this.getFreeRecordingId(session.getSessionId(), shortSessionId); + String secret = openviduConfig.getOpenViduSecret(); + + Recording recording = new Recording(session.getSessionId(), videoId, videoId); + + this.sessionsRecordings.put(session.getSessionId(), recording); + this.startingRecordings.put(recording.getId(), recording); + + String uid = null; + try { + uid = System.getenv("MY_UID"); + if (uid == null) { + uid = CommandExecutor.execCommand("/bin/sh", "-c", "id -u " + System.getProperty("user.name")); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + + String location = OpenViduServer.publicUrl.replaceFirst("wss://", ""); + String layoutUrl = session.getSessionProperties().archiveLayout().name().toLowerCase().replaceAll("_", "-"); + + envs.add("URL=https://OPENVIDUAPP:" + secret + "@" + location + "/#/layout-" + layoutUrl + "/" + shortSessionId + + "/" + secret); + envs.add("RESOLUTION=1920x1080"); + envs.add("FRAMERATE=30"); + envs.add("VIDEO_NAME=" + videoId); + envs.add("VIDEO_FORMAT=mkv"); + envs.add("USER_ID=" + uid); + envs.add("RECORDING_JSON=" + recording.toJson().toJSONString()); + + log.info(recording.toJson().toJSONString()); + log.debug("Recorder connecting to url {}", + "https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/" + secret); + + String containerId = this.runRecordingContainer(envs, "recording_" + videoId); + + this.waitForVideoFileNotEmpty(videoId); + + this.sessionsContainers.put(session.getSessionId(), containerId); + + recording.setStatus(Recording.Status.started); + + this.startedRecordings.put(recording.getId(), recording); + this.startingRecordings.remove(recording.getId()); + + return recording; + } + + public Recording stopRecording(Session session) { + Recording recording = this.sessionsRecordings.remove(session.getSessionId()); + String containerId = this.sessionsContainers.remove(session.getSessionId()); + this.startedRecordings.remove(recording.getId()); + + // Gracefully stop ffmpeg process + ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId).withAttachStdout(true) + .withAttachStderr(true).withCmd("bash", "-c", "echo 'q' > stop").exec(); + try { + dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback()) + .awaitCompletion(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // Wait for the container to be gracefully self-stopped + CountDownLatch latch = new CountDownLatch(1); + WaitForContainerStoppedCallback callback = new WaitForContainerStoppedCallback(latch); + dockerClient.waitContainerCmd(containerId).exec(callback); + + boolean stopped = false; + try { + stopped = latch.await(60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + recording.setStatus(Recording.Status.failed); + failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, + "The recording completion process has been unexpectedly interrupted")); + } + if (!stopped) { + recording.setStatus(Recording.Status.failed); + failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE, + "The recording completion process couldn't finish in 60 seconds")); + } + + // Remove container + this.removeDockerContainer(containerId); + + // Update recording attributes reading from video report file + try { + RecordingInfoUtils infoUtils = new RecordingInfoUtils( + this.openviduConfig.getOpenViduRecordingPath() + recording.getName() + ".info"); + + if (openviduConfig.getOpenViduRecordingFreeAccess()) { + recording.setStatus(Recording.Status.available); + } else { + recording.setStatus(Recording.Status.stopped); + } + recording.setDuration(infoUtils.getDurationInSeconds()); + recording.setSize(infoUtils.getSizeInBytes()); + recording.setHasAudio(infoUtils.hasAudio()); + recording.setHasVideo(infoUtils.hasVideo()); + + if (openviduConfig.getOpenViduRecordingFreeAccess()) { + recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mkv"); + } + + } catch (IOException | ParseException e) { + throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE, + "There was an error generating the metadata report file for the recording"); + } + + return recording; + } + + public boolean recordingImageExistsLocally() { + boolean imageExists = false; + try { + dockerClient.inspectImageCmd(IMAGE_NAME).exec(); + imageExists = true; + } catch (NotFoundException nfe) { + imageExists = false; + } catch (ProcessingException e) { + throw e; + } + return imageExists; + } + + public void downloadRecordingImage() { + try { + dockerClient.pullImageCmd(IMAGE_NAME).exec(new PullImageResultCallback()).awaitSuccess(); + } catch (NotFoundException | InternalServerErrorException e) { + if (imageExistsLocally(IMAGE_NAME)) { + log.info("Docker image '{}' exists locally", IMAGE_NAME); + } else { + throw e; + } + } catch (DockerClientException e) { + log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", IMAGE_NAME); + throw e; + } + } + + 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); + } + + private String runRecordingContainer(List envs, String containerName) { + Volume volume1 = new Volume("/recordings"); + CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME).withName(containerName).withEnv(envs) + .withNetworkMode("host").withVolumes(volume1) + .withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1)); + CreateContainerResponse container = null; + try { + container = cmd.exec(); + dockerClient.startContainerCmd(container.getId()).exec(); + containers.put(container.getId(), containerName); + log.info("Container ID: {}", container.getId()); + return container.getId(); + } catch (ConflictException e) { + log.error( + "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", + containerName); + return null; + } + } + + private void removeDockerContainer(String containerId) { + dockerClient.removeContainerCmd(containerId).exec(); + containers.remove(containerId); + } + + private void stopDockerContainer(String containerId) { + dockerClient.stopContainerCmd(containerId).exec(); + } + + private boolean imageExistsLocally(String imageName) { + boolean imageExists = false; + try { + dockerClient.inspectImageCmd(imageName).exec(); + imageExists = true; + } catch (NotFoundException nfe) { + imageExists = false; + } + return imageExists; + } + + public Collection getAllRecordings() { + return this.getRecordingEntitiesFromHost(); + } + + public Collection getStartingRecordings() { + return this.startingRecordings.values(); + } + + public Collection getStartedRecordings() { + return this.startedRecordings.values(); + } + + public Collection getFinishedRecordings() { + return this.getRecordingEntitiesFromHost().stream() + .filter(recording -> (recording.getStatus().equals(Recording.Status.stopped) + || recording.getStatus().equals(Recording.Status.available))) + .collect(Collectors.toSet()); + } + + private Set getRecordingIdsFromHost() { + File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + Set fileNamesNoExtension = new HashSet<>(); + for (int i = 0; i < files.length; i++) { + if (files[i].isFile() && !files[i].getName().startsWith(RECORDING_ENTITY_FILE)) { + fileNamesNoExtension.add(FilenameUtils.removeExtension(files[i].getName())); + } + } + return fileNamesNoExtension; + } + + private Set getRecordingEntitiesFromHost() { + File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + Set recordingEntities = new HashSet<>(); + for (int i = 0; i < files.length; i++) { + Recording recording = this.getRecordingFromFile(files[i]); + if (recording != null) { + if (openviduConfig.getOpenViduRecordingFreeAccess()) { + if (Recording.Status.stopped.equals(recording.getStatus())) { + recording.setStatus(Recording.Status.available); + recording.setUrl( + this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mkv"); + } + } + recordingEntities.add(recording); + } + } + return recordingEntities; + } + + public HttpStatus deleteRecordingFromHost(String recordingId) { + + if (this.startedRecordings.containsKey(recordingId) || this.startingRecordings.containsKey(recordingId)) { + // Cannot delete an active recording + return HttpStatus.CONFLICT; + } + + File folder = new File(this.openviduConfig.getOpenViduRecordingPath()); + File[] files = folder.listFiles(); + int numFilesDeleted = 0; + for (int i = 0; i < files.length; i++) { + if (files[i].isFile() && isFileFromRecording(files[i], recordingId)) { + files[i].delete(); + numFilesDeleted++; + } + } + + HttpStatus status; + if (numFilesDeleted == 3) { + status = HttpStatus.NO_CONTENT; + } else { + status = HttpStatus.NOT_FOUND; + } + return status; + } + + private Recording getRecordingFromFile(File file) { + if (file.isFile() && file.getName().startsWith(RECORDING_ENTITY_FILE)) { + JSONParser parser = new JSONParser(); + JSONObject json = null; + try { + json = (JSONObject) parser.parse(new FileReader(file)); + } catch (IOException | ParseException e) { + return null; + } + return new Recording(json); + } + return null; + } + + private boolean isFileFromRecording(File file, String recordingId) { + return (((recordingId + ".info").equals(file.getName())) || ((recordingId + ".mkv").equals(file.getName())) + || ((".recording." + recordingId).equals(file.getName()))); + } + + private String getFreeRecordingId(String sessionId, String shortSessionId) { + Set 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; + } + + private void waitForVideoFileNotEmpty(String recordingId) { + boolean isPresent = false; + while (!isPresent) { + try { + Thread.sleep(150); + File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + ".mkv"); + isPresent = ((f.isFile()) && (f.length() > 0)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + private void failRecordingCompletion(String containerId, OpenViduException e) { + this.stopDockerContainer(containerId); + this.removeDockerContainer(containerId); + throw e; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java b/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java new file mode 100644 index 00000000..9fa07cbd --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/Recording.java @@ -0,0 +1,145 @@ +package io.openvidu.server.recording; + +import org.json.simple.JSONObject; + +public class Recording { + + public enum Status { + starting, // The recording is starting (cannot be stopped) + started, // The recording has started and is going on + stopped, // The recording has finished OK + available, // The recording is available for downloading. This status is reached for all + // stopped recordings if property 'openvidu.recording.free-access' is true + failed; // The recording has failed + } + + private Status status; + + private String id; + private String name; + private String sessionId; + private long createdAt; // milliseconds (UNIX Epoch time) + private long size = 0; // bytes + private double duration = 0; // seconds + private String url; + private boolean hasAudio = true; + private boolean hasVideo = true; + + public Recording(String sessionId, String id, String name) { + this.sessionId = sessionId; + this.createdAt = System.currentTimeMillis(); + this.id = id; + this.name = id; // For now the name of the recording file is the same as its id + this.status = Status.started; + } + + public Recording(JSONObject json) { + this.id = (String) json.get("id"); + this.name = (String) json.get("name"); + this.sessionId = (String) json.get("sessionId"); + this.createdAt = (long) json.get("createdAt"); + this.size = (long) json.get("size"); + this.duration = (double) json.get("duration"); + this.url = (String) json.get("url"); + this.hasAudio = (boolean) json.get("hasAudio"); + this.hasVideo = (boolean) json.get("hasVideo"); + this.status = Status.valueOf((String) json.get("status")); + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + public long getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(long createdAt) { + this.createdAt = createdAt; + } + + public long getSize() { + return size; + } + + public void setSize(long l) { + this.size = l; + } + + public double getDuration() { + return duration; + } + + public void setDuration(double duration) { + this.duration = duration; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public boolean hasAudio() { + return hasAudio; + } + + public void setHasAudio(boolean hasAudio) { + this.hasAudio = hasAudio; + } + + public boolean hasVideo() { + return hasVideo; + } + + public void setHasVideo(boolean hasVideo) { + this.hasVideo = hasVideo; + } + + @SuppressWarnings("unchecked") + public JSONObject toJson() { + JSONObject json = new JSONObject(); + json.put("id", this.id); + json.put("name", this.name); + json.put("sessionId", this.sessionId); + json.put("createdAt", this.createdAt); + json.put("size", this.size); + json.put("duration", this.duration); + json.put("url", this.url); + json.put("hasAudio", this.hasAudio); + json.put("hasVideo", this.hasVideo); + json.put("status", this.status.toString()); + return json; + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java new file mode 100644 index 00000000..444bc93b --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingInfoUtils.java @@ -0,0 +1,104 @@ +package io.openvidu.server.recording; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; + +public class RecordingInfoUtils { + + private JSONParser parser; + private JSONObject json; + private JSONObject jsonFormat; + private JSONObject videoStream; + private JSONObject audioStream; + + public RecordingInfoUtils(String fullVideoPath) + throws FileNotFoundException, IOException, ParseException, OpenViduException { + + this.parser = new JSONParser(); + this.json = (JSONObject) parser.parse(new FileReader(fullVideoPath)); + + if (json.isEmpty()) { + // Recording metadata from ffprobe is empty: video file is corrupted or empty + throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty or corrupted"); + } + + this.jsonFormat = (JSONObject) json.get("format"); + + JSONArray streams = (JSONArray) json.get("streams"); + + for (int i = 0; i < streams.size(); i++) { + JSONObject stream = (JSONObject) streams.get(i); + if ("video".equals(stream.get("codec_type").toString())) { + this.videoStream = stream; + } else if ("audio".equals(stream.get("codec_type").toString())) { + this.audioStream = stream; + } + } + + } + + public double getDurationInSeconds() { + return Double.parseDouble(jsonFormat.get("duration").toString()); + } + + public int getSizeInBytes() { + return Integer.parseInt(jsonFormat.get("size").toString()); + } + + public int getNumberOfStreams() { + return Integer.parseInt(jsonFormat.get("nb_streams").toString()); + } + + public int getBitRate() { + return (Integer.parseInt(jsonFormat.get("bit_rate").toString()) / 1000); + } + + public boolean hasVideo() { + return this.videoStream != null; + } + + public boolean hasAudio() { + return this.audioStream != null; + } + + public int videoWidth() { + return Integer.parseInt(videoStream.get("width").toString()); + } + + public int videoHeight() { + return Integer.parseInt(videoStream.get("height").toString()); + } + + public int getVideoFramerate() { + String frameRate = videoStream.get("r_frame_rate").toString(); + String[] frameRateParts = frameRate.split("/"); + + return Integer.parseInt(frameRateParts[0]) / Integer.parseInt(frameRateParts[1]); + } + + public String getVideoCodec() { + return videoStream.get("codec_name").toString(); + } + + public String getLongVideoCodec() { + return videoStream.get("codec_long_name").toString(); + } + + public String getAudioCodec() { + return audioStream.get("codec_name").toString(); + } + + public String getLongAudioCodec() { + return audioStream.get("codec_long_name").toString(); + } + +} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingService.java deleted file mode 100644 index 88a9a3ab..00000000 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingService.java +++ /dev/null @@ -1,174 +0,0 @@ -package io.openvidu.server.recording; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.ws.rs.ProcessingException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import com.github.dockerjava.api.DockerClient; -import com.github.dockerjava.api.command.CreateContainerCmd; -import com.github.dockerjava.api.command.CreateContainerResponse; -import com.github.dockerjava.api.command.ExecCreateCmdResponse; -import com.github.dockerjava.api.exception.ConflictException; -import com.github.dockerjava.api.exception.DockerClientException; -import com.github.dockerjava.api.exception.InternalServerErrorException; -import com.github.dockerjava.api.exception.NotFoundException; -import com.github.dockerjava.api.model.Bind; -import com.github.dockerjava.api.model.Volume; -import com.github.dockerjava.core.DefaultDockerClientConfig; -import com.github.dockerjava.core.DockerClientBuilder; -import com.github.dockerjava.core.DockerClientConfig; -import com.github.dockerjava.core.command.ExecStartResultCallback; -import com.github.dockerjava.core.command.PullImageResultCallback; - -import io.openvidu.server.CommandExecutor; -import io.openvidu.server.OpenViduServer; -import io.openvidu.server.config.OpenviduConfig; -import io.openvidu.server.core.Session; - -@Service -public class RecordingService { - - private Logger log = LoggerFactory.getLogger(RecordingService.class); - - @Autowired - OpenviduConfig openviduConfig; - - private static final Map createdContainers = new HashMap<>(); - private final String IMAGE_NAME = "openvidu/openvidu-recording"; - - private DockerClient dockerClient; - private Map recordingSessions = new HashMap<>();; - - public RecordingService() { - DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build(); - this.dockerClient = DockerClientBuilder.getInstance(config).build(); - } - - public void startRecording(Session session) { - List envs = new ArrayList<>(); - String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1, - session.getSessionId().length()); - String secret = openviduConfig.getOpenViduSecret(); - - String uid = null; - try { - uid = System.getenv("MY_UID"); - if (uid==null) { - uid = CommandExecutor.execCommand("/bin/sh", "-c", "id -u " + System.getProperty("user.name")); - } - } catch (IOException | InterruptedException e) { - e.printStackTrace(); - } - - String location = OpenViduServer.publicUrl.replaceFirst("wss://", ""); - - envs.add("URL=https://OPENVIDUAPP:" + secret + "@" + location + "/#/layout-best-fit/" + shortSessionId + "/" - + secret); - envs.add("RESOLUTION=1920x1080"); - envs.add("FRAMERATE=30"); - envs.add("VIDEO_NAME=" + shortSessionId); - envs.add("VIDEO_FORMAT=mp4"); - envs.add("USER_ID=" + uid); - - System.out.println( - "https://OPENVIDUAPP:" + secret + "@localhost:8443/#/layout-best-fit/" + shortSessionId + "/" + secret); - - String containerId = this.runRecordingContainer(envs, "recording" + shortSessionId); - this.recordingSessions.putIfAbsent(session.getSessionId(), containerId); - } - - public void stopRecording(Session session) { - String containerId = this.recordingSessions.remove(session.getSessionId()); - ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(containerId) - .withAttachStdout(true) - .withAttachStderr(true) - .withCmd("bash", "-c", "echo 'q' > stop") - .exec(); - try { - dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(new ExecStartResultCallback()).awaitCompletion(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - this.stopDockerContainer(containerId); - this.removeDockerContainer(containerId); - } - - public boolean recordingImageExistsLocally() { - boolean imageExists = false; - try { - dockerClient.inspectImageCmd(IMAGE_NAME).exec(); - imageExists = true; - } catch (NotFoundException nfe) { - imageExists = false; - } catch (ProcessingException e) { - throw e; - } - return imageExists; - } - - public void downloadRecordingImage() { - try { - dockerClient.pullImageCmd(IMAGE_NAME).exec(new PullImageResultCallback()).awaitSuccess(); - } catch (NotFoundException | InternalServerErrorException e) { - if (imageExistsLocally(IMAGE_NAME)) { - log.info("Docker image '{}' exists locally", IMAGE_NAME); - } else { - throw e; - } - } catch (DockerClientException e) { - log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution", - IMAGE_NAME); - throw e; - } - } - - private String runRecordingContainer(List envs, String containerName) { - Volume volume1 = new Volume("/recordings"); - CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME).withName(containerName).withEnv(envs) - .withNetworkMode("host").withVolumes(volume1) - .withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1)); - CreateContainerResponse container = null; - try { - container = cmd.exec(); - dockerClient.startContainerCmd(container.getId()).exec(); - createdContainers.put(container.getId(), containerName); - log.info("Container ID: {}", container.getId()); - return container.getId(); - } catch (ConflictException e) { - log.error( - "The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream", - containerName); - return null; - } - } - - private void removeDockerContainer(String containerId) { - dockerClient.removeContainerCmd(containerId).exec(); - createdContainers.remove(containerId); - } - - private void stopDockerContainer(String containerId) { - dockerClient.stopContainerCmd(containerId).exec(); - } - - private boolean imageExistsLocally(String imageName) { - boolean imageExists = false; - try { - dockerClient.inspectImageCmd(imageName).exec(); - imageExists = true; - } catch (NotFoundException nfe) { - imageExists = false; - } - return imageExists; - } - -} diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingsHttpHandler.java b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingsHttpHandler.java index 2ba82b1e..04f79a3a 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingsHttpHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/RecordingsHttpHandler.java @@ -19,6 +19,8 @@ public class RecordingsHttpHandler extends WebMvcConfigurerAdapter { String recordingsPath = openviduConfig.getOpenViduRecordingPath(); recordingsPath = recordingsPath.endsWith("/") ? recordingsPath : recordingsPath + "/"; + openviduConfig.setOpenViduRecordingPath(recordingsPath); + registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/WaitForContainerStoppedCallback.java b/openvidu-server/src/main/java/io/openvidu/server/recording/WaitForContainerStoppedCallback.java new file mode 100644 index 00000000..7eb24ed8 --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/WaitForContainerStoppedCallback.java @@ -0,0 +1,43 @@ +package io.openvidu.server.recording; + +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +import com.github.dockerjava.api.async.ResultCallback; +import com.github.dockerjava.api.model.WaitResponse; + +public class WaitForContainerStoppedCallback implements ResultCallback { + + CountDownLatch latch; + + public WaitForContainerStoppedCallback(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void close() throws IOException { + // TODO Auto-generated method stub + } + + @Override + public void onComplete() { + latch.countDown(); + } + + @Override + public void onError(Throwable arg0) { + // TODO Auto-generated method stub + } + + @Override + public void onStart(Closeable arg0) { + // TODO Auto-generated method stub + } + + @Override + public void onNext(WaitResponse arg0) { + // TODO Auto-generated method stub + } + +} 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 9af448ac..6b664d28 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 @@ -18,14 +18,18 @@ package io.openvidu.server.rest; import static org.kurento.commons.PropertiesManager.getProperty; +import java.util.Collection; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Set; +import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -37,7 +41,10 @@ import io.openvidu.java.client.ArchiveMode; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.SessionProperties; import io.openvidu.server.core.ParticipantRole; +import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionManager; +import io.openvidu.server.recording.Recording; +import io.openvidu.server.recording.ComposedRecordingService; /** * @@ -54,6 +61,9 @@ public class SessionRestController { @Autowired private SessionManager sessionManager; + @Autowired + private ComposedRecordingService recordingService; + @RequestMapping(value = "/sessions", method = RequestMethod.GET) public Set getAllSessions() { return sessionManager.getSessions(); @@ -72,7 +82,7 @@ public class SessionRestController { @SuppressWarnings("unchecked") @RequestMapping(value = "/sessions", method = RequestMethod.POST) public ResponseEntity getSessionId(@RequestBody(required = false) Map params) { - + SessionProperties.Builder builder = new SessionProperties.Builder(); if (params != null) { String archiveModeString = (String) params.get("archiveMode"); @@ -95,7 +105,7 @@ public class SessionRestController { } catch (IllegalArgumentException e) { return this.generateErrorResponse("ArchiveMode " + params.get("archiveMode") + " | " + "ArchiveLayout " + params.get("archiveLayout") + " | " + "MediaMode " + params.get("mediaMode") - + " are not defined", "/api/tokens"); + + " are not defined", "/api/tokens", HttpStatus.BAD_REQUEST); } } @@ -104,7 +114,7 @@ public class SessionRestController { String sessionId = sessionManager.newSessionId(sessionProperties); JSONObject responseJson = new JSONObject(); responseJson.put("id", sessionId); - return new ResponseEntity(responseJson, HttpStatus.OK); + return new ResponseEntity<>(responseJson, HttpStatus.OK); } @SuppressWarnings("unchecked") @@ -123,25 +133,122 @@ public class SessionRestController { responseJson.put("role", role.toString()); responseJson.put("data", metadata); responseJson.put("token", token); - return new ResponseEntity(responseJson, HttpStatus.OK); + return new ResponseEntity<>(responseJson, HttpStatus.OK); } catch (IllegalArgumentException e) { - return this.generateErrorResponse("Role " + params.get("role") + " is not defined", "/api/tokens"); + return this.generateErrorResponse("Role " + params.get("role") + " is not defined", "/api/tokens", + HttpStatus.BAD_REQUEST); } catch (OpenViduException e) { return this.generateErrorResponse( "Metadata [" + params.get("data") + "] unexpected format. Max length allowed is 1000 chars", - "/api/tokens"); + "/api/tokens", HttpStatus.BAD_REQUEST); + } + } + + @RequestMapping(value = "/recordings/start", method = RequestMethod.POST) + public ResponseEntity startRecordingSession(@RequestBody Map params) { + + String sessionId = (String) params.get("session"); + + if (sessionId == null) { + // "session" parameter not found + return new ResponseEntity(HttpStatus.BAD_REQUEST); + } + + Session session = sessionManager.getSession(sessionId); + + if (session == null) { + // Session does not exist + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + if (session.getParticipants().isEmpty()) { + // Session has no participants + return new ResponseEntity(HttpStatus.BAD_REQUEST); + } + if (!(session.getSessionProperties().mediaMode().equals(MediaMode.ROUTED)) + || this.recordingService.sessionIsBeingRecorded(session.getSessionId())) { + // Session is not in ROUTED MediMode or it is already being recorded + return new ResponseEntity(HttpStatus.CONFLICT); + } + + Recording startedRecording = this.recordingService.startRecording(session); + return new ResponseEntity<>(startedRecording.toJson(), HttpStatus.OK); + } + + @RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST) + public ResponseEntity stopRecordingSession(@PathVariable("recordingId") String recordingId) { + + if (recordingId == null) { + // "recordingId" parameter not found + return new ResponseEntity(HttpStatus.BAD_REQUEST); + } + + Recording recording = recordingService.getStartedRecording(recordingId); + + if (recording == null) { + if (recordingService.getStartingRecording(recordingId) != null) { + // Recording is still starting + return new ResponseEntity(HttpStatus.NOT_ACCEPTABLE); + } + // Recording does not exist + return new ResponseEntity(HttpStatus.NOT_FOUND); + } + if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) { + // Session is not being recorded + return new ResponseEntity(HttpStatus.CONFLICT); + } + + Recording stoppedRecording = this.recordingService + .stopRecording(sessionManager.getSession(recording.getSessionId())); + return new ResponseEntity<>(stoppedRecording.toJson(), HttpStatus.OK); + } + + @RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET) + public ResponseEntity getRecording(@PathVariable("recordingId") String recordingId) { + try { + Recording recording = this.recordingService.getAllRecordings().stream() + .filter(rec -> rec.getId().equals(recordingId)).findFirst().get(); + if (Recording.Status.started.equals(recording.getStatus()) + && recordingService.getStartingRecording(recording.getId()) != null) { + recording.setStatus(Recording.Status.starting); + } + return new ResponseEntity<>(recording.toJson(), HttpStatus.OK); + } catch (NoSuchElementException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } @SuppressWarnings("unchecked") - private ResponseEntity generateErrorResponse(String errorMessage, String path) { + @RequestMapping(value = "/recordings", method = RequestMethod.GET) + public ResponseEntity getAllRecordings() { + Collection recordings = this.recordingService.getAllRecordings(); + JSONObject json = new JSONObject(); + JSONArray jsonArray = new JSONArray(); + recordings.forEach(rec -> { + if (Recording.Status.started.equals(rec.getStatus()) + && recordingService.getStartingRecording(rec.getId()) != null) { + rec.setStatus(Recording.Status.starting); + } + jsonArray.add(rec.toJson()); + }); + json.put("count", recordings.size()); + json.put("items", jsonArray); + return new ResponseEntity<>(json, HttpStatus.OK); + } + + @RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE) + public ResponseEntity deleteRecording(@PathVariable("recordingId") String recordingId) { + return new ResponseEntity<>(this.recordingService.deleteRecordingFromHost(recordingId)); + } + + @SuppressWarnings("unchecked") + private ResponseEntity generateErrorResponse(String errorMessage, String path, HttpStatus status) { JSONObject responseJson = new JSONObject(); responseJson.put("timestamp", System.currentTimeMillis()); - responseJson.put("status", HttpStatus.BAD_REQUEST.value()); - responseJson.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase()); + responseJson.put("status", status.value()); + responseJson.put("error", status.getReasonPhrase()); responseJson.put("message", errorMessage); responseJson.put("path", path); - return new ResponseEntity(responseJson, HttpStatus.BAD_REQUEST); + return new ResponseEntity(responseJson, status); } }