From 42a2fed910442be6f72f947b824531e11244da44 Mon Sep 17 00:00:00 2001 From: cruizba Date: Thu, 3 Sep 2020 19:39:43 +0200 Subject: [PATCH] Force codec to avoid transcoding working. --- .../io/openvidu/client/OpenViduException.java | 4 +- openvidu-java-client/generate-docs.sh | 4 +- .../java/io/openvidu/java/client/Session.java | 21 ++- .../java/client/SessionProperties.java | 62 +++++++- .../io/openvidu/java/client/VideoCodec.java | 25 ++++ openvidu-node-client/src/Session.ts | 12 +- openvidu-node-client/src/SessionProperties.ts | 20 +++ openvidu-node-client/src/VideoCodec.ts | 10 ++ openvidu-node-client/src/index.ts | 3 +- .../deployments/ce/docker-compose/.env | 2 +- .../pro/docker-compose/media-node/.env | 2 +- .../io/openvidu/server/OpenViduServer.java | 7 + .../java/io/openvidu/server/core/Session.java | 8 +- .../openvidu/server/core/SessionManager.java | 10 +- .../kurento/core/KurentoSessionManager.java | 74 ++++++++-- .../server/rest/SessionRestController.java | 27 +++- .../io/openvidu/server/rpc/RpcHandler.java | 10 +- .../io/openvidu/server/utils/SDPMunging.java | 133 ++++++++++++++++++ .../session-properties-dialog.component.css | 4 + .../session-properties-dialog.component.html | 12 ++ .../session-properties-dialog.component.ts | 3 +- .../openvidu-instance.component.ts | 7 +- 22 files changed, 414 insertions(+), 46 deletions(-) create mode 100644 openvidu-java-client/src/main/java/io/openvidu/java/client/VideoCodec.java create mode 100644 openvidu-node-client/src/VideoCodec.ts create mode 100644 openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java 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 c6757c23..c2c47515 100644 --- a/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java +++ b/openvidu-client/src/main/java/io/openvidu/client/OpenViduException.java @@ -49,7 +49,9 @@ public class OpenViduException extends JsonRpcErrorException { 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); + RECORDING_START_ERROR_CODE(703), RECORDING_REPORT_ERROR_CODE(702), RECORDING_COMPLETION_ERROR_CODE(701), + + FORCED_CODEC_NOT_FOUND_IN_SDPOFFER(800); private int value; diff --git a/openvidu-java-client/generate-docs.sh b/openvidu-java-client/generate-docs.sh index 3abb098a..e686c5fe 100755 --- a/openvidu-java-client/generate-docs.sh +++ b/openvidu-java-client/generate-docs.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -x if [[ -z "$BASEHREF_VERSION" ]]; then echo "Example of use: \"BASEHREF_VERSION=2.12.0 ${0}\"" 1>&2 @@ -9,7 +9,7 @@ fi grep -rl '/en/stable/' ./src | xargs sed -i -e 's|/en/stable/|/en/'${BASEHREF_VERSION}'/|g' # Generate JavaDoc -mvn javadoc:javadoc +mvn javadoc:javadoc -DadditionalJOption=-Xdoclint:none rm -rf ../../openvidu.io/api/openvidu-java-client/* cp -R ./target/site/apidocs/. ../../openvidu.io-docs/docs/api/openvidu-java-client diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java index c2e3bcc4..8b7da5d8 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java @@ -25,6 +25,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; + import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpDelete; @@ -35,11 +40,6 @@ import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; - public class Session { private static final Logger log = LoggerFactory.getLogger(Session.class); @@ -458,6 +458,8 @@ public class Session { json.addProperty("defaultRecordingLayout", properties.defaultRecordingLayout().name()); json.addProperty("defaultCustomLayout", properties.defaultCustomLayout()); json.addProperty("customSessionId", properties.customSessionId()); + json.addProperty("forcedVideoCodec", properties.forcedVideoCodec().name()); + json.addProperty("allowTranscoding", properties.isTranscodingAllowed()); StringEntity params = null; try { params = new StringEntity(json.toString()); @@ -520,6 +522,12 @@ public class Session { if (json.has("defaultCustomLayout")) { builder.defaultCustomLayout(json.get("defaultCustomLayout").getAsString()); } + if (json.has("forcedVideoCodec")) { + builder.forcedVideoCodec(VideoCodec.valueOf(json.get("forcedVideoCodec").getAsString())); + } + if (json.has("allowTranscoding")) { + builder.allowTranscoding(json.get("allowTranscoding").getAsBoolean()); + } if (this.properties != null && this.properties.customSessionId() != null) { builder.customSessionId(this.properties.customSessionId()); } else if (json.has("customSessionId")) { @@ -572,6 +580,9 @@ public class Session { json.addProperty("defaultOutputMode", this.properties.defaultOutputMode().name()); json.addProperty("defaultRecordingLayout", this.properties.defaultRecordingLayout().name()); json.addProperty("defaultCustomLayout", this.properties.defaultCustomLayout()); + json.addProperty("forcedVideoCodec", this.properties.forcedVideoCodec().name()); + json.addProperty("allowTranscoding", this.properties.isTranscodingAllowed()); + JsonObject connections = new JsonObject(); connections.addProperty("numberOfElements", this.getActiveConnections().size()); JsonArray jsonArrayConnections = new JsonArray(); diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java index 8f436721..59842409 100644 --- a/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/SessionProperties.java @@ -30,6 +30,8 @@ public class SessionProperties { private RecordingLayout defaultRecordingLayout; private String defaultCustomLayout; private String customSessionId; + private VideoCodec forcedVideoCodec; + private boolean allowTranscoding; /** * Builder for {@link io.openvidu.java.client.SessionProperties} @@ -42,6 +44,8 @@ public class SessionProperties { private RecordingLayout defaultRecordingLayout = RecordingLayout.BEST_FIT; private String defaultCustomLayout = ""; private String customSessionId = ""; + private VideoCodec forcedVideoCodec = VideoCodec.VP8; + private boolean allowTranscoding = false; /** * Returns the {@link io.openvidu.java.client.SessionProperties} object properly @@ -49,7 +53,8 @@ public class SessionProperties { */ public SessionProperties build() { return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultOutputMode, - this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId); + this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, + this.forcedVideoCodec, this.allowTranscoding); } /** @@ -137,6 +142,34 @@ public class SessionProperties { this.customSessionId = customSessionId; return this; } + + /** + * + * Call this method to define which video codec do you want to be forcibly used for this session. + * This allows browsers to use the same codec avoiding transcoding in the media server. + * To force this video codec you need to set {@link #allowTranscoding(boolean)} to false. + * + */ + public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) { + this.forcedVideoCodec = forcedVideoCodec; + return this; + } + + /** + * + * Call this method to define if you want to allowTranscoding or not. If you define it as + * as false, the default video codec VP8 will be used for all browsers, and the media + * server will not do any transcoding. If you define it as true, transcoding can be + * executed by the media server when necessary. + * + * If you want to set a different video codec, you can configure it + * by calling {@link #forcedVideoCodec(VideoCodec)} to your preferred one. + * + */ + public SessionProperties.Builder allowTranscoding(boolean allowTranscoding) { + this.allowTranscoding = allowTranscoding; + return this; + } } @@ -147,16 +180,21 @@ public class SessionProperties { this.defaultRecordingLayout = RecordingLayout.BEST_FIT; this.defaultCustomLayout = ""; this.customSessionId = ""; + this.forcedVideoCodec = VideoCodec.VP8; + this.allowTranscoding = false; } private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode, OutputMode outputMode, - RecordingLayout layout, String defaultCustomLayout, String customSessionId) { + RecordingLayout layout, String defaultCustomLayout, String customSessionId, + VideoCodec forcedVideoCodec, boolean allowTranscoding) { this.mediaMode = mediaMode; this.recordingMode = recordingMode; this.defaultOutputMode = outputMode; this.defaultRecordingLayout = layout; this.defaultCustomLayout = defaultCustomLayout; this.customSessionId = customSessionId; + this.forcedVideoCodec = forcedVideoCodec; + this.allowTranscoding = allowTranscoding; } /** @@ -230,5 +268,25 @@ public class SessionProperties { public String customSessionId() { return this.customSessionId; } + + /** + * + * Defines which video codec is being forced to be used when + * {@link io.openvidu.java.client.SessionProperties.Builder#allowTranscoding(boolean)} + * has been set to false + */ + public VideoCodec forcedVideoCodec() { + return this.forcedVideoCodec; + } + + /** + * + * Defines if transcoding is allowed or not. If this method returns false, a video codec + * will be forcibly used for all browsers (See + * {@link io.openvidu.java.client.SessionProperties.Builder#forcedVideoCodec(VideoCodec)}). + */ + public boolean isTranscodingAllowed() { + return this.allowTranscoding; + } } \ No newline at end of file diff --git a/openvidu-java-client/src/main/java/io/openvidu/java/client/VideoCodec.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/VideoCodec.java new file mode 100644 index 00000000..9f6e002f --- /dev/null +++ b/openvidu-java-client/src/main/java/io/openvidu/java/client/VideoCodec.java @@ -0,0 +1,25 @@ +/* + * (C) Copyright 2017-2020 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.java.client; + +/** + * See {@link io.openvidu.java.client.SessionProperties.Builder#forcedVideoCodec(VideoCodec)} + */ +public enum VideoCodec { + VP8, VP9, H264 +} \ No newline at end of file diff --git a/openvidu-node-client/src/Session.ts b/openvidu-node-client/src/Session.ts index a9fead7b..3b5b4486 100644 --- a/openvidu-node-client/src/Session.ts +++ b/openvidu-node-client/src/Session.ts @@ -26,6 +26,7 @@ import { RecordingLayout } from './RecordingLayout'; import { RecordingMode } from './RecordingMode'; import { SessionProperties } from './SessionProperties'; import { TokenOptions } from './TokenOptions'; +import { VideoCodec } from './VideoCodec'; export class Session { @@ -83,6 +84,8 @@ export class Session { this.properties.recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; this.properties.defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED; this.properties.defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; + this.properties.allowTranscoding = !!this.properties.allowTranscoding ? this.properties.allowTranscoding : false; + this.properties.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : VideoCodec.VP8; } /** @@ -401,7 +404,10 @@ export class Session { defaultOutputMode: !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED, defaultRecordingLayout: !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT, defaultCustomLayout: !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : '', - customSessionId: !!this.properties.customSessionId ? this.properties.customSessionId : '' + customSessionId: !!this.properties.customSessionId ? this.properties.customSessionId : '', + forcedVideoCodec: !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : VideoCodec.VP8, + allowTranscoding: !!this.properties.allowTranscoding ? this.properties.allowTranscoding : false + }); axios.post( @@ -466,7 +472,9 @@ export class Session { mediaMode: json.mediaMode, recordingMode: json.recordingMode, defaultOutputMode: json.defaultOutputMode, - defaultRecordingLayout: json.defaultRecordingLayout + defaultRecordingLayout: json.defaultRecordingLayout, + forcedVideoCodec: !!json.forcedVideoCodec ? json.forcedVideoCodec : VideoCodec.VP8, + allowTranscoding: !!json.allowTranscoding ? json.allowTranscoding : false }; if (!!customSessionId) { this.properties.customSessionId = customSessionId; diff --git a/openvidu-node-client/src/SessionProperties.ts b/openvidu-node-client/src/SessionProperties.ts index f866e1b8..df45ac41 100644 --- a/openvidu-node-client/src/SessionProperties.ts +++ b/openvidu-node-client/src/SessionProperties.ts @@ -64,4 +64,24 @@ export interface SessionProperties { * If this parameter is undefined or an empty string, OpenVidu Server will generate a random sessionId for you. */ customSessionId?: string; + + /** + * Call this method to define which video codec do you want to be forcibly used for this session. + * This allows browsers to use the same codec avoiding transcoding in the media server. + * To force this video codec you need to set [[allowTranscoding]] to false. + */ + forcedVideoCodec?: string; + + /** + * Call this method to define if you want to allowTranscoding or not. If you define it as + * as false, the default video codec VP8 will be used for all browsers, and the media + * server will not do any transcoding. If you define it as true, transcoding can be + * executed by the media server when necessary. + * + * If you want to set a different video codec, you can configure it + * by calling [[forcedVideoCodec]] to your preferred one. + * + */ + allowTranscoding?: boolean; + } diff --git a/openvidu-node-client/src/VideoCodec.ts b/openvidu-node-client/src/VideoCodec.ts new file mode 100644 index 00000000..e1077f77 --- /dev/null +++ b/openvidu-node-client/src/VideoCodec.ts @@ -0,0 +1,10 @@ +/** + * See [[SessionProperties.forcedVideoCodec]] + */ +export enum VideoCodec { + + VP8 = 'VP8', + VP9 = 'VP9', + H264 = 'H264' + +} \ No newline at end of file diff --git a/openvidu-node-client/src/index.ts b/openvidu-node-client/src/index.ts index 50ce477a..4113ac84 100644 --- a/openvidu-node-client/src/index.ts +++ b/openvidu-node-client/src/index.ts @@ -9,4 +9,5 @@ export * from './RecordingMode'; export * from './Recording'; export * from './RecordingProperties'; export * from './Connection'; -export * from './Publisher'; \ No newline at end of file +export * from './Publisher'; +export * from './VideoCodec'; \ No newline at end of file diff --git a/openvidu-server/deployments/ce/docker-compose/.env b/openvidu-server/deployments/ce/docker-compose/.env index 279ec85e..29ac0cb5 100644 --- a/openvidu-server/deployments/ce/docker-compose/.env +++ b/openvidu-server/deployments/ce/docker-compose/.env @@ -149,7 +149,7 @@ OPENVIDU_CDR_PATH=/opt/openvidu/cdr # -------------------------- # Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev # Uncomment the next line and define this variable with KMS image that you want use -# KMS_IMAGE=kurento/kurento-media-server-dev:6.14.0 +# KMS_IMAGE=kurento/kurento-media-server:6.14.0 # Kurento Media Server Level logs # ------------------------------- diff --git a/openvidu-server/deployments/pro/docker-compose/media-node/.env b/openvidu-server/deployments/pro/docker-compose/media-node/.env index 704c9211..8108613b 100644 --- a/openvidu-server/deployments/pro/docker-compose/media-node/.env +++ b/openvidu-server/deployments/pro/docker-compose/media-node/.env @@ -8,7 +8,7 @@ # -------------------------- # Docker hub kurento media server: https://hub.docker.com/r/kurento/kurento-media-server-dev # Uncomment the next line and define this variable with KMS image that you want use -# KMS_IMAGE=kurento/kurento-media-server-dev:6.14.0 +# KMS_IMAGE=kurento/kurento-media-server:6.14.0 # Kurento Media Server Level logs # ------------------------------- diff --git a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java index ee2d8cbe..a81943f1 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -72,6 +72,7 @@ import io.openvidu.server.utils.MediaNodeStatusManager; import io.openvidu.server.utils.MediaNodeStatusManagerDummy; import io.openvidu.server.utils.QuarantineKiller; import io.openvidu.server.utils.QuarantineKillerDummy; +import io.openvidu.server.utils.SDPMunging; import io.openvidu.server.webhook.CDRLoggerWebhook; /** @@ -195,6 +196,12 @@ public class OpenViduServer implements JsonRpcConfigurer { public GeoLocationByIp geoLocationByIp() { return new GeoLocationByIpDummy(); } + + @Bean + @ConditionalOnMissingBean + public SDPMunging sdpMunging() { + return new SDPMunging(); + } @Bean @ConditionalOnMissingBean 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 7edf339c..bf0c0807 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 @@ -29,12 +29,12 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.Function; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; @@ -200,6 +200,8 @@ public class Session implements SessionInterface { json.addProperty("mediaMode", this.sessionProperties.mediaMode().name()); json.addProperty("recordingMode", this.sessionProperties.recordingMode().name()); json.addProperty("defaultOutputMode", this.sessionProperties.defaultOutputMode().name()); + json.addProperty("forcedVideoCodec", this.sessionProperties.forcedVideoCodec().name()); + json.addProperty("allowTranscoding", this.sessionProperties.isTranscodingAllowed()); if (RecordingUtils.IS_COMPOSED(this.sessionProperties.defaultOutputMode())) { json.addProperty("defaultRecordingLayout", this.sessionProperties.defaultRecordingLayout().name()); if (RecordingLayout.CUSTOM.equals(this.sessionProperties.defaultRecordingLayout())) { 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 af55da6e..a2e0411b 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 @@ -32,17 +32,17 @@ import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + import org.apache.commons.lang3.RandomStringUtils; import org.kurento.jsonrpc.message.Request; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; - import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; 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 681941ea..e546ff5e 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 @@ -30,6 +30,9 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + import org.apache.commons.lang3.RandomStringUtils; import org.kurento.client.GenericMediaElement; import org.kurento.client.IceCandidate; @@ -41,9 +44,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; @@ -53,6 +53,7 @@ import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.SessionProperties; +import io.openvidu.java.client.VideoCodec; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.FinalUser; import io.openvidu.server.core.IdentifierPrefixes; @@ -68,6 +69,7 @@ import io.openvidu.server.kurento.kms.KmsManager; import io.openvidu.server.rpc.RpcHandler; import io.openvidu.server.utils.GeoLocation; import io.openvidu.server.utils.JsonUtils; +import io.openvidu.server.utils.SDPMunging; public class KurentoSessionManager extends SessionManager { @@ -81,6 +83,9 @@ public class KurentoSessionManager extends SessionManager { @Autowired private KurentoParticipantEndpointConfig kurentoEndpointConfig; + + @Autowired + private SDPMunging sdpMunging; @Override /* Protected by Session.closingLock.readLock */ @@ -365,15 +370,29 @@ public class KurentoSessionManager extends SessionManager { KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) mediaOptions; KurentoParticipant kParticipant = (KurentoParticipant) participant; - + KurentoSession kSession = kParticipant.getSession(); + + // Modify sdp if transcoding is not allowed + if(!kSession.getSessionProperties().isTranscodingAllowed()) { + VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); + String sdpOffer = kurentoOptions.sdpOffer; + + try { + kurentoOptions.sdpOffer = modifySdpToForceCodec(forcedVideoCodec, sdpOffer); + } catch (OpenViduException e) { + String errorMessage = "Error forcing codec: ''" + forcedVideoCodec + "', for publisher on Session: '" + kSession.getSessionId() + + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpOffer; + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); + } + + } + log.debug( "Request [PUBLISH_MEDIA] isOffer={} sdp={} " + "loopbackAltSrc={} lpbkConnType={} doLoopback={} rtspUri={} ({})", kurentoOptions.isOffer, kurentoOptions.sdpOffer, kurentoOptions.doLoopback, kurentoOptions.rtspUri, participant.getParticipantPublicId()); - KurentoSession kSession = kParticipant.getSession(); - kParticipant.createPublishingEndpoint(mediaOptions, null); /* @@ -397,7 +416,7 @@ public class KurentoSessionManager extends SessionManager { throw e; } } - + sdpAnswer = kParticipant.publishToRoom(kurentoOptions.sdpOffer, kurentoOptions.doLoopback, false); if (sdpAnswer == null) { @@ -558,6 +577,18 @@ public class KurentoSessionManager extends SessionManager { KurentoParticipant kParticipant = (KurentoParticipant) participant; session = ((KurentoParticipant) participant).getSession(); Participant senderParticipant = session.getParticipantByPublicId(senderName); + + // Modify sdp if transcoding is not allowed + if (!session.getSessionProperties().isTranscodingAllowed()) { + VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec(); + try { + sdpAnswer = this.modifySdpToForceCodec(forcedVideoCodec, sdpAnswer); + } catch (OpenViduException e) { + String errorMessage = "Error forcing codec: ''" + forcedVideoCodec + "', for subscriber on Session: '" + + session.getSessionId() + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpAnswer; + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); + } + } if (senderParticipant == null) { log.warn( @@ -1086,10 +1117,26 @@ public class KurentoSessionManager extends SessionManager { @Override public void reconnectStream(Participant participant, String streamId, String sdpString, Integer transactionId) { + KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoSession kSession = kParticipant.getSession(); + boolean isPublisher = streamId.equals(participant.getPublisherStreamId()); + + // Modify sdp if transcoding is not allowed + if (!kSession.getSessionProperties().isTranscodingAllowed()) { + VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); + try { + sdpString = modifySdpToForceCodec(forcedVideoCodec, sdpString); + } catch (OpenViduException e) { + String errorMessage = "Error on reconnecting and forcing codec: ''" + forcedVideoCodec + "', for " + + (isPublisher ? "publisher" : "subscriber") + " on Session: '" + kSession.getSessionId() + + "'\nException: " + e.getMessage() + "\nSDP:\n" + sdpString; + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); + } + + } - if (streamId.equals(participant.getPublisherStreamId())) { + if (isPublisher) { // Reconnect publisher final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher() @@ -1192,5 +1239,14 @@ public class KurentoSessionManager extends SessionManager { filter.removeEventListener(pub.removeListener(eventType)); } } - + + + private String modifySdpToForceCodec(VideoCodec codec, String sdpOffer) { + // Modify sdpOffer if transcoding is not allowed + String modSdpOffer = this.sdpMunging.setCodecPreference(codec, sdpOffer); + if (modSdpOffer != null) { + sdpOffer = modSdpOffer; + } + return sdpOffer; + } } 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 28da7b54..e773c314 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 @@ -24,6 +24,12 @@ import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,12 +46,6 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; - import io.openvidu.client.OpenViduException; import io.openvidu.client.internal.ProtocolElements; import io.openvidu.java.client.MediaMode; @@ -55,6 +55,7 @@ import io.openvidu.java.client.RecordingLayout; import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.SessionProperties; +import io.openvidu.java.client.VideoCodec; import io.openvidu.server.config.OpenviduConfig; import io.openvidu.server.core.EndReason; import io.openvidu.server.core.IdentifierPrefixes; @@ -102,6 +103,8 @@ public class SessionRestController { String defaultOutputModeString; String defaultRecordingLayoutString; String defaultCustomLayout; + String forcedVideoCodec; + Boolean allowTranscoding; try { mediaModeString = (String) params.get("mediaMode"); recordingModeString = (String) params.get("recordingMode"); @@ -109,6 +112,8 @@ public class SessionRestController { defaultRecordingLayoutString = (String) params.get("defaultRecordingLayout"); defaultCustomLayout = (String) params.get("defaultCustomLayout"); customSessionId = (String) params.get("customSessionId"); + forcedVideoCodec = (String) params.get("forcedVideoCodec"); + allowTranscoding = (Boolean) params.get("allowTranscoding"); } catch (ClassCastException e) { return this.generateErrorResponse("Type error in some parameter", "/api/sessions", HttpStatus.BAD_REQUEST); @@ -150,6 +155,16 @@ public class SessionRestController { builder = builder.customSessionId(customSessionId); } builder = builder.defaultCustomLayout((defaultCustomLayout != null) ? defaultCustomLayout : ""); + if (forcedVideoCodec != null) { + builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec)); + } else { + builder = builder.forcedVideoCodec(VideoCodec.VP8); + } + if (allowTranscoding != null) { + builder = builder.allowTranscoding(allowTranscoding); + } else { + builder = builder.allowTranscoding(false); + } } catch (IllegalArgumentException e) { return this.generateErrorResponse("RecordingMode " + params.get("recordingMode") + " | " diff --git a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java index 4e742517..f9cc6f00 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rpc/RpcHandler.java @@ -26,6 +26,11 @@ import java.util.concurrent.ConcurrentMap; import javax.servlet.http.HttpSession; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; + import org.apache.commons.lang3.RandomStringUtils; import org.kurento.jsonrpc.DefaultJsonRpcHandler; import org.kurento.jsonrpc.Session; @@ -37,11 +42,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.google.gson.JsonSyntaxException; - import io.openvidu.client.OpenViduException; import io.openvidu.client.OpenViduException.Code; import io.openvidu.client.internal.ProtocolElements; diff --git a/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java b/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java new file mode 100644 index 00000000..8537c39e --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java @@ -0,0 +1,133 @@ +package io.openvidu.server.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.openvidu.client.OpenViduException; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.java.client.VideoCodec; + +public class SDPMunging { + + private static final Logger log = LoggerFactory.getLogger(SDPMunging.class); + + /** + * `codec` is a uppercase SDP-style codec name: "VP8", "H264". + * + * This looks for all video m-sections (lines starting with "m=video"), + * then searches all of its related PayloadTypes trying to find those which + * correspond to the preferred codec. If any is found, they are moved to the + * front of the PayloadTypes list in the m= line, without removing the other + * codecs that might be present. + * + * If our preferred codec is not found, the m= line is left without changes. + * + * This works based on the basis that RFC 3264 "Offer/Answer Model SDP" section + * 6.1 "Unicast Streams" allows the answerer to list media formats in a + * different order of preference from what it got in the offer: + * + * > Although the answerer MAY list the formats in their desired order of + * > preference, it is RECOMMENDED that unless there is a specific reason, + * > the answerer list formats in the same relative order they were + * > present in the offer. + * + * Here we have a specific reason, thus we use this allowance to change the + * ordering of formats. Browsers (tested with Chrome 84) honor this change and + * use the first codec provided in the answer, so this operation actually works. + */ + public String setCodecPreference(VideoCodec codec, String sdp) throws OpenViduException { + String codecStr = codec.name(); + log.info("[setCodecPreference] codec: {}", codecStr); + + List codecPts = new ArrayList(); + String[] lines = sdp.split("\\R+"); + Pattern ptRegex = Pattern.compile(String.format("a=rtpmap:(\\d+) %s/90000", codecStr)); + + for (int sl = 0; sl < lines.length; sl++) { + String sdpLine = lines[sl]; + + if (!sdpLine.startsWith("m=video")) { + continue; + } + + // m-section found. Prepare an array to store PayloadTypes. + codecPts.clear(); + + // Search the m-section to find our codec's PayloadType, if any. + for (int ml = sl + 1; ml < lines.length; ml++) { + String mediaLine = lines[ml]; + + // Abort if we reach the next m-section. + if (mediaLine.startsWith("m=")) { + break; + } + + Matcher ptMatch = ptRegex.matcher(mediaLine); + if (ptMatch.find()) { + // PayloadType found. + String pt = ptMatch.group(1); + codecPts.add(pt); + + // Search the m-section to find the APT subtype, if any. + Pattern aptRegex = Pattern.compile(String.format("a=fmtp:(\\d+) apt=%s", pt)); + + for (int al = sl + 1; al < lines.length; al++) { + String aptLine = lines[al]; + + // Abort if we reach the next m-section. + if (aptLine.startsWith("m=")) { + break; + } + + Matcher aptMatch = aptRegex.matcher(aptLine); + if (aptMatch.find()) { + // APT found. + String apt = aptMatch.group(1); + codecPts.add(apt); + } + } + } + } + + if (codecPts.isEmpty()) { + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, "The specified forced codec " + codecStr + " is not present in the SDP"); + } + + // Build a new m= line where any PayloadTypes found have been moved + // to the front of the PT list. + StringBuilder newLine = new StringBuilder(sdpLine.length()); + List lineParts = new ArrayList(Arrays.asList(sdpLine.split(" "))); + + if (lineParts.size() < 4) { + log.error("[setCodecPreference] BUG in m= line: Expects at least 4 fields: '{}'", sdpLine); + continue; + } + + // Add "m=video", Port, and Protocol. + for (int i = 0; i < 3; i++) { + newLine.append(lineParts.remove(0) + " "); + } + + // Add the PayloadTypes that correspond to our preferred codec. + for (String pt : codecPts) { + lineParts.remove(pt); + newLine.append(pt + " "); + } + + // Add the rest of PayloadTypes. + newLine.append(String.join(" ", lineParts)); + + // Replace the original m= line with the one we just built. + lines[sl] = newLine.toString(); + } + + return String.join("\r\n", lines); + } + +} \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css index 5c4c9594..782565c8 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.css @@ -36,4 +36,8 @@ mat-radio-button:first-child { #role-div { padding-top: 6px; padding-bottom: 15px; +} + +#allow-transcoding-div { + margin-bottom: 10px; } \ No newline at end of file diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html index de5608d5..bf816c9b 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.html @@ -38,6 +38,18 @@ +
+ Allow Transcoding +
+ + + + {{ enumerator }} + + + diff --git a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts index b7427ed2..f06f3201 100644 --- a/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts +++ b/openvidu-testapp/src/app/components/dialogs/session-properties-dialog/session-properties-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; -import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, TokenOptions } from 'openvidu-node-client'; +import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, TokenOptions, VideoCodec } from 'openvidu-node-client'; @Component({ selector: 'app-session-properties-dialog', @@ -23,6 +23,7 @@ export class SessionPropertiesDialogComponent { recordingMode = RecordingMode; defaultOutputMode = Recording.OutputMode; defaultRecordingLayout = RecordingLayout; + forceVideoCodec = VideoCodec; constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data) { diff --git a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts index eda242a8..fc324f9f 100644 --- a/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts +++ b/openvidu-testapp/src/app/components/openvidu-instance/openvidu-instance.component.ts @@ -18,7 +18,8 @@ import { TokenOptions, OpenViduRole, RecordingProperties, - Recording + Recording, + VideoCodec } from 'openvidu-node-client'; import { MatDialog, MAT_CHECKBOX_CLICK_ACTION } from '@angular/material'; import { ExtensionDialogComponent } from '../dialogs/extension-dialog/extension-dialog.component'; @@ -92,7 +93,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { defaultOutputMode: Recording.OutputMode.COMPOSED, defaultRecordingLayout: RecordingLayout.BEST_FIT, defaultCustomLayout: '', - customSessionId: '' + customSessionId: '', + forcedVideoCodec: VideoCodec.VP8, + allowTranscoding: false }; publisherProperties: PublisherProperties = {