mirror of https://github.com/OpenVidu/openvidu.git
openvidu-server: refactoring to support rtmp streaming
parent
a41749602f
commit
2043c33761
|
@ -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),
|
||||
|
||||
|
|
|
@ -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<String, String> getConfigProps() {
|
||||
return configProps;
|
||||
}
|
||||
|
||||
|
||||
public Set<String> 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<NameValuePair> params = URLEncodedUtils.parse(uri, Charset.forName("UTF-8"));
|
||||
Iterator<NameValuePair> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Participant> 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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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<Participant> filterParticipantsByRole(Set<OpenViduRole> roles, Set<Participant> participants) {
|
||||
return participants.stream().filter(part -> {
|
||||
if (part.isRecorderOrSttParticipant()) {
|
||||
if (part.isRecorderOrSttOrRtmpParticipant()) {
|
||||
return false;
|
||||
}
|
||||
return roles.contains(part.getToken().getRole());
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String> 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
|
||||
|
|
|
@ -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<String> 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<NameValuePair> params = URLEncodedUtils.parse(uri, Charset.forName("UTF-8"));
|
||||
Iterator<NameValuePair> 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, () -> {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue