From 7a25233b8bae1e016b4b81422c007eed92f64476 Mon Sep 17 00:00:00 2001 From: cruizba Date: Thu, 19 Nov 2020 17:06:50 +0100 Subject: [PATCH] Force codec parameters to avoid transcoding --- .../io/openvidu/client/OpenViduException.java | 4 +- .../java/io/openvidu/java/client/Session.java | 38 +- .../java/client/SessionProperties.java | 55 +- .../io/openvidu/java/client/VideoCodec.java | 25 + openvidu-node-client/src/OpenVidu.ts | 4 +- openvidu-node-client/src/Session.ts | 48 +- openvidu-node-client/src/SessionProperties.ts | 16 + openvidu-node-client/src/VideoCodec.ts | 11 + openvidu-node-client/src/index.ts | 3 +- .../deployments/ce/docker-compose/.env | 9 + .../docker-compose/openvidu-server-pro/.env | 9 + .../io/openvidu/server/OpenViduServer.java | 7 + .../server/config/OpenviduConfig.java | 16 + .../io/openvidu/server/core/Participant.java | 2 +- .../java/io/openvidu/server/core/Session.java | 2 + .../kurento/core/KurentoSessionManager.java | 44 +- .../server/rest/ConfigRestController.java | 2 + .../server/rest/SessionRestController.java | 15 + .../io/openvidu/server/utils/SDPMunging.java | 185 +++ .../src/main/resources/application.properties | 3 + .../server/test/unit/SDPMungingTest.java | 177 +++ .../resources/integration-test.properties | 3 + .../src/test/resources/sdp/sdp_chrome84.txt | 143 ++ .../src/test/resources/sdp/sdp_firefox79.txt | 67 + .../test/resources/sdp/sdp_kurento_h264.txt | 61 + .../src/test/resources/sdp/sdp_safari13-1.txt | 107 ++ .../openvidu/test/browsers/FirefoxUser.java | 6 +- .../e2e/AbstractOpenViduTestAppE2eTest.java | 7 +- .../test/e2e/OpenViduTestAppE2eTest.java | 205 ++- openvidu-testapp/src/app/app.module.ts | 5 +- .../session-properties-dialog.component.css | 6 +- .../session-properties-dialog.component.html | 12 + .../session-properties-dialog.component.ts | 3 +- .../show-codec-dialog.component.ts | 27 + .../openvidu-instance.component.ts | 11 +- .../app/components/video/video.component.html | 8 +- .../app/components/video/video.component.ts | 1253 +++++++++-------- 37 files changed, 1946 insertions(+), 653 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 create mode 100644 openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java create mode 100644 openvidu-server/src/test/resources/sdp/sdp_chrome84.txt create mode 100644 openvidu-server/src/test/resources/sdp/sdp_firefox79.txt create mode 100644 openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt create mode 100644 openvidu-server/src/test/resources/sdp/sdp_safari13-1.txt create mode 100644 openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts 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..74c7c295 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/src/main/java/io/openvidu/java/client/Session.java b/openvidu-java-client/src/main/java/io/openvidu/java/client/Session.java index 13dd609e..031db9f2 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 @@ -56,14 +56,14 @@ public class Session { protected Session(OpenVidu openVidu) throws OpenViduJavaClientException, OpenViduHttpException { this.openVidu = openVidu; this.properties = new SessionProperties.Builder().build(); - this.getSessionIdHttp(); + this.getSessionHttp(); } protected Session(OpenVidu openVidu, SessionProperties properties) throws OpenViduJavaClientException, OpenViduHttpException { this.openVidu = openVidu; this.properties = properties; - this.getSessionIdHttp(); + this.getSessionHttp(); } protected Session(OpenVidu openVidu, JsonObject json) { @@ -655,7 +655,7 @@ public class Session { return (this.sessionId != null && !this.sessionId.isEmpty()); } - private void getSessionIdHttp() throws OpenViduJavaClientException, OpenViduHttpException { + private void getSessionHttp() throws OpenViduJavaClientException, OpenViduHttpException { if (this.hasSessionId()) { return; } @@ -683,6 +683,25 @@ public class Session { JsonObject responseJson = httpResponseToJson(response); this.sessionId = responseJson.get("id").getAsString(); this.createdAt = responseJson.get("createdAt").getAsLong(); + + // forcedVideoCodec and allowTranscoding values are configured in OpenVidu Server + // via configuration or session + VideoCodec forcedVideoCodec = VideoCodec.valueOf(responseJson.get("forcedVideoCodec").getAsString()); + Boolean allowTranscoding = responseJson.get("allowTranscoding").getAsBoolean(); + + SessionProperties responseProperties = new SessionProperties.Builder() + .customSessionId(properties.customSessionId()) + .mediaMode(properties.mediaMode()) + .recordingMode(properties.recordingMode()) + .defaultOutputMode(properties.defaultOutputMode()) + .defaultRecordingLayout(properties.defaultRecordingLayout()) + .defaultCustomLayout(properties.defaultCustomLayout()) + .mediaNode(properties.mediaNode()) + .forcedVideoCodec(forcedVideoCodec) + .allowTranscoding(allowTranscoding) + .build(); + + this.properties = responseProperties; log.info("Session '{}' created", this.sessionId); } else if (statusCode == org.apache.http.HttpStatus.SC_CONFLICT) { // 'customSessionId' already existed @@ -727,6 +746,13 @@ public class Session { builder.customSessionId(json.get("customSessionId").getAsString()); } + if (json.has("forcedVideoCodec")) { + builder.forcedVideoCodec(VideoCodec.valueOf(json.get("forcedVideoCodec").getAsString())); + } + if (json.has("allowTranscoding")) { + builder.allowTranscoding(json.get("allowTranscoding").getAsBoolean()); + } + this.properties = builder.build(); JsonArray jsonArrayConnections = (json.get("connections").getAsJsonObject()).get("content").getAsJsonArray(); @@ -768,6 +794,12 @@ public class Session { json.addProperty("defaultOutputMode", this.properties.defaultOutputMode().name()); json.addProperty("defaultRecordingLayout", this.properties.defaultRecordingLayout().name()); json.addProperty("defaultCustomLayout", this.properties.defaultCustomLayout()); + if(this.properties.forcedVideoCodec() != null) { + json.addProperty("forcedVideoCodec", this.properties.forcedVideoCodec().name()); + } + if (this.properties.isTranscodingAllowed() != null) { + json.addProperty("allowTranscoding", this.properties.isTranscodingAllowed()); + } JsonObject connections = new JsonObject(); connections.addProperty("numberOfElements", this.getConnections().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 1305a0ab..935b4ed5 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 @@ -33,6 +33,8 @@ public class SessionProperties { private String defaultCustomLayout; private String customSessionId; private String mediaNode; + private VideoCodec forcedVideoCodec; + private Boolean allowTranscoding; /** * Builder for {@link io.openvidu.java.client.SessionProperties} @@ -46,6 +48,8 @@ public class SessionProperties { private String defaultCustomLayout = ""; private String customSessionId = ""; private String mediaNode; + private VideoCodec forcedVideoCodec; + private Boolean allowTranscoding; /** * Returns the {@link io.openvidu.java.client.SessionProperties} object properly @@ -53,7 +57,8 @@ public class SessionProperties { */ public SessionProperties build() { return new SessionProperties(this.mediaMode, this.recordingMode, this.defaultOutputMode, - this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, this.mediaNode); + this.defaultRecordingLayout, this.defaultCustomLayout, this.customSessionId, this.mediaNode, + this.forcedVideoCodec, this.allowTranscoding); } /** @@ -155,6 +160,28 @@ public class SessionProperties { return this; } + /** + * Call this method to define which video codec do you want to be forcibly used for this session. + * This allows browsers/clients to use the same codec avoiding transcoding in the media server. + * If the browser/client is not compatible with the specified codec and {@link #allowTranscoding(Boolean)} + * is false and exception will occur. + * + * If forcedVideoCodec is set to NONE, no codec will be forced. + */ + public SessionProperties.Builder forcedVideoCodec(VideoCodec forcedVideoCodec) { + this.forcedVideoCodec = forcedVideoCodec; + return this; + } + + /** + * Call this method to define if you want to allow transcoding in the media server or not + * when {@link #forcedVideoCodec(VideoCodec)} is not compatible with the browser/client. + */ + public SessionProperties.Builder allowTranscoding(Boolean allowTranscoding) { + this.allowTranscoding = allowTranscoding; + return this; + } + } protected SessionProperties() { @@ -168,7 +195,8 @@ public class SessionProperties { } private SessionProperties(MediaMode mediaMode, RecordingMode recordingMode, OutputMode outputMode, - RecordingLayout layout, String defaultCustomLayout, String customSessionId, String mediaNode) { + RecordingLayout layout, String defaultCustomLayout, String customSessionId, String mediaNode, + VideoCodec forcedVideoCodec, Boolean allowTranscoding) { this.mediaMode = mediaMode; this.recordingMode = recordingMode; this.defaultOutputMode = outputMode; @@ -176,6 +204,8 @@ public class SessionProperties { this.defaultCustomLayout = defaultCustomLayout; this.customSessionId = customSessionId; this.mediaNode = mediaNode; + this.forcedVideoCodec = forcedVideoCodec; + this.allowTranscoding = allowTranscoding; } /** @@ -263,6 +293,21 @@ public class SessionProperties { return this.mediaNode; } + /** + * Defines which video codec is being forced to be used in the browser/client + */ + public VideoCodec forcedVideoCodec() { + return this.forcedVideoCodec; + } + + /** + * Defines if transcoding is allowed or not when {@link #forcedVideoCodec} + * is not a compatible codec with the browser/client. + */ + public Boolean isTranscodingAllowed() { + return this.allowTranscoding; + } + protected JsonObject toJson() { JsonObject json = new JsonObject(); json.addProperty("mediaMode", mediaMode().name()); @@ -276,6 +321,12 @@ public class SessionProperties { mediaNodeJson.addProperty("id", mediaNode()); json.add("mediaNode", mediaNodeJson); } + if (forcedVideoCodec() != null) { + json.addProperty("forcedVideoCodec", forcedVideoCodec().name()); + } + if (isTranscodingAllowed() != null) { + json.addProperty("allowTranscoding", isTranscodingAllowed()); + } return json; } 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..3f088fa1 --- /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, NONE +} \ No newline at end of file diff --git a/openvidu-node-client/src/OpenVidu.ts b/openvidu-node-client/src/OpenVidu.ts index 463a91c9..dcdbcd8e 100644 --- a/openvidu-node-client/src/OpenVidu.ts +++ b/openvidu-node-client/src/OpenVidu.ts @@ -106,8 +106,8 @@ export class OpenVidu { public createSession(properties?: SessionProperties): Promise { return new Promise((resolve, reject) => { const session = new Session(this, properties); - session.getSessionIdHttp() - .then(sessionId => { + session.getSessionHttp() + .then(response => { this.activeSessions.push(session); resolve(session); }) diff --git a/openvidu-node-client/src/Session.ts b/openvidu-node-client/src/Session.ts index a374c1eb..6393179c 100644 --- a/openvidu-node-client/src/Session.ts +++ b/openvidu-node-client/src/Session.ts @@ -94,6 +94,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.forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined; + this.properties.allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined; } /** @@ -452,22 +454,25 @@ export class Session { /** * @hidden */ - public getSessionIdHttp(): Promise { + public getSessionHttp(): Promise { return new Promise((resolve, reject) => { if (!!this.sessionId) { resolve(this.sessionId); } - const data = JSON.stringify({ - mediaMode: !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED, - recordingMode: !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL, - 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 : '', - mediaNode: !!this.properties.mediaNode ? this.properties.mediaNode : undefined - }); + const mediaMode = !!this.properties.mediaMode ? this.properties.mediaMode : MediaMode.ROUTED; + const recordingMode = !!this.properties.recordingMode ? this.properties.recordingMode : RecordingMode.MANUAL; + const defaultOutputMode = !!this.properties.defaultOutputMode ? this.properties.defaultOutputMode : Recording.OutputMode.COMPOSED; + const defaultRecordingLayout = !!this.properties.defaultRecordingLayout ? this.properties.defaultRecordingLayout : RecordingLayout.BEST_FIT; + const defaultCustomLayout = !!this.properties.defaultCustomLayout ? this.properties.defaultCustomLayout : ''; + const customSessionId = !!this.properties.customSessionId ? this.properties.customSessionId : ''; + const mediaNode = !!this.properties.mediaNode ? this.properties.mediaNode : undefined; + const forcedVideoCodec = !!this.properties.forcedVideoCodec ? this.properties.forcedVideoCodec : undefined; + const allowTranscoding = this.properties.allowTranscoding != null ? this.properties.allowTranscoding : undefined; + + const data = JSON.stringify({mediaMode, recordingMode, defaultOutputMode, defaultRecordingLayout, defaultCustomLayout, + customSessionId, mediaNode, forcedVideoCodec, allowTranscoding}); axios.post( this.ov.host + OpenVidu.API_SESSIONS, @@ -484,6 +489,15 @@ export class Session { // SUCCESS response from openvidu-server. Resolve token this.sessionId = res.data.id; this.createdAt = res.data.createdAt; + this.properties.mediaMode = mediaMode; + this.properties.recordingMode = recordingMode; + this.properties.defaultOutputMode = defaultOutputMode; + this.properties.defaultRecordingLayout = defaultRecordingLayout; + this.properties.defaultCustomLayout = defaultCustomLayout; + this.properties.customSessionId = customSessionId; + this.properties.mediaNode = mediaNode; + this.properties.forcedVideoCodec = res.data.forcedVideoCodec; + this.properties.allowTranscoding = res.data.allowTranscoding; resolve(this.sessionId); } else { // ERROR response from openvidu-server. Resolve HTTP status @@ -527,7 +541,9 @@ export class Session { recordingMode: json.recordingMode, defaultOutputMode: json.defaultOutputMode, defaultRecordingLayout: json.defaultRecordingLayout, - defaultCustomLayout: json.defaultCustomLayout + defaultCustomLayout: json.defaultCustomLayout, + forcedVideoCodec: json.forcedVideoCodec, + allowTranscoding: json.allowTranscoding }; if (json.defaultRecordingLayout == null) { delete this.properties.defaultRecordingLayout; @@ -538,6 +554,15 @@ export class Session { if (json.defaultCustomLayout == null) { delete this.properties.defaultCustomLayout; } + if (json.mediaNode == null) { + delete this.properties.mediaNode; + } + if (json.forcedVideoCodec == null) { + delete this.properties.forcedVideoCodec; + } + if (json.allowTranscoding == null) { + delete this.properties.allowTranscoding; + } // 1. Array to store fetched connections and later remove closed ones const fetchedConnectionIds: string[] = []; @@ -567,7 +592,6 @@ export class Session { this.connections.sort((c1, c2) => (c1.createdAt > c2.createdAt) ? 1 : ((c2.createdAt > c1.createdAt) ? -1 : 0)); // Populate activeConnections array this.updateActiveConnectionsArray(); - return this; } diff --git a/openvidu-node-client/src/SessionProperties.ts b/openvidu-node-client/src/SessionProperties.ts index 3c2ad5ef..4c718e10 100644 --- a/openvidu-node-client/src/SessionProperties.ts +++ b/openvidu-node-client/src/SessionProperties.ts @@ -76,4 +76,20 @@ export interface SessionProperties { id: string; } + /** + * It defines which video codec do you want to be forcibly used for this session. + * This allows browsers/clients to use the same codec avoiding transcoding in the media server. + * If the browser/client is not compatible with the specified codec and [[allowTranscoding]] + * is false and exception will occur. + * + * If forcedVideoCodec is set to NONE, no codec will be forced. + */ + forcedVideoCodec?: string; + + /** + * It defines if you want to allow transcoding in the media server or not + * when [[forcedVideoCodec]] is not compatible with the browser/client. + */ + allowTranscoding?: boolean; + } diff --git a/openvidu-node-client/src/VideoCodec.ts b/openvidu-node-client/src/VideoCodec.ts new file mode 100644 index 00000000..464bef34 --- /dev/null +++ b/openvidu-node-client/src/VideoCodec.ts @@ -0,0 +1,11 @@ +/** + * See [[SessionProperties.forcedVideoCodec]] + */ +export enum VideoCodec { + + VP8 = 'VP8', + VP9 = 'VP9', + H264 = 'H264', + NONE = 'NONE' + +} \ No newline at end of file diff --git a/openvidu-node-client/src/index.ts b/openvidu-node-client/src/index.ts index 0878ad05..669b4e90 100644 --- a/openvidu-node-client/src/index.ts +++ b/openvidu-node-client/src/index.ts @@ -11,4 +11,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 ecc25054..460f8f10 100644 --- a/openvidu-server/deployments/ce/docker-compose/.env +++ b/openvidu-server/deployments/ce/docker-compose/.env @@ -153,6 +153,15 @@ OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 # (property 'OPENVIDU_SESSIONS_GARBAGE_INTERVAL' to 0) this property is ignored OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 +# All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true +# when a codec can not be forced, transcoding will be allowed +# Default value is VP8 +# OPENVIDU_FORCED_CODEC=VP8 + +# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied +# Default value is false +# OPENVIDU_ALLOW_TRANSCODING=false + # Call Detail Record enabled # Whether to enable Call Detail Record or not # Values: true | false diff --git a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env index 1eb7d95f..7dd958ce 100644 --- a/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env +++ b/openvidu-server/deployments/pro/docker-compose/openvidu-server-pro/.env @@ -259,6 +259,15 @@ OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 # (property 'OPENVIDU_SESSIONS_GARBAGE_INTERVAL' to 0) this property is ignored OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 +# All sessions of OpenVidu will try to force this codec. If OPENVIDU_ALLOW_TRANSCODING=true +# when a codec can not be forced, transcoding will be allowed +# Default value is VP8 +# OPENVIDU_FORCED_CODEC=VP8 + +# Allow transcoding if codec specified in OPENVIDU_FORCED_CODEC can not be applied +# Default value is false +# OPENVIDU_ALLOW_TRANSCODING=false + # Call Detail Record enabled # Whether to enable Call Detail Record or not # Values: true | false 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 92964b48..2bd34593 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java +++ b/openvidu-server/src/main/java/io/openvidu/server/OpenViduServer.java @@ -79,6 +79,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; /** @@ -216,6 +217,12 @@ public class OpenViduServer implements JsonRpcConfigurer { return new GeoLocationByIpDummy(); } + @Bean + @ConditionalOnMissingBean + public SDPMunging sdpMunging() { + return new SDPMunging(); + } + @Bean @ConditionalOnMissingBean public QuarantineKiller quarantineKiller() { 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 31cd4e72..2ff20a73 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 @@ -37,6 +37,7 @@ import java.util.Map; import javax.annotation.PostConstruct; +import io.openvidu.java.client.VideoCodec; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.Header; @@ -180,6 +181,10 @@ public class OpenviduConfig { protected int openviduSessionsGarbageThreshold; + private VideoCodec openviduForcedCodec; + + private boolean openviduAllowTranscoding; + private String dotenvPath; // Derived properties @@ -314,6 +319,14 @@ public class OpenviduConfig { return openviduSessionsGarbageThreshold; } + public VideoCodec getOpenviduForcedCodec() { + return openviduForcedCodec; + } + + public boolean isOpenviduAllowingTranscoding() { + return openviduAllowTranscoding; + } + public String getDotenvPath() { return dotenvPath; } @@ -510,6 +523,9 @@ public class OpenviduConfig { openviduSessionsGarbageInterval = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_INTERVAL"); openviduSessionsGarbageThreshold = asNonNegativeInteger("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD"); + openviduForcedCodec = asEnumValue("OPENVIDU_FORCED_CODEC", VideoCodec.class); + openviduAllowTranscoding = asBoolean("OPENVIDU_ALLOW_TRANSCODING"); + kmsUrisList = checkKmsUris(); checkCoturnIp(); 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 312bc874..8869815b 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 @@ -210,7 +210,7 @@ public class Participant { } public boolean isIpcam() { - return this.platform.equals("IPCAM") && this.participantPrivatetId.startsWith(IdentifierPrefixes.IPCAM_ID); + return this.platform != null && this.platform.equals("IPCAM") && this.participantPrivatetId.startsWith(IdentifierPrefixes.IPCAM_ID); } 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 f40f027f..28686a2d 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 @@ -256,6 +256,8 @@ public class Session implements SessionInterface { connections.add("content", participants); json.add("connections", connections); json.addProperty("recording", this.recordingManager.sessionIsBeingRecorded(this.sessionId)); + json.addProperty("forcedVideoCodec", this.sessionProperties.forcedVideoCodec().name()); + json.addProperty("allowTranscoding", this.sessionProperties.isTranscodingAllowed()); return json; } 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 39a1ce36..87e55094 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 @@ -31,6 +31,7 @@ import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import io.openvidu.server.utils.SDPMunging; import org.apache.commons.lang3.RandomStringUtils; import org.kurento.client.GenericMediaElement; import org.kurento.client.IceCandidate; @@ -56,6 +57,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; @@ -86,6 +88,9 @@ public class KurentoSessionManager extends SessionManager { @Autowired protected KurentoParticipantEndpointConfig kurentoEndpointConfig; + @Autowired + private SDPMunging sdpMunging; + @Override /* Protected by Session.closingLock.readLock */ public void joinRoom(Participant participant, String sessionId, Integer transactionId) { @@ -345,7 +350,7 @@ public class KurentoSessionManager extends SessionManager { * generated by the WebRTC endpoint on the server. * * @param participant Participant publishing video - * @param MediaOptions configuration of the stream to publish + * @param mediaOptions configuration of the stream to publish * @param transactionId identifier of the Transaction * @throws OpenViduException on error */ @@ -358,16 +363,22 @@ public class KurentoSessionManager extends SessionManager { KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) mediaOptions; KurentoParticipant kParticipant = (KurentoParticipant) participant; + KurentoSession kSession = kParticipant.getSession(); + SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER; + boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed(); + VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); + // Modify sdp if forced codec is defined + if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { + kurentoOptions.sdpOffer = sdpMunging.forceCodec(participant, kurentoOptions.sdpOffer, kurentoOptions.isOffer, + kSession, true, false, isTranscodingAllowed, forcedVideoCodec); + } log.debug( "Request [PUBLISH_MEDIA] isOffer={} sdp={} " + "loopbackAltSrc={} lpbkConnType={} doLoopback={} rtspUri={} ({})", kurentoOptions.isOffer, kurentoOptions.sdpOffer, kurentoOptions.doLoopback, kurentoOptions.rtspUri, participant.getParticipantPublicId()); - SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER; - KurentoSession kSession = kParticipant.getSession(); - kParticipant.createPublishingEndpoint(mediaOptions, null); /* @@ -462,6 +473,7 @@ public class KurentoSessionManager extends SessionManager { participants = kParticipant.getSession().getParticipants(); if (sdpAnswer != null) { + log.debug("SDP Answer for publishing PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), kParticipant.getPublisher().createdAt(), kSession.getSessionId(), mediaOptions, sdpAnswer, participants, transactionId, null); @@ -510,6 +522,14 @@ public class KurentoSessionManager extends SessionManager { KurentoParticipant kParticipant = (KurentoParticipant) participant; session = ((KurentoParticipant) participant).getSession(); Participant senderParticipant = session.getParticipantByPublicId(senderName); + boolean isTranscodingAllowed = session.getSessionProperties().isTranscodingAllowed(); + VideoCodec forcedVideoCodec = session.getSessionProperties().forcedVideoCodec(); + + // Modify sdp if forced codec is defined + if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { + sdpOffer = sdpMunging.forceCodec(participant, sdpOffer, true, session, false, false, + isTranscodingAllowed, forcedVideoCodec); + } if (senderParticipant == null) { log.warn( @@ -539,6 +559,7 @@ public class KurentoSessionManager extends SessionManager { sessionEventsHandler.onSubscribe(participant, session, null, transactionId, e); } if (sdpAnswer != null) { + log.debug("SDP Answer for subscribing PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); sessionEventsHandler.onSubscribe(participant, session, sdpAnswer, transactionId, null); } } @@ -1049,8 +1070,17 @@ public class KurentoSessionManager extends SessionManager { public void reconnectStream(Participant participant, String streamId, String sdpOffer, Integer transactionId) { KurentoParticipant kParticipant = (KurentoParticipant) participant; KurentoSession kSession = kParticipant.getSession(); + boolean isPublisher = streamId.equals(participant.getPublisherStreamId()); + boolean isTranscodingAllowed = kSession.getSessionProperties().isTranscodingAllowed(); + VideoCodec forcedVideoCodec = kSession.getSessionProperties().forcedVideoCodec(); - if (streamId.equals(participant.getPublisherStreamId())) { + // Modify sdp if forced codec is defined + if (forcedVideoCodec != VideoCodec.NONE && !participant.isIpcam()) { + sdpOffer = sdpMunging.forceCodec(participant, sdpOffer, true, kSession, isPublisher, + true, isTranscodingAllowed, forcedVideoCodec); + } + + if (isPublisher) { // Reconnect publisher final KurentoMediaOptions kurentoOptions = (KurentoMediaOptions) kParticipant.getPublisher() @@ -1069,7 +1099,7 @@ public class KurentoSessionManager extends SessionManager { kParticipant.createPublishingEndpoint(kurentoOptions, streamId); SdpType sdpType = kurentoOptions.isOffer ? SdpType.OFFER : SdpType.ANSWER; String sdpAnswer = kParticipant.publishToRoom(sdpType, sdpOffer, kurentoOptions.doLoopback, true); - + log.debug("SDP Answer for publishing reconnection PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); sessionEventsHandler.onPublishMedia(participant, participant.getPublisherStreamId(), kParticipant.getPublisher().createdAt(), kSession.getSessionId(), kurentoOptions, sdpAnswer, new HashSet(), transactionId, null); @@ -1086,6 +1116,8 @@ public class KurentoSessionManager extends SessionManager { throw new OpenViduException(Code.MEDIA_SDP_ERROR_CODE, "Unable to generate SDP answer when reconnecting subscriber to '" + streamId + "'"); } + + log.debug("SDP Answer for subscribing reconnection PARTICIPANT {}: {}", participant.getParticipantPublicId(), sdpAnswer); sessionEventsHandler.onSubscribe(participant, kSession, sdpAnswer, transactionId, null); } else { throw new OpenViduException(Code.USER_NOT_STREAMING_ERROR_CODE, diff --git a/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java b/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java index f343bf16..39819d1d 100644 --- a/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java +++ b/openvidu-server/src/main/java/io/openvidu/server/rest/ConfigRestController.java @@ -116,6 +116,8 @@ public class ConfigRestController { json.addProperty("OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH", openviduConfig.getVideoMinSendBandwidth()); json.addProperty("OPENVIDU_SESSIONS_GARBAGE_INTERVAL", openviduConfig.getSessionGarbageInterval()); json.addProperty("OPENVIDU_SESSIONS_GARBAGE_THRESHOLD", openviduConfig.getSessionGarbageThreshold()); + json.addProperty("OPENVIDU_FORCED_CODEC", openviduConfig.getOpenviduForcedCodec().name()); + json.addProperty("OPENVIDU_ALLOW_TRANSCODING", openviduConfig.isOpenviduAllowingTranscoding()); json.addProperty("OPENVIDU_RECORDING", openviduConfig.isRecordingModuleEnabled()); if (openviduConfig.isRecordingModuleEnabled()) { json.addProperty("OPENVIDU_RECORDING_VERSION", openviduConfig.getOpenViduRecordingVersion()); 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 7efe9d74..aec9c8c3 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 @@ -60,6 +60,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; @@ -715,6 +716,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"); @@ -722,6 +725,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) { throw new Exception("Type error in some parameter: " + e.getMessage()); } @@ -764,6 +769,16 @@ public class SessionRestController { } builder = builder.customSessionId(customSessionId); } + if (forcedVideoCodec != null) { + builder = builder.forcedVideoCodec(VideoCodec.valueOf(forcedVideoCodec)); + } else { + builder = builder.forcedVideoCodec(openviduConfig.getOpenviduForcedCodec()); + } + if (allowTranscoding != null) { + builder = builder.allowTranscoding(allowTranscoding); + } else { + builder = builder.allowTranscoding(openviduConfig.isOpenviduAllowingTranscoding()); + } } catch (IllegalArgumentException e) { throw new Exception("RecordingMode " + params.get("recordingMode") + " | " + "Default OutputMode " 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..0995278b --- /dev/null +++ b/openvidu-server/src/main/java/io/openvidu/server/utils/SDPMunging.java @@ -0,0 +1,185 @@ +/* + * (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.server.utils; + +import io.openvidu.client.OpenViduException; +import io.openvidu.java.client.VideoCodec; +import io.openvidu.client.OpenViduException.Code; +import io.openvidu.server.core.Participant; +import io.openvidu.server.core.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SDPMunging { + + private static final Logger log = LoggerFactory.getLogger(SDPMunging.class); + + private Set supportedVideoCodecs = new HashSet<>(Arrays.asList( + VideoCodec.VP8, + VideoCodec.H264 + )); + + /** + * `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 + " "); + } + + // Replace the original m= line with the one we just built. + lines[sl] = newLine.toString().trim(); + } + + return String.join("\r\n", lines) + "\r\n"; + } + + /** + * Return a SDP modified to force a specific codec + */ + public String forceCodec(Participant participant, String sdp, boolean isOffer, Session session, boolean isPublisher, + boolean isReconnecting, boolean isTranscodingAllowed, VideoCodec forcedVideoCodec) throws OpenViduException { + try { + if (supportedVideoCodecs.contains(forcedVideoCodec)) { + String mungedSdpOffer; + log.debug("PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'. Is Offer SDP: '{}'. " + + "Is Answer SDP: '{}'. Is Reconnecting '{}'. SDP before munging: \n {}", participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting, sdp); + mungedSdpOffer = this.setCodecPreference(forcedVideoCodec, sdp); + log.debug("PARTICIPANT '{}' in Session '{}'. Is Publisher: '{}'. Is Subscriber: '{}'. Is Offer SDP: '{}'. " + + "Is Answer SDP: '{}'. Is Reconnecting '{}'. SDP after munging: \n {}", participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting, mungedSdpOffer); + return mungedSdpOffer; + } else { + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, "Codec not supported by Media Server"); + } + + } catch (OpenViduException e) { + String errorMessage = "Error forcing codec: '" + forcedVideoCodec + "', for PARTICIPANT: '" + participant.getParticipantPublicId() + + "' in Session: '" + session.getSessionId() + "'. Is publishing: '" + isPublisher + "'. Is Subscriber: '" + !isPublisher + + "'. Is Offer: '" + isOffer + "'. Is Answer: '" + !isOffer + "'. Is Reconnecting: '" + + isReconnecting + "'.\nException: " + e.getMessage() + "\nSDP:\n" + sdp; + if(!isTranscodingAllowed) { + throw new OpenViduException(Code.FORCED_CODEC_NOT_FOUND_IN_SDPOFFER, errorMessage); + } + log.info("Codec: '{}' is not supported for PARTICIPANT: '{}' in Session: '{}'. Is publishing: '{}'. Is Subscriber: '{}' " + + "Is Offer SDP: '{}'. Is Answer SDP: '{}'. Is Reconnecting: '{}'. Transcoding will be allowed", forcedVideoCodec, participant.getParticipantPublicId(), + session.getSessionId(), isPublisher, !isPublisher, isOffer, !isOffer, isReconnecting); + return sdp; + } + } + +} diff --git a/openvidu-server/src/main/resources/application.properties b/openvidu-server/src/main/resources/application.properties index 917f6324..ba2be1af 100644 --- a/openvidu-server/src/main/resources/application.properties +++ b/openvidu-server/src/main/resources/application.properties @@ -45,6 +45,9 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 +OPENVIDU_FORCED_CODEC=VP8 +OPENVIDU_ALLOW_TRANSCODING=false + COTURN_REDIS_IP=127.0.0.1 COTURN_REDIS_DBNAME=0 COTURN_REDIS_PASSWORD=turn diff --git a/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java b/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java new file mode 100644 index 00000000..e3b097b6 --- /dev/null +++ b/openvidu-server/src/test/java/io/openvidu/server/test/unit/SDPMungingTest.java @@ -0,0 +1,177 @@ +package io.openvidu.server.test.unit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Test; +import org.junit.jupiter.api.DisplayName; + +import io.openvidu.client.OpenViduException; +import io.openvidu.java.client.VideoCodec; +import io.openvidu.server.utils.SDPMunging; + +public class SDPMungingTest { + + private SDPMunging sdpMungin = new SDPMunging(); + + private String oldSdp; + + private String newSdp; + + List h264codecPayloads; + + List forceCodecPayloads; + + String validSDPH264Files[] = new String[]{ + "sdp_kurento_h264.txt", + "sdp_chrome84.txt", + "sdp_firefox79.txt", + "sdp_safari13-1.txt" + }; + + String validSDPVP8Files[] = new String[]{ + "sdp_kurento_h264.txt", + "sdp_chrome84.txt", + "sdp_firefox79.txt", + "sdp_safari13-1.txt" + }; + + String validSDPVP9Files[] = new String[] { + "sdp_chrome84.txt", + "sdp_firefox79.txt" + }; + + String notValidVP9Files[] = new String[] { + "sdp_kurento_h264.txt", + "sdp_safari13-1.txt" + }; + + @Test + @DisplayName("[setCodecPreference] Force VP8 Codec prevalence in 'm=video' line") + public void checkPreferenceCodecVP8() throws IOException { + for(String sdpFileName: validSDPVP8Files) { + initTestsSetCodecPrevalence(VideoCodec.VP8, sdpFileName); + checkPrevalenceCodecInML(); + } + } + + @Test + @DisplayName("[setCodecPreference] Force VP8 Codec prevalence in 'm=video' line") + public void checkPreferenceCodecVP9() throws IOException { + for(String sdpFileName: validSDPVP9Files) { + initTestsSetCodecPrevalence(VideoCodec.VP9, sdpFileName); + checkPrevalenceCodecInML(); + } + } + + @Test + @DisplayName("[setCodecPreference] Force H264 Codec prevalence in 'm=video' line") + public void checkPreferenceCodecH264() throws IOException { + for(String sdpFileName: validSDPH264Files) { + initTestsSetCodecPrevalence(VideoCodec.H264, sdpFileName); + checkPrevalenceCodecInML(); + } + } + + @Test + @DisplayName("[setCodecPreference] Exception when codec does not exists on SDP") + public void checkPreferenceCodecException() throws IOException { + for(String sdpFile: notValidVP9Files) { + Exception exception = assertThrows(OpenViduException.class, () -> { + initTestsSetCodecPrevalence(VideoCodec.VP9, sdpFile); + }); + String expectedMessage = "The specified forced codec VP9 is not present in the SDP"; + assertTrue(exception.getMessage().contains(expectedMessage)); + } + } + + private String getSdpFile(String sdpNameFile) throws IOException { + Path sdpFile = Files.createTempFile("sdp-test", ".tmp"); + Files.copy(getClass().getResourceAsStream("/sdp/" + sdpNameFile), sdpFile, StandardCopyOption.REPLACE_EXISTING); + String sdpUnformatted = new String(Files.readAllBytes(sdpFile)); + return String.join("\r\n", sdpUnformatted.split("\\R+")) + "\r\n"; + } + + private void initTestsSetCodecPrevalence(VideoCodec codec, String sdpNameFile) throws IOException { + this.oldSdp = getSdpFile(sdpNameFile); + this.newSdp = this.sdpMungin.setCodecPreference(codec, oldSdp); + this.forceCodecPayloads = new ArrayList<>(); + + // Get all Payload-Type for video Codec + for(String oldSdpLine: oldSdp.split("\\R+")) { + if(oldSdpLine.startsWith("a=rtpmap") && oldSdpLine.endsWith(codec.name() + "/90000")) { + String pt = oldSdpLine.split(":")[1].split(" ")[0]; + this.forceCodecPayloads.add(pt); + } + } + + // Get all Payload-Types rtx related with codec + // Not the best way to do it, but enough to check if the sdp + // generated is correct + String[] oldSdpLines = oldSdp.split("\\R+"); + List rtxForcedCodecs = new ArrayList<>(); + for(String oldSdpLine: oldSdpLines) { + if(oldSdpLine.startsWith("a=rtpmap") && oldSdpLine.endsWith("rtx/90000")) { + String rtxPayload = oldSdpLine.split(":")[1].split(" ")[0]; + for (String auxOldSdpLine: oldSdpLines) { + if (auxOldSdpLine.contains("a=fmtp:" + rtxPayload + " apt=")) { + for (String auxForcedCodec: this.forceCodecPayloads) { + if (auxOldSdpLine.contains("a=fmtp:" + rtxPayload + " apt=" + auxForcedCodec)) { + String pt = oldSdpLine.split(":")[1].split(" ")[0]; + rtxForcedCodecs.add(pt); + } + } + } + } + } + } + this.forceCodecPayloads.addAll(rtxForcedCodecs); + } + + private void checkPrevalenceCodecInML() { + + String newml = null; + String[] newSdpLines = this.newSdp.split("\\R+"); + for(String newSdpLine: newSdpLines) { + if (newSdpLine.startsWith("m=video")) { + newml = newSdpLine; + break; + } + } + + if (newml == null) { + fail("'m=video' line not found in SDP"); + } + + List newMlCodecPrevalenceList = new ArrayList<>(); + String[] lmParams = newml.split(" "); + int numOfCodecsWithPrevalence = this.forceCodecPayloads.size(); + int indexStartCodecs = 3; + int indexEndPreferencedCodecs = 3 + numOfCodecsWithPrevalence; + for(int i = indexStartCodecs; i < indexEndPreferencedCodecs; i++) { + newMlCodecPrevalenceList.add(lmParams[i]); + } + + for(int j = 0; j < numOfCodecsWithPrevalence; j++) { + String codecToCheck = newMlCodecPrevalenceList.get(j); + boolean codecFoundInPrevalenceList = false; + for(String codecToForce: this.forceCodecPayloads) { + if (codecToCheck.equals(codecToForce)) { + codecFoundInPrevalenceList = true; + break; + } + } + assertTrue(codecFoundInPrevalenceList); + } + } + +} \ No newline at end of file diff --git a/openvidu-server/src/test/resources/integration-test.properties b/openvidu-server/src/test/resources/integration-test.properties index 5747263f..db323402 100644 --- a/openvidu-server/src/test/resources/integration-test.properties +++ b/openvidu-server/src/test/resources/integration-test.properties @@ -40,6 +40,9 @@ OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH=300 OPENVIDU_SESSIONS_GARBAGE_INTERVAL=900 OPENVIDU_SESSIONS_GARBAGE_THRESHOLD=3600 +OPENVIDU_FORCED_CODEC=VP8 +OPENVIDU_ALLOW_TRANSCODING=false + COTURN_REDIS_IP=127.0.0.1 COTURN_REDIS_DBNAME=0 COTURN_REDIS_PASSWORD=turn diff --git a/openvidu-server/src/test/resources/sdp/sdp_chrome84.txt b/openvidu-server/src/test/resources/sdp/sdp_chrome84.txt new file mode 100644 index 00000000..83792b3f --- /dev/null +++ b/openvidu-server/src/test/resources/sdp/sdp_chrome84.txt @@ -0,0 +1,143 @@ +v=0 +o=- 5217540180782877494 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 1 +a=msid-semantic: WMS eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul +m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:RWim +a=ice-pwd:seCgmyE+AkRJKgqD4SdIuALd +a=ice-options:trickle +a=fingerprint:sha-256 59:47:FE:36:82:34:B1:7B:4C:D5:D4:76:78:24:89:67:5E:3C:84:4F:52:BD:86:83:67:10:98:8C:79:9D:89:7D +a=setup:actpass +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendonly +a=msid:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul 7bbc37fe-6e36-479f-990f-988a07ac3f00 +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:104 ISAC/32000 +a=rtpmap:9 G722/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:106 CN/32000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:112 telephone-event/32000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:913601848 cname:BkItF+kVUhq9L3k4 +a=ssrc:913601848 msid:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul 7bbc37fe-6e36-479f-990f-988a07ac3f00 +a=ssrc:913601848 mslabel:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul +a=ssrc:913601848 label:7bbc37fe-6e36-479f-990f-988a07ac3f00 +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:RWim +a=ice-pwd:seCgmyE+AkRJKgqD4SdIuALd +a=ice-options:trickle +a=fingerprint:sha-256 59:47:FE:36:82:34:B1:7B:4C:D5:D4:76:78:24:89:67:5E:3C:84:4F:52:BD:86:83:67:10:98:8C:79:9D:89:7D +a=setup:actpass +a=mid:1 +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 +a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendonly +a=msid:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul 29491954-cc1e-4ca3-b290-cb8d5d0ad685 +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 VP8/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 VP9/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 profile-id=0 +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 VP9/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=fmtp:100 profile-id=2 +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:102 H264/90000 +a=rtcp-fb:102 goog-remb +a=rtcp-fb:102 transport-cc +a=rtcp-fb:102 ccm fir +a=rtcp-fb:102 nack +a=rtcp-fb:102 nack pli +a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f +a=rtpmap:122 rtx/90000 +a=fmtp:122 apt=102 +a=rtpmap:127 H264/90000 +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 transport-cc +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f +a=rtpmap:121 rtx/90000 +a=fmtp:121 apt=127 +a=rtpmap:125 H264/90000 +a=rtcp-fb:125 goog-remb +a=rtcp-fb:125 transport-cc +a=rtcp-fb:125 ccm fir +a=rtcp-fb:125 nack +a=rtcp-fb:125 nack pli +a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=rtpmap:107 rtx/90000 +a=fmtp:107 apt=125 +a=rtpmap:108 H264/90000 +a=rtcp-fb:108 goog-remb +a=rtcp-fb:108 transport-cc +a=rtcp-fb:108 ccm fir +a=rtcp-fb:108 nack +a=rtcp-fb:108 nack pli +a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f +a=rtpmap:109 rtx/90000 +a=fmtp:109 apt=108 +a=rtpmap:124 red/90000 +a=rtpmap:120 rtx/90000 +a=fmtp:120 apt=124 +a=rtpmap:123 ulpfec/90000 +a=ssrc-group:FID 796117241 4235816742 +a=ssrc:796117241 cname:BkItF+kVUhq9L3k4 +a=ssrc:796117241 msid:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul 29491954-cc1e-4ca3-b290-cb8d5d0ad685 +a=ssrc:796117241 mslabel:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul +a=ssrc:796117241 label:29491954-cc1e-4ca3-b290-cb8d5d0ad685 +a=ssrc:4235816742 cname:BkItF+kVUhq9L3k4 +a=ssrc:4235816742 msid:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul 29491954-cc1e-4ca3-b290-cb8d5d0ad685 +a=ssrc:4235816742 mslabel:eEMVYR4txYWGErUa55KnHm0mMBdfrqSbtQul +a=ssrc:4235816742 label:29491954-cc1e-4ca3-b290-cb8d5d0ad685 \ No newline at end of file diff --git a/openvidu-server/src/test/resources/sdp/sdp_firefox79.txt b/openvidu-server/src/test/resources/sdp/sdp_firefox79.txt new file mode 100644 index 00000000..b2918cf7 --- /dev/null +++ b/openvidu-server/src/test/resources/sdp/sdp_firefox79.txt @@ -0,0 +1,67 @@ +v=0 +o=mozilla...THIS_IS_SDPARTA-79.0 1574413241511582424 0 IN IP4 0.0.0.0 +s=- +t=0 0 +a=sendrecv +a=fingerprint:sha-256 43:63:A0:1A:D4:F3:6A:0B:B9:DC:AD:8B:A4:20:22:17:B9:BD:FC:81:9F:EC:E9:46:E0:61:3B:8B:2A:05:9A:D9 +a=group:BUNDLE 0 1 +a=ice-options:trickle +a=msid-semantic:WMS * +m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101 +c=IN IP4 0.0.0.0 +a=sendonly +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level +a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid +a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1 +a=fmtp:101 0-15 +a=ice-pwd:4be109f1ce637a5423d9229aa10c43bd +a=ice-ufrag:03567d22 +a=mid:0 +a=msid:{77708aaa-ed09-403a-b19a-26b8f44aff96} {f18baa5e-b689-4b17-9db8-83620b6c1f2b} +a=rtcp-mux +a=rtpmap:109 opus/48000/2 +a=rtpmap:9 G722/8000/1 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:101 telephone-event/8000 +a=setup:actpass +a=ssrc:2626852385 cname:{4ff79710-1727-45f8-bbe7-7532884b9652} +m=video 9 UDP/TLS/RTP/SAVPF 120 121 126 97 +c=IN IP4 0.0.0.0 +a=sendonly +a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:4 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:5 urn:ietf:params:rtp-hdrext:toffset +a=extmap:6/recvonly http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1 +a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1 +a=fmtp:120 max-fs=12288;max-fr=60 +a=fmtp:121 max-fs=12288;max-fr=60 +a=ice-pwd:4be109f1ce637a5423d9229aa10c43bd +a=ice-ufrag:03567d22 +a=mid:1 +a=msid:{77708aaa-ed09-403a-b19a-26b8f44aff96} {a68009be-6483-45fd-a48d-7bc60fa6f055} +a=rtcp-fb:120 nack +a=rtcp-fb:120 nack pli +a=rtcp-fb:120 ccm fir +a=rtcp-fb:120 goog-remb +a=rtcp-fb:121 nack +a=rtcp-fb:121 nack pli +a=rtcp-fb:121 ccm fir +a=rtcp-fb:121 goog-remb +a=rtcp-fb:126 nack +a=rtcp-fb:126 nack pli +a=rtcp-fb:126 ccm fir +a=rtcp-fb:126 goog-remb +a=rtcp-fb:97 nack +a=rtcp-fb:97 nack pli +a=rtcp-fb:97 ccm fir +a=rtcp-fb:97 goog-remb +a=rtcp-mux +a=rtpmap:120 VP8/90000 +a=rtpmap:121 VP9/90000 +a=rtpmap:126 H264/90000 +a=rtpmap:97 H264/90000 +a=setup:actpass +a=ssrc:3743317987 cname:{4ff79710-1727-45f8-bbe7-7532884b9652} \ No newline at end of file diff --git a/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt b/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt new file mode 100644 index 00000000..18d81183 --- /dev/null +++ b/openvidu-server/src/test/resources/sdp/sdp_kurento_h264.txt @@ -0,0 +1,61 @@ +v=0 +o=- 3808465464 3808465464 IN IP4 0.0.0.0 +s=Kurento Media Server +c=IN IP4 0.0.0.0 +t=0 0 +a=msid-semantic: WMS m0W2gMak7LgkgzgJeDQhxBX0ivcsejWjQ0jD +a=group:BUNDLE 0 1 +m=audio 1 UDP/TLS/RTP/SAVPF 111 0 +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=recvonly +a=mid:0 +a=rtcp:9 IN IP4 0.0.0.0 +a=rtpmap:111 opus/48000/2 +a=rtpmap:0 PCMU/8000 +a=setup:active +a=rtcp-mux +a=fmtp:111 minptime=10;useinbandfec=1 +a=ssrc:1929271881 cname:user4129876135@host-ed881df6 +a=ice-ufrag:cXmf +a=ice-pwd:9giZcfpsuoHRuxCgbnCLRy +a=fingerprint:sha-256 C8:D4:B5:56:A7:89:E5:E1:C8:28:0A:47:2B:49:F6:7A:E2:2E:B3:0A:40:10:AD:79:82:E7:FD:A0:ED:6C:F6:51 +m=video 1 UDP/TLS/RTP/SAVPF 96 102 127 125 108 +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=recvonly +a=mid:1 +a=rtcp:9 IN IP4 0.0.0.0 +a=rtpmap:96 VP8/90000 +a=rtpmap:102 H264/90000 +a=rtpmap:127 H264/90000 +a=rtpmap:125 H264/90000 +a=rtpmap:108 H264/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=rtcp-fb:102 goog-remb +a=rtcp-fb:102 ccm fir +a=rtcp-fb:102 nack +a=rtcp-fb:102 nack pli +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=rtcp-fb:125 goog-remb +a=rtcp-fb:125 ccm fir +a=rtcp-fb:125 nack +a=rtcp-fb:125 nack pli +a=rtcp-fb:108 goog-remb +a=rtcp-fb:108 ccm fir +a=rtcp-fb:108 nack +a=rtcp-fb:108 nack pli +a=setup:active +a=rtcp-mux +a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f +a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f +a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f +a=ssrc:3762875210 cname:user4129876135@host-ed881df6 +a=ice-ufrag:cXmf +a=ice-pwd:9giZcfpsuoHRuxCgbnCLRy +a=fingerprint:sha-256 C8:D4:B5:56:A7:89:E5:E1:C8:28:0A:47:2B:49:F6:7A:E2:2E:B3:0A:40:10:AD:79:82:E7:FD:A0:ED:6C:F6:51 \ No newline at end of file diff --git a/openvidu-server/src/test/resources/sdp/sdp_safari13-1.txt b/openvidu-server/src/test/resources/sdp/sdp_safari13-1.txt new file mode 100644 index 00000000..ee329dad --- /dev/null +++ b/openvidu-server/src/test/resources/sdp/sdp_safari13-1.txt @@ -0,0 +1,107 @@ +v=0 +o=- 4920969914039852086 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 1 +a=msid-semantic: WMS 90c21009-8d9f-4b31-8091-d98deb8361c8 +m=audio 61842 UDP/TLS/RTP/SAVPF 111 103 9 102 0 8 105 13 110 113 126 +c=IN IP4 192.168.1.105 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2222700650 1 udp 2113937151 192.168.1.105 61842 typ host generation 0 network-cost 999 +a=ice-ufrag:zbK7 +a=ice-pwd:o4ZsJFGBXHNzOWqX23brKpiG +a=ice-options:trickle +a=fingerprint:sha-256 18:BD:1F:64:28:C6:BC:7B:AD:83:42:E0:B1:78:BA:13:F4:BF:F5:5E:AD:20:62:CC:AF:DF:99:AD:20:CB:1B:7E +a=setup:actpass +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendonly +a=msid:90c21009-8d9f-4b31-8091-d98deb8361c8 1d7a65f7-59c9-42a7-abbb-d9fb5548e3ba +a=rtcp-mux +a=rtpmap:111 opus/48000/2 +a=rtcp-fb:111 transport-cc +a=fmtp:111 minptime=10;useinbandfec=1 +a=rtpmap:103 ISAC/16000 +a=rtpmap:9 G722/8000 +a=rtpmap:102 ILBC/8000 +a=rtpmap:0 PCMU/8000 +a=rtpmap:8 PCMA/8000 +a=rtpmap:105 CN/16000 +a=rtpmap:13 CN/8000 +a=rtpmap:110 telephone-event/48000 +a=rtpmap:113 telephone-event/16000 +a=rtpmap:126 telephone-event/8000 +a=ssrc:1052596434 cname:L3DqI2bKTDkcNVsn +a=ssrc:1052596434 msid:90c21009-8d9f-4b31-8091-d98deb8361c8 1d7a65f7-59c9-42a7-abbb-d9fb5548e3ba +a=ssrc:1052596434 mslabel:90c21009-8d9f-4b31-8091-d98deb8361c8 +a=ssrc:1052596434 label:1d7a65f7-59c9-42a7-abbb-d9fb5548e3ba +m=video 62559 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 125 104 +c=IN IP4 192.168.1.105 +a=rtcp:9 IN IP4 0.0.0.0 +a=candidate:2222700650 1 udp 2113937151 192.168.1.105 62559 typ host generation 0 network-cost 999 +a=ice-ufrag:zbK7 +a=ice-pwd:o4ZsJFGBXHNzOWqX23brKpiG +a=ice-options:trickle +a=fingerprint:sha-256 18:BD:1F:64:28:C6:BC:7B:AD:83:42:E0:B1:78:BA:13:F4:BF:F5:5E:AD:20:62:CC:AF:DF:99:AD:20:CB:1B:7E +a=setup:actpass +a=mid:1 +a=extmap:14 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:13 urn:3gpp:video-orientation +a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:12 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:11 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07 +a=extmap:9 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=sendonly +a=msid:90c21009-8d9f-4b31-8091-d98deb8361c8 fc45e668-c301-41fe-ae8a-1265a5a355e7 +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 H264/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 H264/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 VP8/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 red/90000 +a=rtpmap:125 rtx/90000 +a=fmtp:125 apt=127 +a=rtpmap:104 ulpfec/90000 +a=ssrc-group:FID 2734983896 3694891391 +a=ssrc:2734983896 cname:L3DqI2bKTDkcNVsn +a=ssrc:2734983896 msid:90c21009-8d9f-4b31-8091-d98deb8361c8 fc45e668-c301-41fe-ae8a-1265a5a355e7 +a=ssrc:2734983896 mslabel:90c21009-8d9f-4b31-8091-d98deb8361c8 +a=ssrc:2734983896 label:fc45e668-c301-41fe-ae8a-1265a5a355e7 +a=ssrc:3694891391 cname:L3DqI2bKTDkcNVsn +a=ssrc:3694891391 msid:90c21009-8d9f-4b31-8091-d98deb8361c8 fc45e668-c301-41fe-ae8a-1265a5a355e7 +a=ssrc:3694891391 mslabel:90c21009-8d9f-4b31-8091-d98deb8361c8 +a=ssrc:3694891391 label:fc45e668-c301-41fe-ae8a-1265a5a355e7 \ No newline at end of file diff --git a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java index 7048077a..76c2d216 100644 --- a/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java +++ b/openvidu-test-browsers/src/main/java/io/openvidu/test/browsers/FirefoxUser.java @@ -30,7 +30,7 @@ import org.openqa.selenium.remote.RemoteWebDriver; public class FirefoxUser extends BrowserUser { - public FirefoxUser(String userName, int timeOfWaitInSeconds) { + public FirefoxUser(String userName, int timeOfWaitInSeconds, boolean disableOpenH264) { super(userName, timeOfWaitInSeconds); DesiredCapabilities capabilities = DesiredCapabilities.firefox(); @@ -43,6 +43,10 @@ public class FirefoxUser extends BrowserUser { // This flag force to use fake user media (synthetic video of multiple color) profile.setPreference("media.navigator.streams.fake", true); + if (disableOpenH264) { + profile.setPreference("media.gmp-gmpopenh264.enabled", false); + } + capabilities.setCapability(FirefoxDriver.PROFILE, profile); String REMOTE_URL = System.getProperty("REMOTE_URL_FIREFOX"); diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java index 1eea26af..00998dae 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java @@ -64,7 +64,7 @@ import io.openvidu.test.browsers.utils.Unzipper; public class AbstractOpenViduTestAppE2eTest { - final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false}"; + final protected String DEFAULT_JSON_SESSION = "{'id':'STR','object':'session','sessionId':'STR','createdAt':0,'mediaMode':'STR','recordingMode':'STR','defaultOutputMode':'STR','defaultRecordingLayout':'STR','customSessionId':'STR','connections':{'numberOfElements':0,'content':[]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}"; final protected String DEFAULT_JSON_PENDING_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'pending','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':null,'location':null,'platform':null,'token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':null,'publishers':null,'subscribers':null}"; final protected String DEFAULT_JSON_ACTIVE_CONNECTION = "{'id':'STR','object':'connection','type':'WEBRTC','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'STR','token':'STR','serverData':'STR','record':true,'role':'STR','kurentoOptions':null,'rtspUri':null,'adaptativeBitrate':null,'onlyPlayWithSubscribers':null,'networkCache':null,'clientData':'STR','publishers':[],'subscribers':[]}"; final protected String DEFAULT_JSON_IPCAM_CONNECTION = "{'id':'STR','object':'connection','type':'IPCAM','status':'active','connectionId':'STR','sessionId':'STR','createdAt':0,'activeAt':0,'location':'STR','platform':'IPCAM','token':null,'serverData':'STR','record':true,'role':null,'kurentoOptions':null,'rtspUri':'STR','adaptativeBitrate':true,'onlyPlayWithSubscribers':true,'networkCache':2000,'clientData':null,'publishers':[],'subscribers':[]}"; @@ -177,7 +177,10 @@ public class AbstractOpenViduTestAppE2eTest { browserUser = new ChromeUser("TestUser", 50, false); break; case "firefox": - browserUser = new FirefoxUser("TestUser", 50); + browserUser = new FirefoxUser("TestUser", 50, false); + break; + case "firefoxDisabledOpenH264": + browserUser = new FirefoxUser("TestUser", 50, true); break; case "opera": browserUser = new OperaUser("TestUser", 50); diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java index 01ad7c27..be25b15e 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java @@ -18,6 +18,7 @@ package io.openvidu.test.e2e; import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.File; import java.nio.file.Paths; @@ -70,6 +71,7 @@ import io.openvidu.java.client.RecordingMode; import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.Session; import io.openvidu.java.client.SessionProperties; +import io.openvidu.java.client.VideoCodec; import io.openvidu.test.browsers.FirefoxUser; import io.openvidu.test.browsers.utils.CustomHttpClient; import io.openvidu.test.browsers.utils.layout.CustomLayoutHandler; @@ -366,7 +368,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { final CountDownLatch latch = new CountDownLatch(2); Thread threadFirefox = new Thread(() -> { - MyUser user2 = new MyUser(new FirefoxUser("TestUser", 30)); + MyUser user2 = new MyUser(new FirefoxUser("TestUser", 30, false)); otherUsers.add(user2); user2.getDriver().get(APP_URL); WebElement urlInput = user2.getDriver().findElement(By.id("openvidu-url")); @@ -1417,7 +1419,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { }; Thread t = new Thread(() -> { - MyUser user2 = new MyUser(new FirefoxUser("FirefoxUser", 30)); + MyUser user2 = new MyUser(new FirefoxUser("FirefoxUser", 30, false)); otherUsers.add(user2); user2.getDriver().get(APP_URL); WebElement urlInput = user2.getDriver().findElement(By.id("openvidu-url")); @@ -2923,7 +2925,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { + "{'createdAt':0,'streamId':'STR','mediaOptions':{'hasAudio':false,'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0," + "'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]},{'connectionId':'STR','createdAt':0,'location':'STR'," + "'platform':'STR','token':'STR','role':'STR','serverData':'STR','clientData':'STR','publishers':[{'createdAt':0,'streamId':'STR','mediaOptions':{'hasAudio':false," - + "'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0,'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]}]},'recording':false}"); + + "'audioActive':false,'hasVideo':false,'videoActive':false,'typeOfVideo':'STR','frameRate':0,'videoDimensions':'STR','filter':{}}}],'subscribers':[{'createdAt':0,'streamId':'STR','publisher':'STR'}]}]},'recording':false,'forcedVideoCodec':'STR','allowTranscoding':false}"); String streamId = res.get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0) .getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject().get("streamId") .getAsString(); @@ -3044,7 +3046,7 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { restClient.rest(HttpMethod.GET, "/openvidu/api/config", null, HttpStatus.SC_OK, true, false, true, "{'VERSION':'STR','DOMAIN_OR_PUBLIC_IP':'STR','HTTPS_PORT':0,'OPENVIDU_PUBLICURL':'STR','OPENVIDU_CDR':false,'OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH':0," + "'OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH':0,'OPENVIDU_SESSIONS_GARBAGE_INTERVAL':0,'OPENVIDU_SESSIONS_GARBAGE_THRESHOLD':0," - + "'OPENVIDU_RECORDING':false,'OPENVIDU_RECORDING_VERSION':'STR','OPENVIDU_RECORDING_PATH':'STR','OPENVIDU_RECORDING_PUBLIC_ACCESS':false,'OPENVIDU_RECORDING_NOTIFICATION':'STR'," + + "'OPENVIDU_FORCED_CODEC':'STR','OPENVIDU_ALLOW_TRANSCODING':false,'OPENVIDU_RECORDING':false,'OPENVIDU_RECORDING_VERSION':'STR','OPENVIDU_RECORDING_PATH':'STR','OPENVIDU_RECORDING_PUBLIC_ACCESS':false,'OPENVIDU_RECORDING_NOTIFICATION':'STR'," + "'OPENVIDU_RECORDING_CUSTOM_LAYOUT':'STR','OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT':0,'OPENVIDU_WEBHOOK':false,'OPENVIDU_WEBHOOK_ENDPOINT':'STR','OPENVIDU_WEBHOOK_HEADERS':[]," + "'OPENVIDU_WEBHOOK_EVENTS':[]}"); } @@ -3885,6 +3887,57 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { checkNodeFetchChanged(true, false); } + @Test + @DisplayName("Force valid codec - Not Allow Transcoding") + void forceValidCodecNotAllowTranscodingTest() throws Exception { + log.info("Force codec - Force VP8 - Not Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(); + this.user.getDriver().close(); + + log.info("Force codec Chrome - Force VP8 - Not Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.VP8, false); + this.user.getDriver().close(); + + log.info("Force codec Chrome - Force H264 - Not Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.H264, false); + this.user.getDriver().close(); + } + + @Test + @DisplayName("Force valid codec - Allow Transcoding") + void forceValidCodecAllowTranscodingTest() throws Exception { + log.info("Force codec Chrome - Force VP8 - Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.VP8, true); + this.user.getDriver().close(); + + log.info("Force codec Chrome - Force H264 - Allow Transcoding"); + setupBrowser("chrome"); + this.forceCodecGenericE2eTest(VideoCodec.H264, true); + this.user.getDriver().close(); + } + + @Test + @DisplayName("Force not valid codec - Not Allow Transcoding") + void forceCodecNotValidCodecNotAllowTranscoding() throws Exception { + // Start firefox with OpenH264 disabled to check not supported codecs + log.info("Force codec Firefox - Force H264 - Allow Transcoding - Disabled H264 in Firefox"); + setupBrowser("firefoxDisabledOpenH264"); + this.forceCodecNotSupportedCodec(VideoCodec.H264, false); + } + + @Test + @DisplayName("Force not valid codec - Allow Transcoding") + void forceCodecNotValidCodecAllowTranscoding() throws Exception { + // Start firefox with OpenH264 disabled to check not supported codecs + setupBrowser("firefoxDisabledOpenH264"); + log.info("Force codec Firefox - Force H264 - Allow Transcoding - Disabled H264 in Firefox"); + this.forceCodecNotSupportedCodec(VideoCodec.H264, true); + } + private void checkNodeFetchChanged(boolean global, boolean hasChanged) { user.getDriver().findElement(By.id(global ? "list-sessions-btn" : "get-session-btn")).click(); user.getWaiter().until(new NodeFetchHasChanged(hasChanged)); @@ -3905,4 +3958,148 @@ public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { } } + private void forceCodecGenericE2eTest() throws Exception { + forceCodecGenericE2eTest(null, null); + } + + private void forceCodecGenericE2eTest(VideoCodec codec, Boolean allowTranscoding) throws Exception { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + JsonObject ovConfig = restClient.rest(HttpMethod.GET, "/openvidu/api/config", HttpStatus.SC_OK); + VideoCodec defaultCodec = VideoCodec.valueOf(ovConfig.get("OPENVIDU_FORCED_CODEC").getAsString()); + Boolean defaultAllowTranscoding = ovConfig.get("OPENVIDU_ALLOW_TRANSCODING").getAsBoolean(); + + String sessionName = "CUSTOM_SESSION_" + ((codec != null) ? codec.name() : "DEFAULT_FORCE_CODEC"); + + // Configure Session to force Codec + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName1 = user.getDriver().findElement(By.id("session-name-input-0")); + sessionName1.clear(); + sessionName1.sendKeys(sessionName); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + + if (codec != null) { + user.getDriver().findElement(By.id("forced-video-codec-select")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("option-" + codec.name())).click(); + } + if(allowTranscoding != null && allowTranscoding) { + user.getDriver().findElement(By.id("allow-transcoding-checkbox")).click(); + } + + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + // Join Session + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName2 = user.getDriver().findElement(By.id("session-name-input-1")); + sessionName2.clear(); + sessionName2.sendKeys(sessionName); + + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + // Check values + JsonObject sessionJson = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_OK); + VideoCodec sessionCodec = VideoCodec.valueOf(sessionJson.get("forcedVideoCodec").getAsString()); + boolean sessionAllowTranscoding = sessionJson.get("allowTranscoding").getAsBoolean(); + + // Assert Selected Codec + if (codec != null) { + // If specified codec, assert selected codec + Assert.assertEquals(sessionCodec, codec); + } else { + // If not specified, assert default codec + Assert.assertEquals(sessionCodec, defaultCodec); + } + + // Assert Selected allow transcoding + if (allowTranscoding != null) { + Assert.assertEquals(sessionAllowTranscoding, allowTranscoding); + } else { + Assert.assertEquals(sessionAllowTranscoding, defaultAllowTranscoding); + } + + // Check real codecs + VideoCodec codecToCheck = (codec != null) ? codec : defaultCodec; + List statsButtons = user.getDriver().findElements(By.className("stats-button")); + for(WebElement statButton: statsButtons) { + statButton.click(); + Thread.sleep(1000); + String videoCodecUsed = user.getDriver().findElement(By.id("video-codec-used")).getText(); + assertEquals(videoCodecUsed, "video/" + codecToCheck); + user.getDriver().findElement(By.id("close-dialog-btn")).click(); + } + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_NO_CONTENT); + } + + private void forceCodecNotSupportedCodec(VideoCodec codec, boolean allowTranscoding) throws Exception { + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + + String sessionName = "CUSTOM_SESSION_CODEC_NOT_SUPPORTED"; + + // Configure Session to force Codec + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionNameElem = user.getDriver().findElement(By.id("session-name-input-0")); + sessionNameElem.clear(); + sessionNameElem.sendKeys(sessionName); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.id("forced-video-codec-select")).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("option-" + codec.name())).click(); + + if(allowTranscoding) { + user.getDriver().findElement(By.id("allow-transcoding-checkbox")).click(); + } + + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + if (allowTranscoding) { + // If transcoding is enabled everything should work fine + + // Join another user + user.getDriver().findElement(By.id("add-user-btn")).click(); + WebElement sessionName2 = user.getDriver().findElement(By.id("session-name-input-1")); + sessionName2.clear(); + sessionName2.sendKeys(sessionName); + + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 4); + user.getEventManager().waitUntilEventReaches("accessAllowed", 2); + user.getEventManager().waitUntilEventReaches("streamCreated", 4); + user.getEventManager().waitUntilEventReaches("streamPlaying", 4); + + final int numberOfVideos = user.getDriver().findElements(By.tagName("video")).size(); + Assert.assertEquals("Expected 4 videos but found " + numberOfVideos, 4, numberOfVideos); + Assert.assertTrue("Videos were expected to have audio and video tracks", user.getEventManager() + .assertMediaTracks(user.getDriver().findElements(By.tagName("video")), true, true)); + + } else { + // If transcoding not allowed it should return an alert with error + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + user.getWaiter().until(ExpectedConditions.alertIsPresent()); + Alert alert = user.getDriver().switchTo().alert(); + Assert.assertTrue("Alert does not contain expected text", + alert.getText().contains("Error forcing codec: '" + codec.name() + "'")); + alert.accept(); + } + + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_NO_CONTENT); + Thread.sleep(1000); + } } diff --git a/openvidu-testapp/src/app/app.module.ts b/openvidu-testapp/src/app/app.module.ts index 61f3323a..e838fb8e 100644 --- a/openvidu-testapp/src/app/app.module.ts +++ b/openvidu-testapp/src/app/app.module.ts @@ -25,6 +25,7 @@ import { EventsDialogComponent } from './components/dialogs/events-dialog/events import { PublisherPropertiesDialogComponent } from './components/dialogs/publisher-properties-dialog/publisher-properties-dialog.component'; import { ScenarioPropertiesDialogComponent } from './components/dialogs/scenario-properties-dialog/scenario-properties-dialog.component'; import { FilterDialogComponent } from './components/dialogs/filter-dialog/filter-dialog.component'; +import { ShowCodecDialogComponent } from './components/dialogs/show-codec-dialog/show-codec-dialog.component'; import { OpenviduRestService } from './services/openvidu-rest.service'; import { OpenviduParamsService } from './services/openvidu-params.service'; @@ -48,6 +49,7 @@ import { MuteSubscribersService } from './services/mute-subscribers.service'; PublisherPropertiesDialogComponent, ScenarioPropertiesDialogComponent, FilterDialogComponent, + ShowCodecDialogComponent, UsersTableComponent, TableVideoComponent ], @@ -74,7 +76,8 @@ import { MuteSubscribersService } from './services/mute-subscribers.service'; LocalRecordingDialogComponent, PublisherPropertiesDialogComponent, ScenarioPropertiesDialogComponent, - FilterDialogComponent + FilterDialogComponent, + ShowCodecDialogComponent ], bootstrap: [AppComponent] }) 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 f83aa643..d151819b 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 @@ -38,6 +38,10 @@ mat-radio-button:first-child { padding-bottom: 15px; } +#allow-transcoding-div { + margin-bottom: 10px; +} + #record-div { padding-bottom: 20px; -} \ 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 5ed01d00..997dda60 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 @@ -39,6 +39,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 48d6ab98..965fc831 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, ConnectionProperties } from 'openvidu-node-client'; +import { SessionProperties, MediaMode, Recording, RecordingMode, RecordingLayout, ConnectionProperties, VideoCodec } from 'openvidu-node-client'; @Component({ selector: 'app-session-properties-dialog', @@ -16,6 +16,7 @@ export class SessionPropertiesDialogComponent { customToken: string; forcePublishing: boolean = false; connectionProperties: ConnectionProperties; + forceVideoCodec = VideoCodec; filterName = 'GStreamerFilter'; filters: string[] = []; diff --git a/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts b/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts new file mode 100644 index 00000000..d33cc4cc --- /dev/null +++ b/openvidu-testapp/src/app/components/dialogs/show-codec-dialog/show-codec-dialog.component.ts @@ -0,0 +1,27 @@ +import { Component, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material'; + +@Component({ + selector: 'app-codec-used-dialog', + template: ` +
+

Used Codec: {{usedVideoCodec}}

+ + +
+ `, + styles: [` + #app-codec-dialog-container { + text-align: center + } + `] +}) +export class ShowCodecDialogComponent { + + usedVideoCodec; + + constructor(public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data) { + this.usedVideoCodec = data.usedVideoCodec; + } +} 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 41578430..ceb940d0 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 @@ -93,7 +93,9 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { defaultOutputMode: Recording.OutputMode.COMPOSED, defaultRecordingLayout: RecordingLayout.BEST_FIT, defaultCustomLayout: '', - customSessionId: '' + customSessionId: '', + forcedVideoCodec: null, + allowTranscoding: null }; publisherProperties: PublisherProperties = { @@ -521,7 +523,12 @@ export class OpenviduInstanceComponent implements OnInit, OnChanges, OnDestroy { this.republishPossible = false; }).catch((error: OpenViduError) => { console.error(error); - alert(error.name + ": " + error.message); + if (!error.name) { + alert(error); + } else { + alert(error.name + ": " + error.message); + } + this.republishPossible = true; this.session.unpublish(this.publisher); delete this.publisher; diff --git a/openvidu-testapp/src/app/components/video/video.component.html b/openvidu-testapp/src/app/components/video/video.component.html index 5e52c0e4..0a9830f8 100644 --- a/openvidu-testapp/src/app/components/video/video.component.html +++ b/openvidu-testapp/src/app/components/video/video.component.html @@ -5,6 +5,9 @@ +
+
- \ No newline at end of file + diff --git a/openvidu-testapp/src/app/components/video/video.component.ts b/openvidu-testapp/src/app/components/video/video.component.ts index f9e99f91..2d1e5f24 100644 --- a/openvidu-testapp/src/app/components/video/video.component.ts +++ b/openvidu-testapp/src/app/components/video/video.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core'; import { MatDialog, MatDialogRef } from '@angular/material'; +import { ShowCodecDialogComponent } from '../dialogs/show-codec-dialog/show-codec-dialog.component'; import { StreamManager, @@ -67,664 +68,692 @@ export class VideoComponent implements OnInit, OnDestroy { recordIcon = 'fiber_manual_record'; pauseRecordIcon = ''; - constructor(private dialog: MatDialog, private muteSubscribersService: MuteSubscribersService - ) { } + // Stats + usedVideoCodec: string; - ngOnInit() { + constructor(private dialog: MatDialog, private muteSubscribersService: MuteSubscribersService) { } - if (this.streamManager.remote) { - // Init subscriber events - this.eventCollection = { - videoElementCreated: true, - videoElementDestroyed: true, - streamPlaying: true, - streamAudioVolumeChange: false, - streamPropertyChanged: false - }; - this.updateSubscriberEvents({ - videoElementCreated: false, - videoElementDestroyed: false, - streamPlaying: false, - streamAudioVolumeChange: true, - streamPropertyChanged: true - }); + ngOnInit() { - } else { - // Init publisher events - this.eventCollection = { - videoElementCreated: true, - videoElementDestroyed: true, - streamPlaying: true, - streamAudioVolumeChange: false, - accessAllowed: true, - accessDenied: true, - accessDialogOpened: true, - accessDialogClosed: true, - streamCreated: true, - streamDestroyed: true, - streamPropertyChanged: false - }; - this.updatePublisherEvents( - this.streamManager, - { - videoElementCreated: false, - videoElementDestroyed: false, - streamPlaying: false, - streamAudioVolumeChange: true, - accessAllowed: false, - accessDenied: false, - accessDialogOpened: false, - accessDialogClosed: false, - streamCreated: false, - streamDestroyed: false, - streamPropertyChanged: true - }); - this.sendAudio = this.streamManager.stream.hasAudio; - this.sendVideo = this.streamManager.stream.hasVideo; - this.audioMuted = !this.properties.publishAudio; - this.videoMuted = !this.properties.publishVideo; - this.pubSubAudioIcon = this.audioMuted ? 'mic_off' : 'mic'; - this.pubSubVideoIcon = this.videoMuted ? 'videocam_off' : 'videocam'; - this.optionsVideo = this.streamManager.stream.typeOfVideo; - } + if (this.streamManager.remote) { + // Init subscriber events + this.eventCollection = { + videoElementCreated: true, + videoElementDestroyed: true, + streamPlaying: true, + streamAudioVolumeChange: false, + streamPropertyChanged: false + }; + this.updateSubscriberEvents({ + videoElementCreated: false, + videoElementDestroyed: false, + streamPlaying: false, + streamAudioVolumeChange: true, + streamPropertyChanged: true + }); - this.muteSubscribersSubscription = this.muteSubscribersService.mutedEvent$.subscribe(muteOrUnmute => { - if (this.streamManager.remote) { - this.streamManager.videos.forEach(v => { - v.video.muted = muteOrUnmute; - }); - } - }); - } + } else { + // Init publisher events + this.eventCollection = { + videoElementCreated: true, + videoElementDestroyed: true, + streamPlaying: true, + streamAudioVolumeChange: false, + accessAllowed: true, + accessDenied: true, + accessDialogOpened: true, + accessDialogClosed: true, + streamCreated: true, + streamDestroyed: true, + streamPropertyChanged: false + }; + this.updatePublisherEvents( + this.streamManager, + { + videoElementCreated: false, + videoElementDestroyed: false, + streamPlaying: false, + streamAudioVolumeChange: true, + accessAllowed: false, + accessDenied: false, + accessDialogOpened: false, + accessDialogClosed: false, + streamCreated: false, + streamDestroyed: false, + streamPropertyChanged: true + }); + this.sendAudio = this.streamManager.stream.hasAudio; + this.sendVideo = this.streamManager.stream.hasVideo; + this.audioMuted = !this.properties.publishAudio; + this.videoMuted = !this.properties.publishVideo; + this.pubSubAudioIcon = this.audioMuted ? 'mic_off' : 'mic'; + this.pubSubVideoIcon = this.videoMuted ? 'videocam_off' : 'videocam'; + this.optionsVideo = this.streamManager.stream.typeOfVideo; + } - ngOnDestroy() { - if (!!this.recorder) { - this.recorder.clean(); - } - if (!!this.muteSubscribersSubscription) { this.muteSubscribersSubscription.unsubscribe(); } - } + this.muteSubscribersSubscription = this.muteSubscribersService.mutedEvent$.subscribe(muteOrUnmute => { + if (this.streamManager.remote) { + this.streamManager.videos.forEach(v => { + v.video.muted = muteOrUnmute; + }); + } + }); + } - subUnsub() { - const subscriber: Subscriber = this.streamManager; - if (this.subbed) { - this.streamManager.stream.session.unsubscribe(subscriber); - this.restartRecorder(); + ngOnDestroy() { + if (!!this.recorder) { + this.recorder.clean(); + } + if (!!this.muteSubscribersSubscription) { this.muteSubscribersSubscription.unsubscribe(); } + } - this.pubSubVideoIcon = ''; - this.pubSubAudioIcon = ''; - this.recordIcon = ''; - this.pauseRecordIcon = ''; - this.pubSubIcon = 'play_arrow'; - this.subbedVideo = false; - this.subbedAudio = false; - } else { - const oldValues = { - videoElementCreated: this.eventCollection.videoElementCreated, - videoElementDestroyed: this.eventCollection.videoElementDestroyed, - streamPlaying: this.eventCollection.streamPlaying, - streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, - streamPropertyChanged: this.eventCollection.streamPropertyChanged - }; - this.streamManager = this.streamManager.stream.session.subscribe(subscriber.stream, undefined); - this.reSubbed.emit(this.streamManager); + subUnsub() { + const subscriber: Subscriber = this.streamManager; + if (this.subbed) { + this.streamManager.stream.session.unsubscribe(subscriber); + this.restartRecorder(); - this.pubSubVideoIcon = 'videocam'; - this.pubSubAudioIcon = 'mic'; - this.recordIcon = 'fiber_manual_record'; - this.pauseRecordIcon = ''; - this.pubSubIcon = 'stop'; - this.subbedVideo = true; - this.subbedAudio = true; + this.pubSubVideoIcon = ''; + this.pubSubAudioIcon = ''; + this.recordIcon = ''; + this.pauseRecordIcon = ''; + this.pubSubIcon = 'play_arrow'; + this.subbedVideo = false; + this.subbedAudio = false; + } else { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying, + streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, + streamPropertyChanged: this.eventCollection.streamPropertyChanged + }; + this.streamManager = this.streamManager.stream.session.subscribe(subscriber.stream, undefined); + this.reSubbed.emit(this.streamManager); - this.updateSubscriberEvents(oldValues); - } - this.subbed = !this.subbed; - } + this.pubSubVideoIcon = 'videocam'; + this.pubSubAudioIcon = 'mic'; + this.recordIcon = 'fiber_manual_record'; + this.pauseRecordIcon = ''; + this.pubSubIcon = 'stop'; + this.subbedVideo = true; + this.subbedAudio = true; - subUnsubVideo(connectionId: string) { - const subscriber: Subscriber = this.streamManager; - this.subbedVideo = !this.subbedVideo; - subscriber.subscribeToVideo(this.subbedVideo); - this.pubSubVideoIcon = this.subbedVideo ? 'videocam' : 'videocam_off'; - } + this.updateSubscriberEvents(oldValues); + } + this.subbed = !this.subbed; + } - subUnsubAudio(connectionId: string) { - const subscriber: Subscriber = this.streamManager; - this.subbedAudio = !this.subbedAudio; - subscriber.subscribeToAudio(this.subbedAudio); - this.pubSubAudioIcon = this.subbedAudio ? 'mic' : 'mic_off'; - } + subUnsubVideo(connectionId: string) { + const subscriber: Subscriber = this.streamManager; + this.subbedVideo = !this.subbedVideo; + subscriber.subscribeToVideo(this.subbedVideo); + this.pubSubVideoIcon = this.subbedVideo ? 'videocam' : 'videocam_off'; + } - pubUnpub() { - const publisher: Publisher = this.streamManager; - if (this.unpublished) { - this.streamManager.stream.session.publish(publisher) - .then(() => { - console.log(publisher); - }) - .catch(e => { - console.error(e); - }); - } else { - this.streamManager.stream.session.unpublish(publisher); - } - this.unpublished = !this.unpublished; - this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; - } + subUnsubAudio(connectionId: string) { + const subscriber: Subscriber = this.streamManager; + this.subbedAudio = !this.subbedAudio; + subscriber.subscribeToAudio(this.subbedAudio); + this.pubSubAudioIcon = this.subbedAudio ? 'mic' : 'mic_off'; + } - pubUnpubVideo() { - const publisher: Publisher = this.streamManager; - this.videoMuted = !this.videoMuted; - publisher.publishVideo(!this.videoMuted); - this.pubSubVideoIcon = this.videoMuted ? 'videocam_off' : 'videocam'; - } + pubUnpub() { + const publisher: Publisher = this.streamManager; + if (this.unpublished) { + this.streamManager.stream.session.publish(publisher) + .then(() => { + console.log(publisher); + }) + .catch(e => { + console.error(e); + }); + } else { + this.streamManager.stream.session.unpublish(publisher); + } + this.unpublished = !this.unpublished; + this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; + } - pubUnpubAudio() { - const publisher: Publisher = this.streamManager; - this.audioMuted = !this.audioMuted; - publisher.publishAudio(!this.audioMuted); - this.pubSubAudioIcon = this.audioMuted ? 'mic_off' : 'mic'; - } + pubUnpubVideo() { + const publisher: Publisher = this.streamManager; + this.videoMuted = !this.videoMuted; + publisher.publishVideo(!this.videoMuted); + this.pubSubVideoIcon = this.videoMuted ? 'videocam_off' : 'videocam'; + } - changePub() { - let screenChange; - if (!this.publisherChanged) { - if (this.sendAudio && !this.sendVideo) { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = false; - } else if (!this.sendAudio && this.sendVideo) { - this.sendAudioChange = true; - this.sendVideoChange = false; - } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'CAMERA') { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = true; - } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'SCREEN') { - this.sendAudioChange = false; - this.sendVideoChange = true; - screenChange = false; - } - } else { - this.sendAudioChange = this.sendAudio; - this.sendVideoChange = this.sendVideo; - screenChange = this.optionsVideo === 'SCREEN' ? true : false; - } + pubUnpubAudio() { + const publisher: Publisher = this.streamManager; + this.audioMuted = !this.audioMuted; + publisher.publishAudio(!this.audioMuted); + this.pubSubAudioIcon = this.audioMuted ? 'mic_off' : 'mic'; + } - this.unpublished = false; + changePub() { + let screenChange; + if (!this.publisherChanged) { + if (this.sendAudio && !this.sendVideo) { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = false; + } else if (!this.sendAudio && this.sendVideo) { + this.sendAudioChange = true; + this.sendVideoChange = false; + } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'CAMERA') { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = true; + } else if (this.sendAudio && this.sendVideo && this.optionsVideo === 'SCREEN') { + this.sendAudioChange = false; + this.sendVideoChange = true; + screenChange = false; + } + } else { + this.sendAudioChange = this.sendAudio; + this.sendVideoChange = this.sendVideo; + screenChange = this.optionsVideo === 'SCREEN' ? true : false; + } - const otherPublisher = this.OV.initPublisher( - undefined, - { - audioSource: this.sendAudioChange ? undefined : false, - videoSource: this.sendVideoChange ? (screenChange ? 'screen' : undefined) : false, - publishAudio: !this.audioMuted, - publishVideo: !this.videoMuted, - resolution: '640x480', - frameRate: 30, - insertMode: VideoInsertMode.APPEND - }, - (err) => { - if (err) { - console.warn(err); - if (err.name === 'SCREEN_EXTENSION_NOT_INSTALLED') { - this.dialog.open(ExtensionDialogComponent, { - data: { url: err.message }, - disableClose: true, - width: '250px' - }); - } - } - }); - this.updatePublisherEvents(otherPublisher, { - videoElementCreated: !this.eventCollection.videoElementCreated, - videoElementDestroyed: !this.eventCollection.videoElementDestroyed, - streamPlaying: !this.eventCollection.streamPlaying, - streamAudioVolumeChange: !this.eventCollection.streamAudioVolumeChange, - accessAllowed: !this.eventCollection.accessAllowed, - accessDenied: !this.eventCollection.accessDenied, - accessDialogOpened: !this.eventCollection.accessDialogOpened, - accessDialogClosed: !this.eventCollection.accessDialogClosed, - streamCreated: !this.eventCollection.streamCreated, - streamDestroyed: !this.eventCollection.streamDestroyed, - streamPropertyChanged: !this.eventCollection.streamPropertyChanged, - }); + this.unpublished = false; - const oldPublisher = this.streamManager; - if (oldPublisher.isSubscribedToRemote) { - otherPublisher.subscribeToRemote(true); - } + const otherPublisher = this.OV.initPublisher( + undefined, + { + audioSource: this.sendAudioChange ? undefined : false, + videoSource: this.sendVideoChange ? (screenChange ? 'screen' : undefined) : false, + publishAudio: !this.audioMuted, + publishVideo: !this.videoMuted, + resolution: '640x480', + frameRate: 30, + insertMode: VideoInsertMode.APPEND + }, + (err) => { + if (err) { + console.warn(err); + if (err.name === 'SCREEN_EXTENSION_NOT_INSTALLED') { + this.dialog.open(ExtensionDialogComponent, { + data: { url: err.message }, + disableClose: true, + width: '250px' + }); + } + } + }); + this.updatePublisherEvents(otherPublisher, { + videoElementCreated: !this.eventCollection.videoElementCreated, + videoElementDestroyed: !this.eventCollection.videoElementDestroyed, + streamPlaying: !this.eventCollection.streamPlaying, + streamAudioVolumeChange: !this.eventCollection.streamAudioVolumeChange, + accessAllowed: !this.eventCollection.accessAllowed, + accessDenied: !this.eventCollection.accessDenied, + accessDialogOpened: !this.eventCollection.accessDialogOpened, + accessDialogClosed: !this.eventCollection.accessDialogClosed, + streamCreated: !this.eventCollection.streamCreated, + streamDestroyed: !this.eventCollection.streamDestroyed, + streamPropertyChanged: !this.eventCollection.streamPropertyChanged, + }); - otherPublisher.once('accessAllowed', () => { - if (!this.unpublished) { - this.streamManager.stream.session.unpublish(oldPublisher); - } - this.streamManager.stream.session.publish(otherPublisher).then(() => { - this.streamManager = otherPublisher; - }); - }); + const oldPublisher = this.streamManager; + if (oldPublisher.isSubscribedToRemote) { + otherPublisher.subscribeToRemote(true); + } - this.publisherChanged = !this.publisherChanged; - } + otherPublisher.once('accessAllowed', () => { + if (!this.unpublished) { + this.streamManager.stream.session.unpublish(oldPublisher); + } + this.streamManager.stream.session.publish(otherPublisher).then(() => { + this.streamManager = otherPublisher; + }); + }); - updateSubscriberEvents(oldValues) { - const sub: Subscriber = this.streamManager; + this.publisherChanged = !this.publisherChanged; + } - if (this.eventCollection.videoElementCreated) { - if (!oldValues.videoElementCreated) { - sub.on('videoElementCreated', (event: VideoElementEvent) => { - this.updateEventListInParent.emit({ - eventName: 'videoElementCreated', - eventContent: event.element.id, - event - }); - }); - } - } else { - sub.off('videoElementCreated'); - } + updateSubscriberEvents(oldValues) { + const sub: Subscriber = this.streamManager; - if (this.eventCollection.videoElementDestroyed) { - if (!oldValues.videoElementDestroyed) { - sub.on('videoElementDestroyed', (event: VideoElementEvent) => { - this.showButtons = false; - this.updateEventListInParent.emit({ - eventName: 'videoElementDestroyed', - eventContent: event.element.id, - event - }); - }); - } - } else { - sub.off('videoElementDestroyed'); - } + if (this.eventCollection.videoElementCreated) { + if (!oldValues.videoElementCreated) { + sub.on('videoElementCreated', (event: VideoElementEvent) => { + this.updateEventListInParent.emit({ + eventName: 'videoElementCreated', + eventContent: event.element.id, + event + }); + }); + } + } else { + sub.off('videoElementCreated'); + } - if (this.eventCollection.streamPlaying) { - if (!oldValues.streamPlaying) { - sub.on('streamPlaying', (event: StreamManagerEvent) => { - if (!sub.stream.hasVideo) { - this.videoClasses = 'grey-background'; - this.videoPoster = 'assets/images/volume.png'; - } else { - this.videoClasses = ''; - this.videoPoster = ''; - } - this.showButtons = true; - this.updateEventListInParent.emit({ - eventName: 'streamPlaying', - eventContent: this.streamManager.stream.streamId, - event - }); - }); - } - } else { - sub.off('streamPlaying'); - } + if (this.eventCollection.videoElementDestroyed) { + if (!oldValues.videoElementDestroyed) { + sub.on('videoElementDestroyed', (event: VideoElementEvent) => { + this.showButtons = false; + this.updateEventListInParent.emit({ + eventName: 'videoElementDestroyed', + eventContent: event.element.id, + event + }); + }); + } + } else { + sub.off('videoElementDestroyed'); + } - if (this.eventCollection.streamAudioVolumeChange) { - if (!oldValues.streamAudioVolumeChange) { - sub.on('streamAudioVolumeChange', (event: StreamManagerEvent) => { - this.updateEventListInParent.emit({ - eventName: 'streamAudioVolumeChange', - eventContent: event.value['newValue'], - event - }); - }); - } - } else { - sub.off('streamAudioVolumeChange'); - } + if (this.eventCollection.streamPlaying) { + if (!oldValues.streamPlaying) { + sub.on('streamPlaying', (event: StreamManagerEvent) => { + if (!sub.stream.hasVideo) { + this.videoClasses = 'grey-background'; + this.videoPoster = 'assets/images/volume.png'; + } else { + this.videoClasses = ''; + this.videoPoster = ''; + } + this.showButtons = true; + this.updateEventListInParent.emit({ + eventName: 'streamPlaying', + eventContent: this.streamManager.stream.streamId, + event + }); + }); + } + } else { + sub.off('streamPlaying'); + } - if (this.eventCollection.streamPropertyChanged) { - if (!oldValues.streamPropertyChanged) { - sub.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => { - const newValue = event.changedProperty === 'videoDimensions' ? - JSON.stringify(event.newValue) : event.newValue.toString(); - this.updateEventListInParent.emit({ - eventName: 'streamPropertyChanged', - eventContent: event.changedProperty + ' [' + newValue + ']', - event - }); - }); - } - } else { - sub.off('streamPropertyChanged'); - } - } + if (this.eventCollection.streamAudioVolumeChange) { + if (!oldValues.streamAudioVolumeChange) { + sub.on('streamAudioVolumeChange', (event: StreamManagerEvent) => { + this.updateEventListInParent.emit({ + eventName: 'streamAudioVolumeChange', + eventContent: event.value['newValue'], + event + }); + }); + } + } else { + sub.off('streamAudioVolumeChange'); + } - updatePublisherEvents(pub: Publisher, oldValues: any) { - if (this.eventCollection.videoElementCreated) { - if (!oldValues.videoElementCreated) { - pub.on('videoElementCreated', (event: VideoElementEvent) => { - this.updateEventListInParent.emit({ - eventName: 'videoElementCreated', - eventContent: event.element.id, - event - }); - }); - } - } else { - pub.off('videoElementCreated'); - } + if (this.eventCollection.streamPropertyChanged) { + if (!oldValues.streamPropertyChanged) { + sub.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => { + const newValue = event.changedProperty === 'videoDimensions' ? + JSON.stringify(event.newValue) : event.newValue.toString(); + this.updateEventListInParent.emit({ + eventName: 'streamPropertyChanged', + eventContent: event.changedProperty + ' [' + newValue + ']', + event + }); + }); + } + } else { + sub.off('streamPropertyChanged'); + } + } - if (this.eventCollection.accessAllowed) { - if (!oldValues.accessAllowed) { - pub.on('accessAllowed', () => { - this.updateEventListInParent.emit({ - eventName: 'accessAllowed', - eventContent: '', - event: { - type: 'accessAllowed', - target: pub, - cancelable: false, - hasBeenPrevented: false, - isDefaultPrevented: () => false, - preventDefault: () => { }, - callDefaultBehavior: () => { } - } - }); - }); - } - } else { - pub.off('accessAllowed'); - } + updatePublisherEvents(pub: Publisher, oldValues: any) { + if (this.eventCollection.videoElementCreated) { + if (!oldValues.videoElementCreated) { + pub.on('videoElementCreated', (event: VideoElementEvent) => { + this.updateEventListInParent.emit({ + eventName: 'videoElementCreated', + eventContent: event.element.id, + event + }); + }); + } + } else { + pub.off('videoElementCreated'); + } - if (this.eventCollection.accessDenied) { - if (!oldValues.accessDenied) { - pub.on('accessDenied', (error) => { - this.updateEventListInParent.emit({ - eventName: 'accessDenied', - eventContent: JSON.stringify(error), - event: { - type: 'accessDenied', - target: pub, - cancelable: false, - hasBeenPrevented: false, - isDefaultPrevented: () => false, - preventDefault: () => { }, - callDefaultBehavior: () => { } - } - }); - }); - } - } else { - pub.off('accessDenied'); - } + if (this.eventCollection.accessAllowed) { + if (!oldValues.accessAllowed) { + pub.on('accessAllowed', () => { + this.updateEventListInParent.emit({ + eventName: 'accessAllowed', + eventContent: '', + event: { + type: 'accessAllowed', + target: pub, + cancelable: false, + hasBeenPrevented: false, + isDefaultPrevented: () => false, + preventDefault: () => { }, + callDefaultBehavior: () => { } + } + }); + }); + } + } else { + pub.off('accessAllowed'); + } - if (this.eventCollection.accessDialogOpened) { - if (!oldValues.accessDialogOpened) { - pub.on('accessDialogOpened', (e) => { - this.updateEventListInParent.emit({ - eventName: 'accessDialogOpened', - eventContent: '', - event: { - type: 'accessDialogOpened', - target: pub, - cancelable: false, - hasBeenPrevented: false, - isDefaultPrevented: () => false, - preventDefault: () => { }, - callDefaultBehavior: () => { } - } - }); - }); - } - } else { - pub.off('accessDialogOpened'); - } + if (this.eventCollection.accessDenied) { + if (!oldValues.accessDenied) { + pub.on('accessDenied', (error) => { + this.updateEventListInParent.emit({ + eventName: 'accessDenied', + eventContent: JSON.stringify(error), + event: { + type: 'accessDenied', + target: pub, + cancelable: false, + hasBeenPrevented: false, + isDefaultPrevented: () => false, + preventDefault: () => { }, + callDefaultBehavior: () => { } + } + }); + }); + } + } else { + pub.off('accessDenied'); + } - if (this.eventCollection.accessDialogClosed) { - if (!oldValues.accessDialogClosed) { - pub.on('accessDialogClosed', (e) => { - this.updateEventListInParent.emit({ - eventName: 'accessDialogClosed', - eventContent: '', - event: { - type: 'accessDialogClosed', - target: pub, - cancelable: false, - hasBeenPrevented: false, - isDefaultPrevented: () => false, - preventDefault: () => { }, - callDefaultBehavior: () => { } - } - }); - }); - } - } else { - pub.off('accessDialogClosed'); - } + if (this.eventCollection.accessDialogOpened) { + if (!oldValues.accessDialogOpened) { + pub.on('accessDialogOpened', (e) => { + this.updateEventListInParent.emit({ + eventName: 'accessDialogOpened', + eventContent: '', + event: { + type: 'accessDialogOpened', + target: pub, + cancelable: false, + hasBeenPrevented: false, + isDefaultPrevented: () => false, + preventDefault: () => { }, + callDefaultBehavior: () => { } + } + }); + }); + } + } else { + pub.off('accessDialogOpened'); + } - if (this.eventCollection.streamCreated) { - if (!oldValues.streamCreated) { - pub.on('streamCreated', (event: StreamEvent) => { - this.updateEventListInParent.emit({ - eventName: 'streamCreated', - eventContent: event.stream.streamId, - event - }); - }); - } - } else { - pub.off('streamCreated'); - } + if (this.eventCollection.accessDialogClosed) { + if (!oldValues.accessDialogClosed) { + pub.on('accessDialogClosed', (e) => { + this.updateEventListInParent.emit({ + eventName: 'accessDialogClosed', + eventContent: '', + event: { + type: 'accessDialogClosed', + target: pub, + cancelable: false, + hasBeenPrevented: false, + isDefaultPrevented: () => false, + preventDefault: () => { }, + callDefaultBehavior: () => { } + } + }); + }); + } + } else { + pub.off('accessDialogClosed'); + } - if (this.eventCollection.streamDestroyed) { - if (!oldValues.streamDestroyed) { - pub.on('streamDestroyed', (event: StreamEvent) => { - this.updateEventListInParent.emit({ - eventName: 'streamDestroyed', - eventContent: event.stream.streamId, - event - }); - if (event.reason.indexOf('forceUnpublish') !== -1) { - this.unpublished = !this.unpublished; - this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; - } - }); - } - } else { - pub.off('streamDestroyed'); - } + if (this.eventCollection.streamCreated) { + if (!oldValues.streamCreated) { + pub.on('streamCreated', (event: StreamEvent) => { + this.updateEventListInParent.emit({ + eventName: 'streamCreated', + eventContent: event.stream.streamId, + event + }); + }); + } + } else { + pub.off('streamCreated'); + } - if (this.eventCollection.streamPropertyChanged) { - if (!oldValues.streamPropertyChanged) { - pub.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => { - const newValue = event.changedProperty === 'videoDimensions' ? - JSON.stringify(event.newValue) : event.newValue.toString(); - this.updateEventListInParent.emit({ - eventName: 'streamPropertyChanged', - eventContent: event.changedProperty + ' [' + newValue + ']', - event - }); - }); - } - } else { - pub.off('streamPropertyChanged'); - } + if (this.eventCollection.streamDestroyed) { + if (!oldValues.streamDestroyed) { + pub.on('streamDestroyed', (event: StreamEvent) => { + this.updateEventListInParent.emit({ + eventName: 'streamDestroyed', + eventContent: event.stream.streamId, + event + }); + if (event.reason.indexOf('forceUnpublish') !== -1) { + this.unpublished = !this.unpublished; + this.unpublished ? this.pubSubIcon = 'play_arrow' : this.pubSubIcon = 'stop'; + } + }); + } + } else { + pub.off('streamDestroyed'); + } - if (this.eventCollection.videoElementDestroyed) { - if (!oldValues.videoElementDestroyed) { - pub.on('videoElementDestroyed', (event: VideoElementEvent) => { - this.updateEventListInParent.emit({ - eventName: 'videoElementDestroyed', - eventContent: '(Publisher)', - event - }); - }); - } - } else { - pub.off('videoElementDestroyed'); - } + if (this.eventCollection.streamPropertyChanged) { + if (!oldValues.streamPropertyChanged) { + pub.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => { + const newValue = event.changedProperty === 'videoDimensions' ? + JSON.stringify(event.newValue) : event.newValue.toString(); + this.updateEventListInParent.emit({ + eventName: 'streamPropertyChanged', + eventContent: event.changedProperty + ' [' + newValue + ']', + event + }); + }); + } + } else { + pub.off('streamPropertyChanged'); + } - if (this.eventCollection.streamPlaying) { - if (!oldValues.streamPlaying) { - pub.on('streamPlaying', (event: StreamManagerEvent) => { - if (!pub.stream.hasVideo) { - this.videoClasses = 'grey-background'; - this.videoPoster = 'assets/images/volume.png'; - } else { - this.videoClasses = ''; - this.videoPoster = ''; - } - this.showButtons = true; - this.updateEventListInParent.emit({ - eventName: 'streamPlaying', - eventContent: pub.stream.streamId, - event - }); - }); - } - } else { - pub.off('streamPlaying'); - } + if (this.eventCollection.videoElementDestroyed) { + if (!oldValues.videoElementDestroyed) { + pub.on('videoElementDestroyed', (event: VideoElementEvent) => { + this.updateEventListInParent.emit({ + eventName: 'videoElementDestroyed', + eventContent: '(Publisher)', + event + }); + }); + } + } else { + pub.off('videoElementDestroyed'); + } - if (this.eventCollection.streamAudioVolumeChange) { - if (!oldValues.streamAudioVolumeChange) { - pub.on('streamAudioVolumeChange', (event: StreamManagerEvent) => { - this.updateEventListInParent.emit({ - eventName: 'streamAudioVolumeChange', - eventContent: event.value['newValue'], - event - }); - }); - } - } else { - pub.off('streamAudioVolumeChange'); - } - } + if (this.eventCollection.streamPlaying) { + if (!oldValues.streamPlaying) { + pub.on('streamPlaying', (event: StreamManagerEvent) => { + if (!pub.stream.hasVideo) { + this.videoClasses = 'grey-background'; + this.videoPoster = 'assets/images/volume.png'; + } else { + this.videoClasses = ''; + this.videoPoster = ''; + } + this.showButtons = true; + this.updateEventListInParent.emit({ + eventName: 'streamPlaying', + eventContent: pub.stream.streamId, + event + }); + }); + } + } else { + pub.off('streamPlaying'); + } - openSubscriberEventsDialog() { - const oldValues = { - videoElementCreated: this.eventCollection.videoElementCreated, - videoElementDestroyed: this.eventCollection.videoElementDestroyed, - streamPlaying: this.eventCollection.streamPlaying, - streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, - streamPropertyChanged: this.eventCollection.streamPropertyChanged - }; - const dialogRef = this.dialog.open(EventsDialogComponent, { - data: { - eventCollection: this.eventCollection, - target: 'Subscriber' - }, - width: '295px', - autoFocus: false, - disableClose: true - }); - dialogRef.afterClosed().subscribe((result) => { - this.updateSubscriberEvents(oldValues); - }); - } + if (this.eventCollection.streamAudioVolumeChange) { + if (!oldValues.streamAudioVolumeChange) { + pub.on('streamAudioVolumeChange', (event: StreamManagerEvent) => { + this.updateEventListInParent.emit({ + eventName: 'streamAudioVolumeChange', + eventContent: event.value['newValue'], + event + }); + }); + } + } else { + pub.off('streamAudioVolumeChange'); + } + } - openPublisherEventsDialog() { - const oldValues = { - videoElementCreated: this.eventCollection.videoElementCreated, - videoElementDestroyed: this.eventCollection.videoElementDestroyed, - streamPlaying: this.eventCollection.streamPlaying, - streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, - accessAllowed: this.eventCollection.accessAllowed, - accessDenied: this.eventCollection.accessDenied, - accessDialogOpened: this.eventCollection.accessDialogOpened, - accessDialogClosed: this.eventCollection.accessDialogClosed, - streamCreated: this.eventCollection.streamCreated, - streamDestroyed: this.eventCollection.streamDestroyed, - streamPropertyChanged: this.eventCollection.streamPropertyChanged - }; - const dialogRef = this.dialog.open(EventsDialogComponent, { - data: { - eventCollection: this.eventCollection, - target: 'Publisher' - }, - width: '295px', - autoFocus: false, - disableClose: true - }); - dialogRef.afterClosed().subscribe((result) => { - this.updatePublisherEvents(this.streamManager, oldValues); - }); - } + openSubscriberEventsDialog() { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying, + streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, + streamPropertyChanged: this.eventCollection.streamPropertyChanged + }; + const dialogRef = this.dialog.open(EventsDialogComponent, { + data: { + eventCollection: this.eventCollection, + target: 'Subscriber' + }, + width: '295px', + autoFocus: false, + disableClose: true + }); + dialogRef.afterClosed().subscribe((result) => { + this.updateSubscriberEvents(oldValues); + }); + } - record(): void { - if (!this.recording) { - this.recorder = this.OV.initLocalRecorder(this.streamManager.stream); - this.recorder.record(); - this.recording = true; - this.recordIcon = 'stop'; - this.pauseRecordIcon = 'pause'; - } else { - this.recorder.stop() - .then(() => { - let dialogRef: MatDialogRef; - dialogRef = this.dialog.open(LocalRecordingDialogComponent, { - disableClose: true, - data: { - recorder: this.recorder - } - }); - dialogRef.componentInstance.myReference = dialogRef; + openPublisherEventsDialog() { + const oldValues = { + videoElementCreated: this.eventCollection.videoElementCreated, + videoElementDestroyed: this.eventCollection.videoElementDestroyed, + streamPlaying: this.eventCollection.streamPlaying, + streamAudioVolumeChange: this.eventCollection.streamAudioVolumeChange, + accessAllowed: this.eventCollection.accessAllowed, + accessDenied: this.eventCollection.accessDenied, + accessDialogOpened: this.eventCollection.accessDialogOpened, + accessDialogClosed: this.eventCollection.accessDialogClosed, + streamCreated: this.eventCollection.streamCreated, + streamDestroyed: this.eventCollection.streamDestroyed, + streamPropertyChanged: this.eventCollection.streamPropertyChanged + }; + const dialogRef = this.dialog.open(EventsDialogComponent, { + data: { + eventCollection: this.eventCollection, + target: 'Publisher' + }, + width: '295px', + autoFocus: false, + disableClose: true + }); + dialogRef.afterClosed().subscribe((result) => { + this.updatePublisherEvents(this.streamManager, oldValues); + }); + } - dialogRef.afterOpen().subscribe(() => { - this.muteSubscribersService.updateMuted(true); - this.recorder.preview('recorder-preview').controls = true; - }); - dialogRef.afterClosed().subscribe(() => { - this.muteSubscribersService.updateMuted(false); - this.restartRecorder(); - }); - }) - .catch((error) => { - console.error('Error stopping LocalRecorder: ' + error); - }); - } - } + record(): void { + if (!this.recording) { + this.recorder = this.OV.initLocalRecorder(this.streamManager.stream); + this.recorder.record(); + this.recording = true; + this.recordIcon = 'stop'; + this.pauseRecordIcon = 'pause'; + } else { + this.recorder.stop() + .then(() => { + let dialogRef: MatDialogRef; + dialogRef = this.dialog.open(LocalRecordingDialogComponent, { + disableClose: true, + data: { + recorder: this.recorder + } + }); + dialogRef.componentInstance.myReference = dialogRef; - pauseRecord(): void { - if (!this.recordingPaused) { - this.recorder.pause(); - this.pauseRecordIcon = 'play_arrow'; - } else { - this.recorder.resume(); - this.pauseRecordIcon = 'pause'; - } - this.recordingPaused = !this.recordingPaused; - } + dialogRef.afterOpen().subscribe(() => { + this.muteSubscribersService.updateMuted(true); + this.recorder.preview('recorder-preview').controls = true; + }); + dialogRef.afterClosed().subscribe(() => { + this.muteSubscribersService.updateMuted(false); + this.restartRecorder(); + }); + }) + .catch((error) => { + console.error('Error stopping LocalRecorder: ' + error); + }); + } + } - private restartRecorder() { - this.recording = false; - this.recordingPaused = false; - this.recordIcon = 'fiber_manual_record'; - this.pauseRecordIcon = ''; - if (!!this.recorder) { - this.recorder.clean(); - } - } + pauseRecord(): void { + if (!this.recordingPaused) { + this.recorder.pause(); + this.pauseRecordIcon = 'play_arrow'; + } else { + this.recorder.resume(); + this.pauseRecordIcon = 'pause'; + } + this.recordingPaused = !this.recordingPaused; + } - forceUnpublish() { - this.OV.session.forceUnpublish(this.streamManager.stream); - } + private restartRecorder() { + this.recording = false; + this.recordingPaused = false; + this.recordIcon = 'fiber_manual_record'; + this.pauseRecordIcon = ''; + if (!!this.recorder) { + this.recorder.clean(); + } + } - forceDisconnect() { - this.OV.session.forceDisconnect(this.streamManager.stream.connection); - } + forceUnpublish() { + this.OV.session.forceUnpublish(this.streamManager.stream); + } - filterConfig() { - this.dialog.open(FilterDialogComponent, { - data: { - session: this.streamManager.stream.session, - stream: this.streamManager.stream, - filterEventHandler: this.emitFilterEventToParent.bind(this) - }, - disableClose: true, - width: '450px' - }); - } + forceDisconnect() { + this.OV.session.forceDisconnect(this.streamManager.stream.connection); + } - emitFilterEventToParent(event: FilterEvent) { - this.updateEventListInParent.emit({ - eventName: 'filterEvent', - eventContent: JSON.stringify(event.data), - event - }); - } + filterConfig() { + this.dialog.open(FilterDialogComponent, { + data: { + session: this.streamManager.stream.session, + stream: this.streamManager.stream, + filterEventHandler: this.emitFilterEventToParent.bind(this) + }, + disableClose: true, + width: '450px' + }); + } + emitFilterEventToParent(event: FilterEvent) { + this.updateEventListInParent.emit({ + eventName: 'filterEvent', + eventContent: JSON.stringify(event.data), + event + }); + } + + async showCodecUsed() { + let stats = await this.streamManager.stream.getWebRtcPeer().pc.getStats(null); + let codecIdIndex = null; + // Search codec Index + stats.forEach(report => { + console.log(report); + if (!this.streamManager.remote && report.id.includes("RTCOutboundRTPVideoStream")) { + codecIdIndex = report.codecId; + + } else if (this.streamManager.remote && report.id.includes("RTCInboundRTPVideoStream")) { + codecIdIndex = report.codecId; + } + }) + // Search codec Info + stats.forEach(report => { + if (report.id === codecIdIndex) { + this.usedVideoCodec = report.mimeType; + } + }) + this.dialog.open(ShowCodecDialogComponent, { + data: { + usedVideoCodec: this.usedVideoCodec + }, + width: '450px' + }); + } }