New RecordingProperty ignoreFailedStreams

pull/630/head
pabloFuente 2021-05-11 12:28:41 +02:00
parent da003448ff
commit f1da724533
4 changed files with 80 additions and 13 deletions

View File

@ -35,6 +35,7 @@ public class RecordingProperties {
public static final String resolution = "1280x720"; public static final String resolution = "1280x720";
public static final Integer frameRate = 25; public static final Integer frameRate = 25;
public static final Long shmSize = 536870912L; public static final Long shmSize = 536870912L;
public static final Boolean ignoreFailedStreams = false;
} }
// For all // For all
@ -49,6 +50,8 @@ public class RecordingProperties {
private Long shmSize; private Long shmSize;
// For COMPOSED/COMPOSED_QUICK_START + hasVideo + RecordingLayout.CUSTOM // For COMPOSED/COMPOSED_QUICK_START + hasVideo + RecordingLayout.CUSTOM
private String customLayout; private String customLayout;
// For INDIVIDUAL
private Boolean ignoreFailedStreams;
// For OpenVidu Pro // For OpenVidu Pro
private String mediaNode; private String mediaNode;
@ -58,14 +61,15 @@ public class RecordingProperties {
public static class Builder { public static class Builder {
private String name = ""; private String name = "";
private Boolean hasAudio = true; private Boolean hasAudio = DefaultValues.hasAudio;
private Boolean hasVideo = true; private Boolean hasVideo = DefaultValues.hasVideo;
private Recording.OutputMode outputMode = Recording.OutputMode.COMPOSED; private Recording.OutputMode outputMode = DefaultValues.outputMode;
private RecordingLayout recordingLayout; private RecordingLayout recordingLayout;
private String resolution; private String resolution;
private Integer frameRate; private Integer frameRate;
private Long shmSize; private Long shmSize;
private String customLayout; private String customLayout;
private Boolean ignoreFailedStreams = DefaultValues.ignoreFailedStreams;
private String mediaNode; private String mediaNode;
public Builder() { public Builder() {
@ -81,6 +85,7 @@ public class RecordingProperties {
this.frameRate = props.frameRate(); this.frameRate = props.frameRate();
this.shmSize = props.shmSize(); this.shmSize = props.shmSize();
this.customLayout = props.customLayout(); this.customLayout = props.customLayout();
this.ignoreFailedStreams = props.ignoreFailedStreams();
this.mediaNode = props.mediaNode(); this.mediaNode = props.mediaNode();
} }
@ -90,7 +95,7 @@ public class RecordingProperties {
public RecordingProperties build() { public RecordingProperties build() {
return new RecordingProperties(this.name, this.hasAudio, this.hasVideo, this.outputMode, return new RecordingProperties(this.name, this.hasAudio, this.hasVideo, this.outputMode,
this.recordingLayout, this.resolution, this.frameRate, this.shmSize, this.customLayout, this.recordingLayout, this.resolution, this.frameRate, this.shmSize, this.customLayout,
this.mediaNode); this.ignoreFailedStreams, this.mediaNode);
} }
/** /**
@ -206,6 +211,27 @@ public class RecordingProperties {
return this; return this;
} }
/**
* Call this method to specify whether to ignore failed streams or not when
* starting the recording. This property only applies to
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} recordings.
* For this type of recordings, when calling
* {@link io.openvidu.java.client.OpenVidu#startRecording} by default all the
* streams available at the moment the recording process starts must be healthy
* and properly sending media. If some stream that should be sending media is
* broken, then the recording process fails after a 10s timeout. In this way
* your application is notified that some stream is not being recorded, so it
* can retry the process again. But you can disable this rollback behavior and
* simply ignore any failed stream, which will be susceptible to be recorded in
* the future if media starts flowing as expected at any point. The downside of
* this behavior is that you will have no guarantee that all streams present at
* the beginning of a recording are actually being recorded.
*/
public RecordingProperties.Builder ignoreFailedStreams(boolean ignoreFailedStreams) {
this.ignoreFailedStreams = ignoreFailedStreams;
return this;
}
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank"
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * style="display: inline-block; background-color: rgb(0, 136, 170); color:
@ -213,8 +239,10 @@ public class RecordingProperties {
* 3px; font-size: 13px; line-height:21px; font-family: Montserrat, * 3px; font-size: 13px; line-height:21px; font-family: Montserrat,
* sans-serif">PRO</a> Call this method to force the recording to be hosted in * sans-serif">PRO</a> Call this method to force the recording to be hosted in
* the Media Node with identifier <code>mediaNodeId</code>. This property only * the Media Node with identifier <code>mediaNodeId</code>. This property only
* applies to COMPOSED or COMPOSED_QUICK_START recordings with * applies to {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED} or
* {@link RecordingProperties#hasVideo()} to true and is ignored for INDIVIDUAL * {@link io.openvidu.java.client.Recording.OutputMode#COMPOSED_QUICK_START}
* recordings with {@link RecordingProperties#hasVideo()} to true and is ignored
* for {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL}
* recordings and audio-only recordings, that are always hosted in the same * recordings and audio-only recordings, that are always hosted in the same
* Media Node hosting its Session * Media Node hosting its Session
*/ */
@ -227,7 +255,7 @@ public class RecordingProperties {
protected RecordingProperties(String name, Boolean hasAudio, Boolean hasVideo, Recording.OutputMode outputMode, protected RecordingProperties(String name, Boolean hasAudio, Boolean hasVideo, Recording.OutputMode outputMode,
RecordingLayout layout, String resolution, Integer frameRate, Long shmSize, String customLayout, RecordingLayout layout, String resolution, Integer frameRate, Long shmSize, String customLayout,
String mediaNode) { Boolean ignoreFailedStreams, String mediaNode) {
this.name = name != null ? name : ""; this.name = name != null ? name : "";
this.hasAudio = hasAudio != null ? hasAudio : DefaultValues.hasAudio; this.hasAudio = hasAudio != null ? hasAudio : DefaultValues.hasAudio;
this.hasVideo = hasVideo != null ? hasVideo : DefaultValues.hasVideo; this.hasVideo = hasVideo != null ? hasVideo : DefaultValues.hasVideo;
@ -242,6 +270,9 @@ public class RecordingProperties {
this.customLayout = customLayout; this.customLayout = customLayout;
} }
} }
if (OutputMode.INDIVIDUAL.equals(this.outputMode)) {
this.ignoreFailedStreams = ignoreFailedStreams;
}
this.mediaNode = mediaNode; this.mediaNode = mediaNode;
} }
@ -366,6 +397,29 @@ public class RecordingProperties {
return this.customLayout; return this.customLayout;
} }
/**
* Defines whether to ignore failed streams or not when starting the recording.
* This property only applies to
* {@link io.openvidu.java.client.Recording.OutputMode#INDIVIDUAL} recordings.
* For this type of recordings, when calling
* {@link io.openvidu.java.client.OpenVidu#startRecording} by default all the
* streams available at the moment the recording process starts must be healthy
* and properly sending media. If some stream that should be sending media is
* broken, then the recording process fails after a 10s timeout. In this way
* your application is notified that some stream is not being recorded, so it
* can retry the process again. But you can disable this rollback behavior and
* simply ignore any failed stream, which will be susceptible to be recorded in
* the future if media starts flowing as expected at any point. The downside of
* this behavior is that you will have no guarantee that all streams present at
* the beginning of a recording are actually being recorded.<br>
* <br>
*
* Default to false
*/
public Boolean ignoreFailedStreams() {
return this.ignoreFailedStreams;
}
/** /**
* <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank" * <a href="https://docs.openvidu.io/en/stable/openvidu-pro/" target="_blank"
* style="display: inline-block; background-color: rgb(0, 136, 170); color: * style="display: inline-block; background-color: rgb(0, 136, 170); color:
@ -401,6 +455,10 @@ public class RecordingProperties {
json.addProperty("customLayout", customLayout != null ? customLayout : ""); json.addProperty("customLayout", customLayout != null ? customLayout : "");
} }
} }
if (OutputMode.INDIVIDUAL.equals(outputMode)) {
json.addProperty("ignoreFailedStreams",
ignoreFailedStreams != null ? ignoreFailedStreams : DefaultValues.ignoreFailedStreams);
}
if (this.mediaNode != null) { if (this.mediaNode != null) {
json.addProperty("mediaNode", mediaNode); json.addProperty("mediaNode", mediaNode);
} }
@ -452,6 +510,9 @@ public class RecordingProperties {
} }
} }
} }
if (OutputMode.INDIVIDUAL.equals(outputModeAux)) {
builder.ignoreFailedStreams(json.get("ignoreFailedStreams").getAsBoolean());
}
if (json.has("mediaNode")) { if (json.has("mediaNode")) {
String mediaNodeId = null; String mediaNodeId = null;
if (json.get("mediaNode").isJsonObject()) { if (json.get("mediaNode").isJsonObject()) {

View File

@ -430,7 +430,6 @@ public class RecordingManager {
// Start new RecorderEndpoint for this stream // Start new RecorderEndpoint for this stream
log.info("Starting new RecorderEndpoint in session {} for new stream of participant {}", log.info("Starting new RecorderEndpoint in session {} for new stream of participant {}",
session.getSessionId(), participant.getParticipantPublicId()); session.getSessionId(), participant.getParticipantPublicId());
final CountDownLatch startedCountDown = new CountDownLatch(1);
MediaProfileSpecType profile = null; MediaProfileSpecType profile = null;
try { try {
@ -443,7 +442,7 @@ public class RecordingManager {
} }
this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile, this.singleStreamRecordingService.startRecorderEndpointForPublisherEndpoint(recording.getId(), profile,
participant, startedCountDown); participant, new CountDownLatch(1));
} else if (RecordingUtils.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) { } else if (RecordingUtils.IS_COMPOSED(recording.getOutputMode()) && !recording.hasVideo()) {
// Connect this stream to existing Composite recorder // Connect this stream to existing Composite recorder
log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}", log.info("Joining PublisherEndpoint to existing Composite in session {} for new stream of participant {}",

View File

@ -122,10 +122,17 @@ public class SingleStreamRecordingService extends RecordingService {
} }
try { try {
if (!properties.ignoreFailedStreams()) {
if (!recordingStartedCountdown.await(10, TimeUnit.SECONDS)) { if (!recordingStartedCountdown.await(10, TimeUnit.SECONDS)) {
log.error("Error waiting for some recorder endpoint to start in session {}", session.getSessionId()); log.error("Error waiting for some recorder endpoint to start in session {}",
session.getSessionId());
throw this.failStartRecording(session, recording, "Couldn't initialize some RecorderEndpoint"); throw this.failStartRecording(session, recording, "Couldn't initialize some RecorderEndpoint");
} }
} else {
log.info(
"Ignoring failed streams in recording {}. Some streams may not be immediately or ever recorded",
recordingId);
}
} catch (InterruptedException e) { } catch (InterruptedException e) {
recording.setStatus(io.openvidu.java.client.Recording.Status.failed); recording.setStatus(io.openvidu.java.client.Recording.Status.failed);
log.error("Exception while waiting for state change", e); log.error("Exception while waiting for state change", e);

View File

@ -46,7 +46,7 @@ import io.openvidu.test.browsers.utils.RecordingUtils;
public class AbstractOpenViduTestAppE2eTest { public class AbstractOpenViduTestAppE2eTest {
final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultRecordingProperties':{'hasVideo':true,'frameRate':25,'hasAudio':true,'shmSize':536870912,'name':'','outputMode':'COMPOSED','resolution':'1280x720','recordingLayout':'BEST_FIT'},'customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}"; final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultRecordingProperties':{'hasVideo':true,'frameRate':25,'hasAudio':true,'shmSize':536870912,'name':'','outputMode':'COMPOSED','resolution':'1280x720','recordingLayout':'BEST_FIT','ignoreFailedStreams':false},'customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}";
final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null}"; final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null}";
final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[]}"; final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[]}";
final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[]}"; final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[]}";