From 2043c337611395f20a2569eaaefa4003e7b1aab8 Mon Sep 17 00:00:00 2001 From: pabloFuente Date: Wed, 18 Jan 2023 18:11:16 +0100 Subject: [PATCH] openvidu-server: refactoring to support rtmp streaming --- .../io/openvidu/client/OpenViduException.java | 2 +- .../server/config/OpenviduConfig.java | 139 +++++++++++++++- .../io/openvidu/server/core/Participant.java | 8 +- .../java/io/openvidu/server/core/Session.java | 15 +- .../server/core/SessionEventsHandler.java | 14 +- .../openvidu/server/core/SessionManager.java | 2 + .../kurento/core/KurentoParticipant.java | 4 +- .../server/kurento/core/KurentoSession.java | 4 +- .../kurento/core/KurentoSessionManager.java | 69 +++++--- .../server/kurento/kms/KmsManager.java | 7 + .../ComposedQuickStartRecordingService.java | 9 +- .../service/ComposedRecordingService.java | 150 +----------------- .../recording/service/RecordingManager.java | 37 ++++- .../server/rest/SessionRestController.java | 2 +- .../src/main/resources/application.properties | 2 +- .../test/e2e/OpenViduMobileE2eTest.java | 1 - 16 files changed, 254 insertions(+), 211 deletions(-) diff --git a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java index 993a7f1c..b2445129 100644 --- a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java +++ b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java @@ -50,7 +50,7 @@ public class OpenViduException extends JsonRpcErrorException { SIGNAL_FORMAT_INVALID_ERROR_CODE(600), SIGNAL_TO_INVALID_ERROR_CODE(601), - DOCKER_NOT_FOUND(709), RECORDING_PATH_NOT_VALID(708), RECORDING_FILE_EMPTY_ERROR(707), + RTMP_START_ERROR_CODE(710), DOCKER_NOT_FOUND(709), 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), diff --git a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java index bf43cb97..e7c4f28e 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java +++ b/openvidu-server/src/main/java/io/openvidu/server/config/OpenviduConfig.java @@ -27,12 +27,14 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; +import java.nio.charset.Charset; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -46,6 +48,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.validator.routines.DomainValidator; import org.apache.commons.validator.routines.InetAddressValidator; import org.apache.http.Header; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.message.BasicHeader; import org.kurento.jsonrpc.JsonUtils; import org.slf4j.Logger; @@ -61,8 +65,12 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; import io.openvidu.java.client.IceServerProperties; import io.openvidu.java.client.OpenViduRole; +import io.openvidu.java.client.RecordingLayout; +import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.VideoCodec; import io.openvidu.server.OpenViduServer; import io.openvidu.server.cdr.CDREventName; @@ -489,7 +497,7 @@ public class OpenviduConfig { public Map getConfigProps() { return configProps; } - + public Set getSecretProperties() { return secretProps; } @@ -1267,5 +1275,132 @@ public class OpenviduConfig { hiddenString = charToReplace.repeat(originalString.length() - numberOfVisibleChars) + hiddenString; return hiddenString; } - + + public String getLayoutUrl(RecordingProperties recordingProperties, String sessionId) throws OpenViduException { + String secret = this.getOpenViduSecret(); + + // Check if "customLayout" property defines a final URL + if (RecordingLayout.CUSTOM.equals(recordingProperties.recordingLayout())) { + String layout = recordingProperties.customLayout(); + if (!layout.isEmpty()) { + try { + URL url = new URL(layout); + log.info("\"customLayout\" property has a URL format ({}). Using it to connect to custom layout", + url.toString()); + return processCustomLayoutUrlFormat(url, sessionId); + } catch (MalformedURLException e) { + String layoutPath = this.getOpenviduRecordingCustomLayout() + layout; + layoutPath = layoutPath.endsWith("/") ? layoutPath : (layoutPath + "/"); + log.info( + "\"customLayout\" property is defined as \"{}\". Using a different custom layout than the default one. Expected path: {}", + layout, layoutPath + "index.html"); + try { + final File indexHtml = new File(layoutPath + "index.html"); + if (!indexHtml.exists()) { + throw new IOException(); + } + log.info("Custom layout path \"{}\" is valid. Found file {}", layout, + indexHtml.getAbsolutePath()); + } catch (IOException e1) { + final String error = "Custom layout path " + layout + " is not valid. Expected file " + + layoutPath + "index.html to exist and be readable"; + log.error(error); + throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, error); + } + } + } + } + + boolean recordingComposedUrlDefined = this.getOpenViduRecordingComposedUrl() != null + && !this.getOpenViduRecordingComposedUrl().isEmpty(); + String recordingUrl = recordingComposedUrlDefined ? this.getOpenViduRecordingComposedUrl() : this.getFinalUrl(); + recordingUrl = recordingUrl.replaceFirst("https://", ""); + boolean startsWithHttp = recordingUrl.startsWith("http://"); + + if (startsWithHttp) { + recordingUrl = recordingUrl.replaceFirst("http://", ""); + } + + if (recordingUrl.endsWith("/")) { + recordingUrl = recordingUrl.substring(0, recordingUrl.length() - 1); + } + + String layout, finalUrl; + final String basicauth = this.isOpenviduRecordingComposedBasicauth() ? ("OPENVIDUAPP:" + secret + "@") : ""; + if (RecordingLayout.CUSTOM.equals(recordingProperties.recordingLayout())) { + layout = recordingProperties.customLayout(); + if (!layout.isEmpty()) { + layout = layout.startsWith("/") ? layout : ("/" + layout); + layout = layout.endsWith("/") ? layout.substring(0, layout.length() - 1) : layout; + } + layout += "/index.html"; + finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl + + RequestMappings.CUSTOM_LAYOUTS + layout + "?sessionId=" + sessionId + "&secret=" + secret; + } else { + layout = recordingProperties.recordingLayout().name().toLowerCase().replaceAll("_", "-"); + int port = startsWithHttp ? 80 : 443; + try { + port = new URL(this.getFinalUrl()).getPort(); + } catch (MalformedURLException e) { + log.error(e.getMessage()); + } + String defaultPathForDefaultLayout = recordingComposedUrlDefined ? "" + : (this.getOpenViduFrontendDefaultPath()); + finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl + + defaultPathForDefaultLayout + "/#/layout-" + layout + "/" + sessionId + "/" + secret + "/" + port + + "/" + !recordingProperties.hasAudio(); + } + + return finalUrl; + } + + private String processCustomLayoutUrlFormat(URL url, String shortSessionId) { + String finalUrl = url.getProtocol() + "://" + url.getAuthority(); + if (!url.getPath().isEmpty()) { + finalUrl += url.getPath(); + } + finalUrl = finalUrl.endsWith("/") ? finalUrl.substring(0, finalUrl.length() - 1) : finalUrl; + if (url.getQuery() != null) { + URI uri; + try { + uri = url.toURI(); + finalUrl += "?"; + } catch (URISyntaxException e) { + String error = "\"customLayout\" property has URL format and query params (" + url.toString() + + "), but does not comply with RFC2396 URI format"; + log.error(error); + throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, error); + } + List params = URLEncodedUtils.parse(uri, Charset.forName("UTF-8")); + Iterator it = params.iterator(); + boolean hasSessionId = false; + boolean hasSecret = false; + while (it.hasNext()) { + NameValuePair param = it.next(); + finalUrl += param.getName() + "=" + param.getValue(); + if (it.hasNext()) { + finalUrl += "&"; + } + if (!hasSessionId) { + hasSessionId = param.getName().equals("sessionId"); + } + if (!hasSecret) { + hasSecret = param.getName().equals("secret"); + } + } + if (!hasSessionId) { + finalUrl += "&sessionId=" + shortSessionId; + } + if (!hasSecret) { + finalUrl += "&secret=" + this.getOpenViduSecret(); + } + } + + if (url.getRef() != null) { + finalUrl += "#" + url.getRef(); + } + + return finalUrl; + } + } diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java index f29bf347..82095363 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Participant.java @@ -229,8 +229,12 @@ public class Participant { return ProtocolElements.STT_PARTICIPANT_PUBLICID.equals(this.participantPublicId); } - public boolean isRecorderOrSttParticipant() { - return (this.isRecorderParticipant() || this.isSttParticipant()); + public boolean isRtmpParticipant() { + return isRecorderParticipant(); + } + + public boolean isRecorderOrSttOrRtmpParticipant() { + return (this.isRecorderParticipant() || this.isSttParticipant() || this.isRtmpParticipant()); } public String getPublisherStreamId() { diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java index b5a35f79..c291b445 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/Session.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/Session.java @@ -139,13 +139,8 @@ public class Session implements SessionInterface { return null; } - public boolean onlyRecorderAndOrSttParticipant() { - if (this.participants.size() == 1) { - return this.participants.values().iterator().next().isRecorderOrSttParticipant(); - } else if (this.participants.size() == 2) { - return this.participants.values().stream().allMatch(p -> p.isRecorderOrSttParticipant()); - } - return false; + public boolean onlyRecorderAndOrSttAndOrRtmpParticipant() { + return this.participants.values().stream().allMatch(p -> p.isRecorderOrSttOrRtmpParticipant()); } public int getActivePublishers() { @@ -207,7 +202,7 @@ public class Session implements SessionInterface { public String getMediaNodeId() { return null; } - + public String getMediaNodeIp() { return null; } @@ -223,8 +218,8 @@ public class Session implements SessionInterface { Set snapshotOfActiveConnections = this.getParticipants().stream().collect(Collectors.toSet()); JsonArray jsonArray = new JsonArray(); snapshotOfActiveConnections.forEach(participant -> { - // Filter RECORDER and STT participants - if (!participant.isRecorderOrSttParticipant()) { + // Filter RECORDER/STT/RTMP participants + if (!participant.isRecorderOrSttOrRtmpParticipant()) { jsonArray.add(withWebrtcStats ? participant.withStatsToJson() : participant.toJson()); } }); diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java index 61f618f2..427fe45f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionEventsHandler.java @@ -130,16 +130,16 @@ public class SessionEventsHandler { participantJson.add(ProtocolElements.JOINROOM_PEERSTREAMS_PARAM, streamsArray); } - // Avoid emitting 'connectionCreated' event of existing RECORDER or STT + // Avoid emitting 'connectionCreated' event of existing RECORDER/STT/RTMP // participant in openvidu-browser in newly joined participants - if (!existingParticipant.isRecorderOrSttParticipant()) { + if (!existingParticipant.isRecorderOrSttOrRtmpParticipant()) { resultArray.add(participantJson); } - // If RECORDER or STT participant has joined do NOT send 'participantJoined' + // If RECORDER/STT/RTMP participant has joined do NOT send 'participantJoined' // notification to existing participants. 'recordingStarted' will be sent to all // existing participants when recorder first subscribe to a stream - if (!participant.isRecorderOrSttParticipant()) { + if (!participant.isRecorderOrSttOrRtmpParticipant()) { JsonObject notifParams = new JsonObject(); // Metadata associated to new participant @@ -519,8 +519,8 @@ public class SessionEventsHandler { evictedParticipant.getParticipantPublicId()); params.addProperty(ProtocolElements.PARTICIPANTEVICTED_REASON_PARAM, reason != null ? reason.name() : ""); - if (evictedParticipant.isRecorderOrSttParticipant()) { - // Do not send a message when evicting RECORDER or STT participant + if (evictedParticipant.isRecorderOrSttOrRtmpParticipant()) { + // Do not send a message when evicting RECORDER/STT/RTMP participant rpcNotificationService.sendNotification(evictedParticipant.getParticipantPrivateId(), ProtocolElements.PARTICIPANTEVICTED_METHOD, params); } @@ -719,7 +719,7 @@ public class SessionEventsHandler { protected Set filterParticipantsByRole(Set roles, Set participants) { return participants.stream().filter(part -> { - if (part.isRecorderOrSttParticipant()) { + if (part.isRecorderOrSttOrRtmpParticipant()) { return false; } return roles.contains(part.getToken().getRole()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java index edfe41c4..bd2684cb 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/core/SessionManager.java @@ -193,6 +193,8 @@ public abstract class SessionManager { public abstract void onUnsubscribeFromSpeechToText(Participant participant, Integer transactionId, String connectionId); + public abstract void stopRtmpIfNecessary(Session session); + public void onEcho(String participantPrivateId, Integer requestId) { sessionEventsHandler.onEcho(participantPrivateId, requestId); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java index 6be4a9f3..3d274bcb 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoParticipant.java @@ -283,7 +283,7 @@ public class KurentoParticipant extends Participant { String sdpAnswer = subscriber.subscribe(sdpString, kSender.getPublisher()); log.info("PARTICIPANT {}: Is now receiving video from {} in room {}", this.getParticipantPublicId(), senderName, this.session.getSessionId()); - if (!silent && !this.isRecorderOrSttParticipant()) { + if (!silent && !this.isRecorderOrSttOrRtmpParticipant()) { endpointConfig.getCdr().recordNewSubscriber(this, sender.getPublisherStreamId(), sender.getParticipantPublicId(), subscriber.createdAt()); } @@ -589,7 +589,7 @@ public class KurentoParticipant extends Participant { } } - if (!this.isRecorderOrSttParticipant()) { + if (!this.isRecorderOrSttOrRtmpParticipant()) { endpointConfig.getCdr().stopSubscriber(this.getParticipantPublicId(), senderName, subscriber.getStreamId(), reason); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java index 482431ab..689b3147 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSession.java @@ -86,7 +86,7 @@ public class KurentoSession extends Session { log.info("SESSION {}: Added participant {}", sessionId, participant); - if (!participant.isRecorderOrSttParticipant()) { + if (!participant.isRecorderOrSttOrRtmpParticipant()) { kurentoEndpointConfig.getCdr().recordParticipantJoined(participant, sessionId); } } @@ -386,7 +386,7 @@ public class KurentoSession extends Session { public int getNumberOfWebrtcConnections() { return this.getActivePublishers() - + this.participants.values().stream().filter(p -> !p.isRecorderOrSttParticipant()) + + this.participants.values().stream().filter(p -> !p.isRecorderOrSttOrRtmpParticipant()) .mapToInt(p -> ((KurentoParticipant) p).getSubscribers().size()).reduce(0, Integer::sum); } diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java index 2ccd98e6..f3609098 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/core/KurentoSessionManager.java @@ -257,22 +257,16 @@ public class KurentoSessionManager extends SessionManager { sessionEventsHandler.onParticipantLeft(participant, sessionId, remainingParticipants, transactionId, null, reason, scheduleWebsocketClose); + // If session is closed by a call to "DELETE /api/sessions" do NOT stop the + // recording. Will be stopped after in method + // "SessionManager.closeSessionAndEmptyCollections" if (!EndReason.sessionClosedByServer.equals(reason)) { - // If session is closed by a call to "DELETE /api/sessions" do NOT stop the - // recording. Will be stopped after in method - // "SessionManager.closeSessionAndEmptyCollections" - - boolean recordingParticipantLeft = (remainingParticipants.size() == 1 - && remainingParticipants.iterator().next().isRecorderParticipant()) - || (remainingParticipants.size() == 2 && remainingParticipants.stream() - .allMatch(p -> p.isRecorderOrSttParticipant())); if (remainingParticipants.isEmpty()) { - if (openviduConfig.isRecordingModuleEnabled() - && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) - && (this.recordingManager.sessionIsBeingRecorded(sessionId))) { + if (this.recordingManager.sessionIsBeingRecorded(sessionId)) { // Start countdown to stop recording. Will be aborted if a Publisher starts - // before timeout + // before timeout. This only applies to INDIVIDUAL recordings, as for COMPOSED + // recordings it would still remain a recorder participant log.info( "Last participant left. Starting {} seconds countdown for stopping recording of session {}", this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); @@ -302,22 +296,41 @@ public class KurentoSessionManager extends SessionManager { sessionId); } } - } else if (recordingParticipantLeft && openviduConfig.isRecordingModuleEnabled() - && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) - && this.recordingManager.sessionIsBeingRecorded(sessionId)) { - // RECORDER or STT participant is the last one standing. Start countdown - log.info( - "Last participant left. Starting {} seconds countdown for stopping recording of session {}", - this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); - recordingManager.initAutomaticRecordingStopThread(session); + } else { - } else if (recordingParticipantLeft && openviduConfig.isRecordingModuleEnabled() - && MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode()) - && session.getSessionProperties().defaultRecordingProperties().outputMode() + boolean recordingParticipantLeft = remainingParticipants.size() > 0 + && remainingParticipants.stream() + .allMatch(p -> p.isRecorderOrSttOrRtmpParticipant()) + && remainingParticipants.stream().anyMatch(p -> p.isRecorderParticipant()); + + if (recordingParticipantLeft) { + + if (this.recordingManager.sessionIsBeingRecorded(sessionId)) { + + // RECORDER or STT participant is the last one standing. Start countdown + log.info( + "Last participant left. Starting {} seconds countdown for stopping recording of session {}", + this.openviduConfig.getOpenviduRecordingAutostopTimeout(), sessionId); + recordingManager.initAutomaticRecordingStopThread(session); + + } else if (session.getSessionProperties().defaultRecordingProperties().outputMode() .equals(Recording.OutputMode.COMPOSED_QUICK_START)) { - // If no recordings are active in COMPOSED_QUICK_START output mode, stop - // container - recordingManager.stopComposedQuickStartContainer(session, reason); + + // If no recordings are active in COMPOSED_QUICK_START output mode, stop + // container + recordingManager.stopComposedQuickStartContainer(session, reason); + + } + } + + boolean rtmpParticipantLeft = remainingParticipants.size() > 0 + && remainingParticipants.stream() + .allMatch(p -> p.isRecorderOrSttOrRtmpParticipant()) + && remainingParticipants.stream().anyMatch(p -> p.isRtmpParticipant()); + + if (rtmpParticipantLeft) { + this.stopRtmpIfNecessary(session); + } } } @@ -1411,6 +1424,10 @@ public class KurentoSessionManager extends SessionManager { return lessLoadedKms; } + @Override + public void stopRtmpIfNecessary(Session session) { + } + private io.openvidu.server.recording.Recording getActiveRecordingIfAllowedByParticipantRole( Participant participant) { io.openvidu.server.recording.Recording recording = null; diff --git a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java index f9032266..14f24a9d 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/kurento/kms/KmsManager.java @@ -44,6 +44,12 @@ import org.springframework.beans.factory.annotation.Autowired; import com.google.gson.JsonObject; +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.client.internal.ProtocolElements; +import io.openvidu.java.client.ConnectionProperties; +import io.openvidu.java.client.ConnectionType; +import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.RecordingProperties; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; @@ -51,6 +57,7 @@ import io.openvidu.server.core.IdentifierPrefixes; import io.openvidu.server.core.Session; import io.openvidu.server.core.SessionEventsHandler; import io.openvidu.server.core.SessionManager; +import io.openvidu.server.core.Token; import io.openvidu.server.kurento.core.KurentoSession; import io.openvidu.server.utils.MediaNodeManager; import io.openvidu.server.utils.RemoteOperationUtils; diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java index 490b3a8f..7c3386ef 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedQuickStartRecordingService.java @@ -202,14 +202,14 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService } private String runContainer(Session session, RecordingProperties properties) throws Exception { + log.info("Starting COMPOSED_QUICK_START container for session id: {}", session.getSessionId()); - Recording recording = new Recording(session.getSessionId(), session.getUniqueSessionId(), null, properties); - String layoutUrl = this.getLayoutUrl(recording); + String layoutUrl = this.openviduConfig.getLayoutUrl(properties, session.getSessionId()); List envs = new ArrayList<>(); envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); - envs.add("RECORDING_TYPE=COMPOSED_QUICK_START"); + envs.add("CONTAINER_WORKING_MODE=COMPOSED_QUICK_START"); envs.add("RESOLUTION=" + properties.resolution()); envs.add("FRAMERATE=" + properties.frameRate()); envs.add("URL=" + layoutUrl); @@ -245,7 +245,8 @@ public class ComposedQuickStartRecordingService extends ComposedRecordingService private void waitForComposedQuickStartFiles(Recording recording) throws Exception { final int SECONDS_MAX_WAIT = fileManager.maxSecondsWaitForFile(); - final String PATH = this.openviduConfig.getOpenViduRecordingPath(recording.getRecordingProperties().mediaNode()) + recording.getId() + "/"; + final String PATH = this.openviduConfig.getOpenViduRecordingPath(recording.getRecordingProperties().mediaNode()) + + recording.getId() + "/"; // Waiting for the files generated at the end of the stopping process: the // ffprobe info and the thumbnail diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java index 7eb5b988..e8f2639f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/ComposedRecordingService.java @@ -19,21 +19,13 @@ package io.openvidu.server.recording.service; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -43,7 +35,6 @@ import com.github.dockerjava.api.model.Volume; 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.cdr.CallDetailRecord; import io.openvidu.server.config.OpenviduConfig; @@ -58,7 +49,6 @@ import io.openvidu.server.recording.Recording; import io.openvidu.server.recording.RecordingDownloader; import io.openvidu.server.recording.RecordingInfoUtils; import io.openvidu.server.recording.RecordingUploader; -import io.openvidu.server.rest.RequestMappings; import io.openvidu.server.utils.CustomFileManager; import io.openvidu.server.utils.DockerManager; @@ -146,9 +136,10 @@ public class ComposedRecordingService extends RecordingService { List envs = new ArrayList<>(); - String layoutUrl = this.getLayoutUrl(recording); + String layoutUrl = this.openviduConfig.getLayoutUrl(properties, session.getSessionId()); envs.add("DEBUG_MODE=" + openviduConfig.isOpenViduRecordingDebug()); + envs.add("CONTAINER_WORKING_MODE=COMPOSED"); envs.add("URL=" + layoutUrl); envs.add("ONLY_VIDEO=" + !properties.hasAudio()); envs.add("RESOLUTION=" + properties.resolution()); @@ -205,8 +196,8 @@ public class ComposedRecordingService extends RecordingService { recording.getSessionId()); CompositeWrapper compositeWrapper = new CompositeWrapper((KurentoSession) session, - "file://" + this.openviduConfig.getOpenViduRecordingPath(properties.mediaNode()) + recording.getId() + "/" + properties.name() - + ".webm"); + "file://" + this.openviduConfig.getOpenViduRecordingPath(properties.mediaNode()) + recording.getId() + + "/" + properties.name() + ".webm"); this.composites.put(session.getSessionId(), compositeWrapper); for (Participant p : session.getParticipants()) { @@ -431,7 +422,8 @@ public class ComposedRecordingService extends RecordingService { } protected void waitForVideoFileNotEmpty(Recording recording) throws Exception { - final String VIDEO_FILE = this.openviduConfig.getOpenViduRecordingPath(recording.getRecordingProperties().mediaNode()) + recording.getId() + "/" + final String VIDEO_FILE = this.openviduConfig + .getOpenViduRecordingPath(recording.getRecordingProperties().mediaNode()) + recording.getId() + "/" + recording.getName() + RecordingService.COMPOSED_RECORDING_EXTENSION; this.fileManager.waitForFileToExistAndNotEmpty(recording.getRecordingProperties().mediaNode(), VIDEO_FILE); log.info("File {} exists and is not empty", VIDEO_FILE); @@ -449,136 +441,6 @@ public class ComposedRecordingService extends RecordingService { throw e; } - protected String getLayoutUrl(Recording recording) throws OpenViduException { - String secret = openviduConfig.getOpenViduSecret(); - - // Check if "customLayout" property defines a final URL - if (RecordingLayout.CUSTOM.equals(recording.getRecordingLayout())) { - String layout = recording.getCustomLayout(); - if (!layout.isEmpty()) { - try { - URL url = new URL(layout); - log.info("\"customLayout\" property has a URL format ({}). Using it to connect to custom layout", - url.toString()); - return this.processCustomLayoutUrlFormat(url, recording.getSessionId()); - } catch (MalformedURLException e) { - String layoutPath = openviduConfig.getOpenviduRecordingCustomLayout() + layout; - layoutPath = layoutPath.endsWith("/") ? layoutPath : (layoutPath + "/"); - log.info( - "\"customLayout\" property is defined as \"{}\". Using a different custom layout than the default one. Expected path: {}", - layout, layoutPath + "index.html"); - try { - final File indexHtml = new File(layoutPath + "index.html"); - if (!indexHtml.exists()) { - throw new IOException(); - } - log.info("Custom layout path \"{}\" is valid. Found file {}", layout, - indexHtml.getAbsolutePath()); - } catch (IOException e1) { - final String error = "Custom layout path " + layout + " is not valid. Expected file " - + layoutPath + "index.html to exist and be readable"; - log.error(error); - throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, error); - } - } - } - } - - boolean recordingComposedUrlDefined = openviduConfig.getOpenViduRecordingComposedUrl() != null - && !openviduConfig.getOpenViduRecordingComposedUrl().isEmpty(); - String recordingUrl = recordingComposedUrlDefined ? openviduConfig.getOpenViduRecordingComposedUrl() - : openviduConfig.getFinalUrl(); - recordingUrl = recordingUrl.replaceFirst("https://", ""); - boolean startsWithHttp = recordingUrl.startsWith("http://"); - - if (startsWithHttp) { - recordingUrl = recordingUrl.replaceFirst("http://", ""); - } - - if (recordingUrl.endsWith("/")) { - recordingUrl = recordingUrl.substring(0, recordingUrl.length() - 1); - } - - String layout, finalUrl; - final String basicauth = openviduConfig.isOpenviduRecordingComposedBasicauth() ? ("OPENVIDUAPP:" + secret + "@") - : ""; - if (RecordingLayout.CUSTOM.equals(recording.getRecordingLayout())) { - layout = recording.getCustomLayout(); - if (!layout.isEmpty()) { - layout = layout.startsWith("/") ? layout : ("/" + layout); - layout = layout.endsWith("/") ? layout.substring(0, layout.length() - 1) : layout; - } - layout += "/index.html"; - finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl - + RequestMappings.CUSTOM_LAYOUTS + layout + "?sessionId=" + recording.getSessionId() + "&secret=" - + secret; - } else { - layout = recording.getRecordingLayout().name().toLowerCase().replaceAll("_", "-"); - int port = startsWithHttp ? 80 : 443; - try { - port = new URL(openviduConfig.getFinalUrl()).getPort(); - } catch (MalformedURLException e) { - log.error(e.getMessage()); - } - String defaultPathForDefaultLayout = recordingComposedUrlDefined ? "" - : (openviduConfig.getOpenViduFrontendDefaultPath()); - finalUrl = (startsWithHttp ? "http" : "https") + "://" + basicauth + recordingUrl - + defaultPathForDefaultLayout + "/#/layout-" + layout + "/" + recording.getSessionId() + "/" - + secret + "/" + port + "/" + !recording.hasAudio(); - } - - return finalUrl; - } - - private String processCustomLayoutUrlFormat(URL url, String shortSessionId) { - String finalUrl = url.getProtocol() + "://" + url.getAuthority(); - if (!url.getPath().isEmpty()) { - finalUrl += url.getPath(); - } - finalUrl = finalUrl.endsWith("/") ? finalUrl.substring(0, finalUrl.length() - 1) : finalUrl; - if (url.getQuery() != null) { - URI uri; - try { - uri = url.toURI(); - finalUrl += "?"; - } catch (URISyntaxException e) { - String error = "\"customLayout\" property has URL format and query params (" + url.toString() - + "), but does not comply with RFC2396 URI format"; - log.error(error); - throw new OpenViduException(Code.RECORDING_PATH_NOT_VALID, error); - } - List params = URLEncodedUtils.parse(uri, Charset.forName("UTF-8")); - Iterator it = params.iterator(); - boolean hasSessionId = false; - boolean hasSecret = false; - while (it.hasNext()) { - NameValuePair param = it.next(); - finalUrl += param.getName() + "=" + param.getValue(); - if (it.hasNext()) { - finalUrl += "&"; - } - if (!hasSessionId) { - hasSessionId = param.getName().equals("sessionId"); - } - if (!hasSecret) { - hasSecret = param.getName().equals("secret"); - } - } - if (!hasSessionId) { - finalUrl += "&sessionId=" + shortSessionId; - } - if (!hasSecret) { - finalUrl += "&secret=" + openviduConfig.getOpenViduSecret(); - } - } - - if (url.getRef() != null) { - finalUrl += "#" + url.getRef(); - } - - return finalUrl; - } - protected void downloadComposedRecording(final Session session, final Recording recording, final EndReason reason) { try { this.recordingDownloader.downloadRecording(recording, null, () -> { diff --git a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java index b0232284..f215be5f 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java +++ b/openvidu-server/src/main/java/io/openvidu/server/recording/service/RecordingManager.java @@ -499,6 +499,22 @@ public class RecordingManager { return recording; } + public boolean sessionIsBeingRecordedComposed(String sessionId) { + if (!sessionIsBeingRecorded(sessionId)) { + return false; + } else { + Recording recording = this.sessionsRecordings.get(sessionId); + if (recording == null) { + recording = this.sessionsRecordingsStarting.get(sessionId); + } + if (recording != null) { + return RecordingProperties.IS_COMPOSED(recording.getOutputMode()); + } else { + return false; + } + } + } + public boolean sessionIsBeingRecordedIndividual(String sessionId) { if (!sessionIsBeingRecorded(sessionId)) { return false; @@ -507,7 +523,11 @@ public class RecordingManager { if (recording == null) { recording = this.sessionsRecordingsStarting.get(sessionId); } - return OutputMode.INDIVIDUAL.equals(recording.getOutputMode()); + if (recording != null) { + return OutputMode.INDIVIDUAL.equals(recording.getOutputMode()); + } else { + return false; + } } } @@ -674,9 +694,9 @@ public class RecordingManager { return; } if (session.getParticipants().size() == 0 - || session.onlyRecorderAndOrSttParticipant()) { - // Close session if there are no participants connected (RECORDER or STT do not - // count) and publishing + || session.onlyRecorderAndOrSttAndOrRtmpParticipant()) { + // Close session if there are no participants connected (RECORDER/STT/RTMP do + // not count) and publishing log.info("Closing session {} after automatic stop of recording {}", session.getSessionId(), recordingId); sessionManager.closeSessionAndEmptyCollections(session, EndReason.automaticStop, @@ -727,10 +747,11 @@ public class RecordingManager { if (session.isClosed()) { return false; } - if (session.getParticipants().size() == 0 || session.onlyRecorderAndOrSttParticipant()) { - // Close session if there are no participants connected (except for RECORDER or - // STT). This code will only be executed if recording is manually stopped during - // the automatic stop timeout, so the session must be also closed + if (session.getParticipants().size() == 0 + || session.onlyRecorderAndOrSttAndOrRtmpParticipant()) { + // Close session if there are no participants connected (except for + // RECORDER/STT/RTMP). This code will only be executed if recording is manually + // stopped during the automatic stop timeout, so the session must be also closed log.info( "Ongoing recording of session {} was explicetly stopped within timeout for automatic recording stop. Closing session", session.getSessionId()); diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java index eee5148d..8ba03603 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/SessionRestController.java @@ -414,7 +414,7 @@ public class SessionRestController { } if (!(MediaMode.ROUTED.equals(session.getSessionProperties().mediaMode())) || this.recordingManager.sessionIsBeingRecorded(session.getSessionId())) { - // Session is not in ROUTED MediMode or it is already being recorded + // Session is not in ROUTED MediaMode or it is already being recorded return new ResponseEntity<>(HttpStatus.CONFLICT); } if (session.getParticipants().isEmpty()) { diff --git a/openvidu-server/src/main/resources/application.properties b/openvidu-server/src/main/resources/application.properties index bff23e85..d5960ccd 100644 --- a/openvidu-server/src/main/resources/application.properties +++ b/openvidu-server/src/main/resources/application.properties @@ -27,7 +27,7 @@ OPENVIDU_WEBHOOK_EVENTS=["sessionCreated","sessionDestroyed","participantJoined" OPENVIDU_RECORDING=false OPENVIDU_RECORDING_DEBUG=false -OPENVIDU_RECORDING_VERSION=2.25.0 +OPENVIDU_RECORDING_VERSION=2.26.0-beta1 OPENVIDU_RECORDING_PATH=/opt/openvidu/recordings OPENVIDU_RECORDING_PUBLIC_ACCESS=false OPENVIDU_RECORDING_NOTIFICATION=publisher_moderator diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduMobileE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduMobileE2eTest.java index 694dd616..3a11709c 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduMobileE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduMobileE2eTest.java @@ -414,7 +414,6 @@ public class OpenViduMobileE2eTest extends AbstractOpenViduTestappE2eTest { // getScreenshotAs(OutputType.BYTES) when // https://github.com/appium/java-client/issues/1783 is fixed String base64 = driver.findElement(locator).getScreenshotAs(OutputType.BASE64); - System.out.println(base64); base64 = base64.replaceAll("[\n\r]", ""); byte[] bytes = Base64.getDecoder().decode(base64); InputStream is = new ByteArrayInputStream(bytes);