mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server manual recording
parent
c63e02bd66
commit
acc1139374
|
@ -48,7 +48,7 @@ import io.openvidu.server.kurento.KurentoClientProvider;
|
||||||
import io.openvidu.server.kurento.core.KurentoSessionEventsHandler;
|
import io.openvidu.server.kurento.core.KurentoSessionEventsHandler;
|
||||||
import io.openvidu.server.kurento.core.KurentoSessionManager;
|
import io.openvidu.server.kurento.core.KurentoSessionManager;
|
||||||
import io.openvidu.server.kurento.kms.FixedOneKmsManager;
|
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.rest.NgrokRestController;
|
||||||
import io.openvidu.server.rpc.RpcHandler;
|
import io.openvidu.server.rpc.RpcHandler;
|
||||||
import io.openvidu.server.rpc.RpcNotificationService;
|
import io.openvidu.server.rpc.RpcNotificationService;
|
||||||
|
@ -150,6 +150,7 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
NEW_LINE;
|
NEW_LINE;
|
||||||
System.out.println(str);
|
System.out.println(str);
|
||||||
OpenViduServer.publicUrl = ngrok.getNgrokServerUrl().replaceFirst("https://", "wss://");
|
OpenViduServer.publicUrl = ngrok.getNgrokServerUrl().replaceFirst("https://", "wss://");
|
||||||
|
openviduConf.setFinalUrl(ngrok.getNgrokServerUrl());
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("Ngrok URL was configured, but there was an error connecting to ngrok: "
|
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":
|
case "docker":
|
||||||
try {
|
try {
|
||||||
OpenViduServer.publicUrl = "wss://" + getContainerIp() + ":" + openviduConf.getServerPort();
|
OpenViduServer.publicUrl = "wss://" + getContainerIp() + ":" + openviduConf.getServerPort();
|
||||||
|
openviduConf.setFinalUrl("https://" + getContainerIp() + ":" + openviduConf.getServerPort());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.err.println("Docker container IP was configured, but there was an error obtaining IP: "
|
System.err.println("Docker container IP was configured, but there was an error obtaining IP: "
|
||||||
+ e.getClass().getName() + " " + e.getMessage());
|
+ e.getClass().getName() + " " + e.getMessage());
|
||||||
|
@ -170,6 +172,9 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
|
|
||||||
case "local":
|
case "local":
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "docker-local":
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
||||||
|
@ -178,6 +183,9 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
|
|
||||||
type = "custom";
|
type = "custom";
|
||||||
OpenViduServer.publicUrl = publicUrl.replaceFirst("https://", "wss://");
|
OpenViduServer.publicUrl = publicUrl.replaceFirst("https://", "wss://");
|
||||||
|
OpenViduServer.publicUrl = publicUrl.replaceFirst("http://", "wss://");
|
||||||
|
openviduConf.setFinalUrl(url.toString());
|
||||||
|
|
||||||
if (!OpenViduServer.publicUrl.startsWith("wss://")) {
|
if (!OpenViduServer.publicUrl.startsWith("wss://")) {
|
||||||
OpenViduServer.publicUrl = "wss://" + OpenViduServer.publicUrl;
|
OpenViduServer.publicUrl = "wss://" + OpenViduServer.publicUrl;
|
||||||
}
|
}
|
||||||
|
@ -191,26 +199,34 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
if (OpenViduServer.publicUrl == null) {
|
if (OpenViduServer.publicUrl == null) {
|
||||||
type = "local";
|
type = "local";
|
||||||
OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort();
|
OpenViduServer.publicUrl = "wss://localhost:" + openviduConf.getServerPort();
|
||||||
|
openviduConf.setFinalUrl("https://localhost:" + openviduConf.getServerPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OpenViduServer.publicUrl.endsWith("/")) {
|
if (OpenViduServer.publicUrl.endsWith("/")) {
|
||||||
OpenViduServer.publicUrl = OpenViduServer.publicUrl.substring(0, OpenViduServer.publicUrl.length() - 1);
|
OpenViduServer.publicUrl = OpenViduServer.publicUrl.substring(0, OpenViduServer.publicUrl.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
|
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
|
||||||
if (recordingModuleEnabled) {
|
if (recordingModuleEnabled) {
|
||||||
RecordingService recordingService = context.getBean(RecordingService.class);
|
ComposedRecordingService recordingService = context.getBean(ComposedRecordingService.class);
|
||||||
System.out.println("Recording module required: Downloading openvidu/openvidu-recording Docker image (800 MB aprox)");
|
System.out.println(
|
||||||
|
"Recording module required: Downloading openvidu/openvidu-recording Docker image (800 MB aprox)");
|
||||||
|
|
||||||
boolean imageExists = false;
|
boolean imageExists = false;
|
||||||
try {
|
try {
|
||||||
imageExists = recordingService.recordingImageExistsLocally();
|
imageExists = recordingService.recordingImageExistsLocally();
|
||||||
} catch (ProcessingException exception) {
|
} catch (ProcessingException exception) {
|
||||||
log.error("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service");
|
String message = "Exception connecting to Docker daemon: ";
|
||||||
throw new RuntimeException("Exception connecting to Docker daemon: you need Docker installed in this machine to enable OpenVidu recorder service");
|
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 {
|
||||||
if (imageExists) {
|
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");
|
System.out.println("Docker image already exists locally");
|
||||||
} else {
|
} else {
|
||||||
Thread t = new Thread(() -> {
|
Thread t = new Thread(() -> {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import org.springframework.stereotype.Component;
|
||||||
public class OpenviduConfig {
|
public class OpenviduConfig {
|
||||||
|
|
||||||
@Value("${openvidu.publicurl}")
|
@Value("${openvidu.publicurl}")
|
||||||
private String openviduPublicUrl; // local, ngrok, docker, [FINAL_URL]
|
private String openviduPublicUrl; // local, docker-local, ngrok, docker, [FINAL_URL]
|
||||||
|
|
||||||
@Value("${server.port}")
|
@Value("${server.port}")
|
||||||
private String serverPort;
|
private String serverPort;
|
||||||
|
@ -27,6 +27,8 @@ public class OpenviduConfig {
|
||||||
@Value("${openvidu.recording.free-access}")
|
@Value("${openvidu.recording.free-access}")
|
||||||
boolean openviduRecordingFreeAccess;
|
boolean openviduRecordingFreeAccess;
|
||||||
|
|
||||||
|
private String finalUrl;
|
||||||
|
|
||||||
public String getOpenViduPublicUrl() {
|
public String getOpenViduPublicUrl() {
|
||||||
return this.openviduPublicUrl;
|
return this.openviduPublicUrl;
|
||||||
}
|
}
|
||||||
|
@ -59,4 +61,16 @@ public class OpenviduConfig {
|
||||||
return this.openviduRecordingFreeAccess;
|
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 + "/");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
.authorizeRequests()
|
.authorizeRequests()
|
||||||
.antMatchers(HttpMethod.POST, "/api/sessions").authenticated()
|
.antMatchers(HttpMethod.POST, "/api/sessions").authenticated()
|
||||||
.antMatchers(HttpMethod.POST, "/api/tokens").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();
|
.antMatchers("/").authenticated();
|
||||||
|
|
||||||
if (openviduConf.getOpenViduRecordingFreeAccess()) {
|
if (openviduConf.getOpenViduRecordingFreeAccess()) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,4 +24,6 @@ public interface Session {
|
||||||
|
|
||||||
Participant getParticipantByPublicId(String participantPublicId);
|
Participant getParticipantByPublicId(String participantPublicId);
|
||||||
|
|
||||||
|
int getActivePublishers();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package io.openvidu.server.core;
|
package io.openvidu.server.core;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -10,6 +8,7 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.kurento.jsonrpc.message.Request;
|
import org.kurento.jsonrpc.message.Request;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -60,6 +59,15 @@ public abstract class SessionManager {
|
||||||
*/
|
*/
|
||||||
public void evictParticipant(String participantPrivateId) throws OpenViduException {
|
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.
|
* Returns all currently active (opened) sessions.
|
||||||
|
@ -141,7 +149,7 @@ public abstract class SessionManager {
|
||||||
|
|
||||||
public String newSessionId(SessionProperties sessionProperties) {
|
public String newSessionId(SessionProperties sessionProperties) {
|
||||||
String sessionId = OpenViduServer.publicUrl;
|
String sessionId = OpenViduServer.publicUrl;
|
||||||
sessionId += "/" + new BigInteger(130, new SecureRandom()).toString(32);
|
sessionId += "/" + RandomStringUtils.randomAlphanumeric(16).toLowerCase();
|
||||||
|
|
||||||
this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>());
|
this.sessionidTokenTokenobj.put(sessionId, new ConcurrentHashMap<>());
|
||||||
this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>());
|
this.sessionidParticipantpublicidParticipant.put(sessionId, new ConcurrentHashMap<>());
|
||||||
|
@ -155,7 +163,7 @@ public abstract class SessionManager {
|
||||||
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null
|
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null
|
||||||
&& this.sessionidTokenTokenobj.get(sessionId) != null) {
|
&& this.sessionidTokenTokenobj.get(sessionId) != null) {
|
||||||
if (isMetadataFormatCorrect(serverMetadata)) {
|
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));
|
this.sessionidTokenTokenobj.get(sessionId).put(token, new Token(token, role, serverMetadata));
|
||||||
showTokens();
|
showTokens();
|
||||||
return token;
|
return token;
|
||||||
|
@ -226,12 +234,12 @@ public abstract class SessionManager {
|
||||||
public Participant newParticipant(String sessionId, String participantPrivatetId, Token token,
|
public Participant newParticipant(String sessionId, String participantPrivatetId, Token token,
|
||||||
String clientMetadata) {
|
String clientMetadata) {
|
||||||
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) {
|
if (this.sessionidParticipantpublicidParticipant.get(sessionId) != null) {
|
||||||
String participantPublicId = new BigInteger(130, new SecureRandom()).toString(32);
|
String participantPublicId = RandomStringUtils.randomAlphanumeric(16).toLowerCase();
|
||||||
ConcurrentHashMap<String, Participant> participantpublicidParticipant = this.sessionidParticipantpublicidParticipant
|
ConcurrentHashMap<String, Participant> participantpublicidParticipant = this.sessionidParticipantpublicidParticipant
|
||||||
.get(sessionId);
|
.get(sessionId);
|
||||||
while (participantpublicidParticipant.containsKey(participantPublicId)) {
|
while (participantpublicidParticipant.containsKey(participantPublicId)) {
|
||||||
// Avoid random 'participantpublicid' collisions
|
// 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);
|
Participant p = new Participant(participantPrivatetId, participantPublicId, token, clientMetadata);
|
||||||
this.sessionidParticipantpublicidParticipant.get(sessionId).put(participantPublicId, p);
|
this.sessionidParticipantpublicidParticipant.get(sessionId).put(participantPublicId, p);
|
||||||
|
|
|
@ -219,7 +219,8 @@ public class KurentoSession implements Session {
|
||||||
other.cancelReceivingMedia(participant.getParticipantPublicId());
|
other.cancelReceivingMedia(participant.getParticipantPublicId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public int getActivePublishers() {
|
public int getActivePublishers() {
|
||||||
return activePublishers.get();
|
return activePublishers.get();
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.google.gson.JsonSyntaxException;
|
||||||
import io.openvidu.client.OpenViduException;
|
import io.openvidu.client.OpenViduException;
|
||||||
import io.openvidu.client.OpenViduException.Code;
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
import io.openvidu.client.internal.ProtocolElements;
|
import io.openvidu.client.internal.ProtocolElements;
|
||||||
|
import io.openvidu.java.client.ArchiveLayout;
|
||||||
import io.openvidu.java.client.ArchiveMode;
|
import io.openvidu.java.client.ArchiveMode;
|
||||||
import io.openvidu.java.client.MediaMode;
|
import io.openvidu.java.client.MediaMode;
|
||||||
import io.openvidu.java.client.SessionProperties;
|
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.KurentoClientSessionInfo;
|
||||||
import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo;
|
import io.openvidu.server.kurento.OpenViduKurentoClientSessionInfo;
|
||||||
import io.openvidu.server.kurento.endpoint.SdpType;
|
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.rpc.RpcHandler;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.core.MediaOptions;
|
import io.openvidu.server.core.MediaOptions;
|
||||||
|
@ -44,7 +45,7 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
private KurentoSessionEventsHandler sessionHandler;
|
private KurentoSessionEventsHandler sessionHandler;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RecordingService recordingService;
|
private ComposedRecordingService recordingService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
OpenviduConfig openviduConfig;
|
OpenviduConfig openviduConfig;
|
||||||
|
@ -62,7 +63,7 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
SessionProperties properties = sessionProperties.get(sessionId);
|
SessionProperties properties = sessionProperties.get(sessionId);
|
||||||
if (properties == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) {
|
if (properties == null && this.isInsecureParticipant(participant.getParticipantPrivateId())) {
|
||||||
properties = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED)
|
properties = new SessionProperties.Builder().mediaMode(MediaMode.ROUTED)
|
||||||
.archiveMode(ArchiveMode.ALWAYS).build();
|
.archiveMode(ArchiveMode.ALWAYS).archiveLayout(ArchiveLayout.BEST_FIT).build();
|
||||||
}
|
}
|
||||||
createSession(kcSessionInfo, properties);
|
createSession(kcSessionInfo, properties);
|
||||||
}
|
}
|
||||||
|
@ -94,7 +95,7 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
@Override
|
@Override
|
||||||
public void leaveRoom(Participant participant, Integer transactionId) {
|
public void leaveRoom(Participant participant, Integer transactionId) {
|
||||||
log.debug("Request [LEAVE_ROOM] ({})", participant.getParticipantPublicId());
|
log.debug("Request [LEAVE_ROOM] ({})", participant.getParticipantPublicId());
|
||||||
|
|
||||||
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||||
KurentoSession session = kParticipant.getSession();
|
KurentoSession session = kParticipant.getSession();
|
||||||
String sessionId = session.getSessionId();
|
String sessionId = session.getSessionId();
|
||||||
|
@ -147,16 +148,15 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
showTokens();
|
showTokens();
|
||||||
|
|
||||||
log.warn("Session '{}' removed and closed", sessionId);
|
log.warn("Session '{}' removed and closed", sessionId);
|
||||||
}
|
}
|
||||||
if (
|
if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
||||||
remainingParticipants.size() == 1 &&
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
openviduConfig.isRecordingModuleEnabled() &&
|
&& ArchiveMode.ALWAYS.equals(session.getSessionProperties().archiveMode())
|
||||||
MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) &&
|
&& ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID
|
||||||
ProtocolElements.RECORDER_PARTICIPANT_ID_PUBLICID.equals(remainingParticipants.iterator().next().getParticipantPublicId())
|
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
||||||
) {
|
log.info("Last participant left. Stopping recording for session {}", sessionId);
|
||||||
log.info("Last participant left. Stopping recording for session {}", sessionId);
|
evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId());
|
||||||
evictParticipant(session.getParticipantByPublicId("RECORDER").getParticipantPrivateId());
|
recordingService.stopRecording(session);
|
||||||
recordingService.stopRecording(session);
|
|
||||||
}
|
}
|
||||||
sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null);
|
sessionHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<String, String> containers = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, Recording> 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<String> 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<String> 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<Recording> getAllRecordings() {
|
||||||
|
return this.getRecordingEntitiesFromHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Recording> getStartingRecordings() {
|
||||||
|
return this.startingRecordings.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Recording> getStartedRecordings() {
|
||||||
|
return this.startedRecordings.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Recording> getFinishedRecordings() {
|
||||||
|
return this.getRecordingEntitiesFromHost().stream()
|
||||||
|
.filter(recording -> (recording.getStatus().equals(Recording.Status.stopped)
|
||||||
|
|| recording.getStatus().equals(Recording.Status.available)))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
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].isFile() && !files[i].getName().startsWith(RECORDING_ENTITY_FILE)) {
|
||||||
|
fileNamesNoExtension.add(FilenameUtils.removeExtension(files[i].getName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileNamesNoExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Recording> getRecordingEntitiesFromHost() {
|
||||||
|
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
Set<Recording> 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<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String, String> createdContainers = new HashMap<>();
|
|
||||||
private final String IMAGE_NAME = "openvidu/openvidu-recording";
|
|
||||||
|
|
||||||
private DockerClient dockerClient;
|
|
||||||
private Map<String, String> recordingSessions = new HashMap<>();;
|
|
||||||
|
|
||||||
public RecordingService() {
|
|
||||||
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
|
|
||||||
this.dockerClient = DockerClientBuilder.getInstance(config).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startRecording(Session session) {
|
|
||||||
List<String> 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<String> 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -19,6 +19,8 @@ public class RecordingsHttpHandler extends WebMvcConfigurerAdapter {
|
||||||
String recordingsPath = openviduConfig.getOpenViduRecordingPath();
|
String recordingsPath = openviduConfig.getOpenViduRecordingPath();
|
||||||
recordingsPath = recordingsPath.endsWith("/") ? recordingsPath : recordingsPath + "/";
|
recordingsPath = recordingsPath.endsWith("/") ? recordingsPath : recordingsPath + "/";
|
||||||
|
|
||||||
|
openviduConfig.setOpenViduRecordingPath(recordingsPath);
|
||||||
|
|
||||||
registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath);
|
registry.addResourceHandler("/recordings/**").addResourceLocations("file:" + recordingsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<WaitResponse> {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,14 +18,18 @@ package io.openvidu.server.rest;
|
||||||
|
|
||||||
import static org.kurento.commons.PropertiesManager.getProperty;
|
import static org.kurento.commons.PropertiesManager.getProperty;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.json.simple.JSONArray;
|
||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
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.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
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.MediaMode;
|
||||||
import io.openvidu.java.client.SessionProperties;
|
import io.openvidu.java.client.SessionProperties;
|
||||||
import io.openvidu.server.core.ParticipantRole;
|
import io.openvidu.server.core.ParticipantRole;
|
||||||
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.core.SessionManager;
|
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
|
@Autowired
|
||||||
private SessionManager sessionManager;
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ComposedRecordingService recordingService;
|
||||||
|
|
||||||
@RequestMapping(value = "/sessions", method = RequestMethod.GET)
|
@RequestMapping(value = "/sessions", method = RequestMethod.GET)
|
||||||
public Set<String> getAllSessions() {
|
public Set<String> getAllSessions() {
|
||||||
return sessionManager.getSessions();
|
return sessionManager.getSessions();
|
||||||
|
@ -72,7 +82,7 @@ public class SessionRestController {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@RequestMapping(value = "/sessions", method = RequestMethod.POST)
|
@RequestMapping(value = "/sessions", method = RequestMethod.POST)
|
||||||
public ResponseEntity<JSONObject> getSessionId(@RequestBody(required = false) Map<?, ?> params) {
|
public ResponseEntity<JSONObject> getSessionId(@RequestBody(required = false) Map<?, ?> params) {
|
||||||
|
|
||||||
SessionProperties.Builder builder = new SessionProperties.Builder();
|
SessionProperties.Builder builder = new SessionProperties.Builder();
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
String archiveModeString = (String) params.get("archiveMode");
|
String archiveModeString = (String) params.get("archiveMode");
|
||||||
|
@ -95,7 +105,7 @@ public class SessionRestController {
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return this.generateErrorResponse("ArchiveMode " + params.get("archiveMode") + " | " + "ArchiveLayout "
|
return this.generateErrorResponse("ArchiveMode " + params.get("archiveMode") + " | " + "ArchiveLayout "
|
||||||
+ params.get("archiveLayout") + " | " + "MediaMode " + params.get("mediaMode")
|
+ 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);
|
String sessionId = sessionManager.newSessionId(sessionProperties);
|
||||||
JSONObject responseJson = new JSONObject();
|
JSONObject responseJson = new JSONObject();
|
||||||
responseJson.put("id", sessionId);
|
responseJson.put("id", sessionId);
|
||||||
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
|
return new ResponseEntity<>(responseJson, HttpStatus.OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
@ -123,25 +133,122 @@ public class SessionRestController {
|
||||||
responseJson.put("role", role.toString());
|
responseJson.put("role", role.toString());
|
||||||
responseJson.put("data", metadata);
|
responseJson.put("data", metadata);
|
||||||
responseJson.put("token", token);
|
responseJson.put("token", token);
|
||||||
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.OK);
|
return new ResponseEntity<>(responseJson, HttpStatus.OK);
|
||||||
|
|
||||||
} catch (IllegalArgumentException e) {
|
} 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) {
|
} catch (OpenViduException e) {
|
||||||
return this.generateErrorResponse(
|
return this.generateErrorResponse(
|
||||||
"Metadata [" + params.get("data") + "] unexpected format. Max length allowed is 1000 chars",
|
"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<JSONObject> startRecordingSession(@RequestBody Map<?, ?> params) {
|
||||||
|
|
||||||
|
String sessionId = (String) params.get("session");
|
||||||
|
|
||||||
|
if (sessionId == null) {
|
||||||
|
// "session" parameter not found
|
||||||
|
return new ResponseEntity<JSONObject>(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = sessionManager.getSession(sessionId);
|
||||||
|
|
||||||
|
if (session == null) {
|
||||||
|
// Session does not exist
|
||||||
|
return new ResponseEntity<JSONObject>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (session.getParticipants().isEmpty()) {
|
||||||
|
// Session has no participants
|
||||||
|
return new ResponseEntity<JSONObject>(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<JSONObject>(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<JSONObject> stopRecordingSession(@PathVariable("recordingId") String recordingId) {
|
||||||
|
|
||||||
|
if (recordingId == null) {
|
||||||
|
// "recordingId" parameter not found
|
||||||
|
return new ResponseEntity<JSONObject>(HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
Recording recording = recordingService.getStartedRecording(recordingId);
|
||||||
|
|
||||||
|
if (recording == null) {
|
||||||
|
if (recordingService.getStartingRecording(recordingId) != null) {
|
||||||
|
// Recording is still starting
|
||||||
|
return new ResponseEntity<JSONObject>(HttpStatus.NOT_ACCEPTABLE);
|
||||||
|
}
|
||||||
|
// Recording does not exist
|
||||||
|
return new ResponseEntity<JSONObject>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) {
|
||||||
|
// Session is not being recorded
|
||||||
|
return new ResponseEntity<JSONObject>(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<JSONObject> 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")
|
@SuppressWarnings("unchecked")
|
||||||
private ResponseEntity<JSONObject> generateErrorResponse(String errorMessage, String path) {
|
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
|
||||||
|
public ResponseEntity<JSONObject> getAllRecordings() {
|
||||||
|
Collection<Recording> 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<JSONObject> deleteRecording(@PathVariable("recordingId") String recordingId) {
|
||||||
|
return new ResponseEntity<>(this.recordingService.deleteRecordingFromHost(recordingId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private ResponseEntity<JSONObject> generateErrorResponse(String errorMessage, String path, HttpStatus status) {
|
||||||
JSONObject responseJson = new JSONObject();
|
JSONObject responseJson = new JSONObject();
|
||||||
responseJson.put("timestamp", System.currentTimeMillis());
|
responseJson.put("timestamp", System.currentTimeMillis());
|
||||||
responseJson.put("status", HttpStatus.BAD_REQUEST.value());
|
responseJson.put("status", status.value());
|
||||||
responseJson.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
|
responseJson.put("error", status.getReasonPhrase());
|
||||||
responseJson.put("message", errorMessage);
|
responseJson.put("message", errorMessage);
|
||||||
responseJson.put("path", path);
|
responseJson.put("path", path);
|
||||||
return new ResponseEntity<JSONObject>(responseJson, HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<JSONObject>(responseJson, status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue