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),
|
||||
|
||||
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(
|
||||
305), MEDIA_WEBRTC_ENDPOINT_ERROR_CODE(
|
||||
304), MEDIA_ENDPOINT_ERROR_CODE(303), MEDIA_SDP_ERROR_CODE(302), MEDIA_GENERIC_ERROR_CODE(301),
|
||||
MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE(309),
|
||||
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(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(
|
||||
202), ROOM_GENERIC_ERROR_CODE(201),
|
||||
ROOM_CANNOT_BE_CREATED_ERROR_CODE(204), ROOM_CLOSED_ERROR_CODE(203), ROOM_NOT_FOUND_ERROR_CODE(202),
|
||||
ROOM_GENERIC_ERROR_CODE(201),
|
||||
|
||||
USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(
|
||||
103), USER_NOT_FOUND_ERROR_CODE(102), USER_GENERIC_ERROR_CODE(10),
|
||||
USER_NOT_STREAMING_ERROR_CODE(105), EXISTING_USER_IN_ROOM_ERROR_CODE(104), USER_CLOSED_ERROR_CODE(103),
|
||||
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(
|
||||
403), TOKEN_CANNOT_BE_CREATED_ERROR_CODE(404), EXISTING_FILTER_ALREADY_APPLIED_ERROR_CODE(405),
|
||||
USER_UNAUTHORIZED_ERROR_CODE(401), ROLE_NOT_FOUND_ERROR_CODE(402), SESSIONID_CANNOT_BE_CREATED_ERROR_CODE(403),
|
||||
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),
|
||||
|
||||
USER_METADATA_FORMAT_INVALID_ERROR_CODE(500),
|
||||
|
||||
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), SIGNAL_MESSAGE_INVALID_ERROR_CODE(
|
||||
602),
|
||||
SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601),
|
||||
SIGNAL_MESSAGE_INVALID_ERROR_CODE(602),
|
||||
|
||||
RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(
|
||||
706), RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(
|
||||
703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
|
||||
RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), RECORDING_DELETE_ERROR_CODE(706),
|
||||
RECORDING_LIST_ERROR_CODE(705), RECORDING_STOP_ERROR_CODE(704), RECORDING_START_ERROR_CODE(703),
|
||||
RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701);
|
||||
|
||||
private int value;
|
||||
|
||||
|
|
|
@ -121,7 +121,7 @@ public class RecordingProperties {
|
|||
* Call this method to specify whether or not to record the audio track
|
||||
*/
|
||||
public RecordingProperties.Builder hasAudio(boolean hasAudio) {
|
||||
this.hasAudio = true;
|
||||
this.hasAudio = hasAudio;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -129,7 +129,7 @@ public class RecordingProperties {
|
|||
* Call this method to specify whether or not to record the video track
|
||||
*/
|
||||
public RecordingProperties.Builder hasVideo(boolean hasVideo) {
|
||||
this.hasVideo = true;
|
||||
this.hasVideo = hasVideo;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ public class CDREventRecording extends CDREventEnd {
|
|||
json.addProperty("id", this.recording.getId());
|
||||
json.addProperty("name", this.recording.getName());
|
||||
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("recordingLayout", this.recording.getRecordingLayout().name());
|
||||
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"
|
||||
* - participantLeft.reason: "unsubscribe", "unpublish", "disconnect", "networkDisconnect", "openviduServerStopped"
|
||||
* - sessionDestroyed.reason: "lastParticipantLeft", "openviduServerStopped"
|
||||
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "openviduServerStopped"
|
||||
* - recordingStopped.reason: "recordingStoppedByServer", "lastParticipantLeft", "sessionClosedByServer", "automaticStop", "openviduServerStopped"
|
||||
*
|
||||
* [OPTIONAL_PROPERTIES]:
|
||||
* - 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.kurento.core.KurentoTokenOptions;
|
||||
import io.openvidu.server.recording.service.RecordingManager;
|
||||
import io.openvidu.server.utils.FormatChecker;
|
||||
|
||||
public abstract class SessionManager {
|
||||
|
||||
|
@ -65,6 +66,8 @@ public abstract class SessionManager {
|
|||
@Autowired
|
||||
protected CoturnCredentialsService coturnCredentialsService;
|
||||
|
||||
public FormatChecker formatChecker = new FormatChecker();
|
||||
|
||||
protected ConcurrentMap<String, Session> sessions = new ConcurrentHashMap<>();
|
||||
protected ConcurrentMap<String, SessionProperties> sessionProperties = new ConcurrentHashMap<>();
|
||||
protected ConcurrentMap<String, Long> sessionCreationTime = new ConcurrentHashMap<>();
|
||||
|
@ -223,7 +226,7 @@ public abstract class SessionManager {
|
|||
new ConcurrentHashMap<>());
|
||||
if (map != null) {
|
||||
|
||||
if (!isMetadataFormatCorrect(serverMetadata)) {
|
||||
if (!formatChecker.isServerMetadataFormatCorrect(serverMetadata)) {
|
||||
log.error("Data invalid format");
|
||||
throw new OpenViduException(Code.GENERIC_ERROR_CODE, "Data invalid format");
|
||||
}
|
||||
|
@ -317,10 +320,6 @@ public abstract class SessionManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isMetadataFormatCorrect(String metadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void newInsecureParticipant(String participantPrivateId) {
|
||||
this.insecureUsers.put(participantPrivateId, true);
|
||||
}
|
||||
|
@ -443,7 +442,7 @@ public abstract class SessionManager {
|
|||
return participants;
|
||||
}
|
||||
|
||||
protected void closeSessionAndEmptyCollections(Session session, String reason) {
|
||||
public void closeSessionAndEmptyCollections(Session session, String reason) {
|
||||
|
||||
if (openviduConfig.isRecordingModuleEnabled()
|
||||
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
|
|
|
@ -238,7 +238,7 @@ public class KurentoParticipant extends Participant {
|
|||
this.session.getSessionId());
|
||||
|
||||
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
||||
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
this.recordingManager.startOneIndividualStreamRecording(session, null, null, this);
|
||||
}
|
||||
|
||||
|
@ -438,7 +438,7 @@ public class KurentoParticipant extends Participant {
|
|||
this.session.publishedStreamIds.remove(this.getPublisherStreamId());
|
||||
|
||||
if (this.openviduConfig.isRecordingModuleEnabled()
|
||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(session.getSessionId())) {
|
||||
&& this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) {
|
||||
this.recordingManager.stopOneIndividualStreamRecording(session.getSessionId(),
|
||||
this.getPublisherStreamId());
|
||||
}
|
||||
|
|
|
@ -172,12 +172,14 @@ public class KurentoSessionManager extends SessionManager {
|
|||
if (remainingParticipants.isEmpty()) {
|
||||
if (openviduConfig.isRecordingModuleEnabled()
|
||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||
&& this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)) {
|
||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||
// a Publisher starts before timeout)
|
||||
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
||||
sessionId);
|
||||
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
||||
&& (this.recordingManager.sessionIsBeingRecordedIndividual(sessionId)
|
||||
|| (this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||
&& this.recordingManager.sessionIsBeingRecordedOnlyAudio(sessionId)))) {
|
||||
// Start countdown to stop recording if INDIVIDUAL mode or COMPOSED audio-only
|
||||
// (will be aborted if a Publisher starts before timeout)
|
||||
log.info("Last participant left. Starting {} seconds countdown for stopping recording of session {}",
|
||||
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
|
||||
recordingManager.initAutomaticRecordingStopThread(session);
|
||||
} else {
|
||||
log.info("No more participants in session '{}', removing it and closing it", sessionId);
|
||||
this.closeSessionAndEmptyCollections(session, reason);
|
||||
|
@ -186,6 +188,7 @@ public class KurentoSessionManager extends SessionManager {
|
|||
} else if (remainingParticipants.size() == 1 && openviduConfig.isRecordingModuleEnabled()
|
||||
&& MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())
|
||||
&& this.recordingManager.sessionIsBeingRecordedComposed(sessionId)
|
||||
&& !this.recordingManager.sessionIsBeingRecordedOnlyAudio(sessionId)
|
||||
&& ProtocolElements.RECORDER_PARTICIPANT_PUBLICID
|
||||
.equals(remainingParticipants.iterator().next().getParticipantPublicId())) {
|
||||
if (RecordingMode.ALWAYS.equals(session.getSessionProperties().recordingMode())) {
|
||||
|
@ -198,9 +201,9 @@ public class KurentoSessionManager extends SessionManager {
|
|||
} else if (RecordingMode.MANUAL.equals(session.getSessionProperties().recordingMode())) {
|
||||
// Start countdown to stop recording if RecordingMode.MANUAL (will be aborted if
|
||||
// a Publisher starts before timeout)
|
||||
log.info("Last participant left. Starting 2 minutes countdown for stopping recording of session {}",
|
||||
sessionId);
|
||||
recordingManager.initAutomaticRecordingStopThread(session.getSessionId());
|
||||
log.info("Last participant left. Starting {} seconds countdown for stopping recording of session {}",
|
||||
this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId);
|
||||
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 {
|
||||
|
||||
RecorderEndpoint recorder;
|
||||
String connectionId;
|
||||
String recordingId;
|
||||
String streamId;
|
||||
String clientData;
|
||||
String serverData;
|
||||
boolean hasAudio;
|
||||
boolean hasVideo;
|
||||
String typeOfVideo;
|
||||
private RecorderEndpoint recorder;
|
||||
private String connectionId;
|
||||
private String recordingId;
|
||||
private String streamId;
|
||||
private String clientData;
|
||||
private String serverData;
|
||||
private boolean hasAudio;
|
||||
private boolean hasVideo;
|
||||
private String typeOfVideo;
|
||||
|
||||
long startTime;
|
||||
long endTime;
|
||||
long size;
|
||||
private long startTime;
|
||||
private long endTime;
|
||||
private long size;
|
||||
|
||||
public RecorderEndpointWrapper(RecorderEndpoint recorder, String connectionId, String recordingId, String streamId,
|
||||
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
|
||||
.valueOf(json.get("outputMode").getAsString());
|
||||
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(json.get("name").getAsString())
|
||||
.outputMode(outputMode).hasAudio(json.get("hasAudio").getAsBoolean())
|
||||
.hasVideo(json.get("hasVideo").getAsBoolean());
|
||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode)) {
|
||||
.outputMode(outputMode).hasAudio(this.hasAudio).hasVideo(this.hasVideo);
|
||||
if (io.openvidu.java.client.Recording.OutputMode.COMPOSED.equals(outputMode) && this.hasVideo) {
|
||||
this.resolution = json.get("resolution").getAsString();
|
||||
builder.resolution(this.resolution);
|
||||
RecordingLayout recordingLayout = RecordingLayout.valueOf(json.get("recordingLayout").getAsString());
|
||||
|
@ -189,7 +188,8 @@ public class Recording {
|
|||
json.addProperty("id", this.id);
|
||||
json.addProperty("name", this.recordingProperties.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("recordingLayout", this.recordingProperties.recordingLayout().name());
|
||||
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.TimeUnit;
|
||||
|
||||
import javax.ws.rs.ProcessingException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.ExecCreateCmdResponse;
|
||||
import com.github.dockerjava.api.exception.ConflictException;
|
||||
import com.github.dockerjava.api.exception.DockerClientException;
|
||||
import com.github.dockerjava.api.exception.InternalServerErrorException;
|
||||
import com.github.dockerjava.api.exception.NotFoundException;
|
||||
import com.github.dockerjava.api.model.Bind;
|
||||
import com.github.dockerjava.api.model.Volume;
|
||||
|
@ -46,7 +42,6 @@ import com.github.dockerjava.core.DefaultDockerClientConfig;
|
|||
import com.github.dockerjava.core.DockerClientBuilder;
|
||||
import com.github.dockerjava.core.DockerClientConfig;
|
||||
import com.github.dockerjava.core.command.ExecStartResultCallback;
|
||||
import com.github.dockerjava.core.command.PullImageResultCallback;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
|
@ -54,7 +49,11 @@ import io.openvidu.java.client.RecordingLayout;
|
|||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.server.OpenViduServer;
|
||||
import io.openvidu.server.config.OpenviduConfig;
|
||||
import io.openvidu.server.core.Participant;
|
||||
import io.openvidu.server.core.Session;
|
||||
import io.openvidu.server.kurento.core.KurentoParticipant;
|
||||
import io.openvidu.server.kurento.core.KurentoSession;
|
||||
import io.openvidu.server.recording.CompositeWrapper;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
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> sessionsContainers = new ConcurrentHashMap<>();
|
||||
private Map<String, CompositeWrapper> composites = new ConcurrentHashMap<>();
|
||||
|
||||
private final String IMAGE_NAME = "openvidu/openvidu-recording";
|
||||
private String IMAGE_TAG;
|
||||
|
||||
private DockerClient dockerClient;
|
||||
DockerClient dockerClient;
|
||||
|
||||
public ComposedRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||
super(recordingManager, openviduConfig);
|
||||
|
@ -78,25 +75,69 @@ public class ComposedRecordingService extends RecordingService {
|
|||
|
||||
@Override
|
||||
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||
List<String> envs = new ArrayList<>();
|
||||
|
||||
PropertiesRecordingId updatePropertiesAndRecordingId = this.setFinalRecordingNameAndGetFreeRecordingId(session,
|
||||
properties);
|
||||
properties = updatePropertiesAndRecordingId.properties;
|
||||
String recordingId = updatePropertiesAndRecordingId.recordingId;
|
||||
|
||||
// Instantiate and store recording object
|
||||
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);
|
||||
|
||||
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));
|
||||
|
||||
envs.add("URL=" + layoutUrl);
|
||||
envs.add("ONLY_VIDEO=" + !properties.hasAudio());
|
||||
envs.add("RESOLUTION=" + properties.resolution());
|
||||
envs.add("FRAMERATE=30");
|
||||
envs.add("VIDEO_ID=" + recordingId);
|
||||
envs.add("VIDEO_ID=" + recording.getId());
|
||||
envs.add("VIDEO_NAME=" + properties.name());
|
||||
envs.add("VIDEO_FORMAT=mp4");
|
||||
envs.add("RECORDING_JSON=" + recording.toJson().toString());
|
||||
|
@ -106,28 +147,56 @@ public class ComposedRecordingService extends RecordingService {
|
|||
|
||||
String containerId;
|
||||
try {
|
||||
containerId = this.runRecordingContainer(envs, "recording_" + recordingId);
|
||||
containerId = this.runRecordingContainer(envs, "recording_" + recording.getId());
|
||||
} catch (Exception e) {
|
||||
this.cleanRecordingMapsAndReturnContainerId(recording);
|
||||
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||
this.cleanRecordingMaps(recording);
|
||||
throw this.failStartRecording(session, recording,
|
||||
"Couldn't initialize recording container. Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
this.waitForVideoFileNotEmpty(recording);
|
||||
|
||||
this.sessionsContainers.put(session.getSessionId(), containerId);
|
||||
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.started);
|
||||
|
||||
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
||||
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||
try {
|
||||
this.waitForVideoFileNotEmpty(recording);
|
||||
} catch (OpenViduException e) {
|
||||
this.cleanRecordingMaps(recording);
|
||||
throw this.failStartRecording(session, recording,
|
||||
"Couldn't initialize recording container. Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Recording stopRecording(Session session, Recording recording, String reason) {
|
||||
String containerId = cleanRecordingMapsAndReturnContainerId(recording);
|
||||
private Recording startRecordingAudioOnly(Session session, Recording recording, RecordingProperties properties)
|
||||
throws OpenViduException {
|
||||
|
||||
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();
|
||||
|
||||
if (session == null) {
|
||||
|
@ -229,46 +298,64 @@ public class ComposedRecordingService extends RecordingService {
|
|||
throw new OpenViduException(Code.RECORDING_REPORT_ERROR_CODE,
|
||||
"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);
|
||||
}
|
||||
}
|
||||
return recording;
|
||||
}
|
||||
|
||||
public boolean recordingImageExistsLocally() {
|
||||
boolean imageExists = false;
|
||||
try {
|
||||
dockerClient.inspectImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec();
|
||||
imageExists = true;
|
||||
} catch (NotFoundException nfe) {
|
||||
imageExists = false;
|
||||
} catch (ProcessingException e) {
|
||||
throw e;
|
||||
private Recording stopRecordingAudioOnly(Session session, Recording recording, String reason) {
|
||||
String sessionId;
|
||||
if (session == null) {
|
||||
log.warn(
|
||||
"Existing recording {} does not have an active session associated. This means the recording "
|
||||
+ "has been automatically stopped after last user left and {} seconds timeout passed",
|
||||
recording.getId(), this.openviduConfig.getOpenviduRecordingAutostopTimeout());
|
||||
sessionId = recording.getSessionId();
|
||||
} 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 {
|
||||
dockerClient.pullImageCmd(IMAGE_NAME + ":" + IMAGE_TAG).exec(new PullImageResultCallback()).awaitSuccess();
|
||||
} catch (NotFoundException | InternalServerErrorException e) {
|
||||
if (imageExistsLocally(IMAGE_NAME + ":" + IMAGE_TAG)) {
|
||||
log.info("Docker image '{}' exists locally", IMAGE_NAME + ":" + IMAGE_TAG);
|
||||
} else {
|
||||
throw e;
|
||||
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
log.error("Error waiting for RecorderEndpoint of Composite to stop in session {}",
|
||||
recording.getSessionId());
|
||||
}
|
||||
} catch (DockerClientException e) {
|
||||
log.info("Error on Pulling '{}' image. Probably because the user has stopped the execution",
|
||||
IMAGE_NAME + ":" + IMAGE_TAG);
|
||||
throw e;
|
||||
} catch (InterruptedException e) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
log.error("Exception while waiting for state change", 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 {
|
||||
Volume volume1 = new Volume("/recordings");
|
||||
CreateContainerCmd cmd = dockerClient.createContainerCmd(IMAGE_NAME + ":" + IMAGE_TAG).withName(containerName)
|
||||
.withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
||||
CreateContainerCmd cmd = dockerClient
|
||||
.createContainerCmd(RecordingManager.IMAGE_NAME + ":" + RecordingManager.IMAGE_TAG)
|
||||
.withName(containerName).withEnv(envs).withNetworkMode("host").withVolumes(volume1)
|
||||
.withBinds(new Bind(openviduConfig.getOpenViduRecordingPath(), volume1));
|
||||
CreateContainerResponse container = null;
|
||||
try {
|
||||
|
@ -283,7 +370,8 @@ public class ComposedRecordingService extends RecordingService {
|
|||
containerName);
|
||||
throw 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;
|
||||
}
|
||||
}
|
||||
|
@ -297,22 +385,14 @@ public class ComposedRecordingService extends RecordingService {
|
|||
dockerClient.stopContainerCmd(containerId).exec();
|
||||
}
|
||||
|
||||
private boolean imageExistsLocally(String imageName) {
|
||||
boolean imageExists = false;
|
||||
try {
|
||||
dockerClient.inspectImageCmd(imageName).exec();
|
||||
imageExists = true;
|
||||
} catch (NotFoundException nfe) {
|
||||
imageExists = false;
|
||||
}
|
||||
return imageExists;
|
||||
}
|
||||
|
||||
private void waitForVideoFileNotEmpty(Recording recording) {
|
||||
private void waitForVideoFileNotEmpty(Recording recording) throws OpenViduException {
|
||||
boolean isPresent = false;
|
||||
while (!isPresent) {
|
||||
int i = 1;
|
||||
int timeout = 150; // Wait for 150*150 = 22500 = 22.5 seconds
|
||||
while (!isPresent && timeout <= 150) {
|
||||
try {
|
||||
Thread.sleep(150);
|
||||
timeout++;
|
||||
File f = new File(this.openviduConfig.getOpenViduRecordingPath() + recording.getId() + "/"
|
||||
+ recording.getName() + ".mp4");
|
||||
isPresent = ((f.isFile()) && (f.length() > 0));
|
||||
|
@ -320,9 +400,15 @@ public class ComposedRecordingService extends RecordingService {
|
|||
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.removeDockerContainer(containerId);
|
||||
throw e;
|
||||
|
@ -351,14 +437,4 @@ public class ComposedRecordingService extends RecordingService {
|
|||
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.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.JsonParser;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.client.internal.ProtocolElements;
|
||||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.server.config.OpenviduConfig;
|
||||
import io.openvidu.server.core.Participant;
|
||||
import io.openvidu.server.core.Session;
|
||||
import io.openvidu.server.core.SessionEventsHandler;
|
||||
import io.openvidu.server.core.SessionManager;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
|
||||
public class RecordingManager {
|
||||
|
@ -67,6 +73,9 @@ public class RecordingManager {
|
|||
@Autowired
|
||||
protected SessionEventsHandler sessionHandler;
|
||||
|
||||
@Autowired
|
||||
private SessionManager sessionManager;
|
||||
|
||||
@Autowired
|
||||
protected OpenviduConfig openviduConfig;
|
||||
|
||||
|
@ -79,6 +88,8 @@ public class RecordingManager {
|
|||
Runtime.getRuntime().availableProcessors());
|
||||
|
||||
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(
|
||||
new String[] { "disconnect", "forceDisconnectByUser", "forceDisconnectByServer", "networkDisconnect" });
|
||||
|
@ -89,18 +100,17 @@ public class RecordingManager {
|
|||
|
||||
public void initializeRecordingManager() {
|
||||
|
||||
RecordingManager.IMAGE_TAG = openviduConfig.getOpenViduRecordingVersion();
|
||||
|
||||
this.composedRecordingService = new ComposedRecordingService(this, openviduConfig);
|
||||
this.singleStreamRecordingService = new SingleStreamRecordingService(this, openviduConfig);
|
||||
|
||||
ComposedRecordingService recServiceAux = this.composedRecordingService;
|
||||
recServiceAux.setRecordingContainerVersion(openviduConfig.getOpenViduRecordingVersion());
|
||||
|
||||
log.info("Recording module required: Downloading openvidu/openvidu-recording:"
|
||||
+ openviduConfig.getOpenViduRecordingVersion() + " Docker image (800 MB aprox)");
|
||||
|
||||
boolean imageExists = false;
|
||||
try {
|
||||
imageExists = recServiceAux.recordingImageExistsLocally();
|
||||
imageExists = this.recordingImageExistsLocally();
|
||||
} catch (ProcessingException exception) {
|
||||
String message = "Exception connecting to Docker daemon: ";
|
||||
if ("docker".equals(openviduConfig.getSpringProfile())) {
|
||||
|
@ -133,7 +143,7 @@ public class RecordingManager {
|
|||
}
|
||||
});
|
||||
t.start();
|
||||
recServiceAux.downloadRecordingImage();
|
||||
this.downloadRecordingImage();
|
||||
t.interrupt();
|
||||
try {
|
||||
t.join();
|
||||
|
@ -142,9 +152,8 @@ public class RecordingManager {
|
|||
}
|
||||
log.info("Docker image available");
|
||||
}
|
||||
this.initRecordingPath();
|
||||
|
||||
this.recordingService = recServiceAux;
|
||||
this.initRecordingPath();
|
||||
}
|
||||
|
||||
public Recording startRecording(Session session, RecordingProperties properties) throws OpenViduException {
|
||||
|
@ -164,7 +173,9 @@ public class RecordingManager {
|
|||
if (session.getActivePublishers() == 0) {
|
||||
// Init automatic recording stop if there are now publishers when starting
|
||||
// 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;
|
||||
}
|
||||
|
@ -172,9 +183,9 @@ public class RecordingManager {
|
|||
public Recording stopRecording(Session session, String recordingId, String reason) {
|
||||
Recording recording;
|
||||
if (session == null) {
|
||||
recording = this.startedRecordings.remove(recordingId);
|
||||
recording = this.startedRecordings.get(recordingId);
|
||||
} else {
|
||||
recording = this.sessionsRecordings.remove(session.getSessionId());
|
||||
recording = this.sessionsRecordings.get(session.getSessionId());
|
||||
}
|
||||
switch (recording.getOutputMode()) {
|
||||
case COMPOSED:
|
||||
|
@ -195,17 +206,33 @@ public class RecordingManager {
|
|||
participant.getPublisherStreamId(), session.getSessionId());
|
||||
}
|
||||
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);
|
||||
this.singleStreamRecordingService.startOneIndividualStreamRecording(session, recordingId, profile,
|
||||
this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(session, recordingId, profile,
|
||||
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) {
|
||||
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())) {
|
||||
// Stop specific RecorderEndpoint for this stream
|
||||
log.info("Stopping RecorderEndpoint in session {} for stream of participant {}", sessionId, streamId);
|
||||
final CountDownLatch stoppedCountDown = new CountDownLatch(1);
|
||||
this.singleStreamRecordingService.stopOneIndividualStreamRecording(sessionId, streamId, stoppedCountDown);
|
||||
this.singleStreamRecordingService.stopRecorderEndpointOfPublisherEndpoint(sessionId, streamId,
|
||||
stoppedCountDown);
|
||||
try {
|
||||
if (!stoppedCountDown.await(5, TimeUnit.SECONDS)) {
|
||||
log.error("Error waiting for recorder endpoint of stream {} to stop in session {}", streamId,
|
||||
|
@ -214,6 +241,12 @@ public class RecordingManager {
|
|||
} catch (InterruptedException 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()));
|
||||
}
|
||||
|
||||
public boolean sessionIsBeingRecordedOnlyAudio(String sessionId) {
|
||||
Recording rec = this.sessionsRecordings.get(sessionId);
|
||||
return (rec != null && !rec.hasVideo());
|
||||
}
|
||||
|
||||
public Recording getStartedRecording(String recordingId) {
|
||||
return this.startedRecordings.get(recordingId);
|
||||
}
|
||||
|
@ -314,14 +352,27 @@ public class RecordingManager {
|
|||
return null;
|
||||
}
|
||||
|
||||
public void initAutomaticRecordingStopThread(String sessionId) {
|
||||
final String recordingId = this.sessionsRecordings.get(sessionId).getId();
|
||||
public void initAutomaticRecordingStopThread(final Session session) {
|
||||
final String recordingId = this.sessionsRecordings.get(session.getSessionId()).getId();
|
||||
ScheduledFuture<?> future = this.automaticRecordingStopExecutor.schedule(() -> {
|
||||
log.info("Stopping recording {} after 2 minutes wait (no publisher published before timeout)", recordingId);
|
||||
this.stopRecording(null, recordingId, "lastParticipantLeft");
|
||||
this.automaticRecordingStopThreads.remove(sessionId);
|
||||
}, 2, TimeUnit.MINUTES);
|
||||
this.automaticRecordingStopThreads.putIfAbsent(sessionId, future);
|
||||
|
||||
log.info("Stopping recording {} after {} seconds wait (no publisher published before timeout)", recordingId,
|
||||
this.openviduConfig.getOpenviduRecordingAutostopTimeout());
|
||||
|
||||
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) {
|
||||
|
@ -340,7 +391,7 @@ public class RecordingManager {
|
|||
String extension;
|
||||
switch (recording.getOutputMode()) {
|
||||
case COMPOSED:
|
||||
extension = "mp4";
|
||||
extension = recording.hasVideo() ? "mp4" : "webm";
|
||||
break;
|
||||
case INDIVIDUAL:
|
||||
extension = "zip";
|
||||
|
@ -357,6 +408,36 @@ public class RecordingManager {
|
|||
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) {
|
||||
log.info(this.openviduConfig.getOpenViduRecordingPath() + recordingId + "/"
|
||||
+ RecordingManager.RECORDING_ENTITY_FILE + recordingId);
|
||||
|
|
|
@ -17,17 +17,25 @@
|
|||
|
||||
package io.openvidu.server.recording.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import io.openvidu.client.OpenViduException;
|
||||
import io.openvidu.client.OpenViduException.Code;
|
||||
import io.openvidu.java.client.RecordingLayout;
|
||||
import io.openvidu.java.client.RecordingProperties;
|
||||
import io.openvidu.server.config.OpenviduConfig;
|
||||
import io.openvidu.server.core.Session;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
import io.openvidu.server.utils.CustomFileWriter;
|
||||
|
||||
public abstract class RecordingService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RecordingService.class);
|
||||
|
||||
protected OpenviduConfig openviduConfig;
|
||||
protected RecordingManager recordingManager;
|
||||
protected CustomFileWriter fileWriter = new CustomFileWriter();
|
||||
|
||||
RecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||
this.recordingManager = recordingManager;
|
||||
|
@ -38,11 +46,56 @@ public abstract class RecordingService {
|
|||
|
||||
public abstract Recording stopRecording(Session session, Recording recording, String reason);
|
||||
|
||||
protected RecordingProperties setFinalRecordingName(Session session, RecordingProperties properties) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
/**
|
||||
* Generates metadata recording file (".recording.RECORDING_ID" JSON file to
|
||||
* 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,
|
||||
RecordingProperties properties) {
|
||||
String recordingId = this.recordingManager.getFreeRecordingId(session.getSessionId(),
|
||||
|
@ -52,7 +105,8 @@ public abstract class RecordingService {
|
|||
RecordingProperties.Builder builder = new RecordingProperties.Builder().name(recordingId)
|
||||
.outputMode(properties.outputMode()).hasAudio(properties.hasAudio())
|
||||
.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.recordingLayout(properties.recordingLayout());
|
||||
if (RecordingLayout.CUSTOM.equals(properties.recordingLayout())) {
|
||||
|
@ -61,6 +115,8 @@ public abstract class RecordingService {
|
|||
}
|
||||
properties = builder.build();
|
||||
}
|
||||
|
||||
log.info("New recording id ({}) and final name ({})", recordingId, properties.name());
|
||||
return new PropertiesRecordingId(properties, recordingId);
|
||||
}
|
||||
|
||||
|
@ -69,6 +125,18 @@ public abstract class RecordingService {
|
|||
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
|
||||
* 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.Recording;
|
||||
import io.openvidu.server.utils.CommandExecutor;
|
||||
import io.openvidu.server.utils.CustomFileWriter;
|
||||
|
||||
public class SingleStreamRecordingService extends RecordingService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SingleStreamRecordingService.class);
|
||||
|
||||
private Map<String, Map<String, RecorderEndpointWrapper>> recorders = new ConcurrentHashMap<>();
|
||||
private CustomFileWriter fileWriter = new CustomFileWriter();
|
||||
private final String INDIVIDUAL_STREAM_METADATA_FILE = ".stream.";
|
||||
|
||||
public SingleStreamRecordingService(RecordingManager recordingManager, OpenviduConfig openviduConfig) {
|
||||
|
@ -80,6 +78,9 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
properties);
|
||||
properties = updatePropertiesAndRecordingId.properties;
|
||||
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>());
|
||||
|
||||
|
@ -97,23 +98,15 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
p.getPublisherStreamId(), session.getSessionId(), e.getMessage());
|
||||
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 {
|
||||
if (!recordingStartedCountdown.await(5, TimeUnit.SECONDS)) {
|
||||
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId());
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||
this.stopRecording(session, recording, null);
|
||||
throw new OpenViduException(Code.RECORDING_START_ERROR_CODE,
|
||||
"Couldn't initialize some RecorderEndpoint");
|
||||
throw this.failStartRecording(session, recording, "Couldn't initialize some RecorderEndpoint");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
|
@ -127,11 +120,7 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
}
|
||||
|
||||
this.generateRecordingMetadataFile(recording);
|
||||
this.recordingManager.sessionHandler.setRecordingStarted(session.getSessionId(), recording);
|
||||
this.recordingManager.sessionsRecordings.put(session.getSessionId(), recording);
|
||||
this.recordingManager.startingRecordings.remove(recording.getId());
|
||||
this.recordingManager.startedRecordings.put(recording.getId(), recording);
|
||||
this.recordingManager.getSessionEventsHandler().sendRecordingStartedNotification(session, recording);
|
||||
this.updateCollectionsAndSendNotifCauseRecordingStarted(session, recording);
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
@ -143,22 +132,23 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
final CountDownLatch stoppedCountDown = new CountDownLatch(numberOfActiveRecorders);
|
||||
|
||||
for (RecorderEndpointWrapper wrapper : recorders.get(recording.getSessionId()).values()) {
|
||||
this.stopOneIndividualStreamRecording(recording.getSessionId(), wrapper.getStreamId(), stoppedCountDown);
|
||||
this.stopRecorderEndpointOfPublisherEndpoint(recording.getSessionId(), wrapper.getStreamId(),
|
||||
stoppedCountDown);
|
||||
}
|
||||
try {
|
||||
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());
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
|
||||
log.error("Exception while waiting for state change", e);
|
||||
}
|
||||
|
||||
this.recordingManager.sessionsRecordings.remove(recording.getSessionId());
|
||||
this.recordingManager.startedRecordings.remove(recording.getId());
|
||||
this.cleanRecordingMaps(recording);
|
||||
this.recorders.remove(recording.getSessionId());
|
||||
|
||||
recording = this.sealMetadataFiles(recording);
|
||||
recording = this.recordingManager.updateRecordingUrl(recording);
|
||||
|
||||
if (reason != null && session != null) {
|
||||
this.recordingManager.sessionHandler.sendRecordingStoppedNotification(session, recording, reason);
|
||||
|
@ -167,8 +157,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
return recording;
|
||||
}
|
||||
|
||||
public void startOneIndividualStreamRecording(Session session, String recordingId, MediaProfileSpecType profile,
|
||||
Participant participant, CountDownLatch globalStartLatch) {
|
||||
public void startRecorderEndpointForPublisherEndpoint(Session session, String recordingId,
|
||||
MediaProfileSpecType profile, Participant participant, CountDownLatch globalStartLatch) {
|
||||
log.info("Starting single stream recorder for stream {} in session {}", participant.getPublisherStreamId(),
|
||||
session.getSessionId());
|
||||
|
||||
|
@ -224,20 +214,22 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
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);
|
||||
RecorderEndpointWrapper wrapper = this.recorders.get(sessionId).remove(streamId);
|
||||
if (wrapper != null) {
|
||||
wrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
|
||||
final RecorderEndpointWrapper finalWrapper = this.recorders.get(sessionId).remove(streamId);
|
||||
if (finalWrapper != null) {
|
||||
finalWrapper.getRecorder().addStoppedListener(new EventListener<StoppedEvent>() {
|
||||
@Override
|
||||
public void onEvent(StoppedEvent event) {
|
||||
wrapper.setEndTime(System.currentTimeMillis());
|
||||
generateIndividualMetadataFile(wrapper);
|
||||
finalWrapper.setEndTime(System.currentTimeMillis());
|
||||
generateIndividualMetadataFile(finalWrapper);
|
||||
log.info("Recording stopped event for stream {}", streamId);
|
||||
finalWrapper.getRecorder().release();
|
||||
globalStopLatch.countDown();
|
||||
}
|
||||
});
|
||||
wrapper.getRecorder().stop();
|
||||
finalWrapper.getRecorder().stop();
|
||||
} else {
|
||||
log.error("Stream {} wasn't being recorded in session {}", streamId, sessionId);
|
||||
}
|
||||
|
@ -264,12 +256,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
} else {
|
||||
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||
}
|
||||
} else if (propertiesHasVideo) {
|
||||
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||
} else {
|
||||
// ERROR: RecordingProperties set to not record audio nor video
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
"RecordingProperties set to \"hasVideo(false)\" and \"hasAudio(false)\"");
|
||||
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||
}
|
||||
} else {
|
||||
// Stream has audio track only
|
||||
|
@ -278,7 +266,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
profile = MediaProfileSpecType.WEBM_AUDIO_ONLY;
|
||||
} else {
|
||||
// ERROR: RecordingProperties set to video only but there's no video track
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
throw new OpenViduException(
|
||||
Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
"RecordingProperties set to \"hasAudio(false)\" but stream is audio-only");
|
||||
}
|
||||
}
|
||||
|
@ -289,12 +278,12 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
profile = MediaProfileSpecType.WEBM_VIDEO_ONLY;
|
||||
} else {
|
||||
// ERROR: RecordingProperties set to audio only but there's no audio track
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
"RecordingProperties set to \"hasVideo(false)\" but stream is video-only");
|
||||
}
|
||||
} else {
|
||||
// ERROR: Stream has no track at all
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
// ERROR: Stream has no track at all. This branch should never be reachable
|
||||
throw new OpenViduException(Code.MEDIA_TYPE_STREAM_INCOMPATIBLE_WITH_RECORDING_PROPERTIES_ERROR_CODE,
|
||||
"Stream has no track at all. Cannot be recorded");
|
||||
}
|
||||
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) {
|
||||
String filesPath = this.openviduConfig.getOpenViduRecordingPath() + wrapper.getRecordingId() + "/";
|
||||
File videoFile = new File(filesPath + wrapper.getStreamId() + ".webm");
|
||||
|
@ -381,8 +363,8 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
jsonFile.addProperty("size", wr.getSize());
|
||||
jsonFile.addProperty("clientData", wr.getClientData());
|
||||
jsonFile.addProperty("serverData", wr.getServerData());
|
||||
jsonFile.addProperty("hasAudio", wr.hasAudio());
|
||||
jsonFile.addProperty("hasVideo", wr.hasVideo());
|
||||
jsonFile.addProperty("hasAudio", wr.hasAudio() && recording.hasAudio());
|
||||
jsonFile.addProperty("hasVideo", wr.hasVideo() && recording.hasVideo());
|
||||
if (wr.hasVideo()) {
|
||||
jsonFile.addProperty("typeOfVideo", wr.getTypeOfVideo());
|
||||
}
|
||||
|
@ -394,17 +376,14 @@ public class SingleStreamRecordingService extends RecordingService {
|
|||
}
|
||||
|
||||
json.add("files", jsonArrayFiles);
|
||||
|
||||
long duration = (maxEndTime - minStartTime) / 1000;
|
||||
|
||||
recording.setSize(accumulatedSize); // Size in bytes
|
||||
recording.setDuration(duration > 0 ? duration : 0); // Duration in seconds
|
||||
recording.setStatus(io.openvidu.java.client.Recording.Status.stopped);
|
||||
|
||||
this.fileWriter.overwriteFile(metadataFilePath, recording.toJson().toString());
|
||||
this.fileWriter.createAndWriteFile(syncFilePath, new GsonBuilder().setPrettyPrinting().create().toJson(json));
|
||||
this.generateZipFileAndCleanFolder(folderPath, recording.getName() + ".zip");
|
||||
|
||||
long duration = (maxEndTime - minStartTime) / 1000;
|
||||
duration = duration > 0 ? duration : 0;
|
||||
|
||||
recording = this.sealRecordingMetadataFile(recording, accumulatedSize, duration, metadataFilePath);
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,6 @@ import io.openvidu.server.core.SessionManager;
|
|||
import io.openvidu.server.kurento.core.KurentoTokenOptions;
|
||||
import io.openvidu.server.recording.Recording;
|
||||
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 outputModeString = (String) params.get("outputMode");
|
||||
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 customLayout = (String) params.get("customLayout");
|
||||
|
||||
|
@ -335,15 +336,18 @@ public class SessionRestController {
|
|||
} catch (Exception e) {
|
||||
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 (resolution != null) {
|
||||
if (new FormatChecker().isAcceptableResolution(resolution)) {
|
||||
if (sessionManager.formatChecker.isAcceptableRecordingResolution(resolution)) {
|
||||
builder.resolution(resolution);
|
||||
} 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 {
|
||||
Recording startedRecording = this.recordingManager.startRecording(session, builder.build());
|
||||
Recording startedRecording = this.recordingManager.startRecording(session, properties);
|
||||
return new ResponseEntity<>(startedRecording.toJson().toString(), getResponseHeaders(), HttpStatus.OK);
|
||||
} catch (OpenViduException e) {
|
||||
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);
|
||||
|
||||
if (sessionManager.isMetadataFormatCorrect(clientMetadata)) {
|
||||
if (sessionManager.formatChecker.isServerMetadataFormatCorrect(clientMetadata)) {
|
||||
|
||||
Token tokenObj = sessionManager.consumeToken(sessionId, participantPrivatetId, token);
|
||||
Participant participant;
|
||||
|
|
|
@ -19,10 +19,14 @@ package io.openvidu.server.utils;
|
|||
|
||||
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
|
||||
// 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}))$");
|
||||
}
|
||||
|
||||
public boolean isServerMetadataFormatCorrect(String metadata) {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue