openvidu-server: audio-only and video-only recordings

pull/203/head
pabloFuente 2019-01-24 11:21:35 +01:00
parent e46aa16157
commit 496d33b139
17 changed files with 594 additions and 215 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -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())

View File

@ -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"

View File

@ -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())) {

View File

@ -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());
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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())) {

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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

View File

@ -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) {
@ -81,6 +79,9 @@ public class SingleStreamRecordingService extends RecordingService {
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>());
final CountDownLatch recordingStartedCountdown = new CountDownLatch(session.getActivePublishers());
@ -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;
}

View File

@ -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(),

View File

@ -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;

View File

@ -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;
}
}