Force codec parameters to avoid transcoding

pull/567/head
cruizba 2020-11-19 17:06:50 +01:00
parent f09b5c97f3
commit 7a25233b8b
37 changed files with 1946 additions and 653 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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 <code>false</code> 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;
}

View File

@ -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
}

View File

@ -106,8 +106,8 @@ export class OpenVidu {
public createSession(properties?: SessionProperties): Promise<Session> {
return new Promise<Session>((resolve, reject) => {
const session = new Session(this, properties);
session.getSessionIdHttp()
.then(sessionId => {
session.getSessionHttp()
.then(response => {
this.activeSessions.push(session);
resolve(session);
})

View File

@ -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<string> {
public getSessionHttp(): Promise<string> {
return new Promise<string>((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;
}

View File

@ -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 <code>false</code> 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;
}

View File

@ -0,0 +1,11 @@
/**
* See [[SessionProperties.forcedVideoCodec]]
*/
export enum VideoCodec {
VP8 = 'VP8',
VP9 = 'VP9',
H264 = 'H264',
NONE = 'NONE'
}

View File

@ -12,3 +12,4 @@ export * from './Recording';
export * from './RecordingProperties';
export * from './Connection';
export * from './Publisher';
export * from './VideoCodec';

View File

@ -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

View File

@ -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

View File

@ -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() {

View File

@ -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();

View File

@ -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() {

View File

@ -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;
}

View File

@ -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<Participant>(), 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,

View File

@ -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());

View File

@ -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 "

View File

@ -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<VideoCodec> 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<String> codecPts = new ArrayList<String>();
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<String> lineParts = new ArrayList<String>(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;
}
}
}

View File

@ -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

View File

@ -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<String> h264codecPayloads;
List<String> 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<String> 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<String> 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);
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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

View File

@ -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");

View File

@ -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);

View File

@ -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<WebElement> 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);
}
}

View File

@ -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]
})

View File

@ -38,6 +38,10 @@ mat-radio-button:first-child {
padding-bottom: 15px;
}
#allow-transcoding-div {
margin-bottom: 10px;
}
#record-div {
padding-bottom: 20px;
}

View File

@ -39,6 +39,18 @@
<input matInput placeholder="DefaultCustomLayout" type="text"
[(ngModel)]="sessionProperties.defaultCustomLayout" id="default-custom-layout-input">
</mat-form-field>
<div id="allow-transcoding-div">
<mat-checkbox class="checkbox-form" [(ngModel)]="sessionProperties.allowTranscoding"
id="allow-transcoding-checkbox">Allow Transcoding</mat-checkbox>
</div>
<mat-form-field>
<mat-select placeholder="ForcedVideoCodec" [(ngModel)]="sessionProperties.forcedVideoCodec"
id="forced-video-codec-select">
<mat-option *ngFor="let enumerator of enumToArray(forceVideoCodec)" [value]="enumerator">
<span [attr.id]="'option-' + enumerator">{{ enumerator }}</span>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field>
<input matInput placeholder="CustomSessionId" type="text" [(ngModel)]="sessionProperties.customSessionId">
</mat-form-field>

View File

@ -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[] = [];

View File

@ -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: `
<div id="app-codec-dialog-container">
<h2 id="codec-used-title-dialog" mat-dialog-title>Used Codec: <span id="video-codec-used">{{usedVideoCodec}}</span></h2>
<button mat-button id="close-dialog-btn" [mat-dialog-close]="{}">CLOSE</button>
</div>
`,
styles: [`
#app-codec-dialog-container {
text-align: center
}
`]
})
export class ShowCodecDialogComponent {
usedVideoCodec;
constructor(public dialogRef: MatDialogRef<ShowCodecDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data) {
this.usedVideoCodec = data.usedVideoCodec;
}
}

View File

@ -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);
if (!error.name) {
alert(error);
} else {
alert(error.name + ": " + error.message);
}
this.republishPossible = true;
this.session.unpublish(this.publisher);
delete this.publisher;

View File

@ -5,6 +5,9 @@
<button class="video-btn events-btn bottom-left-rounded" title="Publisher events" (click)="openPublisherEventsDialog()">
<mat-icon aria-label="Publisher events" class="mat-icon material-icons" role="img" aria-hidden="true">notifications</mat-icon>
</button>
<button class="video-btn stats-button bottom-left-rounded" title="Peer Connection Stats" (click)="showCodecUsed()">
<mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button>
</div>
<div class="bottom-div">
<button class="video-btn pub-btn" title="Publish/Unpublish" (click)="pubUnpub()">
@ -41,6 +44,9 @@
<button *ngIf="subbed" class="video-btn events-btn bottom-left-rounded" title="Subscriber events" (click)="openSubscriberEventsDialog()">
<mat-icon aria-label="Subscriber events" class="mat-icon material-icons" role="img" aria-hidden="true">notifications</mat-icon>
</button>
<button class="video-btn stats-button bottom-left-rounded" title="Peer Connection Stats" (click)="showCodecUsed()">
<mat-icon aria-label="Peer Connection Stats" class="mat-icon material-icons" role="img" aria-hidden="true">info</mat-icon>
</button>
</div>
<div class="bottom-div">
<button class="video-btn sub-btn" title="Subscribe/Unsubscribe" (click)="subUnsub()">

View File

@ -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,8 +68,10 @@ export class VideoComponent implements OnInit, OnDestroy {
recordIcon = 'fiber_manual_record';
pauseRecordIcon = '';
constructor(private dialog: MatDialog, private muteSubscribersService: MuteSubscribersService
) { }
// Stats
usedVideoCodec: string;
constructor(private dialog: MatDialog, private muteSubscribersService: MuteSubscribersService) { }
ngOnInit() {
@ -727,4 +730,30 @@ export class VideoComponent implements OnInit, OnDestroy {
});
}
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'
});
}
}