mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server: audio-only and video-only recordings
parent
e46aa16157
commit
496d33b139
|
@ -27,28 +27,30 @@ public class OpenViduException extends JsonRpcErrorException {
|
||||||
|
|
||||||
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(801),
|
TRANSPORT_ERROR_CODE(803), TRANSPORT_RESPONSE_ERROR_CODE(802), TRANSPORT_REQUEST_ERROR_CODE(801),
|
||||||
|
|
||||||
MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE(308), MEDIA_MUTE_ERROR_CODE(307), MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(306), MEDIA_RTP_ENDPOINT_ERROR_CODE(
|
MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE(309),
|
||||||
305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
|
MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE(308), MEDIA_MUTE_ERROR_CODE(307),
|
||||||
304), MEDIA_ENDPOINT_ERROR_CODE(303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
|
MEDIA_NOT_A_WEB_ENDPOINT_ERROR_CODE(306), MEDIA_RTP_ENDPOINT_ERROR_CODE(305),
|
||||||
|
MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(304), MEDIA_ENDPOINT_ERROR_CODE(303), MEDIA_SDP_ERROR_CODE(302),
|
||||||
|
MEDIA_GENERIC_ERROR_CODE(301),
|
||||||
|
|
||||||
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(
|
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(202),
|
||||||
202), ROOM_GENERIC_ERROR_CODE(201),
|
ROOM_GENERIC_ERROR_CODE(201),
|
||||||
|
|
||||||
USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(
|
USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(103),
|
||||||
103), USER_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(10),
|
USER_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(10),
|
||||||
|
|
||||||
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(
|
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(403),
|
||||||
403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404), EXISTING_FILTER_ALREADY_APPLIED_ERROR_CODE(405),
|
TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404), EXISTING_FILTER_ALREADY_APPLIED_ERROR_CODE(405),
|
||||||
FILTER_NOT_APPLIED_ERROR_CODE(406), FILTER_EVENT_LISTENER_NOT_FOUND(407),
|
FILTER_NOT_APPLIED_ERROR_CODE(406), FILTER_EVENT_LISTENER_NOT_FOUND(407),
|
||||||
|
|
||||||
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
|
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
|
||||||
|
|
||||||
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), SIGNAL_MESSAGE_INVALID_ERROR_CODE(
|
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601),
|
||||||
602),
|
SIGNAL_MESSAGE_INVALID_ERROR_CODE(602),
|
||||||
|
|
||||||
RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(
|
RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(706),
|
||||||
706), RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(
|
RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(703),
|
||||||
703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
|
RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
|
||||||
|
|
||||||
private int value;
|
private int value;
|
||||||
|
|
||||||
|
|
|
@ -121,7 +121,7 @@ public class RecordingProperties {
|
||||||
* Call this method to specify whether or not to record the audio track
|
* Call this method to specify whether or not to record the audio track
|
||||||
*/
|
*/
|
||||||
public RecordingProperties.Builder hasAudio(boolean hasAudio) {
|
public RecordingProperties.Builder hasAudio(boolean hasAudio) {
|
||||||
this.hasAudio = true;
|
this.hasAudio = hasAudio;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +129,7 @@ public class RecordingProperties {
|
||||||
* Call this method to specify whether or not to record the video track
|
* Call this method to specify whether or not to record the video track
|
||||||
*/
|
*/
|
||||||
public RecordingProperties.Builder hasVideo(boolean hasVideo) {
|
public RecordingProperties.Builder hasVideo(boolean hasVideo) {
|
||||||
this.hasVideo = true;
|
this.hasVideo = hasVideo;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ public class CDREventRecording extends CDREventEnd {
|
||||||
json.addProperty("id", this.recording.getId());
|
json.addProperty("id", this.recording.getId());
|
||||||
json.addProperty("name", this.recording.getName());
|
json.addProperty("name", this.recording.getName());
|
||||||
json.addProperty("outputMode", this.recording.getOutputMode().name());
|
json.addProperty("outputMode", this.recording.getOutputMode().name());
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recording.getOutputMode())) {
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recording.getOutputMode()) && this.recording.hasVideo()) {
|
||||||
json.addProperty("resolution", this.recording.getResolution());
|
json.addProperty("resolution", this.recording.getResolution());
|
||||||
json.addProperty("recordingLayout", this.recording.getRecordingLayout().name());
|
json.addProperty("recordingLayout", this.recording.getRecordingLayout().name());
|
||||||
if (RecordingLayout.CUSTOM.equals(this.recording.getRecordingLayout())
|
if (RecordingLayout.CUSTOM.equals(this.recording.getRecordingLayout())
|
||||||
|
|
|
@ -69,7 +69,7 @@ import io.openvidu.server.recording.service.RecordingManager;
|
||||||
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
* - webrtcConnectionDestroyed.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||||
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||||
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
||||||
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "openviduServerStopped"
|
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "automaticStop", "openviduServerStopped"
|
||||||
*
|
*
|
||||||
* [OPTIONAL_PROPERTIES]:
|
* [OPTIONAL_PROPERTIES]:
|
||||||
* - receivingFrom: only if connection = "INBOUND"
|
* - receivingFrom: only if connection = "INBOUND"
|
||||||
|
|
|
@ -45,6 +45,7 @@ import io.openvidu.server.coturn.CoturnCredentialsService;
|
||||||
import io.openvidu.server.coturn.TurnCredentials;
|
import io.openvidu.server.coturn.TurnCredentials;
|
||||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||||
import io.openvidu.server.recording.service.RecordingManager;
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
|
import io.openvidu.server.utils.FormatChecker;
|
||||||
|
|
||||||
public abstract class SessionManager {
|
public abstract class SessionManager {
|
||||||
|
|
||||||
|
@ -65,6 +66,8 @@ public abstract class SessionManager {
|
||||||
@Autowired
|
@Autowired
|
||||||
protected CoturnCredentialsService coturnCredentialsService;
|
protected CoturnCredentialsService coturnCredentialsService;
|
||||||
|
|
||||||
|
public FormatChecker formatChecker = new FormatChecker();
|
||||||
|
|
||||||
protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
|
protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
|
||||||
protected ConcurrentMap<String, SessionProperties> sessionProperties = new ConcurrentHashMap<>();
|
protected ConcurrentMap<String, SessionProperties> sessionProperties = new ConcurrentHashMap<>();
|
||||||
protected ConcurrentMap<String, Long> sessionCreationTime = new ConcurrentHashMap<>();
|
protected ConcurrentMap<String, Long> sessionCreationTime = new ConcurrentHashMap<>();
|
||||||
|
@ -223,7 +226,7 @@ public abstract class SessionManager {
|
||||||
new ConcurrentHashMap<>());
|
new ConcurrentHashMap<>());
|
||||||
if (map != null) {
|
if (map != null) {
|
||||||
|
|
||||||
if (!isMetadataFormatCorrect(serverMetadata)) {
|
if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) {
|
||||||
log.error("Data invalid format");
|
log.error("Data invalid format");
|
||||||
throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format");
|
throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format");
|
||||||
}
|
}
|
||||||
|
@ -317,10 +320,6 @@ public abstract class SessionManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMetadataFormatCorrect(String metadata) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void newInsecureParticipant(String participantPrivateId) {
|
public void newInsecureParticipant(String participantPrivateId) {
|
||||||
this.insecureUsers.put(participantPrivateId, true);
|
this.insecureUsers.put(participantPrivateId, true);
|
||||||
}
|
}
|
||||||
|
@ -443,7 +442,7 @@ public abstract class SessionManager {
|
||||||
return participants;
|
return participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void closeSessionAndEmptyCollections(Session session, String reason) {
|
public void closeSessionAndEmptyCollections(Session session, String reason) {
|
||||||
|
|
||||||
if (openviduConfig.isRecordingModuleEnabled()
|
if (openviduConfig.isRecordingModuleEnabled()
|
||||||
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
|
|
|
@ -238,7 +238,7 @@ public class KurentoParticipant extends Participant {
|
||||||
this.session.getSessionId());
|
this.session.getSessionId());
|
||||||
|
|
||||||
if (this.openviduConfig.isRecordingModuleEnabled()
|
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
|
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,7 +438,7 @@ public class KurentoParticipant extends Participant {
|
||||||
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
||||||
|
|
||||||
if (this.openviduConfig.isRecordingModuleEnabled()
|
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||||
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
|
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
|
||||||
this.getPublisherStreamId());
|
this.getPublisherStreamId());
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,12 +172,14 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
if (remainingParticipants.isEmpty()) {
|
if (remainingParticipants.isEmpty()) {
|
||||||
if (openviduConfig.isRecordingModuleEnabled()
|
if (openviduConfig.isRecordingModuleEnabled()
|
||||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)) {
|
&& (this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)
|
||||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
|| (this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||||
// a Publisher starts before timeout)
|
&& this.recordingManager.sessionIsBeingRecordedOnlyAudio(sessionId)))) {
|
||||||
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
// Start countdown to stop recording if INDIVIDUAL mode or COMPOSED audio-only
|
||||||
sessionId);
|
// (will be aborted if a Publisher starts before timeout)
|
||||||
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
log.info("Last participant left. Starting {} seconds countdown for stopping recording of session {}",
|
||||||
|
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
|
||||||
|
recordingManager.initAutomaticRecordingStopThread(session);
|
||||||
} else {
|
} else {
|
||||||
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
||||||
this.closeSessionAndEmptyCollections(session, reason);
|
this.closeSessionAndEmptyCollections(session, reason);
|
||||||
|
@ -186,6 +188,7 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
||||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||||
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||||
|
&& !this.recordingManager.sessionIsBeingRecordedOnlyAudio(sessionId)
|
||||||
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
||||||
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
||||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
||||||
|
@ -198,9 +201,9 @@ public class KurentoSessionManager extends SessionManager {
|
||||||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
||||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||||
// a Publisher starts before timeout)
|
// a Publisher starts before timeout)
|
||||||
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
log.info("Last participant left. Starting {} seconds countdown for stopping recording of session {}",
|
||||||
sessionId);
|
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
|
||||||
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
recordingManager.initAutomaticRecordingStopThread(session);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
/*
|
||||||
|
* (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.openvidu.server.recording;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import org.kurento.client.Composite;
|
||||||
|
import org.kurento.client.ErrorEvent;
|
||||||
|
import org.kurento.client.EventListener;
|
||||||
|
import org.kurento.client.HubPort;
|
||||||
|
import org.kurento.client.MediaProfileSpecType;
|
||||||
|
import org.kurento.client.RecorderEndpoint;
|
||||||
|
import org.kurento.client.RecordingEvent;
|
||||||
|
import org.kurento.client.StoppedEvent;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import io.openvidu.client.OpenViduException;
|
||||||
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
import io.openvidu.server.kurento.core.KurentoSession;
|
||||||
|
import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||||
|
|
||||||
|
public class CompositeWrapper {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(CompositeWrapper.class);
|
||||||
|
|
||||||
|
KurentoSession session;
|
||||||
|
Composite composite;
|
||||||
|
RecorderEndpoint recorderEndpoint;
|
||||||
|
HubPort compositeToRecorderHubPort;
|
||||||
|
Map<String, HubPort> hubPorts = new ConcurrentHashMap<>();
|
||||||
|
Map<String, PublisherEndpoint> publisherEndpoints = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
AtomicBoolean isRecording = new AtomicBoolean(false);
|
||||||
|
long startTime;
|
||||||
|
long endTime;
|
||||||
|
long size;
|
||||||
|
|
||||||
|
public CompositeWrapper(KurentoSession session, String path) {
|
||||||
|
this.session = session;
|
||||||
|
this.composite = new Composite.Builder(session.getPipeline()).build();
|
||||||
|
this.recorderEndpoint = new RecorderEndpoint.Builder(composite.getMediaPipeline(), path)
|
||||||
|
.withMediaProfile(MediaProfileSpecType.WEBM_AUDIO_ONLY).build();
|
||||||
|
this.compositeToRecorderHubPort = new HubPort.Builder(composite).build();
|
||||||
|
this.compositeToRecorderHubPort.connect(recorderEndpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void startCompositeRecording(CountDownLatch startLatch) {
|
||||||
|
|
||||||
|
this.recorderEndpoint.addRecordingListener(new EventListener<RecordingEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(RecordingEvent event) {
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
log.info("Recording started event for audio-only RecorderEndpoint of Composite in session {}",
|
||||||
|
session.getSessionId());
|
||||||
|
startLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recorderEndpoint.addErrorListener(new EventListener<ErrorEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(ErrorEvent event) {
|
||||||
|
log.error(event.getErrorCode() + " " + event.getDescription());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.recorderEndpoint.record();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void stopCompositeRecording(CountDownLatch stopLatch) {
|
||||||
|
this.recorderEndpoint.addStoppedListener(new EventListener<StoppedEvent>() {
|
||||||
|
@Override
|
||||||
|
public void onEvent(StoppedEvent event) {
|
||||||
|
endTime = System.currentTimeMillis();
|
||||||
|
log.info("Recording stopped event for audio-only RecorderEndpoint of Composite in session {}",
|
||||||
|
session.getSessionId());
|
||||||
|
recorderEndpoint.release();
|
||||||
|
stopLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.recorderEndpoint.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void connectPublisherEndpoint(PublisherEndpoint endpoint) throws OpenViduException {
|
||||||
|
HubPort hubPort = new HubPort.Builder(composite).build();
|
||||||
|
endpoint.connect(hubPort);
|
||||||
|
String streamId = endpoint.getOwner().getPublisherStreamId();
|
||||||
|
this.hubPorts.put(streamId, hubPort);
|
||||||
|
this.publisherEndpoints.put(streamId, endpoint);
|
||||||
|
|
||||||
|
if (isRecording.compareAndSet(false, true)) {
|
||||||
|
// First user publishing. Starting RecorderEndpoint
|
||||||
|
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||||
|
this.startCompositeRecording(startLatch);
|
||||||
|
try {
|
||||||
|
if (!startLatch.await(5, TimeUnit.SECONDS)) {
|
||||||
|
log.error("Error waiting for RecorderEndpoint of Composite to start in session {}",
|
||||||
|
session.getSessionId());
|
||||||
|
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||||
|
"Couldn't initialize RecorderEndpoint of Composite");
|
||||||
|
}
|
||||||
|
log.info("RecorderEnpoint of Composite is now recording for session {}", session.getSessionId());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Exception while waiting for state change", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Composite for session {} has now {} connected publishers", this.session.getSessionId(),
|
||||||
|
this.composite.getChildren().size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnectPublisherEndpoint(String streamId) {
|
||||||
|
HubPort hubPort = this.hubPorts.remove(streamId);
|
||||||
|
PublisherEndpoint publisherEndpoint = this.publisherEndpoints.remove(streamId);
|
||||||
|
publisherEndpoint.disconnectFrom(hubPort);
|
||||||
|
hubPort.release();
|
||||||
|
log.info("Composite for session {} has now {} connected publishers", this.session.getSessionId(),
|
||||||
|
this.composite.getChildren().size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disconnectAllPublisherEndpoints() {
|
||||||
|
this.publisherEndpoints.keySet().forEach(streamId -> {
|
||||||
|
PublisherEndpoint endpoint = this.publisherEndpoints.get(streamId);
|
||||||
|
HubPort hubPort = this.hubPorts.get(streamId);
|
||||||
|
endpoint.disconnectFrom(hubPort);
|
||||||
|
hubPort.release();
|
||||||
|
});
|
||||||
|
this.hubPorts.clear();
|
||||||
|
this.publisherEndpoints.clear();
|
||||||
|
this.composite.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDuration() {
|
||||||
|
return this.endTime - this.startTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,19 +23,19 @@ import com.google.gson.JsonObject;
|
||||||
|
|
||||||
public class RecorderEndpointWrapper {
|
public class RecorderEndpointWrapper {
|
||||||
|
|
||||||
RecorderEndpoint recorder;
|
private RecorderEndpoint recorder;
|
||||||
String connectionId;
|
private String connectionId;
|
||||||
String recordingId;
|
private String recordingId;
|
||||||
String streamId;
|
private String streamId;
|
||||||
String clientData;
|
private String clientData;
|
||||||
String serverData;
|
private String serverData;
|
||||||
boolean hasAudio;
|
private boolean hasAudio;
|
||||||
boolean hasVideo;
|
private boolean hasVideo;
|
||||||
String typeOfVideo;
|
private String typeOfVideo;
|
||||||
|
|
||||||
long startTime;
|
private long startTime;
|
||||||
long endTime;
|
private long endTime;
|
||||||
long size;
|
private long size;
|
||||||
|
|
||||||
public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId,
|
public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId,
|
||||||
String clientData, String serverData, boolean hasAudio, boolean hasVideo, String typeOfVideo) {
|
String clientData, String serverData, boolean hasAudio, boolean hasVideo, String typeOfVideo) {
|
||||||
|
|
|
@ -70,9 +70,8 @@ public class Recording {
|
||||||
io.openvidu.java.client.Recording.OutputMode outputMode = io.openvidu.java.client.Recording.OutputMode
|
io.openvidu.java.client.Recording.OutputMode outputMode = io.openvidu.java.client.Recording.OutputMode
|
||||||
.valueOf(json.get("outputMode").getAsString());
|
.valueOf(json.get("outputMode").getAsString());
|
||||||
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
||||||
.outputMode(outputMode).hasAudio(json.get("hasAudio").getAsBoolean())
|
.outputMode(outputMode).hasAudio(this.hasAudio).hasVideo(this.hasVideo);
|
||||||
.hasVideo(json.get("hasVideo").getAsBoolean());
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode) && this.hasVideo) {
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode)) {
|
|
||||||
this.resolution = json.get("resolution").getAsString();
|
this.resolution = json.get("resolution").getAsString();
|
||||||
builder.resolution(this.resolution);
|
builder.resolution(this.resolution);
|
||||||
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
|
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
|
||||||
|
@ -189,7 +188,8 @@ public class Recording {
|
||||||
json.addProperty("id", this.id);
|
json.addProperty("id", this.id);
|
||||||
json.addProperty("name", this.recordingProperties.name());
|
json.addProperty("name", this.recordingProperties.name());
|
||||||
json.addProperty("outputMode", this.getOutputMode().name());
|
json.addProperty("outputMode", this.getOutputMode().name());
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recordingProperties.outputMode())) {
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(this.recordingProperties.outputMode())
|
||||||
|
&& this.hasVideo) {
|
||||||
json.addProperty("resolution", this.resolution);
|
json.addProperty("resolution", this.resolution);
|
||||||
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
|
json.addProperty("recordingLayout", this.recordingProperties.recordingLayout().name());
|
||||||
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
if (RecordingLayout.CUSTOM.equals(this.recordingProperties.recordingLayout())) {
|
||||||
|
|
|
@ -26,8 +26,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.ws.rs.ProcessingException;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
@ -37,8 +35,6 @@ import com.github.dockerjava.api.command.CreateContainerCmd;
|
||||||
import com.github.dockerjava.api.command.CreateContainerResponse;
|
import com.github.dockerjava.api.command.CreateContainerResponse;
|
||||||
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
|
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
|
||||||
import com.github.dockerjava.api.exception.ConflictException;
|
import com.github.dockerjava.api.exception.ConflictException;
|
||||||
import com.github.dockerjava.api.exception.DockerClientException;
|
|
||||||
import com.github.dockerjava.api.exception.InternalServerErrorException;
|
|
||||||
import com.github.dockerjava.api.exception.NotFoundException;
|
import com.github.dockerjava.api.exception.NotFoundException;
|
||||||
import com.github.dockerjava.api.model.Bind;
|
import com.github.dockerjava.api.model.Bind;
|
||||||
import com.github.dockerjava.api.model.Volume;
|
import com.github.dockerjava.api.model.Volume;
|
||||||
|
@ -46,7 +42,6 @@ import com.github.dockerjava.core.DefaultDockerClientConfig;
|
||||||
import com.github.dockerjava.core.DockerClientBuilder;
|
import com.github.dockerjava.core.DockerClientBuilder;
|
||||||
import com.github.dockerjava.core.DockerClientConfig;
|
import com.github.dockerjava.core.DockerClientConfig;
|
||||||
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
||||||
import com.github.dockerjava.core.command.PullImageResultCallback;
|
|
||||||
|
|
||||||
import io.openvidu.client.OpenViduException;
|
import io.openvidu.client.OpenViduException;
|
||||||
import io.openvidu.client.OpenViduException.Code;
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
@ -54,7 +49,11 @@ import io.openvidu.java.client.RecordingLayout;
|
||||||
import io.openvidu.java.client.RecordingProperties;
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
import io.openvidu.server.OpenViduServer;
|
import io.openvidu.server.OpenViduServer;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
|
import io.openvidu.server.core.Participant;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
|
import io.openvidu.server.kurento.core.KurentoParticipant;
|
||||||
|
import io.openvidu.server.kurento.core.KurentoSession;
|
||||||
|
import io.openvidu.server.recording.CompositeWrapper;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
import io.openvidu.server.recording.RecordingInfoUtils;
|
import io.openvidu.server.recording.RecordingInfoUtils;
|
||||||
|
|
||||||
|
@ -64,11 +63,9 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
|
|
||||||
private Map<String, String> containers = new ConcurrentHashMap<>();
|
private Map<String, String> containers = new ConcurrentHashMap<>();
|
||||||
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
|
private Map<String, String> sessionsContainers = new ConcurrentHashMap<>();
|
||||||
|
private Map<String, CompositeWrapper> composites = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
private final String IMAGE_NAME = "openvidu/openvidu-recording";
|
DockerClient dockerClient;
|
||||||
private String IMAGE_TAG;
|
|
||||||
|
|
||||||
private DockerClient dockerClient;
|
|
||||||
|
|
||||||
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
super(recordingManager, openviduConfig);
|
super(recordingManager, openviduConfig);
|
||||||
|
@ -78,25 +75,69 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||||
List<String> envs = new ArrayList<>();
|
|
||||||
|
|
||||||
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
||||||
properties);
|
properties);
|
||||||
properties = updatePropertiesAndRecordingId.properties;
|
properties = updatePropertiesAndRecordingId.properties;
|
||||||
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||||
|
|
||||||
|
// Instantiate and store recording object
|
||||||
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||||
|
|
||||||
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
|
|
||||||
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
|
||||||
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
||||||
|
|
||||||
|
if (properties.hasVideo()) {
|
||||||
|
// Docker container used
|
||||||
|
recording = this.startRecordingWithVideo(session, recording, properties);
|
||||||
|
} else {
|
||||||
|
// Kurento composite used
|
||||||
|
recording = this.startRecordingAudioOnly(session, recording, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update collections and return recording
|
||||||
|
this.updateCollectionsAndSendNotifCauseRecordingStarted(session, recording);
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Recording stopRecording(Session session, Recording recording, String reason) {
|
||||||
|
if (recording.hasVideo()) {
|
||||||
|
return this.stopRecordingWithVideo(session, recording, reason);
|
||||||
|
} else {
|
||||||
|
return this.stopRecordingAudioOnly(session, recording, reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void joinPublisherEndpointToComposite(Session session, String recordingId, Participant participant)
|
||||||
|
throws OpenViduException {
|
||||||
|
KurentoParticipant kurentoParticipant = (KurentoParticipant) participant;
|
||||||
|
CompositeWrapper compositeWrapper = this.composites.get(session.getSessionId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
compositeWrapper.connectPublisherEndpoint(kurentoParticipant.getPublisher());
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
if (Code.RECORDING_START_ERROR_CODE.getValue() == e.getCodeValue()) {
|
||||||
|
// First user publishing triggered RecorderEnpoint start, but it failed
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removePublisherEndpointFromComposite(String sessionId, String streamId) {
|
||||||
|
CompositeWrapper compositeWrapper = this.composites.get(sessionId);
|
||||||
|
compositeWrapper.disconnectPublisherEndpoint(streamId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recording startRecordingWithVideo(Session session, Recording recording, RecordingProperties properties)
|
||||||
|
throws OpenViduException {
|
||||||
|
List<String> envs = new ArrayList<>();
|
||||||
|
|
||||||
String layoutUrl = this.getLayoutUrl(recording, this.getShortSessionId(session));
|
String layoutUrl = this.getLayoutUrl(recording, this.getShortSessionId(session));
|
||||||
|
|
||||||
envs.add("URL=" + layoutUrl);
|
envs.add("URL=" + layoutUrl);
|
||||||
|
envs.add("ONLY_VIDEO=" + !properties.hasAudio());
|
||||||
envs.add("RESOLUTION=" + properties.resolution());
|
envs.add("RESOLUTION=" + properties.resolution());
|
||||||
envs.add("FRAMERATE=30");
|
envs.add("FRAMERATE=30");
|
||||||
envs.add("VIDEO_ID=" + recordingId);
|
envs.add("VIDEO_ID=" + recording.getId());
|
||||||
envs.add("VIDEO_NAME=" + properties.name());
|
envs.add("VIDEO_NAME=" + properties.name());
|
||||||
envs.add("VIDEO_FORMAT=mp4");
|
envs.add("VIDEO_FORMAT=mp4");
|
||||||
envs.add("RECORDING_JSON=" + recording.toJson().toString());
|
envs.add("RECORDING_JSON=" + recording.toJson().toString());
|
||||||
|
@ -106,28 +147,56 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
|
|
||||||
String containerId;
|
String containerId;
|
||||||
try {
|
try {
|
||||||
containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
containerId = this.runRecordingContainer(envs, "recording_" + recording.getId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
this.cleanRecordingMapsAndReturnContainerId(recording);
|
this.cleanRecordingMaps(recording);
|
||||||
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
throw this.failStartRecording(session, recording,
|
||||||
"Couldn't initialize recording container. Error: " + e.getMessage());
|
"Couldn't initialize recording container. Error: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.waitForVideoFileNotEmpty(recording);
|
|
||||||
|
|
||||||
this.sessionsContainers.put(session.getSessionId(), containerId);
|
this.sessionsContainers.put(session.getSessionId(), containerId);
|
||||||
|
|
||||||
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
|
try {
|
||||||
|
this.waitForVideoFileNotEmpty(recording);
|
||||||
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
} catch (OpenViduException e) {
|
||||||
this.recordingManager.startingRecordings.remove(recording.getId());
|
this.cleanRecordingMaps(recording);
|
||||||
|
throw this.failStartRecording(session, recording,
|
||||||
|
"Couldn't initialize recording container. Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private Recording startRecordingAudioOnly(Session session, Recording recording, RecordingProperties properties)
|
||||||
public Recording stopRecording(Session session, Recording recording, String reason) {
|
throws OpenViduException {
|
||||||
String containerId = cleanRecordingMapsAndReturnContainerId(recording);
|
|
||||||
|
CompositeWrapper compositeWrapper = new CompositeWrapper((KurentoSession) session,
|
||||||
|
"file://" + this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/" + properties.name()
|
||||||
|
+ ".webm");
|
||||||
|
this.composites.put(session.getSessionId(), compositeWrapper);
|
||||||
|
|
||||||
|
for (Participant p : session.getParticipants()) {
|
||||||
|
if (p.isStreaming()) {
|
||||||
|
try {
|
||||||
|
this.joinPublisherEndpointToComposite(session, recording.getId(), p);
|
||||||
|
} catch (OpenViduException e) {
|
||||||
|
log.error("Error waiting for RecorderEndpooint of Composite to start in session {}",
|
||||||
|
session.getSessionId());
|
||||||
|
throw this.failStartRecording(session, recording, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.generateRecordingMetadataFile(recording);
|
||||||
|
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recording stopRecordingWithVideo(Session session, Recording recording, String reason) {
|
||||||
|
|
||||||
|
String containerId = this.sessionsContainers.remove(recording.getSessionId());
|
||||||
|
this.cleanRecordingMaps(recording);
|
||||||
|
|
||||||
final String recordingId = recording.getId();
|
final String recordingId = recording.getId();
|
||||||
|
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
|
@ -229,46 +298,64 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
||||||
"There was an error generating the metadata report file for the recording");
|
"There was an error generating the metadata report file for the recording");
|
||||||
}
|
}
|
||||||
if (session != null) {
|
if (session != null && reason != null) {
|
||||||
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean recordingImageExistsLocally() {
|
private Recording stopRecordingAudioOnly(Session session, Recording recording, String reason) {
|
||||||
boolean imageExists = false;
|
String sessionId;
|
||||||
try {
|
if (session == null) {
|
||||||
dockerClient.inspectImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec();
|
log.warn(
|
||||||
imageExists = true;
|
"Existing recording {} does not have an active session associated. This means the recording "
|
||||||
} catch (NotFoundException nfe) {
|
+ "has been automatically stopped after last user left and {} seconds timeout passed",
|
||||||
imageExists = false;
|
recording.getId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout());
|
||||||
} catch (ProcessingException e) {
|
sessionId = recording.getSessionId();
|
||||||
throw e;
|
} else {
|
||||||
|
sessionId = session.getSessionId();
|
||||||
}
|
}
|
||||||
return imageExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void downloadRecordingImage() {
|
CompositeWrapper compositeWrapper = this.composites.remove(sessionId);
|
||||||
|
|
||||||
|
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
|
||||||
|
compositeWrapper.stopCompositeRecording(stoppedCountDown);
|
||||||
try {
|
try {
|
||||||
dockerClient.pullImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec(new PullImageResultCallback()).awaitSuccess();
|
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||||
} catch (NotFoundException | InternalServerErrorException e) {
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
if (imageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) {
|
log.error("Error waiting for RecorderEndpoint of Composite to stop in session {}",
|
||||||
log.info("Docker image '{}' exists locally", IMAGE_NAME + ":" + IMAGE_TAG);
|
recording.getSessionId());
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
} catch (DockerClientException e) {
|
} catch (InterruptedException e) {
|
||||||
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution",
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
IMAGE_NAME + ":" + IMAGE_TAG);
|
log.error("Exception while waiting for state change", e);
|
||||||
throw e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compositeWrapper.disconnectAllPublisherEndpoints();
|
||||||
|
|
||||||
|
this.cleanRecordingMaps(recording);
|
||||||
|
|
||||||
|
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/";
|
||||||
|
File videoFile = new File(filesPath + recording.getName() + ".webm");
|
||||||
|
long finalSize = videoFile.length();
|
||||||
|
long finalDuration = compositeWrapper.getDuration();
|
||||||
|
|
||||||
|
this.sealRecordingMetadataFile(recording, finalSize, finalDuration,
|
||||||
|
filesPath + RecordingManager.RECORDING_ENTITY_FILE + recording.getId());
|
||||||
|
|
||||||
|
if (reason != null && session != null) {
|
||||||
|
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String runRecordingContainer(List<String> envs, String containerName) throws Exception {
|
private String runRecordingContainer(List<String> envs, String containerName) throws Exception {
|
||||||
Volume volume1 = new Volume("/recordings");
|
Volume volume1 = new Volume("/recordings");
|
||||||
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
|
CreateContainerCmd cmd = dockerClient
|
||||||
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
.createContainerCmd(RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG)
|
||||||
|
.withName(containerName).withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
||||||
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
|
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
|
||||||
CreateContainerResponse container = null;
|
CreateContainerResponse container = null;
|
||||||
try {
|
try {
|
||||||
|
@ -283,7 +370,8 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
containerName);
|
containerName);
|
||||||
throw e;
|
throw e;
|
||||||
} catch (NotFoundException e) {
|
} catch (NotFoundException e) {
|
||||||
log.error("Docker image {} couldn't be found in docker host", IMAGE_NAME + ":" + IMAGE_TAG);
|
log.error("Docker image {} couldn't be found in docker host",
|
||||||
|
RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,22 +385,14 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
dockerClient.stopContainerCmd(containerId).exec();
|
dockerClient.stopContainerCmd(containerId).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean imageExistsLocally(String imageName) {
|
private void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException {
|
||||||
boolean imageExists = false;
|
|
||||||
try {
|
|
||||||
dockerClient.inspectImageCmd(imageName).exec();
|
|
||||||
imageExists = true;
|
|
||||||
} catch (NotFoundException nfe) {
|
|
||||||
imageExists = false;
|
|
||||||
}
|
|
||||||
return imageExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitForVideoFileNotEmpty(Recording recording) {
|
|
||||||
boolean isPresent = false;
|
boolean isPresent = false;
|
||||||
while (!isPresent) {
|
int i = 1;
|
||||||
|
int timeout = 150; // Wait for 150*150 = 22500 = 22.5 seconds
|
||||||
|
while (!isPresent && timeout <= 150) {
|
||||||
try {
|
try {
|
||||||
Thread.sleep(150);
|
Thread.sleep(150);
|
||||||
|
timeout++;
|
||||||
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
|
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
|
||||||
+ recording.getName() + ".mp4");
|
+ recording.getName() + ".mp4");
|
||||||
isPresent = ((f.isFile()) && (f.length() > 0));
|
isPresent = ((f.isFile()) && (f.length() > 0));
|
||||||
|
@ -320,9 +400,15 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (i == timeout) {
|
||||||
|
log.error("Recorder container failed generating video file (is empty) for session {}",
|
||||||
|
recording.getSessionId());
|
||||||
|
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||||
|
"Recorder container failed generating video file (is empty)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void failRecordingCompletion(String containerId, OpenViduException e) {
|
private void failRecordingCompletion(String containerId, OpenViduException e) throws OpenViduException {
|
||||||
this.stopDockerContainer(containerId);
|
this.stopDockerContainer(containerId);
|
||||||
this.removeDockerContainer(containerId);
|
this.removeDockerContainer(containerId);
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -351,14 +437,4 @@ public class ComposedRecordingService extends RecordingService {
|
||||||
return finalUrl;
|
return finalUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,16 +44,22 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.exception.DockerClientException;
|
||||||
|
import com.github.dockerjava.api.exception.InternalServerErrorException;
|
||||||
|
import com.github.dockerjava.api.exception.NotFoundException;
|
||||||
|
import com.github.dockerjava.core.command.PullImageResultCallback;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gson.JsonParser;
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
import io.openvidu.client.OpenViduException;
|
import io.openvidu.client.OpenViduException;
|
||||||
import io.openvidu.client.OpenViduException.Code;
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
|
import io.openvidu.client.internal.ProtocolElements;
|
||||||
import io.openvidu.java.client.RecordingProperties;
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.core.Participant;
|
import io.openvidu.server.core.Participant;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.core.SessionEventsHandler;
|
import io.openvidu.server.core.SessionEventsHandler;
|
||||||
|
import io.openvidu.server.core.SessionManager;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
|
|
||||||
public class RecordingManager {
|
public class RecordingManager {
|
||||||
|
@ -67,6 +73,9 @@ public class RecordingManager {
|
||||||
@Autowired
|
@Autowired
|
||||||
protected SessionEventsHandler sessionHandler;
|
protected SessionEventsHandler sessionHandler;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SessionManager sessionManager;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
protected OpenviduConfig openviduConfig;
|
protected OpenviduConfig openviduConfig;
|
||||||
|
|
||||||
|
@ -79,6 +88,8 @@ public class RecordingManager {
|
||||||
Runtime.getRuntime().availableProcessors());
|
Runtime.getRuntime().availableProcessors());
|
||||||
|
|
||||||
static final String RECORDING_ENTITY_FILE = ".recording.";
|
static final String RECORDING_ENTITY_FILE = ".recording.";
|
||||||
|
static final String IMAGE_NAME = "openvidu/openvidu-recording";
|
||||||
|
static String IMAGE_TAG;
|
||||||
|
|
||||||
private static final List<String> LAST_PARTICIPANT_LEFT_REASONS = Arrays.asList(
|
private static final List<String> LAST_PARTICIPANT_LEFT_REASONS = Arrays.asList(
|
||||||
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
|
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
|
||||||
|
@ -89,18 +100,17 @@ public class RecordingManager {
|
||||||
|
|
||||||
public void initializeRecordingManager() {
|
public void initializeRecordingManager() {
|
||||||
|
|
||||||
|
RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion();
|
||||||
|
|
||||||
this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
|
this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
|
||||||
this.singleStreamRecordingService = new SingleStreamRecordingService(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:"
|
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
|
||||||
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
|
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
|
||||||
|
|
||||||
boolean imageExists = false;
|
boolean imageExists = false;
|
||||||
try {
|
try {
|
||||||
imageExists = recServiceAux.recordingImageExistsLocally();
|
imageExists = this.recordingImageExistsLocally();
|
||||||
} catch (ProcessingException exception) {
|
} catch (ProcessingException exception) {
|
||||||
String message = "Exception connecting to Docker daemon: ";
|
String message = "Exception connecting to Docker daemon: ";
|
||||||
if ("docker".equals(openviduConfig.getSpringProfile())) {
|
if ("docker".equals(openviduConfig.getSpringProfile())) {
|
||||||
|
@ -133,7 +143,7 @@ public class RecordingManager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
t.start();
|
t.start();
|
||||||
recServiceAux.downloadRecordingImage();
|
this.downloadRecordingImage();
|
||||||
t.interrupt();
|
t.interrupt();
|
||||||
try {
|
try {
|
||||||
t.join();
|
t.join();
|
||||||
|
@ -142,9 +152,8 @@ public class RecordingManager {
|
||||||
}
|
}
|
||||||
log.info("Docker image available");
|
log.info("Docker image available");
|
||||||
}
|
}
|
||||||
this.initRecordingPath();
|
|
||||||
|
|
||||||
this.recordingService = recServiceAux;
|
this.initRecordingPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||||
|
@ -164,7 +173,9 @@ public class RecordingManager {
|
||||||
if (session.getActivePublishers() == 0) {
|
if (session.getActivePublishers() == 0) {
|
||||||
// Init automatic recording stop if there are now publishers when starting
|
// Init automatic recording stop if there are now publishers when starting
|
||||||
// recording
|
// recording
|
||||||
this.initAutomaticRecordingStopThread(session.getSessionId());
|
log.info("No publisher in session {}. Starting {} seconds countdown for stopping recording",
|
||||||
|
session.getSessionId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout());
|
||||||
|
this.initAutomaticRecordingStopThread(session);
|
||||||
}
|
}
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
@ -172,9 +183,9 @@ public class RecordingManager {
|
||||||
public Recording stopRecording(Session session, String recordingId, String reason) {
|
public Recording stopRecording(Session session, String recordingId, String reason) {
|
||||||
Recording recording;
|
Recording recording;
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
recording = this.startedRecordings.remove(recordingId);
|
recording = this.startedRecordings.get(recordingId);
|
||||||
} else {
|
} else {
|
||||||
recording = this.sessionsRecordings.remove(session.getSessionId());
|
recording = this.sessionsRecordings.get(session.getSessionId());
|
||||||
}
|
}
|
||||||
switch (recording.getOutputMode()) {
|
switch (recording.getOutputMode()) {
|
||||||
case COMPOSED:
|
case COMPOSED:
|
||||||
|
@ -195,17 +206,33 @@ public class RecordingManager {
|
||||||
participant.getPublisherStreamId(), session.getSessionId());
|
participant.getPublisherStreamId(), session.getSessionId());
|
||||||
}
|
}
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
||||||
|
// Start new RecorderEndpoint for this stream
|
||||||
|
log.info("Starting new RecorderEndpoint in session {} for new stream of participant {}",
|
||||||
|
session.getSessionId(), participant.getParticipantPublicId());
|
||||||
final CountDownLatch startedCountDown = new CountDownLatch(1);
|
final CountDownLatch startedCountDown = new CountDownLatch(1);
|
||||||
this.singleStreamRecordingService.startOneIndividualStreamRecording(session, recordingId, profile,
|
this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(session, recordingId, profile,
|
||||||
participant, startedCountDown);
|
participant, startedCountDown);
|
||||||
|
} else if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(recording.getOutputMode())
|
||||||
|
&& !recording.hasVideo()) {
|
||||||
|
// Connect this stream to existing Composite recorder
|
||||||
|
log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}",
|
||||||
|
session.getSessionId(), participant.getParticipantPublicId());
|
||||||
|
this.composedRecordingService.joinPublisherEndpointToComposite(session, recordingId, participant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopOneIndividualStreamRecording(String sessionId, String streamId) {
|
public void stopOneIndividualStreamRecording(String sessionId, String streamId) {
|
||||||
Recording recording = this.sessionsRecordings.get(sessionId);
|
Recording recording = this.sessionsRecordings.get(sessionId);
|
||||||
|
if (recording == null) {
|
||||||
|
log.error("Cannot stop recording of existing stream {}. Session {} is not being recorded", streamId,
|
||||||
|
sessionId);
|
||||||
|
}
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
if (io.openvidu.java.client.Recording.OutputMode.INDIVIDUAL.equals(recording.getOutputMode())) {
|
||||||
|
// Stop specific RecorderEndpoint for this stream
|
||||||
|
log.info("Stopping RecorderEndpoint in session {} for stream of participant {}", sessionId, streamId);
|
||||||
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
|
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
|
||||||
this.singleStreamRecordingService.stopOneIndividualStreamRecording(sessionId, streamId, stoppedCountDown);
|
this.singleStreamRecordingService.stopRecorderEndpointOfPublisherEndpoint(sessionId, streamId,
|
||||||
|
stoppedCountDown);
|
||||||
try {
|
try {
|
||||||
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||||
log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
|
log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
|
||||||
|
@ -214,6 +241,12 @@ public class RecordingManager {
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
log.error("Exception while waiting for state change", e);
|
log.error("Exception while waiting for state change", e);
|
||||||
}
|
}
|
||||||
|
} else if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(recording.getOutputMode())
|
||||||
|
&& !recording.hasVideo()) {
|
||||||
|
// Disconnect this stream from existing Composite recorder
|
||||||
|
log.info("Removing PublisherEndpoint from Composite in session {} for stream of participant {}", sessionId,
|
||||||
|
streamId);
|
||||||
|
this.composedRecordingService.removePublisherEndpointFromComposite(sessionId, streamId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,6 +264,11 @@ public class RecordingManager {
|
||||||
return (rec != null && io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(rec.getOutputMode()));
|
return (rec != null && io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(rec.getOutputMode()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean sessionIsBeingRecordedOnlyAudio(String sessionId) {
|
||||||
|
Recording rec = this.sessionsRecordings.get(sessionId);
|
||||||
|
return (rec != null && !rec.hasVideo());
|
||||||
|
}
|
||||||
|
|
||||||
public Recording getStartedRecording(String recordingId) {
|
public Recording getStartedRecording(String recordingId) {
|
||||||
return this.startedRecordings.get(recordingId);
|
return this.startedRecordings.get(recordingId);
|
||||||
}
|
}
|
||||||
|
@ -314,14 +352,27 @@ public class RecordingManager {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initAutomaticRecordingStopThread(String sessionId) {
|
public void initAutomaticRecordingStopThread(final Session session) {
|
||||||
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
|
final String recordingId = this.sessionsRecordings.get(session.getSessionId()).getId();
|
||||||
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
|
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
|
||||||
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
|
|
||||||
this.stopRecording(null, recordingId, "lastParticipantLeft");
|
log.info("Stopping recording {} after {} seconds wait (no publisher published before timeout)", recordingId,
|
||||||
this.automaticRecordingStopThreads.remove(sessionId);
|
this.openviduConfig.getOpenviduRecordingAutostopTimeout());
|
||||||
}, 2, TimeUnit.MINUTES);
|
|
||||||
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
|
this.stopRecording(null, recordingId, "automaticStop");
|
||||||
|
this.automaticRecordingStopThreads.remove(session.getSessionId());
|
||||||
|
|
||||||
|
if (session.getParticipants().size() == 0 || (session.getParticipants().size() == 1
|
||||||
|
&& session.getParticipantByPublicId(ProtocolElements.RECORDER_PARTICIPANT_PUBLICID) != null)) {
|
||||||
|
// Close session if there are no participants connected (except for RECORDER).
|
||||||
|
// This code won't be executed only when some user reconnects to the session
|
||||||
|
// but never publishing (publishers automatically abort this thread)
|
||||||
|
sessionManager.closeSessionAndEmptyCollections(session, "automaticStop");
|
||||||
|
sessionManager.showTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
}, this.openviduConfig.getOpenviduRecordingAutostopTimeout(), TimeUnit.SECONDS);
|
||||||
|
this.automaticRecordingStopThreads.putIfAbsent(session.getSessionId(), future);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean abortAutomaticRecordingStopThread(String sessionId) {
|
public boolean abortAutomaticRecordingStopThread(String sessionId) {
|
||||||
|
@ -340,7 +391,7 @@ public class RecordingManager {
|
||||||
String extension;
|
String extension;
|
||||||
switch (recording.getOutputMode()) {
|
switch (recording.getOutputMode()) {
|
||||||
case COMPOSED:
|
case COMPOSED:
|
||||||
extension = "mp4";
|
extension = recording.hasVideo() ? "mp4" : "webm";
|
||||||
break;
|
break;
|
||||||
case INDIVIDUAL:
|
case INDIVIDUAL:
|
||||||
extension = "zip";
|
extension = "zip";
|
||||||
|
@ -357,6 +408,36 @@ public class RecordingManager {
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean recordingImageExistsLocally() {
|
||||||
|
boolean imageExists = false;
|
||||||
|
try {
|
||||||
|
this.composedRecordingService.dockerClient.inspectImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec();
|
||||||
|
imageExists = true;
|
||||||
|
} catch (NotFoundException nfe) {
|
||||||
|
imageExists = false;
|
||||||
|
} catch (ProcessingException e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return imageExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void downloadRecordingImage() {
|
||||||
|
try {
|
||||||
|
this.composedRecordingService.dockerClient.pullImageCmd(IMAGE_NAME + ":" + IMAGE_TAG)
|
||||||
|
.exec(new PullImageResultCallback()).awaitSuccess();
|
||||||
|
} catch (NotFoundException | InternalServerErrorException e) {
|
||||||
|
if (recordingImageExistsLocally()) {
|
||||||
|
log.info("Docker image '{}' exists locally", IMAGE_NAME + ":" + IMAGE_TAG);
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (DockerClientException e) {
|
||||||
|
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution",
|
||||||
|
IMAGE_NAME + ":" + IMAGE_TAG);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Recording getRecordingFromHost(String recordingId) {
|
private Recording getRecordingFromHost(String recordingId) {
|
||||||
log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
||||||
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
|
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
|
||||||
|
|
|
@ -17,17 +17,25 @@
|
||||||
|
|
||||||
package io.openvidu.server.recording.service;
|
package io.openvidu.server.recording.service;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.openvidu.client.OpenViduException;
|
import io.openvidu.client.OpenViduException;
|
||||||
|
import io.openvidu.client.OpenViduException.Code;
|
||||||
import io.openvidu.java.client.RecordingLayout;
|
import io.openvidu.java.client.RecordingLayout;
|
||||||
import io.openvidu.java.client.RecordingProperties;
|
import io.openvidu.java.client.RecordingProperties;
|
||||||
import io.openvidu.server.config.OpenviduConfig;
|
import io.openvidu.server.config.OpenviduConfig;
|
||||||
import io.openvidu.server.core.Session;
|
import io.openvidu.server.core.Session;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
|
import io.openvidu.server.utils.CustomFileWriter;
|
||||||
|
|
||||||
public abstract class RecordingService {
|
public abstract class RecordingService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(RecordingService.class);
|
||||||
|
|
||||||
protected OpenviduConfig openviduConfig;
|
protected OpenviduConfig openviduConfig;
|
||||||
protected RecordingManager recordingManager;
|
protected RecordingManager recordingManager;
|
||||||
|
protected CustomFileWriter fileWriter = new CustomFileWriter();
|
||||||
|
|
||||||
RecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
RecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
this.recordingManager = recordingManager;
|
this.recordingManager = recordingManager;
|
||||||
|
@ -38,11 +46,56 @@ public abstract class RecordingService {
|
||||||
|
|
||||||
public abstract Recording stopRecording(Session session, Recording recording, String reason);
|
public abstract Recording stopRecording(Session session, Recording recording, String reason);
|
||||||
|
|
||||||
protected RecordingProperties setFinalRecordingName(Session session, RecordingProperties properties) {
|
/**
|
||||||
// TODO Auto-generated method stub
|
* Generates metadata recording file (".recording.RECORDING_ID" JSON file to
|
||||||
return null;
|
* store Recording entity)
|
||||||
|
*/
|
||||||
|
protected 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);
|
||||||
|
log.info("Generated recording metadata file at {}", filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update and overwrites metadata recording file with final values on recording
|
||||||
|
* stop (".recording.RECORDING_ID" JSON file to store Recording entity).
|
||||||
|
*
|
||||||
|
* @return updated Recording object
|
||||||
|
*/
|
||||||
|
protected Recording sealRecordingMetadataFile(Recording recording, long size, long duration,
|
||||||
|
String metadataFilePath) {
|
||||||
|
recording.setSize(size); // Size in bytes
|
||||||
|
recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds
|
||||||
|
if (!io.openvidu.java.client.Recording.Status.failed.equals(recording.getStatus())) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
|
||||||
|
}
|
||||||
|
this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString());
|
||||||
|
recording = this.recordingManager.updateRecordingUrl(recording);
|
||||||
|
|
||||||
|
log.info("Sealed recording metadata file at {}", metadataFilePath);
|
||||||
|
|
||||||
|
return recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes recording from starting to started, updates global recording
|
||||||
|
* collections and sends RPC response to clients
|
||||||
|
*/
|
||||||
|
protected void updateCollectionsAndSendNotifCauseRecordingStarted(Session session, Recording 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new available recording identifier (adding a number tag at the end
|
||||||
|
* of the sessionId if it already exists) and rebuilds RecordinProperties object
|
||||||
|
* to set the final value of "name" property
|
||||||
|
*/
|
||||||
protected PropertiesRecordingId setFinalRecordingNameAndGetFreeRecordingId(Session session,
|
protected PropertiesRecordingId setFinalRecordingNameAndGetFreeRecordingId(Session session,
|
||||||
RecordingProperties properties) {
|
RecordingProperties properties) {
|
||||||
String recordingId = this.recordingManager.getFreeRecordingId(session.getSessionId(),
|
String recordingId = this.recordingManager.getFreeRecordingId(session.getSessionId(),
|
||||||
|
@ -52,7 +105,8 @@ public abstract class RecordingService {
|
||||||
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
|
||||||
.outputMode(properties.outputMode()).hasAudio(properties.hasAudio())
|
.outputMode(properties.outputMode()).hasAudio(properties.hasAudio())
|
||||||
.hasVideo(properties.hasVideo());
|
.hasVideo(properties.hasVideo());
|
||||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(properties.outputMode())) {
|
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(properties.outputMode())
|
||||||
|
&& properties.hasVideo()) {
|
||||||
builder.resolution(properties.resolution());
|
builder.resolution(properties.resolution());
|
||||||
builder.recordingLayout(properties.recordingLayout());
|
builder.recordingLayout(properties.recordingLayout());
|
||||||
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
|
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
|
||||||
|
@ -61,6 +115,8 @@ public abstract class RecordingService {
|
||||||
}
|
}
|
||||||
properties = builder.build();
|
properties = builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("New recording id ({}) and final name ({})", recordingId, properties.name());
|
||||||
return new PropertiesRecordingId(properties, recordingId);
|
return new PropertiesRecordingId(properties, recordingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +125,18 @@ public abstract class RecordingService {
|
||||||
session.getSessionId().length());
|
session.getSessionId().length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected OpenViduException failStartRecording(Session session, Recording recording, String errorMessage) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
|
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||||
|
this.stopRecording(session, recording, null);
|
||||||
|
return new OpenViduException(Code.RECORDING_START_ERROR_CODE, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void cleanRecordingMaps(Recording recording) {
|
||||||
|
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
|
||||||
|
this.recordingManager.startedRecordings.remove(recording.getId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple wrapper for returning update RecordingProperties and a free
|
* Simple wrapper for returning update RecordingProperties and a free
|
||||||
* recordingId when starting a new recording
|
* recordingId when starting a new recording
|
||||||
|
|
|
@ -59,14 +59,12 @@ import io.openvidu.server.kurento.endpoint.PublisherEndpoint;
|
||||||
import io.openvidu.server.recording.RecorderEndpointWrapper;
|
import io.openvidu.server.recording.RecorderEndpointWrapper;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
import io.openvidu.server.utils.CommandExecutor;
|
import io.openvidu.server.utils.CommandExecutor;
|
||||||
import io.openvidu.server.utils.CustomFileWriter;
|
|
||||||
|
|
||||||
public class SingleStreamRecordingService extends RecordingService {
|
public class SingleStreamRecordingService extends RecordingService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class);
|
private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class);
|
||||||
|
|
||||||
private Map<String, Map<String, RecorderEndpointWrapper>> recorders = new ConcurrentHashMap<>();
|
private Map<String, Map<String, RecorderEndpointWrapper>> recorders = new ConcurrentHashMap<>();
|
||||||
private CustomFileWriter fileWriter = new CustomFileWriter();
|
|
||||||
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
|
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
|
||||||
|
|
||||||
public SingleStreamRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
public SingleStreamRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||||
|
@ -81,6 +79,9 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
properties = updatePropertiesAndRecordingId.properties;
|
properties = updatePropertiesAndRecordingId.properties;
|
||||||
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||||
|
|
||||||
|
Recording recording = new Recording(session.getSessionId(), recordingId, properties);
|
||||||
|
this.recordingManager.startingRecordings.put(recording.getId(), recording);
|
||||||
|
|
||||||
recorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
|
recorders.put(session.getSessionId(), new ConcurrentHashMap<String, RecorderEndpointWrapper>());
|
||||||
|
|
||||||
final CountDownLatch recordingStartedCountdown = new CountDownLatch(session.getActivePublishers());
|
final CountDownLatch recordingStartedCountdown = new CountDownLatch(session.getActivePublishers());
|
||||||
|
@ -97,23 +98,15 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
p.getPublisherStreamId(), session.getSessionId(), e.getMessage());
|
p.getPublisherStreamId(), session.getSessionId(), e.getMessage());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this.startOneIndividualStreamRecording(session, recordingId, profile, p, recordingStartedCountdown);
|
this.startRecorderEndpointForPublisherEndpoint(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 {
|
try {
|
||||||
if (!recordingStartedCountdown.await(5, TimeUnit.SECONDS)) {
|
if (!recordingStartedCountdown.await(5, TimeUnit.SECONDS)) {
|
||||||
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId());
|
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId());
|
||||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
throw this.failStartRecording(session, recording, "Couldn't initialize some RecorderEndpoint");
|
||||||
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) {
|
} catch (InterruptedException e) {
|
||||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
|
@ -127,11 +120,7 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.generateRecordingMetadataFile(recording);
|
this.generateRecordingMetadataFile(recording);
|
||||||
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
this.updateCollectionsAndSendNotifCauseRecordingStarted(session, 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;
|
return recording;
|
||||||
}
|
}
|
||||||
|
@ -143,22 +132,23 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
final CountDownLatch stoppedCountDown = new CountDownLatch(numberOfActiveRecorders);
|
final CountDownLatch stoppedCountDown = new CountDownLatch(numberOfActiveRecorders);
|
||||||
|
|
||||||
for (RecorderEndpointWrapper wrapper : recorders.get(recording.getSessionId()).values()) {
|
for (RecorderEndpointWrapper wrapper : recorders.get(recording.getSessionId()).values()) {
|
||||||
this.stopOneIndividualStreamRecording(recording.getSessionId(), wrapper.getStreamId(), stoppedCountDown);
|
this.stopRecorderEndpointOfPublisherEndpoint(recording.getSessionId(), wrapper.getStreamId(),
|
||||||
|
stoppedCountDown);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
log.error("Error waiting for some recorder endpoint to stop in session {}", recording.getSessionId());
|
log.error("Error waiting for some recorder endpoint to stop in session {}", recording.getSessionId());
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
|
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||||
log.error("Exception while waiting for state change", e);
|
log.error("Exception while waiting for state change", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
|
this.cleanRecordingMaps(recording);
|
||||||
this.recordingManager.startedRecordings.remove(recording.getId());
|
|
||||||
this.recorders.remove(recording.getSessionId());
|
this.recorders.remove(recording.getSessionId());
|
||||||
|
|
||||||
recording = this.sealMetadataFiles(recording);
|
recording = this.sealMetadataFiles(recording);
|
||||||
recording = this.recordingManager.updateRecordingUrl(recording);
|
|
||||||
|
|
||||||
if (reason != null && session != null) {
|
if (reason != null && session != null) {
|
||||||
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||||
|
@ -167,8 +157,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
|
public void startRecorderEndpointForPublisherEndpoint(Session session, String recordingId,
|
||||||
Participant participant, CountDownLatch globalStartLatch) {
|
MediaProfileSpecType profile, Participant participant, CountDownLatch globalStartLatch) {
|
||||||
log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(),
|
log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(),
|
||||||
session.getSessionId());
|
session.getSessionId());
|
||||||
|
|
||||||
|
@ -224,20 +214,22 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
wrapper.getRecorder().record();
|
wrapper.getRecorder().record();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopOneIndividualStreamRecording(String sessionId, String streamId, CountDownLatch globalStopLatch) {
|
public void stopRecorderEndpointOfPublisherEndpoint(String sessionId, String streamId,
|
||||||
|
CountDownLatch globalStopLatch) {
|
||||||
log.info("Stopping single stream recorder for stream {} in session {}", streamId, sessionId);
|
log.info("Stopping single stream recorder for stream {} in session {}", streamId, sessionId);
|
||||||
RecorderEndpointWrapper wrapper = this.recorders.get(sessionId).remove(streamId);
|
final RecorderEndpointWrapper finalWrapper = this.recorders.get(sessionId).remove(streamId);
|
||||||
if (wrapper != null) {
|
if (finalWrapper != null) {
|
||||||
wrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
|
finalWrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
|
||||||
@Override
|
@Override
|
||||||
public void onEvent(StoppedEvent event) {
|
public void onEvent(StoppedEvent event) {
|
||||||
wrapper.setEndTime(System.currentTimeMillis());
|
finalWrapper.setEndTime(System.currentTimeMillis());
|
||||||
generateIndividualMetadataFile(wrapper);
|
generateIndividualMetadataFile(finalWrapper);
|
||||||
log.info("Recording stopped event for stream {}", streamId);
|
log.info("Recording stopped event for stream {}", streamId);
|
||||||
|
finalWrapper.getRecorder().release();
|
||||||
globalStopLatch.countDown();
|
globalStopLatch.countDown();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
wrapper.getRecorder().stop();
|
finalWrapper.getRecorder().stop();
|
||||||
} else {
|
} else {
|
||||||
log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId);
|
log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId);
|
||||||
}
|
}
|
||||||
|
@ -264,12 +256,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
} else {
|
} else {
|
||||||
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||||
}
|
}
|
||||||
} else if (propertiesHasVideo) {
|
|
||||||
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
|
||||||
} else {
|
} else {
|
||||||
// ERROR: RecordingProperties set to not record audio nor video
|
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
|
||||||
"RecordingProperties set to \"hasVideo(false)\" and \"hasAudio(false)\"");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Stream has audio track only
|
// Stream has audio track only
|
||||||
|
@ -278,7 +266,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||||
} else {
|
} else {
|
||||||
// ERROR: RecordingProperties set to video only but there's no video track
|
// ERROR: RecordingProperties set to video only but there's no video track
|
||||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
throw new OpenViduException(
|
||||||
|
Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
"RecordingProperties set to \"hasAudio(false)\" but stream is audio-only");
|
"RecordingProperties set to \"hasAudio(false)\" but stream is audio-only");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -289,12 +278,12 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||||
} else {
|
} else {
|
||||||
// ERROR: RecordingProperties set to audio only but there's no audio track
|
// ERROR: RecordingProperties set to audio only but there's no audio track
|
||||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
throw new OpenViduException(Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
"RecordingProperties set to \"hasVideo(false)\" but stream is video-only");
|
"RecordingProperties set to \"hasVideo(false)\" but stream is video-only");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// ERROR: Stream has no track at all
|
// ERROR: Stream has no track at all. This branch should never be reachable
|
||||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
throw new OpenViduException(Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||||
"Stream has no track at all. Cannot be recorded");
|
"Stream has no track at all. Cannot be recorded");
|
||||||
}
|
}
|
||||||
return profile;
|
return profile;
|
||||||
|
@ -318,13 +307,6 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
private void generateIndividualMetadataFile(RecorderEndpointWrapper wrapper) {
|
||||||
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/";
|
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/";
|
||||||
File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm");
|
File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm");
|
||||||
|
@ -381,8 +363,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
jsonFile.addProperty("size", wr.getSize());
|
jsonFile.addProperty("size", wr.getSize());
|
||||||
jsonFile.addProperty("clientData", wr.getClientData());
|
jsonFile.addProperty("clientData", wr.getClientData());
|
||||||
jsonFile.addProperty("serverData", wr.getServerData());
|
jsonFile.addProperty("serverData", wr.getServerData());
|
||||||
jsonFile.addProperty("hasAudio", wr.hasAudio());
|
jsonFile.addProperty("hasAudio", wr.hasAudio() && recording.hasAudio());
|
||||||
jsonFile.addProperty("hasVideo", wr.hasVideo());
|
jsonFile.addProperty("hasVideo", wr.hasVideo() && recording.hasVideo());
|
||||||
if (wr.hasVideo()) {
|
if (wr.hasVideo()) {
|
||||||
jsonFile.addProperty("typeOfVideo", wr.getTypeOfVideo());
|
jsonFile.addProperty("typeOfVideo", wr.getTypeOfVideo());
|
||||||
}
|
}
|
||||||
|
@ -394,17 +376,14 @@ public class SingleStreamRecordingService extends RecordingService {
|
||||||
}
|
}
|
||||||
|
|
||||||
json.add("files", jsonArrayFiles);
|
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.fileWriter.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json));
|
||||||
this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip");
|
this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip");
|
||||||
|
|
||||||
|
long duration = (maxEndTime - minStartTime) / 1000;
|
||||||
|
duration = duration > 0 ? duration : 0;
|
||||||
|
|
||||||
|
recording = this.sealRecordingMetadataFile(recording, accumulatedSize, duration, metadataFilePath);
|
||||||
|
|
||||||
return recording;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,6 @@ import io.openvidu.server.core.SessionManager;
|
||||||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||||
import io.openvidu.server.recording.Recording;
|
import io.openvidu.server.recording.Recording;
|
||||||
import io.openvidu.server.recording.service.RecordingManager;
|
import io.openvidu.server.recording.service.RecordingManager;
|
||||||
import io.openvidu.server.utils.FormatChecker;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -300,6 +299,8 @@ public class SessionRestController {
|
||||||
String name = (String) params.get("name");
|
String name = (String) params.get("name");
|
||||||
String outputModeString = (String) params.get("outputMode");
|
String outputModeString = (String) params.get("outputMode");
|
||||||
String resolution = (String) params.get("resolution");
|
String resolution = (String) params.get("resolution");
|
||||||
|
Boolean hasAudio = (Boolean) params.get("hasAudio");
|
||||||
|
Boolean hasVideo = (Boolean) params.get("hasVideo");
|
||||||
String recordingLayoutString = (String) params.get("recordingLayout");
|
String recordingLayoutString = (String) params.get("recordingLayout");
|
||||||
String customLayout = (String) params.get("customLayout");
|
String customLayout = (String) params.get("customLayout");
|
||||||
|
|
||||||
|
@ -335,15 +336,18 @@ public class SessionRestController {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
outputMode = io.openvidu.java.client.Recording.OutputMode.COMPOSED;
|
outputMode = io.openvidu.java.client.Recording.OutputMode.COMPOSED;
|
||||||
}
|
}
|
||||||
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(name).outputMode(outputMode);
|
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(name).outputMode(outputMode)
|
||||||
|
.hasAudio(hasAudio != null ? hasAudio : true).hasVideo(hasVideo != null ? hasVideo : true);
|
||||||
|
|
||||||
if (outputMode.equals(io.openvidu.java.client.Recording.OutputMode.COMPOSED)) {
|
if (outputMode.equals(io.openvidu.java.client.Recording.OutputMode.COMPOSED)) {
|
||||||
|
|
||||||
if (resolution != null) {
|
if (resolution != null) {
|
||||||
if (new FormatChecker().isAcceptableResolution(resolution)) {
|
if (sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) {
|
||||||
builder.resolution(resolution);
|
builder.resolution(resolution);
|
||||||
} else {
|
} else {
|
||||||
return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY);
|
return new ResponseEntity<>(
|
||||||
|
"Wrong \"resolution\" parameter. Acceptable values from 100 to 1999 for both width and height",
|
||||||
|
HttpStatus.UNPROCESSABLE_ENTITY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,8 +371,15 @@ public class SessionRestController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordingProperties properties = builder.build();
|
||||||
|
if (!properties.hasAudio() && !properties.hasVideo()) {
|
||||||
|
// Cannot start a recording with both "hasAudio" and "hasVideo" to false
|
||||||
|
return new ResponseEntity<>("Cannot start a recording with both \"hasAudio\" and \"hasVideo\" set to false",
|
||||||
|
HttpStatus.UNPROCESSABLE_ENTITY);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Recording startedRecording = this.recordingManager.startRecording(session, builder.build());
|
Recording startedRecording = this.recordingManager.startRecording(session, properties);
|
||||||
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||||
} catch (OpenViduException e) {
|
} catch (OpenViduException e) {
|
||||||
return new ResponseEntity<>("Error starting recording: " + e.getMessage(), getResponseHeaders(),
|
return new ResponseEntity<>("Error starting recording: " + e.getMessage(), getResponseHeaders(),
|
||||||
|
|
|
@ -208,7 +208,7 @@ public class RpcHandler extends DefaultJsonRpcHandler<JsonObject> {
|
||||||
|
|
||||||
String clientMetadata = getStringParam(request, ProtocolElements.JOINROOM_METADATA_PARAM);
|
String clientMetadata = getStringParam(request, ProtocolElements.JOINROOM_METADATA_PARAM);
|
||||||
|
|
||||||
if (sessionManager.isMetadataFormatCorrect(clientMetadata)) {
|
if (sessionManager.formatChecker.isServerMetadataFormatCorrect(clientMetadata)) {
|
||||||
|
|
||||||
Token tokenObj = sessionManager.consumeToken(sessionId, participantPrivatetId, token);
|
Token tokenObj = sessionManager.consumeToken(sessionId, participantPrivatetId, token);
|
||||||
Participant participant;
|
Participant participant;
|
||||||
|
|
|
@ -19,10 +19,14 @@ package io.openvidu.server.utils;
|
||||||
|
|
||||||
public class FormatChecker {
|
public class FormatChecker {
|
||||||
|
|
||||||
public boolean isAcceptableResolution(String stringResolution) {
|
public boolean isAcceptableRecordingResolution(String stringResolution) {
|
||||||
// Matches every string with format "AxB", being A and B any number not starting
|
// Matches every string with format "AxB", being A and B any number not starting
|
||||||
// with 0 and 3 digits long or 4 digits long if they start with 1
|
// with 0 and 3 digits long or 4 digits long if they start with 1
|
||||||
return stringResolution.matches("^(?!(0))(([0-9]{3})|1([0-9]{3}))x(?!0)(([0-9]{3})|1([0-9]{3}))$");
|
return stringResolution.matches("^(?!(0))(([0-9]{3})|1([0-9]{3}))x(?!0)(([0-9]{3})|1([0-9]{3}))$");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isServerMetadataFormatCorrect(String metadata) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue