mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server: individual stream recording
parent
902470e7a4
commit
6d392d7e4a
|
@ -23,7 +23,6 @@ import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import javax.ws.rs.ProcessingException;
|
|
||||||
|
|
||||||
import org.kurento.jsonrpc.JsonUtils;
|
import org.kurento.jsonrpc.JsonUtils;
|
||||||
import org.kurento.jsonrpc.internal.server.config.JsonRpcConfiguration;
|
import org.kurento.jsonrpc.internal.server.config.JsonRpcConfiguration;
|
||||||
|
@ -57,7 +56,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.ComposedRecordingService;
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
import io.openvidu.server.rpc.RpcHandler;
|
import io.openvidu.server.rpc.RpcHandler;
|
||||||
import io.openvidu.server.rpc.RpcNotificationService;
|
import io.openvidu.server.rpc.RpcNotificationService;
|
||||||
import io.openvidu.server.utils.CommandExecutor;
|
import io.openvidu.server.utils.CommandExecutor;
|
||||||
|
@ -147,8 +146,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public ComposedRecordingService composedRecordingService() {
|
public RecordingManager recordingManager() {
|
||||||
return new ComposedRecordingService();
|
return new RecordingManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ -233,57 +232,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
||||||
OpenViduServer.wsUrl = OpenViduServer.wsUrl.substring(0, OpenViduServer.wsUrl.length() - 1);
|
OpenViduServer.wsUrl = OpenViduServer.wsUrl.substring(0, OpenViduServer.wsUrl.length() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
|
if (this.openviduConfig().isRecordingModuleEnabled()) {
|
||||||
if (recordingModuleEnabled) {
|
this.recordingManager().initializeRecordingManager();
|
||||||
ComposedRecordingService recordingService = composedRecordingService();
|
|
||||||
recordingService.setRecordingVersion(openviduConf.getOpenViduRecordingVersion());
|
|
||||||
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
|
|
||||||
+ openviduConf.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
|
|
||||||
|
|
||||||
boolean imageExists = false;
|
|
||||||
try {
|
|
||||||
imageExists = recordingService.recordingImageExistsLocally();
|
|
||||||
} catch (ProcessingException exception) {
|
|
||||||
String message = "Exception connecting to Docker daemon: ";
|
|
||||||
if ("docker".equals(openviduConf.getSpringProfile())) {
|
|
||||||
final String NEW_LINE = System.getProperty("line.separator");
|
|
||||||
message += "make sure you include the following flags in your \"docker run\" command:" +
|
|
||||||
NEW_LINE + " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" +
|
|
||||||
NEW_LINE + " -e MY_UID=$(id -u $USER)" +
|
|
||||||
NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock" +
|
|
||||||
NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" +
|
|
||||||
NEW_LINE;
|
|
||||||
} else {
|
|
||||||
message += "you need Docker installed in this machine to enable OpenVidu recording service";
|
|
||||||
}
|
|
||||||
log.error(message);
|
|
||||||
throw new RuntimeException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageExists) {
|
|
||||||
log.info("Docker image already exists locally");
|
|
||||||
} else {
|
|
||||||
Thread t = new Thread(() -> {
|
|
||||||
boolean keep = true;
|
|
||||||
log.info("Downloading ");
|
|
||||||
while (keep) {
|
|
||||||
System.out.print(".");
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
keep = false;
|
|
||||||
log.info("\nDownload complete");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
t.start();
|
|
||||||
recordingService.downloadRecordingImage();
|
|
||||||
t.interrupt();
|
|
||||||
t.join();
|
|
||||||
log.info("Docker image available");
|
|
||||||
}
|
|
||||||
|
|
||||||
recordingService.initRecordingPath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
httpUrl = openviduConf.getFinalUrl();
|
httpUrl = openviduConf.getFinalUrl();
|
||||||
|
|
|
@ -17,9 +17,7 @@
|
||||||
|
|
||||||
package io.openvidu.server.cdr;
|
package io.openvidu.server.cdr;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -32,6 +30,7 @@ import io.openvidu.server.core.MediaOptions;
|
||||||
import io.openvidu.server.core.Participant;
|
import io.openvidu.server.core.Participant;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CDR logger to register all information of a Session.
|
* CDR logger to register all information of a Session.
|
||||||
|
@ -69,6 +68,7 @@ import io.openvidu.server.recording.Recording;
|
||||||
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||||
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||||
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
||||||
|
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "openviduServerStopped"
|
||||||
*
|
*
|
||||||
* [OPTIONAL_PROPERTIES]:
|
* [OPTIONAL_PROPERTIES]:
|
||||||
* - receivingFrom: only if connection = "INBOUND"
|
* - receivingFrom: only if connection = "INBOUND"
|
||||||
|
@ -91,9 +91,6 @@ public class CallDetailRecord {
|
||||||
private Map<String, Set<CDREventWebrtcConnection>> subscriptions = new ConcurrentHashMap<>();
|
private Map<String, Set<CDREventWebrtcConnection>> subscriptions = new ConcurrentHashMap<>();
|
||||||
private Map<String, CDREventRecording> recordings = new ConcurrentHashMap<>();
|
private Map<String, CDREventRecording> recordings = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final List<String> lastParticipantLeftReasons = Arrays.asList(
|
|
||||||
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
|
|
||||||
|
|
||||||
public CallDetailRecord(CDRLogger logger) {
|
public CallDetailRecord(CDRLogger logger) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +105,7 @@ public class CallDetailRecord {
|
||||||
public void recordSessionDestroyed(String sessionId, String reason) {
|
public void recordSessionDestroyed(String sessionId, String reason) {
|
||||||
CDREvent e = this.sessions.remove(sessionId);
|
CDREvent e = this.sessions.remove(sessionId);
|
||||||
if (openviduConfig.isCdrEnabled())
|
if (openviduConfig.isCdrEnabled())
|
||||||
this.logger.log(new CDREventSession(e, this.finalReason(reason)));
|
this.logger.log(new CDREventSession(e, RecordingManager.finalReason(reason)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordParticipantJoined(Participant participant, String sessionId) {
|
public void recordParticipantJoined(Participant participant, String sessionId) {
|
||||||
|
@ -124,7 +121,8 @@ public class CallDetailRecord {
|
||||||
this.logger.log(new CDREventParticipant(e, reason));
|
this.logger.log(new CDREventParticipant(e, reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void recordNewPublisher(Participant participant, String sessionId, MediaOptions mediaOptions, Long timestamp) {
|
public void recordNewPublisher(Participant participant, String sessionId, MediaOptions mediaOptions,
|
||||||
|
Long timestamp) {
|
||||||
CDREventWebrtcConnection publisher = new CDREventWebrtcConnection(sessionId,
|
CDREventWebrtcConnection publisher = new CDREventWebrtcConnection(sessionId,
|
||||||
participant.getParticipantPublicId(), mediaOptions, null, timestamp);
|
participant.getParticipantPublicId(), mediaOptions, null, timestamp);
|
||||||
this.publications.put(participant.getParticipantPublicId(), publisher);
|
this.publications.put(participant.getParticipantPublicId(), publisher);
|
||||||
|
@ -181,15 +179,7 @@ public class CallDetailRecord {
|
||||||
public void recordRecordingStopped(String sessionId, Recording recording, String reason) {
|
public void recordRecordingStopped(String sessionId, Recording recording, String reason) {
|
||||||
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
|
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
|
||||||
if (openviduConfig.isCdrEnabled())
|
if (openviduConfig.isCdrEnabled())
|
||||||
this.logger.log(new CDREventRecording(recordingStartedEvent, this.finalReason(reason)));
|
this.logger.log(new CDREventRecording(recordingStartedEvent, RecordingManager.finalReason(reason)));
|
||||||
}
|
|
||||||
|
|
||||||
private String finalReason(String reason) {
|
|
||||||
if (lastParticipantLeftReasons.contains(reason)) {
|
|
||||||
return "lastParticipantLeft";
|
|
||||||
} else {
|
|
||||||
return reason;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import org.springframework.security.config.annotation.authentication.builders.Au
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
|
@ -143,8 +143,7 @@ public class SessionEventsHandler {
|
||||||
// Metadata associated to new participant
|
// Metadata associated to new participant
|
||||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM,
|
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM,
|
||||||
participant.getParticipantPublicId());
|
participant.getParticipantPublicId());
|
||||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM,
|
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getCreatedAt());
|
||||||
participant.getCreatedAt());
|
|
||||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM,
|
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM,
|
||||||
participant.getFullMetadata());
|
participant.getFullMetadata());
|
||||||
|
|
||||||
|
@ -404,13 +403,18 @@ public class SessionEventsHandler {
|
||||||
evictedParticipant.getParticipantPublicId());
|
evictedParticipant.getParticipantPublicId());
|
||||||
params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason);
|
params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason);
|
||||||
|
|
||||||
|
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
|
||||||
|
// Do not send a message when evicting RECORDER participant
|
||||||
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
|
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
|
||||||
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
||||||
|
}
|
||||||
for (Participant p : participants) {
|
for (Participant p : participants) {
|
||||||
|
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
|
||||||
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
|
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
|
||||||
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void sendRecordingStartedNotification(Session session, Recording recording) {
|
public void sendRecordingStartedNotification(Session session, Recording recording) {
|
||||||
|
|
||||||
|
@ -550,6 +554,9 @@ public class SessionEventsHandler {
|
||||||
|
|
||||||
private Set<Participant> filterParticipantsByRole(ParticipantRole[] roles, Set<Participant> participants) {
|
private Set<Participant> filterParticipantsByRole(ParticipantRole[] roles, Set<Participant> participants) {
|
||||||
return participants.stream().filter(part -> {
|
return participants.stream().filter(part -> {
|
||||||
|
if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(part.getParticipantPublicId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
boolean isRole = false;
|
boolean isRole = false;
|
||||||
for (ParticipantRole role : roles) {
|
for (ParticipantRole role : roles) {
|
||||||
isRole = role.equals(part.getToken().getRole());
|
isRole = role.equals(part.getToken().getRole());
|
||||||
|
|
|
@ -44,7 +44,7 @@ import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.coturn.CoturnCredentialsService;
|
import io.openvidu.server.coturn.CoturnCredentialsService;
|
||||||
import io.openvidu.server.coturn.TurnCredentials;
|
import io.openvidu.server.coturn.TurnCredentials;
|
||||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||||
import io.openvidu.server.recording.ComposedRecordingService;
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
|
||||||
public abstract class SessionManager {
|
public abstract class SessionManager {
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ public abstract class SessionManager {
|
||||||
protected SessionEventsHandler sessionEventsHandler;
|
protected SessionEventsHandler sessionEventsHandler;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected ComposedRecordingService recordingService;
|
protected RecordingManager recordingManager;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected CallDetailRecord CDR;
|
protected CallDetailRecord CDR;
|
||||||
|
@ -440,19 +440,21 @@ public abstract class SessionManager {
|
||||||
|
|
||||||
this.closeSessionAndEmptyCollections(session, reason);
|
this.closeSessionAndEmptyCollections(session, reason);
|
||||||
|
|
||||||
if (recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
|
||||||
recordingService.stopRecording(session, null, reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
return participants;
|
return participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeSessionAndEmptyCollections(Session session, String reason) {
|
protected void closeSessionAndEmptyCollections(Session session, String reason) {
|
||||||
|
|
||||||
|
if (openviduConfig.isRecordingModuleEnabled()
|
||||||
|
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
|
recordingManager.stopRecording(session, null, RecordingManager.finalReason(reason));
|
||||||
|
}
|
||||||
|
|
||||||
if (session.close(reason)) {
|
if (session.close(reason)) {
|
||||||
sessionEventsHandler.onSessionClosed(session.getSessionId(), reason);
|
sessionEventsHandler.onSessionClosed(session.getSessionId(), reason);
|
||||||
}
|
}
|
||||||
sessions.remove(session.getSessionId());
|
|
||||||
|
|
||||||
|
sessions.remove(session.getSessionId());
|
||||||
sessionProperties.remove(session.getSessionId());
|
sessionProperties.remove(session.getSessionId());
|
||||||
sessionCreationTime.remove(session.getSessionId());
|
sessionCreationTime.remove(session.getSessionId());
|
||||||
sessionidParticipantpublicidParticipant.remove(session.getSessionId());
|
sessionidParticipantpublicidParticipant.remove(session.getSessionId());
|
||||||
|
|
|
@ -17,8 +17,6 @@
|
||||||
|
|
||||||
package io.openvidu.server.kurento;
|
package io.openvidu.server.kurento;
|
||||||
|
|
||||||
import io.openvidu.server.kurento.KurentoClientSessionInfo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the session info interface, contains a participant's
|
* Implementation of the session info interface, contains a participant's
|
||||||
* private id and the session's id.
|
* private id and the session's id.
|
||||||
|
|
|
@ -53,19 +53,21 @@ import io.openvidu.server.core.MediaOptions;
|
||||||
import io.openvidu.server.core.Participant;
|
import io.openvidu.server.core.Participant;
|
||||||
import io.openvidu.server.kurento.TrackType;
|
import io.openvidu.server.kurento.TrackType;
|
||||||
import io.openvidu.server.kurento.endpoint.KmsEvent;
|
import io.openvidu.server.kurento.endpoint.KmsEvent;
|
||||||
|
import io.openvidu.server.kurento.endpoint.KmsMediaEvent;
|
||||||
import io.openvidu.server.kurento.endpoint.MediaEndpoint;
|
import io.openvidu.server.kurento.endpoint.MediaEndpoint;
|
||||||
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||||
import io.openvidu.server.kurento.endpoint.SdpType;
|
import io.openvidu.server.kurento.endpoint.SdpType;
|
||||||
import io.openvidu.server.kurento.endpoint.SubscriberEndpoint;
|
import io.openvidu.server.kurento.endpoint.SubscriberEndpoint;
|
||||||
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
|
||||||
public class KurentoParticipant extends Participant {
|
public class KurentoParticipant extends Participant {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(KurentoParticipant.class);
|
private static final Logger log = LoggerFactory.getLogger(KurentoParticipant.class);
|
||||||
|
|
||||||
private OpenviduConfig openviduConfig;
|
|
||||||
|
|
||||||
private InfoHandler infoHandler;
|
private InfoHandler infoHandler;
|
||||||
private CallDetailRecord CDR;
|
private CallDetailRecord CDR;
|
||||||
|
private OpenviduConfig openviduConfig;
|
||||||
|
private RecordingManager recordingManager;
|
||||||
|
|
||||||
private boolean webParticipant = true;
|
private boolean webParticipant = true;
|
||||||
|
|
||||||
|
@ -79,11 +81,15 @@ public class KurentoParticipant extends Participant {
|
||||||
private final ConcurrentMap<String, SubscriberEndpoint> subscribers = new ConcurrentHashMap<String, SubscriberEndpoint>();
|
private final ConcurrentMap<String, SubscriberEndpoint> subscribers = new ConcurrentHashMap<String, SubscriberEndpoint>();
|
||||||
|
|
||||||
public KurentoParticipant(Participant participant, KurentoSession kurentoSession, MediaPipeline pipeline,
|
public KurentoParticipant(Participant participant, KurentoSession kurentoSession, MediaPipeline pipeline,
|
||||||
InfoHandler infoHandler, CallDetailRecord CDR, OpenviduConfig openviduConfig) {
|
InfoHandler infoHandler, CallDetailRecord CDR, OpenviduConfig openviduConfig,
|
||||||
|
RecordingManager recordingManager) {
|
||||||
super(participant.getParticipantPrivateId(), participant.getParticipantPublicId(), participant.getToken(),
|
super(participant.getParticipantPrivateId(), participant.getParticipantPublicId(), participant.getToken(),
|
||||||
participant.getClientMetadata(), participant.getLocation(), participant.getPlatform(),
|
participant.getClientMetadata(), participant.getLocation(), participant.getPlatform(),
|
||||||
participant.getCreatedAt());
|
participant.getCreatedAt());
|
||||||
|
this.infoHandler = infoHandler;
|
||||||
|
this.CDR = CDR;
|
||||||
this.openviduConfig = openviduConfig;
|
this.openviduConfig = openviduConfig;
|
||||||
|
this.recordingManager = recordingManager;
|
||||||
this.session = kurentoSession;
|
this.session = kurentoSession;
|
||||||
this.pipeline = pipeline;
|
this.pipeline = pipeline;
|
||||||
this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline,
|
this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline,
|
||||||
|
@ -94,8 +100,6 @@ public class KurentoParticipant extends Participant {
|
||||||
getNewOrExistingSubscriber(other.getParticipantPublicId());
|
getNewOrExistingSubscriber(other.getParticipantPublicId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.infoHandler = infoHandler;
|
|
||||||
this.CDR = CDR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createPublishingEndpoint(MediaOptions mediaOptions) {
|
public void createPublishingEndpoint(MediaOptions mediaOptions) {
|
||||||
|
@ -233,6 +237,11 @@ public class KurentoParticipant extends Participant {
|
||||||
log.info("PARTICIPANT {}: Is now publishing video in room {}", this.getParticipantPublicId(),
|
log.info("PARTICIPANT {}: Is now publishing video in room {}", this.getParticipantPublicId(),
|
||||||
this.session.getSessionId());
|
this.session.getSessionId());
|
||||||
|
|
||||||
|
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||||
|
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
||||||
|
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
|
||||||
|
}
|
||||||
|
|
||||||
CDR.recordNewPublisher(this, this.session.getSessionId(), this.publisher.getMediaOptions(),
|
CDR.recordNewPublisher(this, this.session.getSessionId(), this.publisher.getMediaOptions(),
|
||||||
this.publisher.createdAt());
|
this.publisher.createdAt());
|
||||||
|
|
||||||
|
@ -425,9 +434,15 @@ public class KurentoParticipant extends Participant {
|
||||||
private void releasePublisherEndpoint(String reason) {
|
private void releasePublisherEndpoint(String reason) {
|
||||||
if (publisher != null && publisher.getEndpoint() != null) {
|
if (publisher != null && publisher.getEndpoint() != null) {
|
||||||
|
|
||||||
// Store streamId from publisher's map
|
// Remove streamId from publisher's map
|
||||||
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
||||||
|
|
||||||
|
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||||
|
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
||||||
|
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
|
||||||
|
this.getPublisherStreamId());
|
||||||
|
}
|
||||||
|
|
||||||
publisher.unregisterErrorListeners();
|
publisher.unregisterErrorListeners();
|
||||||
for (MediaElement el : publisher.getMediaElements()) {
|
for (MediaElement el : publisher.getMediaElements()) {
|
||||||
releaseElement(getParticipantPublicId(), el);
|
releaseElement(getParticipantPublicId(), el);
|
||||||
|
@ -507,53 +522,6 @@ public class KurentoParticipant extends Participant {
|
||||||
* " | TIMESTAMP: " + System.currentTimeMillis(); log.debug(msg);
|
* " | TIMESTAMP: " + System.currentTimeMillis(); log.debug(msg);
|
||||||
* this.infoHandler.sendInfo(msg); });
|
* this.infoHandler.sendInfo(msg); });
|
||||||
*
|
*
|
||||||
* endpoint.getWebEndpoint().addMediaFlowInStateChangeListener((event) -> {
|
|
||||||
* String msg1 = " Media flow in state change (" +
|
|
||||||
* endpoint.getEndpoint().getName() + ") -> " + "STATE: " +
|
|
||||||
* event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " +
|
|
||||||
* event.getPadName() + " | MEDIATYPE: " + event.getMediaType() +
|
|
||||||
* " | TIMESTAMP: " + System.currentTimeMillis();
|
|
||||||
*
|
|
||||||
* endpoint.flowInMedia.put(event.getSource().getName() + "/" +
|
|
||||||
* event.getMediaType(), event.getSource());
|
|
||||||
*
|
|
||||||
* String msg2;
|
|
||||||
*
|
|
||||||
* if (endpoint.flowInMedia.values().size() != 2) { msg2 =
|
|
||||||
* " THERE ARE LESS FLOW IN MEDIA'S THAN EXPECTED IN " +
|
|
||||||
* endpoint.getEndpoint().getName() + " (" +
|
|
||||||
* endpoint.flowInMedia.values().size() + ")"; } else { msg2 =
|
|
||||||
* " NUMBER OF FLOW IN MEDIA'S IS NOW CORRECT IN " +
|
|
||||||
* endpoint.getEndpoint().getName() + " (" +
|
|
||||||
* endpoint.flowInMedia.values().size() + ")"; }
|
|
||||||
*
|
|
||||||
* log.debug(msg1); log.debug(msg2); this.infoHandler.sendInfo(msg1);
|
|
||||||
* this.infoHandler.sendInfo(msg2); });
|
|
||||||
*
|
|
||||||
* endpoint.getWebEndpoint().addMediaFlowOutStateChangeListener((event) -> {
|
|
||||||
* String msg1 = " Media flow out state change (" +
|
|
||||||
* endpoint.getEndpoint().getName() + ") -> " + "STATE: " +
|
|
||||||
* event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " +
|
|
||||||
* event.getPadName() + " | MEDIATYPE: " + event.getMediaType() +
|
|
||||||
* " | TIMESTAMP: " + System.currentTimeMillis();
|
|
||||||
*
|
|
||||||
* endpoint.flowOutMedia. @SuppressWarnings("unchecked")
|
|
||||||
* put(event.getSource().getName() + "/" + event.getMediaType(),
|
|
||||||
* event.getSource());
|
|
||||||
*
|
|
||||||
* String msg2;
|
|
||||||
*
|
|
||||||
* if (endpoint.flowOutMedia.values().size() != 2) { msg2 =
|
|
||||||
* " THERE ARE LESS FLOW OUT MEDIA'S THAN EXPECTED IN " +
|
|
||||||
* endpoint.getEndpoint().getName() + " (" +
|
|
||||||
* endpoint.flowOutMedia.values().size() + ")"; } else { msg2 =
|
|
||||||
* " NUMBER OF FLOW OUT MEDIA'S IS NOW CORRECT IN " +
|
|
||||||
* endpoint.getEndpoint().getName() + " (" +
|
|
||||||
* endpoint.flowOutMedia.values().size() + ")"; }
|
|
||||||
*
|
|
||||||
* log.debug(msg1); log.debug(msg2); this.infoHandler.sendInfo(msg1);
|
|
||||||
* this.infoHandler.sendInfo(msg2); });
|
|
||||||
*
|
|
||||||
* endpoint.getWebEndpoint().addMediaSessionStartedListener((event) -> { String
|
* endpoint.getWebEndpoint().addMediaSessionStartedListener((event) -> { String
|
||||||
* msg = " Media session started (" +
|
* msg = " Media session started (" +
|
||||||
* endpoint.getEndpoint().getName() + ") | TIMESTAMP: " +
|
* endpoint.getEndpoint().getName() + ") | TIMESTAMP: " +
|
||||||
|
@ -568,15 +536,8 @@ public class KurentoParticipant extends Participant {
|
||||||
*
|
*
|
||||||
* endpoint.getWebEndpoint().addMediaStateChangedListener((event) -> { String
|
* endpoint.getWebEndpoint().addMediaStateChangedListener((event) -> { String
|
||||||
* msg = " Media state changed (" +
|
* msg = " Media state changed (" +
|
||||||
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() +
|
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() + " to " +
|
||||||
* " to " + event.getNewState(); log.debug(msg); this.infoHandler.sendInfo(msg);
|
* event.getNewState(); log.debug(msg); this.infoHandler.sendInfo(msg); });
|
||||||
* });
|
|
||||||
*
|
|
||||||
* endpoint.getWebEndpoint().addConnectionStateChangedListener((event) -> {
|
|
||||||
* String msg = " Connection state changed (" +
|
|
||||||
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() +
|
|
||||||
* " to " + event.getNewState() + " | TIMESTAMP: " + System.currentTimeMillis();
|
|
||||||
* log.debug(msg); this.infoHandler.sendInfo(msg); });
|
|
||||||
*
|
*
|
||||||
* endpoint.getWebEndpoint().addIceCandidateFoundListener((event) -> { String
|
* endpoint.getWebEndpoint().addIceCandidateFoundListener((event) -> { String
|
||||||
* msg = " ICE CANDIDATE FOUND (" +
|
* msg = " ICE CANDIDATE FOUND (" +
|
||||||
|
@ -584,38 +545,13 @@ public class KurentoParticipant extends Participant {
|
||||||
* event.getCandidate().getCandidate() + " | TIMESTAMP: " +
|
* event.getCandidate().getCandidate() + " | TIMESTAMP: " +
|
||||||
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
|
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
|
||||||
* });
|
* });
|
||||||
*
|
|
||||||
* endpoint.getWebEndpoint().addIceComponentStateChangeListener((event) -> {
|
|
||||||
* String msg = " ICE COMPONENT STATE CHANGE (" +
|
|
||||||
* endpoint.getEndpoint().getName() + "): for component " +
|
|
||||||
* event.getComponentId() + " - STATE: " + event.getState() + " | TIMESTAMP: " +
|
|
||||||
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* endpoint.getWebEndpoint().addIceGatheringDoneListener((event) -> { String msg
|
|
||||||
* = " ICE GATHERING DONE! (" +
|
|
||||||
* endpoint.getEndpoint().getName() + ")" + " | TIMESTAMP: " +
|
|
||||||
* System.currentTimeMillis(); log.debug(msg); this.infoHandler.sendInfo(msg);
|
|
||||||
* });
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
endpoint.getWebEndpoint().addMediaFlowInStateChangeListener(event -> {
|
endpoint.getWebEndpoint().addMediaFlowInStateChangeListener(event -> {
|
||||||
String msg1 = "Media flow in state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
|
String msg1 = "Media flow in state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
|
||||||
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
|
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
|
||||||
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
|
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
|
||||||
|
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
|
||||||
/*
|
|
||||||
* endpoint.flowInMedia.put(event.getSource().getName(), event.getMediaType());
|
|
||||||
* if (endpoint.getPublisher().getMediaOptions().hasAudio() &&
|
|
||||||
* endpoint.getPublisher().getMediaOptions().hasVideo() &&
|
|
||||||
* endpoint.flowInMedia.values().size() == 2) {
|
|
||||||
*/
|
|
||||||
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
|
|
||||||
/*
|
|
||||||
* } else if (endpoint.flowInMedia.values().size() == 1) {
|
|
||||||
* endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt())); }
|
|
||||||
*/
|
|
||||||
|
|
||||||
log.info(msg1);
|
log.info(msg1);
|
||||||
this.infoHandler.sendInfo(msg1);
|
this.infoHandler.sendInfo(msg1);
|
||||||
});
|
});
|
||||||
|
@ -624,19 +560,7 @@ public class KurentoParticipant extends Participant {
|
||||||
String msg1 = "Media flow out state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
|
String msg1 = "Media flow out state change (" + endpoint.getEndpoint().getName() + ") -> " + "STATE: "
|
||||||
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
|
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
|
||||||
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
|
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
|
||||||
|
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
|
||||||
/*
|
|
||||||
* endpoint.flowOutMedia.put(event.getSource().getName(), event.getMediaType());
|
|
||||||
* if (endpoint.getPublisher().getMediaOptions().hasAudio() &&
|
|
||||||
* endpoint.getPublisher().getMediaOptions().hasVideo() &&
|
|
||||||
* endpoint.flowOutMedia.values().size() == 2) {
|
|
||||||
*/
|
|
||||||
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
|
|
||||||
/*
|
|
||||||
* } else if (endpoint.flowOutMedia.values().size() == 1) {
|
|
||||||
* endpoint.kmsEvents.add(new KmsEvent(event)); }
|
|
||||||
*/
|
|
||||||
|
|
||||||
log.info(msg1);
|
log.info(msg1);
|
||||||
this.infoHandler.sendInfo(msg1);
|
this.infoHandler.sendInfo(msg1);
|
||||||
});
|
});
|
||||||
|
@ -661,7 +585,7 @@ public class KurentoParticipant extends Participant {
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoint.getEndpoint().addMediaTranscodingStateChangeListener(event -> {
|
endpoint.getEndpoint().addMediaTranscodingStateChangeListener(event -> {
|
||||||
endpoint.kmsEvents.add(new KmsEvent(event, endpoint.createdAt()));
|
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
|
||||||
});
|
});
|
||||||
|
|
||||||
endpoint.getWebEndpoint().addIceComponentStateChangeListener(event -> {
|
endpoint.getWebEndpoint().addIceComponentStateChangeListener(event -> {
|
||||||
|
|
|
@ -47,6 +47,7 @@ import io.openvidu.server.cdr.CallDetailRecord;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.core.Participant;
|
import io.openvidu.server.core.Participant;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Pablo Fuente (pablofuenteperez@gmail.com)
|
* @author Pablo Fuente (pablofuenteperez@gmail.com)
|
||||||
|
@ -57,6 +58,7 @@ public class KurentoSession implements Session {
|
||||||
public static final int ASYNC_LATCH_TIMEOUT = 30;
|
public static final int ASYNC_LATCH_TIMEOUT = 30;
|
||||||
|
|
||||||
private OpenviduConfig openviduConfig;
|
private OpenviduConfig openviduConfig;
|
||||||
|
private RecordingManager recordingManager;
|
||||||
|
|
||||||
private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>();
|
private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>();
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
@ -85,7 +87,8 @@ public class KurentoSession implements Session {
|
||||||
|
|
||||||
public KurentoSession(String sessionId, Long startTime, SessionProperties sessionProperties,
|
public KurentoSession(String sessionId, Long startTime, SessionProperties sessionProperties,
|
||||||
KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler,
|
KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler,
|
||||||
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig) {
|
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig,
|
||||||
|
RecordingManager recordingManager) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.sessionProperties = sessionProperties;
|
this.sessionProperties = sessionProperties;
|
||||||
this.kurentoClient = kurentoClient;
|
this.kurentoClient = kurentoClient;
|
||||||
|
@ -93,6 +96,7 @@ public class KurentoSession implements Session {
|
||||||
this.kurentoSessionHandler = kurentoSessionHandler;
|
this.kurentoSessionHandler = kurentoSessionHandler;
|
||||||
this.CDR = CDR;
|
this.CDR = CDR;
|
||||||
this.openviduConfig = openviduConfig;
|
this.openviduConfig = openviduConfig;
|
||||||
|
this.recordingManager = recordingManager;
|
||||||
this.startTime = startTime;
|
this.startTime = startTime;
|
||||||
log.debug("New SESSION instance with id '{}'", sessionId);
|
log.debug("New SESSION instance with id '{}'", sessionId);
|
||||||
}
|
}
|
||||||
|
@ -113,7 +117,7 @@ public class KurentoSession implements Session {
|
||||||
createPipeline();
|
createPipeline();
|
||||||
|
|
||||||
KurentoParticipant kurentoParticipant = new KurentoParticipant(participant, this, getPipeline(),
|
KurentoParticipant kurentoParticipant = new KurentoParticipant(participant, this, getPipeline(),
|
||||||
kurentoSessionHandler.getInfoHandler(), this.CDR, this.openviduConfig);
|
kurentoSessionHandler.getInfoHandler(), this.CDR, this.openviduConfig, this.recordingManager);
|
||||||
participants.put(participant.getParticipantPrivateId(), kurentoParticipant);
|
participants.put(participant.getParticipantPrivateId(), kurentoParticipant);
|
||||||
|
|
||||||
filterStates.forEach((filterId, state) -> {
|
filterStates.forEach((filterId, state) -> {
|
||||||
|
|
|
@ -170,25 +170,37 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
reason);
|
reason);
|
||||||
|
|
||||||
if (remainingParticipants.isEmpty()) {
|
if (remainingParticipants.isEmpty()) {
|
||||||
|
if (openviduConfig.isRecordingModuleEnabled()
|
||||||
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
|
&& this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)) {
|
||||||
|
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||||
|
// a Publisher starts before timeout)
|
||||||
|
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
||||||
|
sessionId);
|
||||||
|
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
||||||
|
} else {
|
||||||
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
||||||
this.closeSessionAndEmptyCollections(session, reason);
|
this.closeSessionAndEmptyCollections(session, reason);
|
||||||
showTokens();
|
showTokens();
|
||||||
|
}
|
||||||
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
||||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
|
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||||
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
||||||
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
||||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
||||||
// Immediately stop recording when last real participant left if
|
// Immediately stop recording when last real participant left if
|
||||||
// RecordingMode.ALWAYS
|
// RecordingMode.ALWAYS
|
||||||
log.info("Last participant left. Stopping recording for session {}", sessionId);
|
log.info("Last participant left. Stopping recording for session {}", sessionId);
|
||||||
recordingService.stopRecording(session, null, reason);
|
recordingManager.stopRecording(session, null, reason);
|
||||||
evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null,
|
evictParticipant(session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID), null,
|
||||||
null, "EVICT_RECORDER");
|
null, "EVICT_RECORDER");
|
||||||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
||||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||||
// a Publisher starts before timeout)
|
// a Publisher starts before timeout)
|
||||||
log.info("Last participant left. Starting countdown for stopping recording of session {}", sessionId);
|
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
||||||
recordingService.initAutomaticRecordingStopThread(session.getSessionId());
|
sessionId);
|
||||||
|
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,20 +289,21 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
&& session.getActivePublishers() == 0) {
|
&& session.getActivePublishers() == 0) {
|
||||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
|
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
|
||||||
&& !recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
&& !recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
// Insecure session recording
|
// Insecure session recording
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
recordingService.startRecording(session,
|
recordingManager.startRecording(session,
|
||||||
new RecordingProperties.Builder().name("")
|
new RecordingProperties.Builder().name("")
|
||||||
|
.outputMode(io.openvidu.java.client.Recording.OutputMode.COMPOSED)
|
||||||
.recordingLayout(session.getSessionProperties().defaultRecordingLayout())
|
.recordingLayout(session.getSessionProperties().defaultRecordingLayout())
|
||||||
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
|
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
|
||||||
}).start();
|
}).start();
|
||||||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())
|
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())
|
||||||
&& recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
&& recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
// Abort automatic recording stop (user published before timeout)
|
// Abort automatic recording stop (user published before timeout)
|
||||||
log.info("Participant {} published before timeout finished. Aborting automatic recording stop",
|
log.info("Participant {} published before timeout finished. Aborting automatic recording stop",
|
||||||
participant.getParticipantPublicId());
|
participant.getParticipantPublicId());
|
||||||
boolean stopAborted = recordingService.abortAutomaticRecordingStopThread(session.getSessionId());
|
boolean stopAborted = recordingManager.abortAutomaticRecordingStopThread(session.getSessionId());
|
||||||
if (stopAborted) {
|
if (stopAborted) {
|
||||||
log.info("Automatic recording stopped succesfully aborted");
|
log.info("Automatic recording stopped succesfully aborted");
|
||||||
} else {
|
} else {
|
||||||
|
@ -491,7 +504,7 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
this.kurentoClient = kcProvider.getKurentoClient(kcSessionInfo);
|
this.kurentoClient = kcProvider.getKurentoClient(kcSessionInfo);
|
||||||
session = new KurentoSession(sessionId, this.sessionCreationTime.get(sessionId), sessionProperties,
|
session = new KurentoSession(sessionId, this.sessionCreationTime.get(sessionId), sessionProperties,
|
||||||
kurentoClient, kurentoSessionEventsHandler, kcProvider.destroyWhenUnused(), this.CDR,
|
kurentoClient, kurentoSessionEventsHandler, kcProvider.destroyWhenUnused(), this.CDR,
|
||||||
this.openviduConfig);
|
this.openviduConfig, this.recordingManager);
|
||||||
|
|
||||||
KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session);
|
KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session);
|
||||||
if (oldSession != null) {
|
if (oldSession != null) {
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
package io.openvidu.server.kurento.endpoint;
|
package io.openvidu.server.kurento.endpoint;
|
||||||
|
|
||||||
import org.kurento.client.MediaEvent;
|
import org.kurento.client.MediaEvent;
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.kurento.endpoint;
|
||||||
|
|
||||||
|
import org.kurento.client.MediaEvent;
|
||||||
|
import org.kurento.client.MediaType;
|
||||||
|
|
||||||
|
public class KmsMediaEvent extends KmsEvent {
|
||||||
|
|
||||||
|
MediaType mediaType;
|
||||||
|
|
||||||
|
public KmsMediaEvent(MediaEvent event, MediaType mediaType, long createdAt) {
|
||||||
|
super(event, createdAt);
|
||||||
|
this.mediaType = mediaType;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -19,9 +19,7 @@ package io.openvidu.server.kurento.endpoint;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
@ -32,7 +30,6 @@ import org.kurento.client.IceCandidate;
|
||||||
import org.kurento.client.ListenerSubscription;
|
import org.kurento.client.ListenerSubscription;
|
||||||
import org.kurento.client.MediaElement;
|
import org.kurento.client.MediaElement;
|
||||||
import org.kurento.client.MediaPipeline;
|
import org.kurento.client.MediaPipeline;
|
||||||
import org.kurento.client.MediaType;
|
|
||||||
import org.kurento.client.OnIceCandidateEvent;
|
import org.kurento.client.OnIceCandidateEvent;
|
||||||
import org.kurento.client.RtpEndpoint;
|
import org.kurento.client.RtpEndpoint;
|
||||||
import org.kurento.client.SdpEndpoint;
|
import org.kurento.client.SdpEndpoint;
|
||||||
|
@ -85,9 +82,6 @@ public abstract class MediaEndpoint {
|
||||||
private final List<IceCandidate> receivedCandidateList = new LinkedList<IceCandidate>();
|
private final List<IceCandidate> receivedCandidateList = new LinkedList<IceCandidate>();
|
||||||
private LinkedList<IceCandidate> candidates = new LinkedList<IceCandidate>();
|
private LinkedList<IceCandidate> candidates = new LinkedList<IceCandidate>();
|
||||||
|
|
||||||
public Map<String, MediaType> flowInMedia = new ConcurrentHashMap<>();
|
|
||||||
public Map<String, MediaType> flowOutMedia = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public String selectedLocalIceCandidate;
|
public String selectedLocalIceCandidate;
|
||||||
public String selectedRemoteIceCandidate;
|
public String selectedRemoteIceCandidate;
|
||||||
public Queue<KmsEvent> kmsEvents = new ConcurrentLinkedQueue<>();
|
public Queue<KmsEvent> kmsEvents = new ConcurrentLinkedQueue<>();
|
||||||
|
|
|
@ -18,19 +18,42 @@
|
||||||
package io.openvidu.server.kurento.kms;
|
package io.openvidu.server.kurento.kms;
|
||||||
|
|
||||||
import org.kurento.client.KurentoClient;
|
import org.kurento.client.KurentoClient;
|
||||||
|
import org.kurento.client.KurentoConnectionListener;
|
||||||
import io.openvidu.server.kurento.kms.Kms;
|
import org.slf4j.Logger;
|
||||||
import io.openvidu.server.kurento.kms.KmsManager;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class FixedOneKmsManager extends KmsManager {
|
public class FixedOneKmsManager extends KmsManager {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FixedOneKmsManager.class);
|
||||||
|
|
||||||
public FixedOneKmsManager(String kmsWsUri) {
|
public FixedOneKmsManager(String kmsWsUri) {
|
||||||
this(kmsWsUri, 1);
|
this(kmsWsUri, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
|
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
|
||||||
for (int i = 0; i < numKmss; i++) {
|
for (int i = 0; i < numKmss; i++) {
|
||||||
this.addKms(new Kms(KurentoClient.create(kmsWsUri), kmsWsUri));
|
this.addKms(new Kms(KurentoClient.create(kmsWsUri, new KurentoConnectionListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reconnected(boolean isReconnected) {
|
||||||
|
log.warn("Kurento Client reconnected ({}) to KMS with uri {}", isReconnected, kmsWsUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnected() {
|
||||||
|
log.warn("Kurento Client disconnected from KMS with uri {}", kmsWsUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectionFailed() {
|
||||||
|
log.warn("Kurento Client failed connecting to KMS with uri {}", kmsWsUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connected() {
|
||||||
|
log.warn("Kurento Client is now connected to KMS with uri {}", kmsWsUri);
|
||||||
|
}
|
||||||
|
}), kmsWsUri));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.recording;
|
||||||
|
|
||||||
|
import org.kurento.client.RecorderEndpoint;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
public class RecorderEndpointWrapper {
|
||||||
|
|
||||||
|
RecorderEndpoint recorder;
|
||||||
|
String connectionId;
|
||||||
|
String recordingId;
|
||||||
|
String streamId;
|
||||||
|
String clientData;
|
||||||
|
String serverData;
|
||||||
|
boolean hasAudio;
|
||||||
|
boolean hasVideo;
|
||||||
|
String typeOfVideo;
|
||||||
|
|
||||||
|
long startTime;
|
||||||
|
long endTime;
|
||||||
|
long size;
|
||||||
|
|
||||||
|
public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId,
|
||||||
|
String clientData, String serverData, boolean hasAudio, boolean hasVideo, String typeOfVideo) {
|
||||||
|
this.recorder = recorder;
|
||||||
|
this.connectionId = connectionId;
|
||||||
|
this.recordingId = recordingId;
|
||||||
|
this.streamId = streamId;
|
||||||
|
this.clientData = clientData;
|
||||||
|
this.serverData = serverData;
|
||||||
|
this.hasAudio = hasAudio;
|
||||||
|
this.hasVideo = hasVideo;
|
||||||
|
this.typeOfVideo = typeOfVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecorderEndpoint getRecorder() {
|
||||||
|
return recorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getConnectionId() {
|
||||||
|
return connectionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRecordingId() {
|
||||||
|
return recordingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStreamId() {
|
||||||
|
return streamId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientData() {
|
||||||
|
return clientData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerData() {
|
||||||
|
return serverData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getStartTime() {
|
||||||
|
return startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStartTime(long startTime) {
|
||||||
|
this.startTime = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getEndTime() {
|
||||||
|
return endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEndTime(long endTime) {
|
||||||
|
this.endTime = endTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(long size) {
|
||||||
|
this.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAudio() {
|
||||||
|
return hasAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasVideo() {
|
||||||
|
return hasVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTypeOfVideo() {
|
||||||
|
return typeOfVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject toJson() {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("connectionId", this.connectionId);
|
||||||
|
json.addProperty("streamId", this.streamId);
|
||||||
|
json.addProperty("clientData", this.clientData);
|
||||||
|
json.addProperty("serverData", this.serverData);
|
||||||
|
json.addProperty("startTime", this.startTime);
|
||||||
|
json.addProperty("endTime", this.endTime);
|
||||||
|
json.addProperty("duration", this.endTime - this.startTime);
|
||||||
|
json.addProperty("size", this.size);
|
||||||
|
json.addProperty("hasAudio", this.hasAudio);
|
||||||
|
json.addProperty("hasVideo", this.hasVideo);
|
||||||
|
if (this.hasVideo) {
|
||||||
|
json.addProperty("typeOfVideo", this.typeOfVideo);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -24,16 +24,7 @@ import io.openvidu.java.client.RecordingProperties;
|
||||||
|
|
||||||
public class Recording {
|
public class Recording {
|
||||||
|
|
||||||
public enum Status {
|
private io.openvidu.java.client.Recording.Status 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.public-access' is true
|
|
||||||
failed; // The recording has failed
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recording.Status status;
|
|
||||||
|
|
||||||
private String id;
|
private String id;
|
||||||
private String sessionId;
|
private String sessionId;
|
||||||
|
@ -49,7 +40,7 @@ public class Recording {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.createdAt = System.currentTimeMillis();
|
this.createdAt = System.currentTimeMillis();
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.status = Status.started;
|
this.status = io.openvidu.java.client.Recording.Status.started;
|
||||||
this.recordingProperties = recordingProperties;
|
this.recordingProperties = recordingProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,16 +61,27 @@ public class Recording {
|
||||||
}
|
}
|
||||||
this.hasAudio = json.get("hasAudio").getAsBoolean();
|
this.hasAudio = json.get("hasAudio").getAsBoolean();
|
||||||
this.hasVideo = json.get("hasVideo").getAsBoolean();
|
this.hasVideo = json.get("hasVideo").getAsBoolean();
|
||||||
this.status = Status.valueOf(json.get("status").getAsString());
|
this.status = io.openvidu.java.client.Recording.Status.valueOf(json.get("status").getAsString());
|
||||||
this.recordingProperties = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
|
||||||
.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString())).build();
|
io.openvidu.java.client.Recording.OutputMode outputMode = io.openvidu.java.client.Recording.OutputMode
|
||||||
|
.valueOf(json.get("outputMode").getAsString());
|
||||||
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
||||||
|
.outputMode(outputMode);
|
||||||
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode)) {
|
||||||
|
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
|
||||||
|
builder.recordingLayout(recordingLayout);
|
||||||
|
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
|
||||||
|
builder.customLayout(json.get("customLayout").getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.recordingProperties = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Status getStatus() {
|
public io.openvidu.java.client.Recording.Status getStatus() {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStatus(Status status) {
|
public void setStatus(io.openvidu.java.client.Recording.Status status) {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +97,10 @@ public class Recording {
|
||||||
return this.recordingProperties.name();
|
return this.recordingProperties.name();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public io.openvidu.java.client.Recording.OutputMode getOutputMode() {
|
||||||
|
return this.recordingProperties.outputMode();
|
||||||
|
}
|
||||||
|
|
||||||
public RecordingLayout getRecordingLayout() {
|
public RecordingLayout getRecordingLayout() {
|
||||||
return this.recordingProperties.recordingLayout();
|
return this.recordingProperties.recordingLayout();
|
||||||
}
|
}
|
||||||
|
@ -103,6 +109,10 @@ public class Recording {
|
||||||
return this.recordingProperties.customLayout();
|
return this.recordingProperties.customLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RecordingProperties getRecordingProperties() {
|
||||||
|
return this.recordingProperties;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
@ -163,10 +173,13 @@ public class Recording {
|
||||||
JsonObject json = new JsonObject();
|
JsonObject json = new JsonObject();
|
||||||
json.addProperty("id", this.id);
|
json.addProperty("id", this.id);
|
||||||
json.addProperty("name", this.recordingProperties.name());
|
json.addProperty("name", this.recordingProperties.name());
|
||||||
|
json.addProperty("outputMode", this.getOutputMode().name());
|
||||||
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recordingProperties.outputMode())) {
|
||||||
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
|
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
|
||||||
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
||||||
json.addProperty("customLayout", this.recordingProperties.customLayout());
|
json.addProperty("customLayout", this.recordingProperties.customLayout());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
json.addProperty("sessionId", this.sessionId);
|
json.addProperty("sessionId", this.sessionId);
|
||||||
json.addProperty("createdAt", this.createdAt);
|
json.addProperty("createdAt", this.createdAt);
|
||||||
json.addProperty("size", this.size);
|
json.addProperty("size", this.size);
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package io.openvidu.server.recording;
|
package io.openvidu.server.recording;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileReader;
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -38,35 +39,35 @@ public class RecordingInfoUtils {
|
||||||
private JsonObject videoStream;
|
private JsonObject videoStream;
|
||||||
private JsonObject audioStream;
|
private JsonObject audioStream;
|
||||||
|
|
||||||
public RecordingInfoUtils(String fullVideoPath) throws FileNotFoundException, IOException, OpenViduException {
|
private String infoFilePath;
|
||||||
|
|
||||||
|
public RecordingInfoUtils(String infoFilePath) throws FileNotFoundException, IOException, OpenViduException {
|
||||||
|
|
||||||
|
this.infoFilePath = infoFilePath;
|
||||||
this.parser = new JsonParser();
|
this.parser = new JsonParser();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.json = parser.parse(new FileReader(fullVideoPath)).getAsJsonObject();
|
this.json = parser.parse(new FileReader(infoFilePath)).getAsJsonObject();
|
||||||
} catch (JsonIOException | JsonSyntaxException e) {
|
} catch (JsonIOException | JsonSyntaxException e) {
|
||||||
// Recording metadata from ffprobe is not a JSON: video file is corrupted
|
// Recording metadata from ffprobe is not a JSON: video file is corrupted
|
||||||
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is corrupted");
|
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is corrupted");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.json.size() == 0) {
|
if (this.json.size() == 0) {
|
||||||
// Recording metadata from ffprobe is an emtpy JSON
|
// Recording metadata from ffprobe is an emtpy JSON
|
||||||
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty");
|
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jsonFormat = json.get("format").getAsJsonObject();
|
this.jsonFormat = json.get("format").getAsJsonObject();
|
||||||
|
|
||||||
JsonArray streams = json.get("streams").getAsJsonArray();
|
JsonArray streams = json.get("streams").getAsJsonArray();
|
||||||
|
|
||||||
for (int i = 0; i < streams.size(); i++) {
|
for (int i = 0; i < streams.size(); i++) {
|
||||||
JsonObject stream = streams.get(i).getAsJsonObject();
|
JsonObject stream = streams.get(i).getAsJsonObject();
|
||||||
if ("video".equals(stream.get("codec_type").toString())) {
|
if ("video".equals(stream.get("codec_type").getAsString())) {
|
||||||
this.videoStream = stream;
|
this.videoStream = stream;
|
||||||
} else if ("audio".equals(stream.get("codec_type").toString())) {
|
} else if ("audio".equals(stream.get("codec_type").getAsString())) {
|
||||||
this.audioStream = stream;
|
this.audioStream = stream;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public double getDurationInSeconds() {
|
public double getDurationInSeconds() {
|
||||||
|
@ -124,4 +125,8 @@ public class RecordingInfoUtils {
|
||||||
return audioStream.get("codec_long_name").toString();
|
return audioStream.get("codec_long_name").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean deleteFilePath() {
|
||||||
|
return new File(this.infoFilePath).delete();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,35 +15,22 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.openvidu.server.recording;
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import javax.ws.rs.ProcessingException;
|
import javax.ws.rs.ProcessingException;
|
||||||
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import com.github.dockerjava.api.DockerClient;
|
import com.github.dockerjava.api.DockerClient;
|
||||||
import com.github.dockerjava.api.command.CreateContainerCmd;
|
import com.github.dockerjava.api.command.CreateContainerCmd;
|
||||||
|
@ -60,8 +47,6 @@ import com.github.dockerjava.core.DockerClientBuilder;
|
||||||
import com.github.dockerjava.core.DockerClientConfig;
|
import com.github.dockerjava.core.DockerClientConfig;
|
||||||
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
||||||
import com.github.dockerjava.core.command.PullImageResultCallback;
|
import com.github.dockerjava.core.command.PullImageResultCallback;
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
|
|
||||||
import io.openvidu.client.OpenViduException;
|
import io.openvidu.client.OpenViduException;
|
||||||
import io.openvidu.client.OpenViduException.Code;
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
@ -70,58 +55,42 @@ import io.openvidu.java.client.RecordingProperties;
|
||||||
import io.openvidu.server.OpenViduServer;
|
import io.openvidu.server.OpenViduServer;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.core.SessionEventsHandler;
|
import io.openvidu.server.recording.Recording;
|
||||||
|
import io.openvidu.server.recording.RecordingInfoUtils;
|
||||||
import io.openvidu.server.utils.CommandExecutor;
|
import io.openvidu.server.utils.CommandExecutor;
|
||||||
|
|
||||||
@Service
|
public class ComposedRecordingService extends RecordingService {
|
||||||
public class ComposedRecordingService {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
|
private static final Logger log = LoggerFactory.getLogger(ComposedRecordingService.class);
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private OpenviduConfig openviduConfig;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private SessionEventsHandler sessionHandler;
|
|
||||||
|
|
||||||
private Map<String, String> containers = new ConcurrentHashMap<>();
|
private Map<String, String> containers = new ConcurrentHashMap<>();
|
||||||
private Map<String, String> sessionsContainers = 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 Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
|
|
||||||
Runtime.getRuntime().availableProcessors());
|
|
||||||
|
|
||||||
private final String IMAGE_NAME = "openvidu/openvidu-recording";
|
private final String IMAGE_NAME = "openvidu/openvidu-recording";
|
||||||
private String IMAGE_TAG;
|
private String IMAGE_TAG;
|
||||||
private final String RECORDING_ENTITY_FILE = ".recording.";
|
|
||||||
|
|
||||||
private DockerClient dockerClient;
|
private DockerClient dockerClient;
|
||||||
|
|
||||||
public ComposedRecordingService() {
|
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
|
super(recordingManager, openviduConfig);
|
||||||
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
|
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
|
||||||
this.dockerClient = DockerClientBuilder.getInstance(config).build();
|
this.dockerClient = DockerClientBuilder.getInstance(config).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recording startRecording(Session session, RecordingProperties properties) {
|
@Override
|
||||||
|
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||||
List<String> envs = new ArrayList<>();
|
List<String> envs = new ArrayList<>();
|
||||||
String shortSessionId = session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
|
|
||||||
session.getSessionId().length());
|
|
||||||
String recordingId = this.getFreeRecordingId(session.getSessionId(), shortSessionId);
|
|
||||||
|
|
||||||
if (properties.name() == null || properties.name().isEmpty()) {
|
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
||||||
// No name provided for the recording file
|
properties);
|
||||||
properties = new RecordingProperties.Builder().name(recordingId)
|
properties = updatePropertiesAndRecordingId.properties;
|
||||||
.recordingLayout(properties.recordingLayout()).customLayout(properties.customLayout()).build();
|
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||||
}
|
|
||||||
|
|
||||||
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||||
|
|
||||||
this.sessionsRecordings.put(session.getSessionId(), recording);
|
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
|
||||||
this.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
||||||
this.startingRecordings.put(recording.getId(), recording);
|
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
||||||
|
|
||||||
String uid = null;
|
String uid = null;
|
||||||
try {
|
try {
|
||||||
|
@ -133,7 +102,7 @@ public class ComposedRecordingService {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
String layoutUrl = this.getLayoutUrl(recording, shortSessionId);
|
String layoutUrl = this.getLayoutUrl(recording, this.getShortSessionId(session));
|
||||||
|
|
||||||
envs.add("URL=" + layoutUrl);
|
envs.add("URL=" + layoutUrl);
|
||||||
envs.add("RESOLUTION=1920x1080");
|
envs.add("RESOLUTION=1920x1080");
|
||||||
|
@ -147,37 +116,38 @@ public class ComposedRecordingService {
|
||||||
log.info(recording.toJson().toString());
|
log.info(recording.toJson().toString());
|
||||||
log.info("Recorder connecting to url {}", layoutUrl);
|
log.info("Recorder connecting to url {}", layoutUrl);
|
||||||
|
|
||||||
String containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
String containerId;
|
||||||
|
try {
|
||||||
|
containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
this.cleanRecordingMapsAndReturnContainerId(recording);
|
||||||
|
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||||
|
"Couldn't initialize recording container. Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
this.waitForVideoFileNotEmpty(properties.name());
|
this.waitForVideoFileNotEmpty(recording);
|
||||||
|
|
||||||
this.sessionsContainers.put(session.getSessionId(), containerId);
|
this.sessionsContainers.put(session.getSessionId(), containerId);
|
||||||
|
|
||||||
recording.setStatus(Recording.Status.started);
|
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
|
||||||
|
|
||||||
this.startedRecordings.put(recording.getId(), recording);
|
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
||||||
this.startingRecordings.remove(recording.getId());
|
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||||
|
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recording stopRecording(Session session, String recordingId, String reason) {
|
@Override
|
||||||
Recording recording;
|
public Recording stopRecording(Session session, Recording recording, String reason) {
|
||||||
String containerId;
|
String containerId = cleanRecordingMapsAndReturnContainerId(recording);
|
||||||
|
final String recordingId = recording.getId();
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
log.warn(
|
log.warn(
|
||||||
"Existing recording {} does not have an active session associated. This usually means the recording"
|
"Existing recording {} does not have an active session associated. This usually means the recording"
|
||||||
+ " layout did not join a recorded participant or the recording has been automatically"
|
+ " layout did not join a recorded participant or the recording has been automatically"
|
||||||
+ " stopped after last user left and timeout passed",
|
+ " stopped after last user left and timeout passed",
|
||||||
recordingId);
|
recording.getId());
|
||||||
recording = this.startedRecordings.remove(recordingId);
|
|
||||||
containerId = this.sessionsContainers.remove(recording.getSessionId());
|
|
||||||
this.sessionsRecordings.remove(recording.getSessionId());
|
|
||||||
} else {
|
|
||||||
recording = this.sessionsRecordings.remove(session.getSessionId());
|
|
||||||
containerId = this.sessionsContainers.remove(session.getSessionId());
|
|
||||||
this.startedRecordings.remove(recording.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (containerId == null) {
|
if (containerId == null) {
|
||||||
|
@ -205,8 +175,9 @@ public class ComposedRecordingService {
|
||||||
containerClosed = true;
|
containerClosed = true;
|
||||||
log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux,
|
log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux,
|
||||||
session.getSessionId());
|
session.getSessionId());
|
||||||
log.warn("Deleting unusable files for recording {}", recording.getId());
|
log.warn("Deleting unusable files for recording {}", recordingId);
|
||||||
if (HttpStatus.NO_CONTENT.equals(this.deleteRecordingFromHost(recording.getId(), true))) {
|
if (HttpStatus.NO_CONTENT
|
||||||
|
.equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) {
|
||||||
log.warn("Files properly deleted");
|
log.warn("Files properly deleted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -234,12 +205,12 @@ public class ComposedRecordingService {
|
||||||
try {
|
try {
|
||||||
stopped = latch.await(60, TimeUnit.SECONDS);
|
stopped = latch.await(60, TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
recording.setStatus(Recording.Status.failed);
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
|
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
|
||||||
"The recording completion process has been unexpectedly interrupted"));
|
"The recording completion process has been unexpectedly interrupted"));
|
||||||
}
|
}
|
||||||
if (!stopped) {
|
if (!stopped) {
|
||||||
recording.setStatus(Recording.Status.failed);
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
|
failRecordingCompletion(containerId, new OpenViduException(Code.RECORDING_COMPLETION_ERROR_CODE,
|
||||||
"The recording completion process couldn't finish in 60 seconds"));
|
"The recording completion process couldn't finish in 60 seconds"));
|
||||||
}
|
}
|
||||||
|
@ -250,28 +221,25 @@ public class ComposedRecordingService {
|
||||||
// Update recording attributes reading from video report file
|
// Update recording attributes reading from video report file
|
||||||
try {
|
try {
|
||||||
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
|
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
|
||||||
this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + ".info");
|
this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + recordingId + ".info");
|
||||||
|
|
||||||
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
|
||||||
recording.setStatus(Recording.Status.available);
|
|
||||||
} else {
|
|
||||||
recording.setStatus(Recording.Status.stopped);
|
|
||||||
}
|
|
||||||
recording.setDuration(infoUtils.getDurationInSeconds());
|
recording.setDuration(infoUtils.getDurationInSeconds());
|
||||||
recording.setSize(infoUtils.getSizeInBytes());
|
recording.setSize(infoUtils.getSizeInBytes());
|
||||||
recording.setHasAudio(infoUtils.hasAudio());
|
recording.setHasAudio(infoUtils.hasAudio());
|
||||||
recording.setHasVideo(infoUtils.hasVideo());
|
recording.setHasVideo(infoUtils.hasVideo());
|
||||||
|
|
||||||
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
infoUtils.deleteFilePath();
|
||||||
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
|
|
||||||
}
|
recording = this.recordingManager.updateRecordingUrl(recording);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
||||||
"There was an error generating the metadata report file for the recording");
|
"There was an error generating the metadata report file for the recording");
|
||||||
}
|
}
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
this.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return recording;
|
return recording;
|
||||||
|
@ -306,19 +274,7 @@ public class ComposedRecordingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sessionIsBeingRecorded(String sessionId) {
|
private String runRecordingContainer(List<String> envs, String containerName) throws Exception {
|
||||||
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");
|
Volume volume1 = new Volume("/recordings");
|
||||||
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
|
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
|
||||||
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
||||||
|
@ -334,7 +290,10 @@ public class ComposedRecordingService {
|
||||||
log.error(
|
log.error(
|
||||||
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
|
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
|
||||||
containerName);
|
containerName);
|
||||||
return null;
|
throw e;
|
||||||
|
} catch (NotFoundException e) {
|
||||||
|
log.error("Docker image {} couldn't be found in docker host", IMAGE_NAME + ":" + IMAGE_TAG);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,159 +317,13 @@ public class ComposedRecordingService {
|
||||||
return imageExists;
|
return imageExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Recording> getAllRecordings() {
|
private void waitForVideoFileNotEmpty(Recording recording) {
|
||||||
return this.getAllRecordingsFromHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Recording> getStartingRecordings() {
|
|
||||||
return this.startingRecordings.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Recording> getStartedRecordings() {
|
|
||||||
return this.startedRecordings.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<Recording> getFinishedRecordings() {
|
|
||||||
return this.getAllRecordingsFromHost().stream()
|
|
||||||
.filter(recording -> (recording.getStatus().equals(Recording.Status.stopped)
|
|
||||||
|| recording.getStatus().equals(Recording.Status.available)))
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public File initRecordingPath() throws OpenViduException {
|
|
||||||
try {
|
|
||||||
Path path = Files.createDirectories(Paths.get(this.openviduConfig.getOpenViduRecordingPath()));
|
|
||||||
|
|
||||||
if (!Files.isWritable(path)) {
|
|
||||||
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
|
|
||||||
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath()
|
|
||||||
+ "' is not valid. Reason: OpenVidu Server process needs write permissions");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("Recording path: {}", this.openviduConfig.getOpenViduRecordingPath());
|
|
||||||
return path.toFile();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
|
|
||||||
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath() + "' is not valid. Reason: "
|
|
||||||
+ e.getClass().getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recording getRecordingFromHost(String recordingId) {
|
|
||||||
log.info(this.openviduConfig.getOpenViduRecordingPath() + RECORDING_ENTITY_FILE + recordingId);
|
|
||||||
File file = new File(this.openviduConfig.getOpenViduRecordingPath() + RECORDING_ENTITY_FILE + recordingId);
|
|
||||||
log.info("File exists: " + file.exists());
|
|
||||||
return this.getRecordingFromEntityFile(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<Recording> getAllRecordingsFromHost() {
|
|
||||||
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
|
||||||
File[] files = folder.listFiles();
|
|
||||||
|
|
||||||
if (files == null) {
|
|
||||||
files = initRecordingPath().listFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Recording> recordingEntities = new HashSet<>();
|
|
||||||
for (int i = 0; i < files.length; i++) {
|
|
||||||
Recording recording = this.getRecordingFromEntityFile(files[i]);
|
|
||||||
if (recording != null) {
|
|
||||||
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
|
||||||
if (Recording.Status.stopped.equals(recording.getStatus())) {
|
|
||||||
recording.setStatus(Recording.Status.available);
|
|
||||||
recording.setUrl(
|
|
||||||
this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recordingEntities.add(recording);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return recordingEntities;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> getRecordingIdsFromHost() {
|
|
||||||
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
|
||||||
File[] files = folder.listFiles();
|
|
||||||
|
|
||||||
if (files == null) {
|
|
||||||
files = initRecordingPath().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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {
|
|
||||||
|
|
||||||
if (!force && (this.startedRecordings.containsKey(recordingId)
|
|
||||||
|| this.startingRecordings.containsKey(recordingId))) {
|
|
||||||
// Cannot delete an active recording
|
|
||||||
return HttpStatus.CONFLICT;
|
|
||||||
}
|
|
||||||
|
|
||||||
Recording recording = getRecordingFromHost(recordingId);
|
|
||||||
if (recording == null) {
|
|
||||||
return HttpStatus.NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = getRecordingFromHost(recordingId).getName();
|
|
||||||
|
|
||||||
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
|
||||||
File[] files = folder.listFiles();
|
|
||||||
for (int i = 0; i < files.length; i++) {
|
|
||||||
if (files[i].isFile() && isFileFromRecording(files[i], recordingId, name)) {
|
|
||||||
files[i].delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return HttpStatus.NO_CONTENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recording getRecordingFromEntityFile(File file) {
|
|
||||||
if (file.isFile() && file.getName().startsWith(RECORDING_ENTITY_FILE)) {
|
|
||||||
JsonObject json = null;
|
|
||||||
try {
|
|
||||||
json = new JsonParser().parse(new FileReader(file)).getAsJsonObject();
|
|
||||||
} catch (IOException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Recording(json);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFileFromRecording(File file, String recordingId, String recordingName) {
|
|
||||||
return (((recordingId + ".info").equals(file.getName()))
|
|
||||||
|| ((RECORDING_ENTITY_FILE + recordingId).equals(file.getName()))
|
|
||||||
|| (recordingName + ".mp4").equals(file.getName()) || (recordingId + ".jpg").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 videoName) {
|
|
||||||
boolean isPresent = false;
|
boolean isPresent = false;
|
||||||
while (!isPresent) {
|
while (!isPresent) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(150);
|
Thread.sleep(150);
|
||||||
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + videoName + ".mp4");
|
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
|
||||||
|
+ recording.getName() + ".mp4");
|
||||||
isPresent = ((f.isFile()) && (f.length() > 0));
|
isPresent = ((f.isFile()) && (f.length() > 0));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
@ -547,23 +360,14 @@ public class ComposedRecordingService {
|
||||||
return finalUrl;
|
return finalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecordingVersion(String version) {
|
private String cleanRecordingMapsAndReturnContainerId(Recording recording) {
|
||||||
|
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
|
||||||
|
this.recordingManager.startedRecordings.remove(recording.getId());
|
||||||
|
return this.sessionsContainers.remove(recording.getSessionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecordingContainerVersion(String version) {
|
||||||
this.IMAGE_TAG = version;
|
this.IMAGE_TAG = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initAutomaticRecordingStopThread(String sessionId) {
|
|
||||||
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
|
|
||||||
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
|
|
||||||
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
|
|
||||||
this.stopRecording(null, recordingId, "lastParticipantLeft");
|
|
||||||
this.automaticRecordingStopThreads.remove(sessionId);
|
|
||||||
}, 2, TimeUnit.MINUTES);
|
|
||||||
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean abortAutomaticRecordingStopThread(String sessionId) {
|
|
||||||
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(sessionId);
|
|
||||||
return future.cancel(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,441 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import javax.ws.rs.ProcessingException;
|
||||||
|
|
||||||
|
import org.kurento.client.MediaProfileSpecType;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import io.openvidu.client.OpenViduException;
|
||||||
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
|
import io.openvidu.server.core.Participant;
|
||||||
|
import io.openvidu.server.core.Session;
|
||||||
|
import io.openvidu.server.core.SessionEventsHandler;
|
||||||
|
import io.openvidu.server.recording.Recording;
|
||||||
|
|
||||||
|
public class RecordingManager {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RecordingManager.class);
|
||||||
|
|
||||||
|
RecordingService recordingService;
|
||||||
|
private ComposedRecordingService composedRecordingService;
|
||||||
|
private SingleStreamRecordingService singleStreamRecordingService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected SessionEventsHandler sessionHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
protected OpenviduConfig openviduConfig;
|
||||||
|
|
||||||
|
protected Map<String, Recording> startingRecordings = new ConcurrentHashMap<>();
|
||||||
|
protected Map<String, Recording> startedRecordings = new ConcurrentHashMap<>();
|
||||||
|
protected Map<String, Recording> sessionsRecordings = new ConcurrentHashMap<>();
|
||||||
|
private final Map<String, ScheduledFuture<?>> automaticRecordingStopThreads = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private ScheduledThreadPoolExecutor automaticRecordingStopExecutor = new ScheduledThreadPoolExecutor(
|
||||||
|
Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
|
static final String RECORDING_ENTITY_FILE = ".recording.";
|
||||||
|
|
||||||
|
private static final List<String> LAST_PARTICIPANT_LEFT_REASONS = Arrays.asList(
|
||||||
|
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
|
||||||
|
|
||||||
|
public SessionEventsHandler getSessionEventsHandler() {
|
||||||
|
return this.sessionHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeRecordingManager() {
|
||||||
|
|
||||||
|
this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
|
||||||
|
this.singleStreamRecordingService = new SingleStreamRecordingService(this, openviduConfig);
|
||||||
|
|
||||||
|
ComposedRecordingService recServiceAux = this.composedRecordingService;
|
||||||
|
recServiceAux.setRecordingContainerVersion(openviduConfig.getOpenViduRecordingVersion());
|
||||||
|
|
||||||
|
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
|
||||||
|
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
|
||||||
|
|
||||||
|
boolean imageExists = false;
|
||||||
|
try {
|
||||||
|
imageExists = recServiceAux.recordingImageExistsLocally();
|
||||||
|
} catch (ProcessingException exception) {
|
||||||
|
String message = "Exception connecting to Docker daemon: ";
|
||||||
|
if ("docker".equals(openviduConfig.getSpringProfile())) {
|
||||||
|
final String NEW_LINE = System.getProperty("line.separator");
|
||||||
|
message += "make sure you include the following flags in your \"docker run\" command:" + NEW_LINE
|
||||||
|
+ " -e openvidu.recording.path=/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE
|
||||||
|
+ " -e MY_UID=$(id -u $USER)" + NEW_LINE + " -v /var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
+ NEW_LINE + " -v /YOUR/PATH/TO/VIDEO/FILES:/YOUR/PATH/TO/VIDEO/FILES" + NEW_LINE;
|
||||||
|
} else {
|
||||||
|
message += "you need Docker installed in this machine to enable OpenVidu recording service";
|
||||||
|
}
|
||||||
|
log.error(message);
|
||||||
|
throw new RuntimeException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageExists) {
|
||||||
|
log.info("Docker image already exists locally");
|
||||||
|
} else {
|
||||||
|
Thread t = new Thread(() -> {
|
||||||
|
boolean keep = true;
|
||||||
|
log.info("Downloading ");
|
||||||
|
while (keep) {
|
||||||
|
System.out.print(".");
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
keep = false;
|
||||||
|
log.info("\nDownload complete");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
t.start();
|
||||||
|
recServiceAux.downloadRecordingImage();
|
||||||
|
t.interrupt();
|
||||||
|
try {
|
||||||
|
t.join();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
log.info("Docker image available");
|
||||||
|
}
|
||||||
|
this.initRecordingPath();
|
||||||
|
|
||||||
|
this.recordingService = recServiceAux;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||||
|
Recording recording = null;
|
||||||
|
try {
|
||||||
|
switch (properties.outputMode()) {
|
||||||
|
case COMPOSED:
|
||||||
|
recording = this.composedRecordingService.startRecording(session, properties);
|
||||||
|
break;
|
||||||
|
case INDIVIDUAL:
|
||||||
|
recording = this.singleStreamRecordingService.startRecording(session, properties);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (session.getActivePublishers() == 0) {
|
||||||
|
// Init automatic recording stop if there are now publishers when starting
|
||||||
|
// recording
|
||||||
|
this.initAutomaticRecordingStopThread(session.getSessionId());
|
||||||
|
}
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording stopRecording(Session session, String recordingId, String reason) {
|
||||||
|
Recording recording;
|
||||||
|
if (session == null) {
|
||||||
|
recording = this.startedRecordings.remove(recordingId);
|
||||||
|
} else {
|
||||||
|
recording = this.sessionsRecordings.remove(session.getSessionId());
|
||||||
|
}
|
||||||
|
switch (recording.getOutputMode()) {
|
||||||
|
case COMPOSED:
|
||||||
|
recording = this.composedRecordingService.stopRecording(session, recording, reason);
|
||||||
|
break;
|
||||||
|
case INDIVIDUAL:
|
||||||
|
recording = this.singleStreamRecordingService.stopRecording(session, recording, reason);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
|
||||||
|
Participant participant) {
|
||||||
|
Recording recording = this.sessionsRecordings.get(session.getSessionId());
|
||||||
|
if (recording == null) {
|
||||||
|
log.error("Cannot start recording of new stream {}. Session {} is not being recorded",
|
||||||
|
participant.getPublisherStreamId(), session.getSessionId());
|
||||||
|
}
|
||||||
|
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
||||||
|
final CountDownLatch startedCountDown = new CountDownLatch(1);
|
||||||
|
this.singleStreamRecordingService.startOneIndividualStreamRecording(session, recordingId, profile,
|
||||||
|
participant, startedCountDown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopOneIndividualStreamRecording(String sessionId, String streamId) {
|
||||||
|
Recording recording = this.sessionsRecordings.get(sessionId);
|
||||||
|
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
||||||
|
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
|
||||||
|
this.singleStreamRecordingService.stopOneIndividualStreamRecording(sessionId, streamId, stoppedCountDown);
|
||||||
|
try {
|
||||||
|
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
|
||||||
|
sessionId);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Exception while waiting for state change", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sessionIsBeingRecorded(String sessionId) {
|
||||||
|
return (this.sessionsRecordings.get(sessionId) != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sessionIsBeingRecordedIndividual(String sessionId) {
|
||||||
|
Recording rec = this.sessionsRecordings.get(sessionId);
|
||||||
|
return (rec != null && io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(rec.getOutputMode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sessionIsBeingRecordedComposed(String sessionId) {
|
||||||
|
Recording rec = this.sessionsRecordings.get(sessionId);
|
||||||
|
return (rec != null && io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(rec.getOutputMode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording getStartedRecording(String recordingId) {
|
||||||
|
return this.startedRecordings.get(recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording getStartingRecording(String recordingId) {
|
||||||
|
return this.startingRecordings.get(recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Recording> getFinishedRecordings() {
|
||||||
|
return this.getAllRecordingsFromHost().stream()
|
||||||
|
.filter(recording -> (recording.getStatus().equals(io.openvidu.java.client.Recording.Status.stopped)
|
||||||
|
|| recording.getStatus().equals(io.openvidu.java.client.Recording.Status.available)))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording getRecording(String recordingId) {
|
||||||
|
return this.getRecordingFromHost(recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Collection<Recording> getAllRecordings() {
|
||||||
|
return this.getAllRecordingsFromHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFreeRecordingId(String sessionId, String shortSessionId) {
|
||||||
|
Set<String> recordingIds = this.getRecordingIdsFromHost();
|
||||||
|
String recordingId = shortSessionId;
|
||||||
|
boolean isPresent = recordingIds.contains(recordingId);
|
||||||
|
int i = 1;
|
||||||
|
|
||||||
|
while (isPresent) {
|
||||||
|
recordingId = shortSessionId + "-" + i;
|
||||||
|
i++;
|
||||||
|
isPresent = recordingIds.contains(recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recordingId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpStatus deleteRecordingFromHost(String recordingId, boolean force) {
|
||||||
|
|
||||||
|
if (!force && (this.startedRecordings.containsKey(recordingId)
|
||||||
|
|| this.startingRecordings.containsKey(recordingId))) {
|
||||||
|
// Cannot delete an active recording
|
||||||
|
return HttpStatus.CONFLICT;
|
||||||
|
}
|
||||||
|
|
||||||
|
Recording recording = getRecordingFromHost(recordingId);
|
||||||
|
if (recording == null) {
|
||||||
|
return HttpStatus.NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
if (files[i].isDirectory() && files[i].getName().equals(recordingId)) {
|
||||||
|
// Correct folder. Delete all content and the folder itself
|
||||||
|
File[] allContents = files[i].listFiles();
|
||||||
|
if (allContents != null) {
|
||||||
|
for (File file : allContents) {
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files[i].delete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HttpStatus.NO_CONTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording getRecordingFromEntityFile(File file) {
|
||||||
|
if (file.isFile() && file.getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
|
||||||
|
JsonObject json = null;
|
||||||
|
try {
|
||||||
|
json = new JsonParser().parse(new FileReader(file)).getAsJsonObject();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Recording(json);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initAutomaticRecordingStopThread(String sessionId) {
|
||||||
|
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
|
||||||
|
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
|
||||||
|
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
|
||||||
|
this.stopRecording(null, recordingId, "lastParticipantLeft");
|
||||||
|
this.automaticRecordingStopThreads.remove(sessionId);
|
||||||
|
}, 2, TimeUnit.MINUTES);
|
||||||
|
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean abortAutomaticRecordingStopThread(String sessionId) {
|
||||||
|
ScheduledFuture<?> future = this.automaticRecordingStopThreads.remove(sessionId);
|
||||||
|
if (future != null) {
|
||||||
|
return future.cancel(false);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Recording updateRecordingUrl(Recording recording) {
|
||||||
|
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
||||||
|
if (io.openvidu.java.client.Recording.Status.stopped.equals(recording.getStatus())) {
|
||||||
|
|
||||||
|
String extension;
|
||||||
|
switch (recording.getOutputMode()) {
|
||||||
|
case COMPOSED:
|
||||||
|
extension = "mp4";
|
||||||
|
break;
|
||||||
|
case INDIVIDUAL:
|
||||||
|
extension = "zip";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
extension = "mp4";
|
||||||
|
}
|
||||||
|
|
||||||
|
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getId() + "/"
|
||||||
|
+ recording.getName() + "." + extension);
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.available);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recording getRecordingFromHost(String recordingId) {
|
||||||
|
log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
||||||
|
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
|
||||||
|
File file = new File(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
||||||
|
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
|
||||||
|
log.info("File exists: " + file.exists());
|
||||||
|
Recording recording = this.getRecordingFromEntityFile(file);
|
||||||
|
if (recording != null) {
|
||||||
|
this.updateRecordingUrl(recording);
|
||||||
|
}
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Recording> getAllRecordingsFromHost() {
|
||||||
|
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
|
||||||
|
Set<Recording> recordingEntities = new HashSet<>();
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
if (files[i].isDirectory()) {
|
||||||
|
File[] innerFiles = files[i].listFiles();
|
||||||
|
for (int j = 0; j < innerFiles.length; j++) {
|
||||||
|
Recording recording = this.getRecordingFromEntityFile(innerFiles[j]);
|
||||||
|
if (recording != null) {
|
||||||
|
this.updateRecordingUrl(recording);
|
||||||
|
recordingEntities.add(recording);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return recordingEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<String> getRecordingIdsFromHost() {
|
||||||
|
File folder = new File(this.openviduConfig.getOpenViduRecordingPath());
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
|
||||||
|
Set<String> fileNamesNoExtension = new HashSet<>();
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
if (files[i].isDirectory()) {
|
||||||
|
File[] innerFiles = files[i].listFiles();
|
||||||
|
for (int j = 0; j < innerFiles.length; j++) {
|
||||||
|
if (innerFiles[j].isFile()
|
||||||
|
&& innerFiles[j].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
|
||||||
|
fileNamesNoExtension
|
||||||
|
.add(innerFiles[j].getName().replaceFirst(RecordingManager.RECORDING_ENTITY_FILE, ""));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fileNamesNoExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File initRecordingPath() throws OpenViduException {
|
||||||
|
try {
|
||||||
|
Path path = Files.createDirectories(Paths.get(this.openviduConfig.getOpenViduRecordingPath()));
|
||||||
|
|
||||||
|
if (!Files.isWritable(path)) {
|
||||||
|
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
|
||||||
|
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath()
|
||||||
|
+ "' is not valid. Reason: OpenVidu Server process needs write permissions");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Recording path: {}", this.openviduConfig.getOpenViduRecordingPath());
|
||||||
|
return path.toFile();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID,
|
||||||
|
"The recording path '" + this.openviduConfig.getOpenViduRecordingPath() + "' is not valid. Reason: "
|
||||||
|
+ e.getClass().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String finalReason(String reason) {
|
||||||
|
if (RecordingManager.LAST_PARTICIPANT_LEFT_REASONS.contains(reason)) {
|
||||||
|
return "lastParticipantLeft";
|
||||||
|
} else {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
|
import io.openvidu.client.OpenViduException;
|
||||||
|
import io.openvidu.java.client.RecordingLayout;
|
||||||
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
|
import io.openvidu.server.core.Session;
|
||||||
|
import io.openvidu.server.recording.Recording;
|
||||||
|
|
||||||
|
public abstract class RecordingService {
|
||||||
|
|
||||||
|
protected OpenviduConfig openviduConfig;
|
||||||
|
protected RecordingManager recordingManager;
|
||||||
|
|
||||||
|
RecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
|
this.recordingManager = recordingManager;
|
||||||
|
this.openviduConfig = openviduConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException;
|
||||||
|
|
||||||
|
public abstract Recording stopRecording(Session session, Recording recording, String reason);
|
||||||
|
|
||||||
|
protected RecordingProperties setFinalRecordingName(Session session, RecordingProperties properties) {
|
||||||
|
// TODO Auto-generated method stub
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PropertiesRecordingId setFinalRecordingNameAndGetFreeRecordingId(Session session,
|
||||||
|
RecordingProperties properties) {
|
||||||
|
String recordingId = this.recordingManager.getFreeRecordingId(session.getSessionId(),
|
||||||
|
this.getShortSessionId(session));
|
||||||
|
if (properties.name() == null || properties.name().isEmpty()) {
|
||||||
|
// No name provided for the recording file. Use recordingId
|
||||||
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
|
||||||
|
.outputMode(properties.outputMode());
|
||||||
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(properties.outputMode())) {
|
||||||
|
builder.recordingLayout(properties.recordingLayout());
|
||||||
|
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
|
||||||
|
builder.customLayout(properties.customLayout());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
properties = builder.build();
|
||||||
|
}
|
||||||
|
return new PropertiesRecordingId(properties, recordingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getShortSessionId(Session session) {
|
||||||
|
return session.getSessionId().substring(session.getSessionId().lastIndexOf('/') + 1,
|
||||||
|
session.getSessionId().length());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple wrapper for returning update RecordingProperties and a free
|
||||||
|
* recordingId when starting a new recording
|
||||||
|
*/
|
||||||
|
protected class PropertiesRecordingId {
|
||||||
|
|
||||||
|
RecordingProperties properties;
|
||||||
|
String recordingId;
|
||||||
|
|
||||||
|
PropertiesRecordingId(RecordingProperties properties, String recordingId) {
|
||||||
|
this.properties = properties;
|
||||||
|
this.recordingId = recordingId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,456 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.kurento.client.ErrorEvent;
|
||||||
|
import org.kurento.client.EventListener;
|
||||||
|
import org.kurento.client.MediaPipeline;
|
||||||
|
import org.kurento.client.MediaProfileSpecType;
|
||||||
|
import org.kurento.client.MediaType;
|
||||||
|
import org.kurento.client.RecorderEndpoint;
|
||||||
|
import org.kurento.client.RecordingEvent;
|
||||||
|
import org.kurento.client.StoppedEvent;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import io.openvidu.client.OpenViduException;
|
||||||
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
|
import io.openvidu.server.core.Participant;
|
||||||
|
import io.openvidu.server.core.Session;
|
||||||
|
import io.openvidu.server.kurento.core.KurentoParticipant;
|
||||||
|
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||||
|
import io.openvidu.server.recording.RecorderEndpointWrapper;
|
||||||
|
import io.openvidu.server.recording.Recording;
|
||||||
|
import io.openvidu.server.utils.CustomFileWriter;
|
||||||
|
|
||||||
|
public class SingleStreamRecordingService extends RecordingService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class);
|
||||||
|
|
||||||
|
private Map<String, Map<String, RecorderEndpointWrapper>> recorders = new ConcurrentHashMap<>();
|
||||||
|
private CustomFileWriter fileWriter = new CustomFileWriter();
|
||||||
|
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
|
||||||
|
|
||||||
|
public SingleStreamRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
|
super(recordingManager, openviduConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||||
|
|
||||||
|
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
||||||
|
properties);
|
||||||
|
properties = updatePropertiesAndRecordingId.properties;
|
||||||
|
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||||
|
|
||||||
|
recorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
|
||||||
|
|
||||||
|
final CountDownLatch recordingStartedCountdown = new CountDownLatch(session.getActivePublishers());
|
||||||
|
|
||||||
|
for (Participant p : session.getParticipants()) {
|
||||||
|
if (p.isStreaming()) {
|
||||||
|
|
||||||
|
MediaProfileSpecType profile = null;
|
||||||
|
try {
|
||||||
|
profile = generateMediaProfile(properties, p);
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
log.error(
|
||||||
|
"Cannot start single stream recorder for stream {} in session {}: {}. Skipping to next stream being published",
|
||||||
|
p.getPublisherStreamId(), session.getSessionId(), e.getMessage());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.startOneIndividualStreamRecording(session, recordingId, profile, p, recordingStartedCountdown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
|
||||||
|
|
||||||
|
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!recordingStartedCountdown.await(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId());
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
|
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||||
|
this.stopRecording(session, recording, null);
|
||||||
|
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||||
|
"Couldn't initialize some RecorderEndpoint");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
|
log.error("Exception while waiting for state change", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (session.getActivePublishers() == 0) {
|
||||||
|
// Recording started for a session with some user connected but no publishers
|
||||||
|
// Must create recording root folder for storing metadata archive
|
||||||
|
this.fileWriter.createFolder(this.openviduConfig.getOpenViduRecordingPath() + recording.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generateRecordingMetadataFile(recording);
|
||||||
|
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
||||||
|
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
|
||||||
|
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||||
|
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
||||||
|
this.recordingManager.getSessionEventsHandler().sendRecordingStartedNotification(session, recording);
|
||||||
|
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Recording stopRecording(Session session, Recording recording, String reason) {
|
||||||
|
|
||||||
|
final int numberOfActiveRecorders = recorders.get(recording.getSessionId()).size();
|
||||||
|
final CountDownLatch stoppedCountDown = new CountDownLatch(numberOfActiveRecorders);
|
||||||
|
|
||||||
|
for (RecorderEndpointWrapper wrapper : recorders.get(recording.getSessionId()).values()) {
|
||||||
|
this.stopOneIndividualStreamRecording(recording.getSessionId(), wrapper.getStreamId(), stoppedCountDown);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("Error waiting for some recorder endpoint to stop in session {}", recording.getSessionId());
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Exception while waiting for state change", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
|
||||||
|
this.recordingManager.startedRecordings.remove(recording.getId());
|
||||||
|
this.recorders.remove(recording.getSessionId());
|
||||||
|
|
||||||
|
recording = this.sealMetadataFiles(recording);
|
||||||
|
recording = this.recordingManager.updateRecordingUrl(recording);
|
||||||
|
|
||||||
|
if (reason != null && session != null) {
|
||||||
|
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
|
||||||
|
Participant participant, CountDownLatch globalStartLatch) {
|
||||||
|
log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(),
|
||||||
|
session.getSessionId());
|
||||||
|
|
||||||
|
if (recordingId == null) {
|
||||||
|
// Stream is being recorded because is a new publisher in an ongoing recorded
|
||||||
|
// session. If recordingId is defined is because Stream is being recorded from
|
||||||
|
// "startRecording" method
|
||||||
|
Recording recording = this.recordingManager.sessionsRecordings.get(session.getSessionId());
|
||||||
|
recordingId = recording.getId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile = generateMediaProfile(recording.getRecordingProperties(), participant);
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
log.error("Cannot start single stream recorder for stream {} in session {}: {}",
|
||||||
|
participant.getPublisherStreamId(), session.getSessionId(), e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KurentoParticipant kurentoParticipant = (KurentoParticipant) participant;
|
||||||
|
MediaPipeline pipeline = kurentoParticipant.getPublisher().getPipeline();
|
||||||
|
|
||||||
|
RecorderEndpoint recorder = new RecorderEndpoint.Builder(pipeline,
|
||||||
|
"file://" + this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
||||||
|
+ participant.getPublisherStreamId() + ".webm").withMediaProfile(profile).build();
|
||||||
|
|
||||||
|
recorder.addRecordingListener(new EventListener<RecordingEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(RecordingEvent event) {
|
||||||
|
recorders.get(session.getSessionId()).get(participant.getPublisherStreamId())
|
||||||
|
.setStartTime(System.currentTimeMillis());
|
||||||
|
log.info("Recording started event for stream {}", participant.getPublisherStreamId());
|
||||||
|
globalStartLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recorder.addErrorListener(new EventListener<ErrorEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(ErrorEvent event) {
|
||||||
|
log.error(event.getErrorCode() + " " + event.getDescription());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connectAccordingToProfile(kurentoParticipant.getPublisher(), recorder, profile);
|
||||||
|
|
||||||
|
RecorderEndpointWrapper wrapper = new RecorderEndpointWrapper(recorder, participant.getParticipantPublicId(),
|
||||||
|
recordingId, participant.getPublisherStreamId(), participant.getClientMetadata(),
|
||||||
|
participant.getServerMetadata(), kurentoParticipant.getPublisher().getMediaOptions().hasAudio(),
|
||||||
|
kurentoParticipant.getPublisher().getMediaOptions().hasVideo(),
|
||||||
|
kurentoParticipant.getPublisher().getMediaOptions().getTypeOfVideo());
|
||||||
|
|
||||||
|
recorders.get(session.getSessionId()).put(participant.getPublisherStreamId(), wrapper);
|
||||||
|
wrapper.getRecorder().record();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopOneIndividualStreamRecording(String sessionId, String streamId, CountDownLatch globalStopLatch) {
|
||||||
|
log.info("Stopping single stream recorder for stream {} in session {}", streamId, sessionId);
|
||||||
|
RecorderEndpointWrapper wrapper = this.recorders.get(sessionId).remove(streamId);
|
||||||
|
if (wrapper != null) {
|
||||||
|
wrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(StoppedEvent event) {
|
||||||
|
wrapper.setEndTime(System.currentTimeMillis());
|
||||||
|
generateIndividualMetadataFile(wrapper);
|
||||||
|
log.info("Recording stopped event for stream {}", streamId);
|
||||||
|
globalStopLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrapper.getRecorder().stop();
|
||||||
|
} else {
|
||||||
|
log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MediaProfileSpecType generateMediaProfile(RecordingProperties properties, Participant participant)
|
||||||
|
throws OpenViduException {
|
||||||
|
|
||||||
|
KurentoParticipant kParticipant = (KurentoParticipant) participant;
|
||||||
|
MediaProfileSpecType profile = null;
|
||||||
|
|
||||||
|
boolean streamHasAudio = kParticipant.getPublisher().getMediaOptions().hasAudio();
|
||||||
|
boolean streamHasVideo = kParticipant.getPublisher().getMediaOptions().hasVideo();
|
||||||
|
boolean propertiesHasAudio = properties.hasAudio();
|
||||||
|
boolean propertiesHasVideo = properties.hasVideo();
|
||||||
|
|
||||||
|
if (streamHasAudio) {
|
||||||
|
if (streamHasVideo) {
|
||||||
|
// Stream has both audio and video tracks
|
||||||
|
|
||||||
|
if (propertiesHasAudio) {
|
||||||
|
if (propertiesHasVideo) {
|
||||||
|
profile = MediaProfileSpecType.WEBM;
|
||||||
|
} else {
|
||||||
|
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||||
|
}
|
||||||
|
} else if (propertiesHasVideo) {
|
||||||
|
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||||
|
} else {
|
||||||
|
// ERROR: RecordingProperties set to not record audio nor video
|
||||||
|
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
|
"RecordingProperties set to \"hasVideo(false)\" and \"hasAudio(false)\"");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stream has audio track only
|
||||||
|
|
||||||
|
if (propertiesHasAudio) {
|
||||||
|
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||||
|
} else {
|
||||||
|
// ERROR: RecordingProperties set to video only but there's no video track
|
||||||
|
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
|
"RecordingProperties set to \"hasAudio(false)\" but stream is audio-only");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (streamHasVideo) {
|
||||||
|
// Stream has video track only
|
||||||
|
|
||||||
|
if (propertiesHasVideo) {
|
||||||
|
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||||
|
} else {
|
||||||
|
// ERROR: RecordingProperties set to audio only but there's no audio track
|
||||||
|
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
|
"RecordingProperties set to \"hasVideo(false)\" but stream is video-only");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ERROR: Stream has no track at all
|
||||||
|
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
|
"Stream has no track at all. Cannot be recorded");
|
||||||
|
}
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectAccordingToProfile(PublisherEndpoint publisherEndpoint, RecorderEndpoint recorder,
|
||||||
|
MediaProfileSpecType profile) {
|
||||||
|
switch (profile) {
|
||||||
|
case WEBM:
|
||||||
|
publisherEndpoint.connect(recorder, MediaType.AUDIO);
|
||||||
|
publisherEndpoint.connect(recorder, MediaType.VIDEO);
|
||||||
|
break;
|
||||||
|
case WEBM_AUDIO_ONLY:
|
||||||
|
publisherEndpoint.connect(recorder, MediaType.AUDIO);
|
||||||
|
break;
|
||||||
|
case WEBM_VIDEO_ONLY:
|
||||||
|
publisherEndpoint.connect(recorder, MediaType.VIDEO);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("Unsupported profile when single stream recording: " + profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateRecordingMetadataFile(Recording recording) {
|
||||||
|
String filePath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
|
||||||
|
+ RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
|
||||||
|
String text = recording.toJson().toString();
|
||||||
|
this.fileWriter.createAndWriteFile(filePath, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateIndividualMetadataFile(RecorderEndpointWrapper wrapper) {
|
||||||
|
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/";
|
||||||
|
File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm");
|
||||||
|
wrapper.setSize(videoFile.length());
|
||||||
|
String metadataFilePath = filesPath + INDIVIDUAL_STREAM_METADATA_FILE + wrapper.getStreamId();
|
||||||
|
String metadataFileContent = wrapper.toJson().toString();
|
||||||
|
this.fileWriter.createAndWriteFile(metadataFilePath, metadataFileContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recording sealMetadataFiles(Recording recording) {
|
||||||
|
// Must update recording "status" (to stopped), "duration" (min startTime of all
|
||||||
|
// individual recordings) and "size" (sum of all individual recordings size)
|
||||||
|
|
||||||
|
String folderPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
|
||||||
|
|
||||||
|
String metadataFilePath = folderPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId();
|
||||||
|
String syncFilePath = folderPath + recording.getId() + ".json";
|
||||||
|
|
||||||
|
recording = this.recordingManager.getRecordingFromEntityFile(new File(metadataFilePath));
|
||||||
|
|
||||||
|
long minStartTime = Long.MAX_VALUE;
|
||||||
|
long maxEndTime = 0;
|
||||||
|
long accumulatedSize = 0;
|
||||||
|
|
||||||
|
File folder = new File(folderPath);
|
||||||
|
File[] files = folder.listFiles();
|
||||||
|
|
||||||
|
Reader reader = null;
|
||||||
|
Gson gson = new Gson();
|
||||||
|
|
||||||
|
// Sync metadata json object to store in "RECORDING_ID.json"
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("createdAt", recording.getCreatedAt());
|
||||||
|
json.addProperty("id", recording.getId());
|
||||||
|
json.addProperty("name", recording.getName());
|
||||||
|
json.addProperty("sessionId", recording.getSessionId());
|
||||||
|
JsonArray jsonArrayFiles = new JsonArray();
|
||||||
|
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
if (files[i].isFile() && files[i].getName().startsWith(INDIVIDUAL_STREAM_METADATA_FILE)) {
|
||||||
|
try {
|
||||||
|
reader = new FileReader(files[i].getAbsolutePath());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
log.error("Error reading file {}. Error: {}", files[i].getAbsolutePath(), e.getMessage());
|
||||||
|
}
|
||||||
|
RecorderEndpointWrapper wr = gson.fromJson(reader, RecorderEndpointWrapper.class);
|
||||||
|
minStartTime = Math.min(minStartTime, wr.getStartTime());
|
||||||
|
maxEndTime = Math.max(maxEndTime, wr.getEndTime());
|
||||||
|
accumulatedSize += wr.getSize();
|
||||||
|
|
||||||
|
JsonObject jsonFile = new JsonObject();
|
||||||
|
jsonFile.addProperty("connectionId", wr.getConnectionId());
|
||||||
|
jsonFile.addProperty("streamId", wr.getStreamId());
|
||||||
|
jsonFile.addProperty("size", wr.getSize());
|
||||||
|
jsonFile.addProperty("clientData", wr.getClientData());
|
||||||
|
jsonFile.addProperty("serverData", wr.getServerData());
|
||||||
|
jsonFile.addProperty("hasAudio", wr.hasAudio());
|
||||||
|
jsonFile.addProperty("hasVideo", wr.hasVideo());
|
||||||
|
if (wr.hasVideo()) {
|
||||||
|
jsonFile.addProperty("typeOfVideo", wr.getTypeOfVideo());
|
||||||
|
}
|
||||||
|
jsonFile.addProperty("startTimeOffset", wr.getStartTime() - recording.getCreatedAt());
|
||||||
|
jsonFile.addProperty("endTimeOffset", wr.getEndTime() - recording.getCreatedAt());
|
||||||
|
|
||||||
|
jsonArrayFiles.add(jsonFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.add("files", jsonArrayFiles);
|
||||||
|
|
||||||
|
long duration = (maxEndTime - minStartTime) / 1000;
|
||||||
|
|
||||||
|
recording.setSize(accumulatedSize); // Size in bytes
|
||||||
|
recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
|
||||||
|
|
||||||
|
this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString());
|
||||||
|
this.fileWriter.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json));
|
||||||
|
this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip");
|
||||||
|
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void generateZipFileAndCleanFolder(String folder, String fileName) {
|
||||||
|
FileOutputStream fos = null;
|
||||||
|
ZipOutputStream zipOut = null;
|
||||||
|
|
||||||
|
final File[] files = new File(folder).listFiles();
|
||||||
|
|
||||||
|
try {
|
||||||
|
fos = new FileOutputStream(folder + fileName);
|
||||||
|
zipOut = new ZipOutputStream(fos);
|
||||||
|
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
String fileExtension = FilenameUtils.getExtension(files[i].getName());
|
||||||
|
|
||||||
|
if (files[i].isFile() && (fileExtension.equals("json") || fileExtension.equals("webm"))) {
|
||||||
|
|
||||||
|
// Zip video files and json sync metadata file
|
||||||
|
FileInputStream fis = new FileInputStream(files[i]);
|
||||||
|
ZipEntry zipEntry = new ZipEntry(files[i].getName());
|
||||||
|
zipOut.putNextEntry(zipEntry);
|
||||||
|
byte[] bytes = new byte[1024];
|
||||||
|
int length;
|
||||||
|
while ((length = fis.read(bytes)) >= 0) {
|
||||||
|
zipOut.write(bytes, 0, length);
|
||||||
|
}
|
||||||
|
fis.close();
|
||||||
|
|
||||||
|
}
|
||||||
|
if (!files[i].getName().startsWith(RecordingManager.RECORDING_ENTITY_FILE)) {
|
||||||
|
// Clean inspected file if it is not
|
||||||
|
files[i].delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error generating ZIP file {}. Error: {}", folder + fileName, e.getMessage());
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
zipOut.close();
|
||||||
|
fos.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Error closing FileOutputStream or ZipOutputStream. Error: {}", e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,7 +15,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.openvidu.server.recording;
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
import java.io.Closeable;
|
import java.io.Closeable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
|
@ -19,7 +19,6 @@ package io.openvidu.server.rest;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -52,8 +51,8 @@ import io.openvidu.server.core.ParticipantRole;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.core.SessionManager;
|
import io.openvidu.server.core.SessionManager;
|
||||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||||
import io.openvidu.server.recording.ComposedRecordingService;
|
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -68,7 +67,7 @@ public class SessionRestController {
|
||||||
private SessionManager sessionManager;
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ComposedRecordingService recordingService;
|
private RecordingManager recordingManager;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private OpenviduConfig openviduConfig;
|
private OpenviduConfig openviduConfig;
|
||||||
|
@ -148,7 +147,7 @@ public class SessionRestController {
|
||||||
Session session = this.sessionManager.getSession(sessionId);
|
Session session = this.sessionManager.getSession(sessionId);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
JsonObject response = (webRtcStats == true) ? session.withStatsToJson() : session.toJson();
|
JsonObject response = (webRtcStats == true) ? session.withStatsToJson() : session.toJson();
|
||||||
response.addProperty("recording", this.recordingService.sessionIsBeingRecorded(sessionId));
|
response.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(sessionId));
|
||||||
return new ResponseEntity<>(response.toString(), getResponseHeaders(), HttpStatus.OK);
|
return new ResponseEntity<>(response.toString(), getResponseHeaders(), HttpStatus.OK);
|
||||||
} else {
|
} else {
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
@ -163,7 +162,7 @@ public class SessionRestController {
|
||||||
JsonArray jsonArray = new JsonArray();
|
JsonArray jsonArray = new JsonArray();
|
||||||
sessions.forEach(s -> {
|
sessions.forEach(s -> {
|
||||||
JsonObject sessionJson = (webRtcStats == true) ? s.withStatsToJson() : s.toJson();
|
JsonObject sessionJson = (webRtcStats == true) ? s.withStatsToJson() : s.toJson();
|
||||||
sessionJson.addProperty("recording", this.recordingService.sessionIsBeingRecorded(s.getSessionId()));
|
sessionJson.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(s.getSessionId()));
|
||||||
jsonArray.add(sessionJson);
|
jsonArray.add(sessionJson);
|
||||||
});
|
});
|
||||||
json.addProperty("numberOfElements", sessions.size());
|
json.addProperty("numberOfElements", sessions.size());
|
||||||
|
@ -298,6 +297,7 @@ public class SessionRestController {
|
||||||
|
|
||||||
String sessionId = (String) params.get("session");
|
String sessionId = (String) params.get("session");
|
||||||
String name = (String) params.get("name");
|
String name = (String) params.get("name");
|
||||||
|
String outputModeString = (String) params.get("outputMode");
|
||||||
String recordingLayoutString = (String) params.get("recordingLayout");
|
String recordingLayoutString = (String) params.get("recordingLayout");
|
||||||
String customLayout = (String) params.get("customLayout");
|
String customLayout = (String) params.get("customLayout");
|
||||||
|
|
||||||
|
@ -322,11 +322,20 @@ public class SessionRestController {
|
||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
if (!(session.getSessionProperties().mediaMode().equals(MediaMode.ROUTED))
|
if (!(session.getSessionProperties().mediaMode().equals(MediaMode.ROUTED))
|
||||||
|| this.recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
|| this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
// Session is not in ROUTED MediMode or it is already being recorded
|
// Session is not in ROUTED MediMode or it is already being recorded
|
||||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
io.openvidu.java.client.Recording.OutputMode outputMode;
|
||||||
|
try {
|
||||||
|
outputMode = io.openvidu.java.client.Recording.OutputMode.valueOf(outputModeString);
|
||||||
|
} catch (Exception e) {
|
||||||
|
outputMode = io.openvidu.java.client.Recording.OutputMode.COMPOSED;
|
||||||
|
}
|
||||||
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(name).outputMode(outputMode);
|
||||||
|
|
||||||
|
if (outputMode.equals(io.openvidu.java.client.Recording.OutputMode.COMPOSED)) {
|
||||||
RecordingLayout recordingLayout;
|
RecordingLayout recordingLayout;
|
||||||
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
|
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
|
||||||
// "recordingLayout" parameter not defined. Use global layout from
|
// "recordingLayout" parameter not defined. Use global layout from
|
||||||
|
@ -337,13 +346,25 @@ public class SessionRestController {
|
||||||
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.recordingLayout(recordingLayout);
|
||||||
|
|
||||||
|
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
|
||||||
customLayout = (customLayout == null || customLayout.isEmpty())
|
customLayout = (customLayout == null || customLayout.isEmpty())
|
||||||
? session.getSessionProperties().defaultCustomLayout()
|
? session.getSessionProperties().defaultCustomLayout()
|
||||||
: customLayout;
|
: customLayout;
|
||||||
|
builder.customLayout(customLayout);
|
||||||
|
}
|
||||||
|
|
||||||
Recording startedRecording = this.recordingService.startRecording(session, new RecordingProperties.Builder()
|
builder.build();
|
||||||
.name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Recording startedRecording = this.recordingManager.startRecording(session, builder.build());
|
||||||
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
return new ResponseEntity<>("Error starting recording: " + e.getMessage(), getResponseHeaders(),
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST)
|
@RequestMapping(value = "/recordings/stop/{recordingId}", method = RequestMethod.POST)
|
||||||
|
@ -354,24 +375,24 @@ public class SessionRestController {
|
||||||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
Recording recording = recordingService.getStartedRecording(recordingId);
|
Recording recording = recordingManager.getStartedRecording(recordingId);
|
||||||
|
|
||||||
if (recording == null) {
|
if (recording == null) {
|
||||||
if (recordingService.getStartingRecording(recordingId) != null) {
|
if (recordingManager.getStartingRecording(recordingId) != null) {
|
||||||
// Recording is still starting
|
// Recording is still starting
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
|
return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
|
||||||
}
|
}
|
||||||
// Recording does not exist
|
// Recording does not exist
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) {
|
if (!this.recordingManager.sessionIsBeingRecorded(recording.getSessionId())) {
|
||||||
// Session is not being recorded
|
// Session is not being recorded
|
||||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Session session = sessionManager.getSession(recording.getSessionId());
|
Session session = sessionManager.getSession(recording.getSessionId());
|
||||||
|
|
||||||
Recording stoppedRecording = this.recordingService.stopRecording(session, recording.getId(),
|
Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(),
|
||||||
"recordingStoppedByServer");
|
"recordingStoppedByServer");
|
||||||
|
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
|
@ -386,27 +407,26 @@ public class SessionRestController {
|
||||||
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET)
|
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET)
|
||||||
public ResponseEntity<?> getRecording(@PathVariable("recordingId") String recordingId) {
|
public ResponseEntity<?> getRecording(@PathVariable("recordingId") String recordingId) {
|
||||||
try {
|
try {
|
||||||
Recording recording = this.recordingService.getAllRecordings().stream()
|
Recording recording = this.recordingManager.getRecording(recordingId);
|
||||||
.filter(rec -> rec.getId().equals(recordingId)).findFirst().get();
|
if (io.openvidu.java.client.Recording.Status.started.equals(recording.getStatus())
|
||||||
if (Recording.Status.started.equals(recording.getStatus())
|
&& recordingManager.getStartingRecording(recording.getId()) != null) {
|
||||||
&& recordingService.getStartingRecording(recording.getId()) != null) {
|
recording.setStatus(io.openvidu.java.client.Recording.Status.starting);
|
||||||
recording.setStatus(Recording.Status.starting);
|
|
||||||
}
|
}
|
||||||
return new ResponseEntity<>(recording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
return new ResponseEntity<>(recording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||||
} catch (NoSuchElementException e) {
|
} catch (Exception e) {
|
||||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
|
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
|
||||||
public ResponseEntity<?> getAllRecordings() {
|
public ResponseEntity<?> getAllRecordings() {
|
||||||
Collection<Recording> recordings = this.recordingService.getAllRecordings();
|
Collection<Recording> recordings = this.recordingManager.getAllRecordings();
|
||||||
JsonObject json = new JsonObject();
|
JsonObject json = new JsonObject();
|
||||||
JsonArray jsonArray = new JsonArray();
|
JsonArray jsonArray = new JsonArray();
|
||||||
recordings.forEach(rec -> {
|
recordings.forEach(rec -> {
|
||||||
if (Recording.Status.started.equals(rec.getStatus())
|
if (io.openvidu.java.client.Recording.Status.started.equals(rec.getStatus())
|
||||||
&& recordingService.getStartingRecording(rec.getId()) != null) {
|
&& recordingManager.getStartingRecording(rec.getId()) != null) {
|
||||||
rec.setStatus(Recording.Status.starting);
|
rec.setStatus(io.openvidu.java.client.Recording.Status.starting);
|
||||||
}
|
}
|
||||||
jsonArray.add(rec.toJson());
|
jsonArray.add(rec.toJson());
|
||||||
});
|
});
|
||||||
|
@ -417,7 +437,7 @@ public class SessionRestController {
|
||||||
|
|
||||||
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE)
|
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE)
|
||||||
public ResponseEntity<?> deleteRecording(@PathVariable("recordingId") String recordingId) {
|
public ResponseEntity<?> deleteRecording(@PathVariable("recordingId") String recordingId) {
|
||||||
return new ResponseEntity<>(this.recordingService.deleteRecordingFromHost(recordingId, false));
|
return new ResponseEntity<>(this.recordingManager.deleteRecordingFromHost(recordingId, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<String> generateErrorResponse(String errorMessage, String path, HttpStatus status) {
|
private ResponseEntity<String> generateErrorResponse(String errorMessage, String path, HttpStatus status) {
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2018 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class CustomFileWriter {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CustomFileWriter.class);
|
||||||
|
|
||||||
|
public void createAndWriteFile(String filePath, String text) {
|
||||||
|
try {
|
||||||
|
this.writeAndCloseOnOutputStreamWriter(new FileOutputStream(filePath), text);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Couldn't create file {}. Error: ", filePath, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void overwriteFile(String filePath, String text) {
|
||||||
|
try {
|
||||||
|
this.writeAndCloseOnOutputStreamWriter(new FileOutputStream(filePath, false), text);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Couldn't overwrite file {}. Error: ", filePath, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean createFolder(String path) {
|
||||||
|
return new File(path).mkdir();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeAndCloseOnOutputStreamWriter(FileOutputStream fos, String text) throws IOException {
|
||||||
|
OutputStreamWriter osw = null;
|
||||||
|
try {
|
||||||
|
osw = new OutputStreamWriter(fos);
|
||||||
|
osw.write(text);
|
||||||
|
} finally {
|
||||||
|
osw.close();
|
||||||
|
fos.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1033,7 +1033,7 @@ public class OpenViduTestAppE2eTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Remote record")
|
@DisplayName("Remote record")
|
||||||
void remoteRecordTest() throws Exception {
|
void remoteComposedRecordTest() throws Exception {
|
||||||
setupBrowser("chrome");
|
setupBrowser("chrome");
|
||||||
|
|
||||||
log.info("Remote record");
|
log.info("Remote record");
|
||||||
|
@ -1131,13 +1131,11 @@ public class OpenViduTestAppE2eTest {
|
||||||
user.getEventManager().waitUntilEventReaches("recordingStopped", 1);
|
user.getEventManager().waitUntilEventReaches("recordingStopped", 1);
|
||||||
|
|
||||||
String recordingsPath = "/opt/openvidu/recordings/";
|
String recordingsPath = "/opt/openvidu/recordings/";
|
||||||
File file1 = new File(recordingsPath + sessionName + ".mp4");
|
File file1 = new File(recordingsPath + sessionName + "/" + sessionName + ".mp4");
|
||||||
File file2 = new File(recordingsPath + ".recording." + sessionName);
|
File file2 = new File(recordingsPath + sessionName + "/" + ".recording." + sessionName);
|
||||||
File file3 = new File(recordingsPath + sessionName + ".info");
|
|
||||||
|
|
||||||
Assert.assertTrue(file1.exists() || file1.length() > 0);
|
Assert.assertTrue(file1.exists() || file1.length() > 0);
|
||||||
Assert.assertTrue(file2.exists() || file2.length() > 0);
|
Assert.assertTrue(file2.exists() || file2.length() > 0);
|
||||||
Assert.assertTrue(file3.exists() || file3.length() > 0);
|
|
||||||
|
|
||||||
Assert.assertTrue(
|
Assert.assertTrue(
|
||||||
this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName)));
|
this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName)));
|
||||||
|
@ -1159,7 +1157,6 @@ public class OpenViduTestAppE2eTest {
|
||||||
|
|
||||||
Assert.assertFalse(file1.exists());
|
Assert.assertFalse(file1.exists());
|
||||||
Assert.assertFalse(file2.exists());
|
Assert.assertFalse(file2.exists());
|
||||||
Assert.assertFalse(file3.exists());
|
|
||||||
|
|
||||||
user.getDriver().findElement(By.id("close-dialog-btn")).click();
|
user.getDriver().findElement(By.id("close-dialog-btn")).click();
|
||||||
|
|
||||||
|
@ -1167,6 +1164,12 @@ public class OpenViduTestAppE2eTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Remote record")
|
||||||
|
void remoteIndividualRecordTest() throws Exception {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("REST API: Fetch all, fetch one, force disconnect, force unpublish, close session")
|
@DisplayName("REST API: Fetch all, fetch one, force disconnect, force unpublish, close session")
|
||||||
void restApiFetchForce() throws Exception {
|
void restApiFetchForce() throws Exception {
|
||||||
|
|
Loading…
Reference in New Issue