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 javax.annotation.PostConstruct;
|
||||
import javax.ws.rs.ProcessingException;
|
||||
|
||||
import org.kurento.jsonrpc.JsonUtils;
|
||||
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.KurentoSessionManager;
|
||||
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.RpcNotificationService;
|
||||
import io.openvidu.server.utils.CommandExecutor;
|
||||
|
@ -147,8 +146,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
|||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public ComposedRecordingService composedRecordingService() {
|
||||
return new ComposedRecordingService();
|
||||
public RecordingManager recordingManager() {
|
||||
return new RecordingManager();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
@ -233,57 +232,8 @@ public class OpenViduServer implements JsonRpcConfigurer {
|
|||
OpenViduServer.wsUrl = OpenViduServer.wsUrl.substring(0, OpenViduServer.wsUrl.length() - 1);
|
||||
}
|
||||
|
||||
boolean recordingModuleEnabled = openviduConf.isRecordingModuleEnabled();
|
||||
if (recordingModuleEnabled) {
|
||||
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();
|
||||
if (this.openviduConfig().isRecordingModuleEnabled()) {
|
||||
this.recordingManager().initializeRecordingManager();
|
||||
}
|
||||
|
||||
httpUrl = openviduConf.getFinalUrl();
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
package io.openvidu.server.cdr;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
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.Session;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
/**
|
||||
* 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"
|
||||
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
||||
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "openviduServerStopped"
|
||||
*
|
||||
* [OPTIONAL_PROPERTIES]:
|
||||
* - receivingFrom: only if connection = "INBOUND"
|
||||
|
@ -91,9 +91,6 @@ public class CallDetailRecord {
|
|||
private Map<String, Set<CDREventWebrtcConnection>> subscriptions = 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) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
@ -108,7 +105,7 @@ public class CallDetailRecord {
|
|||
public void recordSessionDestroyed(String sessionId, String reason) {
|
||||
CDREvent e = this.sessions.remove(sessionId);
|
||||
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) {
|
||||
|
@ -124,7 +121,8 @@ public class CallDetailRecord {
|
|||
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,
|
||||
participant.getParticipantPublicId(), mediaOptions, null, timestamp);
|
||||
this.publications.put(participant.getParticipantPublicId(), publisher);
|
||||
|
@ -181,15 +179,7 @@ public class CallDetailRecord {
|
|||
public void recordRecordingStopped(String sessionId, Recording recording, String reason) {
|
||||
CDREventRecording recordingStartedEvent = this.recordings.remove(recording.getId());
|
||||
if (openviduConfig.isCdrEnabled())
|
||||
this.logger.log(new CDREventRecording(recordingStartedEvent, this.finalReason(reason)));
|
||||
}
|
||||
|
||||
private String finalReason(String reason) {
|
||||
if (lastParticipantLeftReasons.contains(reason)) {
|
||||
return "lastParticipantLeft";
|
||||
} else {
|
||||
return reason;
|
||||
}
|
||||
this.logger.log(new CDREventRecording(recordingStartedEvent, RecordingManager.finalReason(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.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
|
|
|
@ -143,8 +143,7 @@ public class SessionEventsHandler {
|
|||
// Metadata associated to new participant
|
||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_USER_PARAM,
|
||||
participant.getParticipantPublicId());
|
||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM,
|
||||
participant.getCreatedAt());
|
||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_CREATEDAT_PARAM, participant.getCreatedAt());
|
||||
notifParams.addProperty(ProtocolElements.PARTICIPANTJOINED_METADATA_PARAM,
|
||||
participant.getFullMetadata());
|
||||
|
||||
|
@ -404,12 +403,17 @@ public class SessionEventsHandler {
|
|||
evictedParticipant.getParticipantPublicId());
|
||||
params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason);
|
||||
|
||||
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
|
||||
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
||||
for (Participant p : participants) {
|
||||
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
|
||||
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
|
||||
// Do not send a message when evicting RECORDER participant
|
||||
rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(),
|
||||
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
||||
}
|
||||
for (Participant p : participants) {
|
||||
if (!ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(evictedParticipant.getParticipantPublicId())) {
|
||||
rpcNotificationService.sendNotification(p.getParticipantPrivateId(),
|
||||
ProtocolElements.PARTICIPANTEVICTED_METHOD, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendRecordingStartedNotification(Session session, Recording recording) {
|
||||
|
@ -550,6 +554,9 @@ public class SessionEventsHandler {
|
|||
|
||||
private Set<Participant> filterParticipantsByRole(ParticipantRole[] roles, Set<Participant> participants) {
|
||||
return participants.stream().filter(part -> {
|
||||
if (ProtocolElements.RECORDER_PARTICIPANT_PUBLICID.equals(part.getParticipantPublicId())) {
|
||||
return false;
|
||||
}
|
||||
boolean isRole = false;
|
||||
for (ParticipantRole role : roles) {
|
||||
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.TurnCredentials;
|
||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||
import io.openvidu.server.recording.ComposedRecordingService;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
public abstract class SessionManager {
|
||||
|
||||
|
@ -54,7 +54,7 @@ public abstract class SessionManager {
|
|||
protected SessionEventsHandler sessionEventsHandler;
|
||||
|
||||
@Autowired
|
||||
protected ComposedRecordingService recordingService;
|
||||
protected RecordingManager recordingManager;
|
||||
|
||||
@Autowired
|
||||
protected CallDetailRecord CDR;
|
||||
|
@ -440,19 +440,21 @@ public abstract class SessionManager {
|
|||
|
||||
this.closeSessionAndEmptyCollections(session, reason);
|
||||
|
||||
if (recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
recordingService.stopRecording(session, null, reason);
|
||||
}
|
||||
|
||||
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)) {
|
||||
sessionEventsHandler.onSessionClosed(session.getSessionId(), reason);
|
||||
}
|
||||
sessions.remove(session.getSessionId());
|
||||
|
||||
sessions.remove(session.getSessionId());
|
||||
sessionProperties.remove(session.getSessionId());
|
||||
sessionCreationTime.remove(session.getSessionId());
|
||||
sessionidParticipantpublicidParticipant.remove(session.getSessionId());
|
||||
|
|
|
@ -17,8 +17,6 @@
|
|||
|
||||
package io.openvidu.server.kurento;
|
||||
|
||||
import io.openvidu.server.kurento.KurentoClientSessionInfo;
|
||||
|
||||
/**
|
||||
* Implementation of the session info interface, contains a participant's
|
||||
* 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.kurento.TrackType;
|
||||
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.PublisherEndpoint;
|
||||
import io.openvidu.server.kurento.endpoint.SdpType;
|
||||
import io.openvidu.server.kurento.endpoint.SubscriberEndpoint;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
public class KurentoParticipant extends Participant {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(KurentoParticipant.class);
|
||||
|
||||
private OpenviduConfig openviduConfig;
|
||||
|
||||
private InfoHandler infoHandler;
|
||||
private CallDetailRecord CDR;
|
||||
private OpenviduConfig openviduConfig;
|
||||
private RecordingManager recordingManager;
|
||||
|
||||
private boolean webParticipant = true;
|
||||
|
||||
|
@ -79,11 +81,15 @@ public class KurentoParticipant extends Participant {
|
|||
private final ConcurrentMap<String, SubscriberEndpoint> subscribers = new ConcurrentHashMap<String, SubscriberEndpoint>();
|
||||
|
||||
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(),
|
||||
participant.getClientMetadata(), participant.getLocation(), participant.getPlatform(),
|
||||
participant.getCreatedAt());
|
||||
this.infoHandler = infoHandler;
|
||||
this.CDR = CDR;
|
||||
this.openviduConfig = openviduConfig;
|
||||
this.recordingManager = recordingManager;
|
||||
this.session = kurentoSession;
|
||||
this.pipeline = pipeline;
|
||||
this.publisher = new PublisherEndpoint(webParticipant, this, participant.getParticipantPublicId(), pipeline,
|
||||
|
@ -94,8 +100,6 @@ public class KurentoParticipant extends Participant {
|
|||
getNewOrExistingSubscriber(other.getParticipantPublicId());
|
||||
}
|
||||
}
|
||||
this.infoHandler = infoHandler;
|
||||
this.CDR = CDR;
|
||||
}
|
||||
|
||||
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(),
|
||||
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(),
|
||||
this.publisher.createdAt());
|
||||
|
||||
|
@ -425,9 +434,15 @@ public class KurentoParticipant extends Participant {
|
|||
private void releasePublisherEndpoint(String reason) {
|
||||
if (publisher != null && publisher.getEndpoint() != null) {
|
||||
|
||||
// Store streamId from publisher's map
|
||||
// Remove streamId from publisher's map
|
||||
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
||||
|
||||
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
||||
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
|
||||
this.getPublisherStreamId());
|
||||
}
|
||||
|
||||
publisher.unregisterErrorListeners();
|
||||
for (MediaElement el : publisher.getMediaElements()) {
|
||||
releaseElement(getParticipantPublicId(), el);
|
||||
|
@ -507,53 +522,6 @@ public class KurentoParticipant extends Participant {
|
|||
* " | TIMESTAMP: " + System.currentTimeMillis(); log.debug(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
|
||||
* msg = " Media session started (" +
|
||||
* endpoint.getEndpoint().getName() + ") | TIMESTAMP: " +
|
||||
|
@ -568,15 +536,8 @@ public class KurentoParticipant extends Participant {
|
|||
*
|
||||
* endpoint.getWebEndpoint().addMediaStateChangedListener((event) -> { String
|
||||
* msg = " Media state changed (" +
|
||||
* endpoint.getEndpoint().getName() + ") from " + event.getOldState() +
|
||||
* " to " + 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.getEndpoint().getName() + ") from " + event.getOldState() + " to " +
|
||||
* event.getNewState(); log.debug(msg); this.infoHandler.sendInfo(msg); });
|
||||
*
|
||||
* endpoint.getWebEndpoint().addIceCandidateFoundListener((event) -> { String
|
||||
* msg = " ICE CANDIDATE FOUND (" +
|
||||
|
@ -584,38 +545,13 @@ public class KurentoParticipant extends Participant {
|
|||
* event.getCandidate().getCandidate() + " | TIMESTAMP: " +
|
||||
* 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 -> {
|
||||
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());
|
||||
* 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())); }
|
||||
*/
|
||||
|
||||
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
|
||||
log.info(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: "
|
||||
+ event.getState() + " | SOURCE: " + event.getSource().getName() + " | PAD: " + event.getPadName()
|
||||
+ " | MEDIATYPE: " + event.getMediaType() + " | TIMESTAMP: " + System.currentTimeMillis();
|
||||
|
||||
/*
|
||||
* 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)); }
|
||||
*/
|
||||
|
||||
endpoint.kmsEvents.add(new KmsMediaEvent(event, event.getMediaType(), endpoint.createdAt()));
|
||||
log.info(msg1);
|
||||
this.infoHandler.sendInfo(msg1);
|
||||
});
|
||||
|
@ -661,7 +585,7 @@ public class KurentoParticipant extends Participant {
|
|||
});
|
||||
|
||||
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 -> {
|
||||
|
|
|
@ -47,6 +47,7 @@ import io.openvidu.server.cdr.CallDetailRecord;
|
|||
import io.openvidu.server.config.OpenviduConfig;
|
||||
import io.openvidu.server.core.Participant;
|
||||
import io.openvidu.server.core.Session;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
/**
|
||||
* @author Pablo Fuente (pablofuenteperez@gmail.com)
|
||||
|
@ -57,6 +58,7 @@ public class KurentoSession implements Session {
|
|||
public static final int ASYNC_LATCH_TIMEOUT = 30;
|
||||
|
||||
private OpenviduConfig openviduConfig;
|
||||
private RecordingManager recordingManager;
|
||||
|
||||
private final ConcurrentMap<String, KurentoParticipant> participants = new ConcurrentHashMap<>();
|
||||
private String sessionId;
|
||||
|
@ -85,7 +87,8 @@ public class KurentoSession implements Session {
|
|||
|
||||
public KurentoSession(String sessionId, Long startTime, SessionProperties sessionProperties,
|
||||
KurentoClient kurentoClient, KurentoSessionEventsHandler kurentoSessionHandler,
|
||||
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig) {
|
||||
boolean destroyKurentoClient, CallDetailRecord CDR, OpenviduConfig openviduConfig,
|
||||
RecordingManager recordingManager) {
|
||||
this.sessionId = sessionId;
|
||||
this.sessionProperties = sessionProperties;
|
||||
this.kurentoClient = kurentoClient;
|
||||
|
@ -93,6 +96,7 @@ public class KurentoSession implements Session {
|
|||
this.kurentoSessionHandler = kurentoSessionHandler;
|
||||
this.CDR = CDR;
|
||||
this.openviduConfig = openviduConfig;
|
||||
this.recordingManager = recordingManager;
|
||||
this.startTime = startTime;
|
||||
log.debug("New SESSION instance with id '{}'", sessionId);
|
||||
}
|
||||
|
@ -113,7 +117,7 @@ public class KurentoSession implements Session {
|
|||
createPipeline();
|
||||
|
||||
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);
|
||||
|
||||
filterStates.forEach((filterId, state) -> {
|
||||
|
|
|
@ -170,25 +170,37 @@ public class KurentoSessionManager extends SessionManager {
|
|||
reason);
|
||||
|
||||
if (remainingParticipants.isEmpty()) {
|
||||
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
||||
this.closeSessionAndEmptyCollections(session, reason);
|
||||
showTokens();
|
||||
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);
|
||||
this.closeSessionAndEmptyCollections(session, reason);
|
||||
showTokens();
|
||||
}
|
||||
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
||||
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
||||
// Immediately stop recording when last real participant left if
|
||||
// RecordingMode.ALWAYS
|
||||
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,
|
||||
null, "EVICT_RECORDER");
|
||||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||
// a Publisher starts before timeout)
|
||||
log.info("Last participant left. Starting countdown for stopping recording of session {}", sessionId);
|
||||
recordingService.initAutomaticRecordingStopThread(session.getSessionId());
|
||||
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
||||
sessionId);
|
||||
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -277,20 +289,21 @@ public class KurentoSessionManager extends SessionManager {
|
|||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||
&& session.getActivePublishers() == 0) {
|
||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())
|
||||
&& !recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
&& !recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
// Insecure session recording
|
||||
new Thread(() -> {
|
||||
recordingService.startRecording(session,
|
||||
recordingManager.startRecording(session,
|
||||
new RecordingProperties.Builder().name("")
|
||||
.outputMode(io.openvidu.java.client.Recording.OutputMode.COMPOSED)
|
||||
.recordingLayout(session.getSessionProperties().defaultRecordingLayout())
|
||||
.customLayout(session.getSessionProperties().defaultCustomLayout()).build());
|
||||
}).start();
|
||||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())
|
||||
&& recordingService.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
&& recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
// Abort automatic recording stop (user published before timeout)
|
||||
log.info("Participant {} published before timeout finished. Aborting automatic recording stop",
|
||||
participant.getParticipantPublicId());
|
||||
boolean stopAborted = recordingService.abortAutomaticRecordingStopThread(session.getSessionId());
|
||||
boolean stopAborted = recordingManager.abortAutomaticRecordingStopThread(session.getSessionId());
|
||||
if (stopAborted) {
|
||||
log.info("Automatic recording stopped succesfully aborted");
|
||||
} else {
|
||||
|
@ -491,7 +504,7 @@ public class KurentoSessionManager extends SessionManager {
|
|||
this.kurentoClient = kcProvider.getKurentoClient(kcSessionInfo);
|
||||
session = new KurentoSession(sessionId, this.sessionCreationTime.get(sessionId), sessionProperties,
|
||||
kurentoClient, kurentoSessionEventsHandler, kcProvider.destroyWhenUnused(), this.CDR,
|
||||
this.openviduConfig);
|
||||
this.openviduConfig, this.recordingManager);
|
||||
|
||||
KurentoSession oldSession = (KurentoSession) sessions.putIfAbsent(sessionId, session);
|
||||
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;
|
||||
|
||||
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.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
||||
|
@ -32,7 +30,6 @@ import org.kurento.client.IceCandidate;
|
|||
import org.kurento.client.ListenerSubscription;
|
||||
import org.kurento.client.MediaElement;
|
||||
import org.kurento.client.MediaPipeline;
|
||||
import org.kurento.client.MediaType;
|
||||
import org.kurento.client.OnIceCandidateEvent;
|
||||
import org.kurento.client.RtpEndpoint;
|
||||
import org.kurento.client.SdpEndpoint;
|
||||
|
@ -85,9 +82,6 @@ public abstract class MediaEndpoint {
|
|||
private final List<IceCandidate> receivedCandidateList = 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 selectedRemoteIceCandidate;
|
||||
public Queue<KmsEvent> kmsEvents = new ConcurrentLinkedQueue<>();
|
||||
|
|
|
@ -18,19 +18,42 @@
|
|||
package io.openvidu.server.kurento.kms;
|
||||
|
||||
import org.kurento.client.KurentoClient;
|
||||
|
||||
import io.openvidu.server.kurento.kms.Kms;
|
||||
import io.openvidu.server.kurento.kms.KmsManager;
|
||||
import org.kurento.client.KurentoConnectionListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class FixedOneKmsManager extends KmsManager {
|
||||
|
||||
public FixedOneKmsManager(String kmsWsUri) {
|
||||
this(kmsWsUri, 1);
|
||||
}
|
||||
private static final Logger log = LoggerFactory.getLogger(FixedOneKmsManager.class);
|
||||
|
||||
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
|
||||
for (int i = 0; i < numKmss; i++) {
|
||||
this.addKms(new Kms(KurentoClient.create(kmsWsUri), kmsWsUri));
|
||||
}
|
||||
}
|
||||
public FixedOneKmsManager(String kmsWsUri) {
|
||||
this(kmsWsUri, 1);
|
||||
}
|
||||
|
||||
public FixedOneKmsManager(String kmsWsUri, int numKmss) {
|
||||
for (int i = 0; i < numKmss; i++) {
|
||||
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 enum Status {
|
||||
starting, // The recording is starting (cannot be stopped)
|
||||
started, // The recording has started and is going on
|
||||
stopped, // The recording has finished OK
|
||||
available, // The recording is available for downloading. This status is reached for all
|
||||
// stopped recordings if property 'openvidu.recording.public-access' is true
|
||||
failed; // The recording has failed
|
||||
}
|
||||
|
||||
private Recording.Status status;
|
||||
private io.openvidu.java.client.Recording.Status status;
|
||||
|
||||
private String id;
|
||||
private String sessionId;
|
||||
|
@ -49,7 +40,7 @@ public class Recording {
|
|||
this.sessionId = sessionId;
|
||||
this.createdAt = System.currentTimeMillis();
|
||||
this.id = id;
|
||||
this.status = Status.started;
|
||||
this.status = io.openvidu.java.client.Recording.Status.started;
|
||||
this.recordingProperties = recordingProperties;
|
||||
}
|
||||
|
||||
|
@ -70,16 +61,27 @@ public class Recording {
|
|||
}
|
||||
this.hasAudio = json.get("hasAudio").getAsBoolean();
|
||||
this.hasVideo = json.get("hasVideo").getAsBoolean();
|
||||
this.status = Status.valueOf(json.get("status").getAsString());
|
||||
this.recordingProperties = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
||||
.recordingLayout(RecordingLayout.valueOf(json.get("recordingLayout").getAsString())).build();
|
||||
this.status = io.openvidu.java.client.Recording.Status.valueOf(json.get("status").getAsString());
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public void setStatus(Status status) {
|
||||
public void setStatus(io.openvidu.java.client.Recording.Status status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
|
@ -95,6 +97,10 @@ public class Recording {
|
|||
return this.recordingProperties.name();
|
||||
}
|
||||
|
||||
public io.openvidu.java.client.Recording.OutputMode getOutputMode() {
|
||||
return this.recordingProperties.outputMode();
|
||||
}
|
||||
|
||||
public RecordingLayout getRecordingLayout() {
|
||||
return this.recordingProperties.recordingLayout();
|
||||
}
|
||||
|
@ -103,6 +109,10 @@ public class Recording {
|
|||
return this.recordingProperties.customLayout();
|
||||
}
|
||||
|
||||
public RecordingProperties getRecordingProperties() {
|
||||
return this.recordingProperties;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
@ -163,9 +173,12 @@ public class Recording {
|
|||
JsonObject json = new JsonObject();
|
||||
json.addProperty("id", this.id);
|
||||
json.addProperty("name", this.recordingProperties.name());
|
||||
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
|
||||
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
||||
json.addProperty("customLayout", this.recordingProperties.customLayout());
|
||||
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());
|
||||
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
||||
json.addProperty("customLayout", this.recordingProperties.customLayout());
|
||||
}
|
||||
}
|
||||
json.addProperty("sessionId", this.sessionId);
|
||||
json.addProperty("createdAt", this.createdAt);
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package io.openvidu.server.recording;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
|
@ -38,35 +39,35 @@ public class RecordingInfoUtils {
|
|||
private JsonObject videoStream;
|
||||
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();
|
||||
|
||||
try {
|
||||
this.json = parser.parse(new FileReader(fullVideoPath)).getAsJsonObject();
|
||||
this.json = parser.parse(new FileReader(infoFilePath)).getAsJsonObject();
|
||||
} catch (JsonIOException | JsonSyntaxException e) {
|
||||
// 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");
|
||||
}
|
||||
|
||||
if (this.json.size() == 0) {
|
||||
// Recording metadata from ffprobe is an emtpy JSON
|
||||
throw new OpenViduException(Code.RECORDING_FILE_EMPTY_ERROR, "The recording file is empty");
|
||||
}
|
||||
|
||||
this.jsonFormat = json.get("format").getAsJsonObject();
|
||||
|
||||
JsonArray streams = json.get("streams").getAsJsonArray();
|
||||
|
||||
for (int i = 0; i < streams.size(); i++) {
|
||||
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;
|
||||
} else if ("audio".equals(stream.get("codec_type").toString())) {
|
||||
} else if ("audio".equals(stream.get("codec_type").getAsString())) {
|
||||
this.audioStream = stream;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public double getDurationInSeconds() {
|
||||
|
@ -124,4 +125,8 @@ public class RecordingInfoUtils {
|
|||
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.FileReader;
|
||||
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.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.apache.commons.io.FilenameUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.github.dockerjava.api.DockerClient;
|
||||
import com.github.dockerjava.api.command.CreateContainerCmd;
|
||||
|
@ -60,8 +47,6 @@ import com.github.dockerjava.core.DockerClientBuilder;
|
|||
import com.github.dockerjava.core.DockerClientConfig;
|
||||
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
||||
import com.github.dockerjava.core.command.PullImageResultCallback;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
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.config.OpenviduConfig;
|
||||
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;
|
||||
|
||||
@Service
|
||||
public class ComposedRecordingService {
|
||||
public class ComposedRecordingService extends RecordingService {
|
||||
|
||||
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> 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 String IMAGE_TAG;
|
||||
private final String RECORDING_ENTITY_FILE = ".recording.";
|
||||
|
||||
private DockerClient dockerClient;
|
||||
|
||||
public ComposedRecordingService() {
|
||||
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||
super(recordingManager, openviduConfig);
|
||||
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().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<>();
|
||||
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()) {
|
||||
// No name provided for the recording file
|
||||
properties = new RecordingProperties.Builder().name(recordingId)
|
||||
.recordingLayout(properties.recordingLayout()).customLayout(properties.customLayout()).build();
|
||||
}
|
||||
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
||||
properties);
|
||||
properties = updatePropertiesAndRecordingId.properties;
|
||||
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||
|
||||
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||
|
||||
this.sessionsRecordings.put(session.getSessionId(), recording);
|
||||
this.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
||||
this.startingRecordings.put(recording.getId(), recording);
|
||||
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
|
||||
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
||||
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
||||
|
||||
String uid = null;
|
||||
try {
|
||||
|
@ -133,7 +102,7 @@ public class ComposedRecordingService {
|
|||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String layoutUrl = this.getLayoutUrl(recording, shortSessionId);
|
||||
String layoutUrl = this.getLayoutUrl(recording, this.getShortSessionId(session));
|
||||
|
||||
envs.add("URL=" + layoutUrl);
|
||||
envs.add("RESOLUTION=1920x1080");
|
||||
|
@ -147,37 +116,38 @@ public class ComposedRecordingService {
|
|||
log.info(recording.toJson().toString());
|
||||
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);
|
||||
|
||||
recording.setStatus(Recording.Status.started);
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
|
||||
|
||||
this.startedRecordings.put(recording.getId(), recording);
|
||||
this.startingRecordings.remove(recording.getId());
|
||||
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
||||
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
||||
public Recording stopRecording(Session session, String recordingId, String reason) {
|
||||
Recording recording;
|
||||
String containerId;
|
||||
@Override
|
||||
public Recording stopRecording(Session session, Recording recording, String reason) {
|
||||
String containerId = cleanRecordingMapsAndReturnContainerId(recording);
|
||||
final String recordingId = recording.getId();
|
||||
|
||||
if (session == null) {
|
||||
log.warn(
|
||||
"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"
|
||||
+ " stopped after last user left and timeout passed",
|
||||
recordingId);
|
||||
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());
|
||||
recording.getId());
|
||||
}
|
||||
|
||||
if (containerId == null) {
|
||||
|
@ -205,8 +175,9 @@ public class ComposedRecordingService {
|
|||
containerClosed = true;
|
||||
log.warn("Container {} for closed session {} succesfully stopped and removed", containerIdAux,
|
||||
session.getSessionId());
|
||||
log.warn("Deleting unusable files for recording {}", recording.getId());
|
||||
if (HttpStatus.NO_CONTENT.equals(this.deleteRecordingFromHost(recording.getId(), true))) {
|
||||
log.warn("Deleting unusable files for recording {}", recordingId);
|
||||
if (HttpStatus.NO_CONTENT
|
||||
.equals(this.recordingManager.deleteRecordingFromHost(recordingId, true))) {
|
||||
log.warn("Files properly deleted");
|
||||
}
|
||||
}
|
||||
|
@ -234,12 +205,12 @@ public class ComposedRecordingService {
|
|||
try {
|
||||
stopped = latch.await(60, TimeUnit.SECONDS);
|
||||
} 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,
|
||||
"The recording completion process has been unexpectedly interrupted"));
|
||||
}
|
||||
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,
|
||||
"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
|
||||
try {
|
||||
RecordingInfoUtils infoUtils = new RecordingInfoUtils(
|
||||
this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + ".info");
|
||||
this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/" + recordingId + ".info");
|
||||
|
||||
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
||||
recording.setStatus(Recording.Status.available);
|
||||
} else {
|
||||
recording.setStatus(Recording.Status.stopped);
|
||||
}
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
|
||||
recording.setDuration(infoUtils.getDurationInSeconds());
|
||||
recording.setSize(infoUtils.getSizeInBytes());
|
||||
recording.setHasAudio(infoUtils.hasAudio());
|
||||
recording.setHasVideo(infoUtils.hasVideo());
|
||||
|
||||
if (openviduConfig.getOpenViduRecordingPublicAccess()) {
|
||||
recording.setUrl(this.openviduConfig.getFinalUrl() + "recordings/" + recording.getName() + ".mp4");
|
||||
}
|
||||
infoUtils.deleteFilePath();
|
||||
|
||||
recording = this.recordingManager.updateRecordingUrl(recording);
|
||||
|
||||
} catch (IOException e) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
||||
"There was an error generating the metadata report file for the recording");
|
||||
}
|
||||
if (session != null) {
|
||||
this.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||
}
|
||||
}
|
||||
return recording;
|
||||
|
@ -306,19 +274,7 @@ public class ComposedRecordingService {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean sessionIsBeingRecorded(String sessionId) {
|
||||
return (this.sessionsRecordings.get(sessionId) != null);
|
||||
}
|
||||
|
||||
public Recording getStartedRecording(String recordingId) {
|
||||
return this.startedRecordings.get(recordingId);
|
||||
}
|
||||
|
||||
public Recording getStartingRecording(String recordingId) {
|
||||
return this.startingRecordings.get(recordingId);
|
||||
}
|
||||
|
||||
private String runRecordingContainer(List<String> envs, String containerName) {
|
||||
private String runRecordingContainer(List<String> envs, String containerName) throws Exception {
|
||||
Volume volume1 = new Volume("/recordings");
|
||||
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
|
||||
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
||||
|
@ -334,7 +290,10 @@ public class ComposedRecordingService {
|
|||
log.error(
|
||||
"The container name {} is already in use. Probably caused by a session with unique publisher re-publishing a stream",
|
||||
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;
|
||||
}
|
||||
|
||||
public Collection<Recording> getAllRecordings() {
|
||||
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) {
|
||||
private void waitForVideoFileNotEmpty(Recording recording) {
|
||||
boolean isPresent = false;
|
||||
while (!isPresent) {
|
||||
try {
|
||||
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));
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -547,23 +360,14 @@ public class ComposedRecordingService {
|
|||
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;
|
||||
}
|
||||
|
||||
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.IOException;
|
|
@ -19,7 +19,6 @@ package io.openvidu.server.rest;
|
|||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
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.SessionManager;
|
||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||
import io.openvidu.server.recording.ComposedRecordingService;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -68,7 +67,7 @@ public class SessionRestController {
|
|||
private SessionManager sessionManager;
|
||||
|
||||
@Autowired
|
||||
private ComposedRecordingService recordingService;
|
||||
private RecordingManager recordingManager;
|
||||
|
||||
@Autowired
|
||||
private OpenviduConfig openviduConfig;
|
||||
|
@ -148,7 +147,7 @@ public class SessionRestController {
|
|||
Session session = this.sessionManager.getSession(sessionId);
|
||||
if (session != null) {
|
||||
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);
|
||||
} else {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
|
@ -163,7 +162,7 @@ public class SessionRestController {
|
|||
JsonArray jsonArray = new JsonArray();
|
||||
sessions.forEach(s -> {
|
||||
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);
|
||||
});
|
||||
json.addProperty("numberOfElements", sessions.size());
|
||||
|
@ -298,6 +297,7 @@ public class SessionRestController {
|
|||
|
||||
String sessionId = (String) params.get("session");
|
||||
String name = (String) params.get("name");
|
||||
String outputModeString = (String) params.get("outputMode");
|
||||
String recordingLayoutString = (String) params.get("recordingLayout");
|
||||
String customLayout = (String) params.get("customLayout");
|
||||
|
||||
|
@ -322,28 +322,49 @@ public class SessionRestController {
|
|||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
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
|
||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
RecordingLayout recordingLayout;
|
||||
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
|
||||
// "recordingLayout" parameter not defined. Use global layout from
|
||||
// SessionProperties (it is always configured as it has RecordingLayout.BEST_FIT
|
||||
// as default value)
|
||||
recordingLayout = session.getSessionProperties().defaultRecordingLayout();
|
||||
} else {
|
||||
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
||||
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;
|
||||
if (recordingLayoutString == null || recordingLayoutString.isEmpty()) {
|
||||
// "recordingLayout" parameter not defined. Use global layout from
|
||||
// SessionProperties (it is always configured as it has RecordingLayout.BEST_FIT
|
||||
// as default value)
|
||||
recordingLayout = session.getSessionProperties().defaultRecordingLayout();
|
||||
} else {
|
||||
recordingLayout = RecordingLayout.valueOf(recordingLayoutString);
|
||||
}
|
||||
|
||||
builder.recordingLayout(recordingLayout);
|
||||
|
||||
if (RecordingLayout.CUSTOM.equals(recordingLayout)) {
|
||||
customLayout = (customLayout == null || customLayout.isEmpty())
|
||||
? session.getSessionProperties().defaultCustomLayout()
|
||||
: customLayout;
|
||||
builder.customLayout(customLayout);
|
||||
}
|
||||
|
||||
builder.build();
|
||||
}
|
||||
|
||||
customLayout = (customLayout == null || customLayout.isEmpty())
|
||||
? session.getSessionProperties().defaultCustomLayout()
|
||||
: customLayout;
|
||||
|
||||
Recording startedRecording = this.recordingService.startRecording(session, new RecordingProperties.Builder()
|
||||
.name(name).recordingLayout(recordingLayout).customLayout(customLayout).build());
|
||||
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||
try {
|
||||
Recording startedRecording = this.recordingManager.startRecording(session, builder.build());
|
||||
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)
|
||||
|
@ -354,24 +375,24 @@ public class SessionRestController {
|
|||
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
Recording recording = recordingService.getStartedRecording(recordingId);
|
||||
Recording recording = recordingManager.getStartedRecording(recordingId);
|
||||
|
||||
if (recording == null) {
|
||||
if (recordingService.getStartingRecording(recordingId) != null) {
|
||||
if (recordingManager.getStartingRecording(recordingId) != null) {
|
||||
// Recording is still starting
|
||||
return new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE);
|
||||
}
|
||||
// Recording does not exist
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!this.recordingService.sessionIsBeingRecorded(recording.getSessionId())) {
|
||||
if (!this.recordingManager.sessionIsBeingRecorded(recording.getSessionId())) {
|
||||
// Session is not being recorded
|
||||
return new ResponseEntity<>(HttpStatus.CONFLICT);
|
||||
}
|
||||
|
||||
Session session = sessionManager.getSession(recording.getSessionId());
|
||||
|
||||
Recording stoppedRecording = this.recordingService.stopRecording(session, recording.getId(),
|
||||
Recording stoppedRecording = this.recordingManager.stopRecording(session, recording.getId(),
|
||||
"recordingStoppedByServer");
|
||||
|
||||
if (session != null) {
|
||||
|
@ -386,27 +407,26 @@ public class SessionRestController {
|
|||
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.GET)
|
||||
public ResponseEntity<?> getRecording(@PathVariable("recordingId") String recordingId) {
|
||||
try {
|
||||
Recording recording = this.recordingService.getAllRecordings().stream()
|
||||
.filter(rec -> rec.getId().equals(recordingId)).findFirst().get();
|
||||
if (Recording.Status.started.equals(recording.getStatus())
|
||||
&& recordingService.getStartingRecording(recording.getId()) != null) {
|
||||
recording.setStatus(Recording.Status.starting);
|
||||
Recording recording = this.recordingManager.getRecording(recordingId);
|
||||
if (io.openvidu.java.client.Recording.Status.started.equals(recording.getStatus())
|
||||
&& recordingManager.getStartingRecording(recording.getId()) != null) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.starting);
|
||||
}
|
||||
return new ResponseEntity<>(recording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||
} catch (NoSuchElementException e) {
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/recordings", method = RequestMethod.GET)
|
||||
public ResponseEntity<?> getAllRecordings() {
|
||||
Collection<Recording> recordings = this.recordingService.getAllRecordings();
|
||||
Collection<Recording> recordings = this.recordingManager.getAllRecordings();
|
||||
JsonObject json = new JsonObject();
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
recordings.forEach(rec -> {
|
||||
if (Recording.Status.started.equals(rec.getStatus())
|
||||
&& recordingService.getStartingRecording(rec.getId()) != null) {
|
||||
rec.setStatus(Recording.Status.starting);
|
||||
if (io.openvidu.java.client.Recording.Status.started.equals(rec.getStatus())
|
||||
&& recordingManager.getStartingRecording(rec.getId()) != null) {
|
||||
rec.setStatus(io.openvidu.java.client.Recording.Status.starting);
|
||||
}
|
||||
jsonArray.add(rec.toJson());
|
||||
});
|
||||
|
@ -417,7 +437,7 @@ public class SessionRestController {
|
|||
|
||||
@RequestMapping(value = "/recordings/{recordingId}", method = RequestMethod.DELETE)
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
@DisplayName("Remote record")
|
||||
void remoteRecordTest() throws Exception {
|
||||
void remoteComposedRecordTest() throws Exception {
|
||||
setupBrowser("chrome");
|
||||
|
||||
log.info("Remote record");
|
||||
|
@ -1131,13 +1131,11 @@ public class OpenViduTestAppE2eTest {
|
|||
user.getEventManager().waitUntilEventReaches("recordingStopped", 1);
|
||||
|
||||
String recordingsPath = "/opt/openvidu/recordings/";
|
||||
File file1 = new File(recordingsPath + sessionName + ".mp4");
|
||||
File file2 = new File(recordingsPath + ".recording." + sessionName);
|
||||
File file3 = new File(recordingsPath + sessionName + ".info");
|
||||
File file1 = new File(recordingsPath + sessionName + "/" + sessionName + ".mp4");
|
||||
File file2 = new File(recordingsPath + sessionName + "/" + ".recording." + sessionName);
|
||||
|
||||
Assert.assertTrue(file1.exists() || file1.length() > 0);
|
||||
Assert.assertTrue(file2.exists() || file2.length() > 0);
|
||||
Assert.assertTrue(file3.exists() || file3.length() > 0);
|
||||
|
||||
Assert.assertTrue(
|
||||
this.recordedFileFine(file1, new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName)));
|
||||
|
@ -1159,7 +1157,6 @@ public class OpenViduTestAppE2eTest {
|
|||
|
||||
Assert.assertFalse(file1.exists());
|
||||
Assert.assertFalse(file2.exists());
|
||||
Assert.assertFalse(file3.exists());
|
||||
|
||||
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
|
||||
@DisplayName("REST API: Fetch all, fetch one, force disconnect, force unpublish, close session")
|
||||
void restApiFetchForce() throws Exception {
|
||||
|
|
Loading…
Reference in New Issue