openvidu-recording tutorials: HTML update. e2e test for recording-java

This commit is contained in:
pabloFuente 2019-02-07 16:06:45 +01:00
parent ba5602f370
commit b28ab51c7d
14 changed files with 877 additions and 256 deletions

View File

@ -22,3 +22,4 @@
hs_err_pid*
target/
.vscode/*

View File

@ -1,4 +1,5 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -13,7 +14,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.1.RELEASE</version>
<version>2.1.2.RELEASE</version>
</parent>
<properties>
@ -21,6 +22,11 @@
<java.version>1.8</java.version>
<start-class>io.openvidu.recording.java.App</start-class>
<docker.image.prefix>openvidu</docker.image.prefix>
<!-- Test dependencies versions -->
<junit.jupiter.version>5.0.3</junit.jupiter.version>
<junit.platform.version>1.3.2</junit.platform.version>
<selenium-jupiter.version>3.1.0</selenium-jupiter.version>
</properties>
<build>
@ -36,7 +42,7 @@
<dependency>
<groupId>org.springframework</groupId>
<artifactId>springloaded</artifactId>
<version>1.2.6.RELEASE</version>
<version>2.1.2.RELEASE</version>
</dependency>
</dependencies>
</plugin>
@ -48,35 +54,11 @@
<mainClass>${start-class}</mainClass>
</configuration>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>0.2.3</version>
<configuration>
<imageName>${docker.image.prefix}/${project.artifactId}</imageName>
<dockerDirectory>src/main/docker</dockerDirectory>
<resources>
<resource>
<targetPath>/</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.war</include>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@ -86,17 +68,59 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
</dependency>
<dependency>
<groupId>io.openvidu</groupId>
<artifactId>openvidu-java-client</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>selenium-jupiter</artifactId>
<version>${selenium-jupiter.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
<scope>test</scope>
</dependency>
<!--<dependency> <groupId>com.googlecode.mp4parser</groupId> <artifactId>isoparser</artifactId>
<version>1.1.22</version> <scope>test</scope> </dependency> -->
<!--<dependency> <groupId>net.bramp.ffmpeg</groupId> <artifactId>ffmpeg</artifactId>
<version>0.6.2</version> </dependency> -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>2.4.5</version>
</dependency>
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>2.4.5</version>
</dependency>
</dependencies>
</project>

View File

@ -1,6 +1,5 @@
package io.openvidu.recording.java;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -92,37 +91,43 @@ public class MyRestController {
// Return the response to the client
return new ResponseEntity<>(responseJson, HttpStatus.OK);
} catch (Exception e) {
// If error generate an error message and return it to client
return getErrorResponse(e);
} catch (OpenViduJavaClientException e1) {
// If internal error generate an error message and return it to client
return getErrorResponse(e1);
} catch (OpenViduHttpException e2) {
if (404 == e2.getStatus()) {
// Invalid sessionId (user left unexpectedly). Session object is not valid
// anymore. Clean collections and continue as new session
this.mapSessions.remove(sessionName);
this.mapSessionNamesTokens.remove(sessionName);
}
}
}
} else {
// New session
System.out.println("New session " + sessionName);
try {
// New session
System.out.println("New session " + sessionName);
try {
// Create a new OpenVidu Session
Session session = this.openVidu.createSession();// new
// SessionProperties.Builder().customSessionId("CUSTOMSESSIONID").defaultRecordingLayout(RecordingLayout.CUSTOM).defaultCustomLayout("CUSTOM/LAYOUT").recordingMode(RecordingMode.ALWAYS).build());
// Generate a new token with the recently created tokenOptions
String token = session.generateToken(tokenOptions);
// Create a new OpenVidu Session
Session session = this.openVidu.createSession();// new
// SessionProperties.Builder().customSessionId("CUSTOMSESSIONID").defaultRecordingLayout(RecordingLayout.CUSTOM).defaultCustomLayout("CUSTOM/LAYOUT").recordingMode(RecordingMode.ALWAYS).build());
// Generate a new token with the recently created tokenOptions
String token = session.generateToken(tokenOptions);
// Store the session and the token in our collections
this.mapSessions.put(sessionName, session);
this.mapSessionNamesTokens.put(sessionName, new ConcurrentHashMap<>());
this.mapSessionNamesTokens.get(sessionName).put(token, role);
// Store the session and the token in our collections
this.mapSessions.put(sessionName, session);
this.mapSessionNamesTokens.put(sessionName, new ConcurrentHashMap<>());
this.mapSessionNamesTokens.get(sessionName).put(token, role);
// Prepare the response with the sessionId and the token
responseJson.put(0, token);
// Prepare the response with the sessionId and the token
responseJson.put(0, token);
// Return the response to the client
return new ResponseEntity<>(responseJson, HttpStatus.OK);
// Return the response to the client
return new ResponseEntity<>(responseJson, HttpStatus.OK);
} catch (Exception e) {
// If error generate an error message and return it to client
return getErrorResponse(e);
}
} catch (Exception e) {
// If error generate an error message and return it to client
return getErrorResponse(e);
}
}

View File

@ -21,27 +21,48 @@ function joinSession() {
// --- 3) Specify the actions when events take place in the session ---
session.on('connectionCreated', event => {
pushEvent(event);
});
session.on('connectionDestroyed', event => {
pushEvent(event);
});
// On every new Stream received...
session.on('streamCreated', (event) => {
session.on('streamCreated', event => {
pushEvent(event);
// Subscribe to the Stream to receive it
// HTML video will be appended to element with 'video-container' id
var subscriber = session.subscribe(event.stream, 'video-container');
// When the HTML video has been appended to DOM...
subscriber.on('videoElementCreated', (event) => {
subscriber.on('videoElementCreated', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(1);
});
// When the HTML video has been appended to DOM...
subscriber.on('videoElementDestroyed', (event) => {
subscriber.on('videoElementDestroyed', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(-1);
});
// When the subscriber stream has started playing media...
subscriber.on('streamPlaying', event => {
pushEvent(event);
});
});
session.on('sessionDisconnected', (event) => {
session.on('streamDestroyed', event => {
pushEvent(event);
});
session.on('sessionDisconnected', event => {
pushEvent(event);
if (event.reason !== 'disconnect') {
removeUser();
}
@ -53,6 +74,14 @@ function joinSession() {
}
});
session.on('recordingStarted', event => {
pushEvent(event);
});
session.on('recordingStopped', event => {
pushEvent(event);
});
// --- 4) Connect to the session passing the retrieved token and some more data from
// the client (in this case a JSON with the nickname chosen by the user) ---
@ -70,28 +99,62 @@ function joinSession() {
var publisher = OV.initPublisher('video-container', {
audioSource: undefined, // The source of audio. If undefined default microphone
videoSource: undefined, // The source of video. If undefined default webcam
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
publishVideo: true, // Whether you want to start publishing with your video enabled or not
resolution: '640x480', // The resolution of your video
frameRate: 30, // The frame rate of your video
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
mirror: false // Whether to mirror your local video or not
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
publishVideo: true, // Whether you want to start publishing with your video enabled or not
resolution: '640x480', // The resolution of your video
frameRate: 30, // The frame rate of your video
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
mirror: false // Whether to mirror your local video or not
});
// --- 7) Specify the actions when events take place in our publisher ---
// When the publisher stream has started playing media...
publisher.on('accessAllowed', event => {
pushEvent({
type: 'accessAllowed'
});
});
publisher.on('accessDenied', event => {
pushEvent(event);
});
publisher.on('accessDialogOpened', event => {
pushEvent({
type: 'accessDialogOpened'
});
});
publisher.on('accessDialogClosed', event => {
pushEvent({
type: 'accessDialogClosed'
});
});
// When the publisher stream has started playing media...
publisher.on('streamCreated', event => {
pushEvent(event);
});
// When our HTML video has been added to DOM...
publisher.on('videoElementCreated', (event) => {
publisher.on('videoElementCreated', event => {
pushEvent(event);
updateNumVideos(1);
$(event.element).prop('muted', true); // Mute local video
});
// When the HTML video has been appended to DOM...
publisher.on('videoElementDestroyed', (event) => {
publisher.on('videoElementDestroyed', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(-1);
});
// When the publisher stream has started playing media...
publisher.on('streamPlaying', event => {
pushEvent(event);
});
// --- 8) Publish your stream ---
@ -128,8 +191,8 @@ function getToken(callback) {
sessionName: sessionName
},
'Request of TOKEN gone WRONG:',
(response) => {
token = response[0]; // Get token from response
res => {
token = res[0]; // Get token from response
console.warn('Request of TOKEN gone WELL (TOKEN:' + token + ')');
callback(token); // Continue the join operation
}
@ -144,7 +207,7 @@ function removeUser() {
token: token
},
'User couldn\'t be removed from session',
(response) => {
res => {
console.warn("You have been removed from session " + sessionName);
}
);
@ -157,7 +220,7 @@ function closeSession() {
sessionName: sessionName
},
'Session couldn\'t be closed',
(response) => {
res => {
console.warn("Session " + sessionName + " has been closed");
}
);
@ -170,9 +233,9 @@ function fetchInfo() {
sessionName: sessionName
},
'Session couldn\'t be fetched',
(response) => {
res => {
console.warn("Session info has been fetched");
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -182,9 +245,9 @@ function fetchAll() {
'GET',
'api/fetch-all', {},
'All session info couldn\'t be fetched',
(response) => {
res => {
console.warn("All session info has been fetched");
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -197,7 +260,7 @@ function forceDisconnect() {
connectionId: document.getElementById('forceValue').value
},
'Connection couldn\'t be closed',
(response) => {
res => {
console.warn("Connection has been closed");
}
);
@ -211,14 +274,14 @@ function forceUnpublish() {
streamId: document.getElementById('forceValue').value
},
'Stream couldn\'t be closed',
(response) => {
res => {
console.warn("Stream has been closed");
}
);
}
function httpRequest(method, url, body, errorMsg, callback) {
$('#text-area').text('');
$('#textarea-http').text('');
var http = new XMLHttpRequest();
http.open(method, url, true);
http.setRequestHeader('Content-type', 'application/json');
@ -236,16 +299,16 @@ function httpRequest(method, url, body, errorMsg, callback) {
} else {
console.warn(errorMsg + ' (' + http.status + ')');
console.warn(http.responseText);
$('#text-area').text(errorMsg + ": HTTP " + http.status + " (" + http.responseText + ")");
$('#textarea-http').text(errorMsg + ": HTTP " + http.status + " (" + http.responseText + ")");
}
}
}
}
function startRecording() {
var outputMode = document.querySelector('input[name="outputMode"]:checked').value;
var hasAudio = !!document.querySelector("#has-audio-checkbox:checked");
var hasVideo = !!document.querySelector("#has-video-checkbox:checked");
var outputMode = $('input[name=outputMode]:checked').val();
var hasAudio = $('#has-audio-checkbox').prop('checked');
var hasVideo = $('#has-video-checkbox').prop('checked');
httpRequest(
'POST',
'api/recording/start', {
@ -255,11 +318,11 @@ function startRecording() {
hasVideo: hasVideo
},
'Start recording WRONG',
(response) => {
console.log(response);
document.getElementById('forceRecordingId').value = response.id;
res => {
console.log(res);
document.getElementById('forceRecordingId').value = res.id;
checkBtnsRecordings();
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -272,9 +335,9 @@ function stopRecording() {
recording: forceRecordingId
},
'Stop recording WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -287,9 +350,9 @@ function deleteRecording() {
recording: forceRecordingId
},
'Delete recording WRONG',
() => {
res => {
console.log("DELETE ok");
$('#text-area').text("DELETE ok");
$('#textarea-http').text("DELETE ok");
}
);
}
@ -300,9 +363,9 @@ function getRecording() {
'GET',
'api/recording/get/' + forceRecordingId, {},
'Get recording WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -312,9 +375,9 @@ function listRecordings() {
'GET',
'api/recording/list', {},
'List recordings WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -325,6 +388,8 @@ function listRecordings() {
/* APPLICATION BROWSER METHODS */
events = '';
window.onbeforeunload = function () { // Gracefully leave session
if (session) {
removeUser();
@ -373,4 +438,18 @@ function checkBtnsRecordings() {
}
}
function pushEvent(event) {
events += (!events ? '' : '\n') + event.type;
$('#textarea-events').text(events);
}
function clearHttpTextarea() {
$('#textarea-http').text('');
}
function clearEventsTextarea() {
$('#textarea-events').text('');
events = '';
}
/* APPLICATION BROWSER METHODS */

View File

@ -60,7 +60,7 @@
<input class="form-control" type="text" id="sessionName" value="SessionA" required>
</p>
<p class="text-center">
<button class="btn btn-lg btn-success" onclick="joinSession()">Join!</button>
<button class="btn btn-lg btn-success" id="join-btn" onclick="joinSession()">Join!</button>
</p>
</form>
<hr>
@ -89,10 +89,10 @@
<input class="btn btn-md" type="button" id="buttonStartRecording" onmouseup="startRecording()" value="Start recording">
<form>
<label class="radio-inline">
<input type="radio" name="outputMode" value="COMPOSED" checked>COMPOSED
<input type="radio" name="outputMode" value="COMPOSED" id="radio-composed" checked>COMPOSED
</label>
<label class="radio-inline">
<input type="radio" name="outputMode" value="INDIVIDUAL">INDIVIDUAL
<input type="radio" name="outputMode" value="INDIVIDUAL" id="radio-individual">INDIVIDUAL
</label>
</form>
<form>
@ -115,7 +115,16 @@
disabled>
<input class="form-control" id="forceRecordingId" type="text" onkeyup="checkBtnsRecordings()">
</div>
<textarea id="text-area" readonly="true" class="form-control" name="comment">HTTP responses...</textarea>
<div class="textarea-container" id="textarea-http-container">
<button type="button" class="btn btn-outline-secondary" id="clear-http-btn" onclick="clearHttpTextarea()">Clear</button>
<span>HTTP responses</span>
<textarea id="textarea-http" readonly="true" class="form-control" name="textarea-http"></textarea>
</div>
<div class="textarea-container" id="textarea-events-container">
<button type="button" class="btn btn-outline-secondary" id="clear-events-btn" onclick="clearEventsTextarea()">Clear</button>
<span>OpenVidu events</span>
<textarea id="textarea-events" readonly="true" class="form-control" name="textarea-events"></textarea>
</div>
</div>
</div>
</div>

View File

@ -250,21 +250,21 @@ a:hover .demo-logo {
#video-container {
width: 100%;
max-height: 45%;
display: inline-block;
max-height: 42%;
display: block;
overflow: hidden;
}
#video-container video.two {
width: 50%;
max-width: 50%;
}
#video-container video.three {
width: 33.33%;
max-width: 33.33%;
}
#video-container video.four {
width: 25%;
max-width: 25%;
}
#video-container div {
@ -294,10 +294,10 @@ a:hover .demo-logo {
}
video {
width: 100%;
height: auto;
max-height: 100%;
object-fit: cover;
width: auto;
height: auto;
max-height: 100%;
object-fit: scale-down;
}
#session {
@ -353,11 +353,11 @@ table i {
}
#recording-btns {
display: flow-root;
padding-left: 15px;
padding-top: 20px;
width: 100%;
height: 40%;
display: inline-block;
padding-left: 15px;
padding-top: 20px;
width: 100%;
height: 40%;
}
#recording-btns .btns {
@ -371,19 +371,57 @@ table i {
#recording-btns .btns form {
display: inline;
margin-left: 5px;
}
#recording-btns #text-area {
display: inline;
width: 100%;
#recording-btns textarea {
height: 100%;
}
.textarea-container {
position: relative;
display: inline-block;
height: 74%;
margin-top: 20px;
resize: none;
}
#textarea-http-container {
width: 69%;
}
#textarea-events-container {
width: 29%;
}
.textarea-container button {
position: absolute;
top: 1px;
right: 1px;
z-index: 1;
}
.textarea-container span {
position: absolute;
bottom: 1px;
right: 1px;
padding: 3px;
border-bottom-right-radius: 4px;
z-index: 1;
color: #a5a5a5;
background-color: #ededee;
font-weight: 600;
}
.textarea-container textarea {
height: 100%;
resize: none;
}
.vertical-separator-bottom {
width: 2px;
height: 34px;
display: inline;
display: inline-block;
background-color: #cbcbcb;
margin: 0 8px 0 8px;
margin-bottom: -12px;

View File

@ -1,38 +0,0 @@
package io.openvidu.js.java.test;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
/**
* Unit test for simple App.
*/
public class AppTest
extends TestCase
{
/**
* Create the test case
*
* @param testName name of the test case
*/
public AppTest( String testName )
{
super( testName );
}
/**
* @return the suite of tests being tested
*/
public static Test suite()
{
return new TestSuite( AppTest.class );
}
/**
* Rigourous Test :-)
*/
public void testApp()
{
assertTrue( true );
}
}

View File

@ -0,0 +1,19 @@
package io.openvidu.recording.java.test;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import org.junit.Assert;
import org.junit.jupiter.api.Test;
/**
* Unit test for openvidu-recording-java
*/
@RunWith(JUnitPlatform.class)
public class AppTest {
@Test
void testApp() {
Assert.assertTrue(true);
}
}

View File

@ -0,0 +1,337 @@
/*
* (C) Copyright 2017-2019 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.recording.java.test;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.junit.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.bonigarcia.seljup.SeleniumExtension;
import io.github.bonigarcia.wdm.WebDriverManager;
import io.openvidu.java.client.OpenVidu;
import io.openvidu.java.client.OpenViduHttpException;
import io.openvidu.java.client.OpenViduJavaClientException;
import io.openvidu.java.client.Recording;
import io.openvidu.java.client.Recording.OutputMode;
import ws.schild.jave.EncoderException;
import ws.schild.jave.MultimediaInfo;
import ws.schild.jave.MultimediaObject;
/**
* E2E tests for openvidu-java-recording app
*
* mvn -Dtest=AppTestE2e -DAPP_URL=https://localhost:5000/
* -DOPENVIDU_URL=https://localhost:4443/ -DOPENVIDU_SECRET=MY_SECRET
* -DNUMBER_OF_ATTEMPTS=30 -DRECORDING_DURATION=5 -DDURATION_THRESHOLD=5 test
*
* @author Pablo Fuente (pablofuenteperez@gmail.com)
*/
@DisplayName("E2E tests for openvidu-java-recording")
@ExtendWith(SeleniumExtension.class)
@RunWith(JUnitPlatform.class)
public class AppTestE2e {
private static final Logger log = LoggerFactory.getLogger(AppTestE2e.class);
static String OPENVIDU_SECRET = "MY_SECRET";
static String OPENVIDU_URL = "https://localhost:4443/";
static String APP_URL = "https://localhost:5000/";
static int NUMBER_OF_ATTEMPTS = 10;
static int RECORDING_DURATION = 5; // seconds
static double DURATION_THRESHOLD = 10.0; // seconds
protected WebDriver driver;
protected WebDriverWait waiter;
private static OpenVidu OV;
boolean deleteRecordings = true;
@BeforeAll()
static void setupAll() {
WebDriverManager.chromedriver().setup();
String appUrl = System.getProperty("APP_URL");
if (appUrl != null) {
APP_URL = appUrl;
}
log.info("Using URL {} to connect to openvidu-recording-java app", APP_URL);
String openviduUrl = System.getProperty("OPENVIDU_URL");
if (openviduUrl != null) {
OPENVIDU_URL = openviduUrl;
}
log.info("Using URL {} to connect to openvidu-server", OPENVIDU_URL);
String openvidusecret = System.getProperty("OPENVIDU_SECRET");
if (openvidusecret != null) {
OPENVIDU_SECRET = openvidusecret;
}
log.info("Using secret {} to connect to openvidu-server", OPENVIDU_SECRET);
String numberOfAttempts = System.getProperty("NUMBER_OF_ATTEMPTS");
if (numberOfAttempts != null) {
NUMBER_OF_ATTEMPTS = Integer.parseInt(numberOfAttempts);
}
log.info("Number of attempts: {}", NUMBER_OF_ATTEMPTS);
String recordingDuration = System.getProperty("RECORDING_DURATION");
if (recordingDuration != null) {
RECORDING_DURATION = Integer.parseInt(recordingDuration);
}
log.info("Recording duration: {} s", RECORDING_DURATION);
String durationThreshold = System.getProperty("DURATION_THRESHOLD");
if (durationThreshold != null) {
DURATION_THRESHOLD = Double.parseDouble(durationThreshold);
}
log.info("Duration threshold: {} s", DURATION_THRESHOLD);
try {
log.info("Cleaning folder /opt/openvidu/recordings");
FileUtils.cleanDirectory(new File("/opt/openvidu/recordings"));
} catch (IOException e) {
log.error(e.getMessage());
}
OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET);
}
@AfterEach()
void dispose() {
try {
OV.fetch();
} catch (OpenViduJavaClientException | OpenViduHttpException e1) {
log.error("Error fetching sessions: {}", e1.getMessage());
}
OV.getActiveSessions().forEach(session -> {
try {
session.close();
log.info("Session {} successfully closed", session.getSessionId());
} catch (OpenViduJavaClientException | OpenViduHttpException e2) {
log.error("Error closing session: {}", e2.getMessage());
}
});
if (deleteRecordings) {
try {
OV.listRecordings().forEach(recording -> {
if (recording.getStatus().equals(Recording.Status.started)) {
try {
OV.stopRecording(recording.getId());
log.info("Recording {} successfully stopped", recording.getId());
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
log.error("Error stopping recording {}: {}", recording.getId(), e.getMessage());
}
}
try {
OV.deleteRecording(recording.getId());
log.info("Recording {} successfully deleted", recording.getId());
} catch (OpenViduJavaClientException | OpenViduHttpException e1) {
log.error("Error deleting recording {}: {}", recording.getId(), e1.getMessage());
}
});
} catch (OpenViduJavaClientException | OpenViduHttpException e2) {
log.error("Error listing recordings: {}", e2.getMessage());
}
}
driver.quit();
}
void setupBrowser(String browser) {
DesiredCapabilities capabilities = DesiredCapabilities.chrome();
capabilities.setAcceptInsecureCerts(true);
ChromeOptions options = new ChromeOptions();
options.addArguments("--use-fake-ui-for-media-stream");
options.addArguments("--use-fake-device-for-media-stream");
options.addArguments("--ignore-certificate-errors");
options.addArguments("--autoplay-policy=no-user-gesture-required");
capabilities.setCapability(ChromeOptions.CAPABILITY, options);
String REMOTE_URL = System.getProperty("REMOTE_URL_CHROME");
if (REMOTE_URL != null) {
log.info("Using URL {} to connect to remote web driver", REMOTE_URL);
try {
this.driver = new RemoteWebDriver(new URL(REMOTE_URL), capabilities);
} catch (MalformedURLException e) {
e.printStackTrace();
}
} else {
log.info("Using local web driver");
this.driver = new ChromeDriver(options);
}
this.driver.manage().timeouts().setScriptTimeout(20, TimeUnit.SECONDS);
this.waiter = new WebDriverWait(this.driver, 10);
this.driver.manage().window().setSize(new Dimension(1920, 1080));
}
@Test
@DisplayName("Composed recording test")
void composedRecordingTest() throws Exception {
boolean durationDifferenceAcceptable = true;
int i = 0;
double realTimeDuration = 0;
double entityDuration = 0;
String videoFile = "";
setupBrowser("chrome");
driver.get(APP_URL);
driver.findElement(By.id("join-btn")).click();
waitUntilEvents("connectionCreated", "videoElementCreated", "accessAllowed", "streamCreated", "streamPlaying");
driver.findElement(By.id("has-video-checkbox")).click();
while (durationDifferenceAcceptable && (i < NUMBER_OF_ATTEMPTS)) {
log.info("----------");
log.info("Attempt {}", i + 1);
log.info("----------");
driver.findElement(By.id("buttonStartRecording")).click();
waitUntilEvents("recordingStarted");
Thread.sleep(RECORDING_DURATION * 1000);
driver.findElement(By.id("buttonStopRecording")).click();
waitUntilEvents("recordingStopped");
Recording rec = OV.listRecordings().get(0);
String extension = rec.getOutputMode().equals(OutputMode.COMPOSED) && rec.hasVideo() ? ".mp4" : ".webm";
videoFile = "/opt/openvidu/recordings/" + rec.getId() + "/" + rec.getName() + extension;
realTimeDuration = getRealTimeDuration(videoFile);
entityDuration = rec.getDuration();
double differenceInDurations = (double) Math.abs(realTimeDuration - entityDuration);
log.info("Real media file duration: {} s", realTimeDuration);
log.info("Entity file duration: {} s", entityDuration);
log.info("Difference between durations: {} s", differenceInDurations);
durationDifferenceAcceptable = differenceInDurations < DURATION_THRESHOLD;
i++;
if (durationDifferenceAcceptable) {
// Delete acceptable recording
try {
OV.deleteRecording(rec.getId());
log.info("Recording {} was acceptable and is succesfully deleted", rec.getId());
} catch (OpenViduJavaClientException | OpenViduHttpException e) {
log.error("Error deleteing acceptable recording {}: {}", rec.getId(), e.getMessage());
}
}
}
if (i == NUMBER_OF_ATTEMPTS) {
log.info("Media file recorded with Composite has not exceeded the duration threshold ({} s) in {} attempts",
DURATION_THRESHOLD, NUMBER_OF_ATTEMPTS);
} else {
log.error(
"Real video duration recorded with Composite ({} s) exceeds threshold of {} s compared to entity duration ({} s), in file {}",
realTimeDuration, DURATION_THRESHOLD, entityDuration, videoFile);
deleteRecordings = false;
Assert.fail("Real video duration recorded with Composite (" + realTimeDuration + " s) exceeds threshold of "
+ DURATION_THRESHOLD + " s compared to entity duration (" + entityDuration + " s), in file "
+ videoFile);
}
}
private void waitUntilEvents(String... events) {
waiter.until(eventsToBe(events));
driver.findElement(By.id("clear-events-btn")).click();
waiter.until(ExpectedConditions.textToBePresentInElementLocated(By.id("textarea-events"), ""));
}
private ExpectedCondition<Boolean> eventsToBe(String... events) {
final Map<String, Integer> expectedEvents = new HashMap<>();
for (String event : events) {
Integer currentNumber = expectedEvents.get(event);
if (currentNumber == null) {
expectedEvents.put(event, 1);
} else {
expectedEvents.put(event, currentNumber++);
}
}
return new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(WebDriver driver) {
boolean eventsCorrect = true;
String events = driver.findElement(By.id("textarea-events")).getText();
for (Entry<String, Integer> entry : expectedEvents.entrySet()) {
eventsCorrect = eventsCorrect
&& StringUtils.countMatches(events, entry.getKey()) == entry.getValue();
if (!eventsCorrect) {
break;
}
}
return eventsCorrect;
}
@Override
public String toString() {
return " OpenVidu events " + expectedEvents.toString();
}
};
}
private double getRealTimeDuration(String pathToVideoFile) {
long time = 0;
File source = new File(pathToVideoFile);
try {
MultimediaObject media = new MultimediaObject(source);
MultimediaInfo info = media.getInfo();
time = info.getDuration();
} catch (EncoderException e) {
log.error("Error getting MultimediaInfo from file {}: {}", pathToVideoFile, e.getMessage());
}
return (double) time / 1000;
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<timestamp key="myTimestamp" timeReference="contextBirth"
datePattern="HH-mm-ss" />
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%p] %d [%.12t] %c \(%M\) - %msg%n</Pattern>
</layout>
</appender>
<root>
<level value="INFO" />
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -21,27 +21,48 @@ function joinSession() {
// --- 3) Specify the actions when events take place in the session ---
session.on('connectionCreated', event => {
pushEvent(event);
});
session.on('connectionDestroyed', event => {
pushEvent(event);
});
// On every new Stream received...
session.on('streamCreated', (event) => {
session.on('streamCreated', event => {
pushEvent(event);
// Subscribe to the Stream to receive it
// HTML video will be appended to element with 'video-container' id
var subscriber = session.subscribe(event.stream, 'video-container');
// When the HTML video has been appended to DOM...
subscriber.on('videoElementCreated', (event) => {
subscriber.on('videoElementCreated', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(1);
});
// When the HTML video has been appended to DOM...
subscriber.on('videoElementDestroyed', (event) => {
subscriber.on('videoElementDestroyed', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(-1);
});
// When the subscriber stream has started playing media...
subscriber.on('streamPlaying', event => {
pushEvent(event);
});
});
session.on('sessionDisconnected', (event) => {
session.on('streamDestroyed', event => {
pushEvent(event);
});
session.on('sessionDisconnected', event => {
pushEvent(event);
if (event.reason !== 'disconnect') {
removeUser();
}
@ -53,6 +74,14 @@ function joinSession() {
}
});
session.on('recordingStarted', event => {
pushEvent(event);
});
session.on('recordingStopped', event => {
pushEvent(event);
});
// --- 4) Connect to the session passing the retrieved token and some more data from
// the client (in this case a JSON with the nickname chosen by the user) ---
@ -70,28 +99,62 @@ function joinSession() {
var publisher = OV.initPublisher('video-container', {
audioSource: undefined, // The source of audio. If undefined default microphone
videoSource: undefined, // The source of video. If undefined default webcam
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
publishVideo: true, // Whether you want to start publishing with your video enabled or not
resolution: '640x480', // The resolution of your video
frameRate: 30, // The frame rate of your video
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
mirror: false // Whether to mirror your local video or not
publishAudio: true, // Whether you want to start publishing with your audio unmuted or not
publishVideo: true, // Whether you want to start publishing with your video enabled or not
resolution: '640x480', // The resolution of your video
frameRate: 30, // The frame rate of your video
insertMode: 'APPEND', // How the video is inserted in the target element 'video-container'
mirror: false // Whether to mirror your local video or not
});
// --- 7) Specify the actions when events take place in our publisher ---
// When the publisher stream has started playing media...
publisher.on('accessAllowed', event => {
pushEvent({
type: 'accessAllowed'
});
});
publisher.on('accessDenied', event => {
pushEvent(event);
});
publisher.on('accessDialogOpened', event => {
pushEvent({
type: 'accessDialogOpened'
});
});
publisher.on('accessDialogClosed', event => {
pushEvent({
type: 'accessDialogClosed'
});
});
// When the publisher stream has started playing media...
publisher.on('streamCreated', event => {
pushEvent(event);
});
// When our HTML video has been added to DOM...
publisher.on('videoElementCreated', (event) => {
publisher.on('videoElementCreated', event => {
pushEvent(event);
updateNumVideos(1);
$(event.element).prop('muted', true); // Mute local video
});
// When the HTML video has been appended to DOM...
publisher.on('videoElementDestroyed', (event) => {
publisher.on('videoElementDestroyed', event => {
pushEvent(event);
// Add a new HTML element for the user's name and nickname over its video
updateNumVideos(-1);
});
// When the publisher stream has started playing media...
publisher.on('streamPlaying', event => {
pushEvent(event);
});
// --- 8) Publish your stream ---
@ -128,8 +191,8 @@ function getToken(callback) {
sessionName: sessionName
},
'Request of TOKEN gone WRONG:',
(response) => {
token = response[0]; // Get token from response
res => {
token = res[0]; // Get token from response
console.warn('Request of TOKEN gone WELL (TOKEN:' + token + ')');
callback(token); // Continue the join operation
}
@ -144,7 +207,7 @@ function removeUser() {
token: token
},
'User couldn\'t be removed from session',
(response) => {
res => {
console.warn("You have been removed from session " + sessionName);
}
);
@ -157,7 +220,7 @@ function closeSession() {
sessionName: sessionName
},
'Session couldn\'t be closed',
(response) => {
res => {
console.warn("Session " + sessionName + " has been closed");
}
);
@ -170,9 +233,9 @@ function fetchInfo() {
sessionName: sessionName
},
'Session couldn\'t be fetched',
(response) => {
res => {
console.warn("Session info has been fetched");
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -182,9 +245,9 @@ function fetchAll() {
'GET',
'api/fetch-all', {},
'All session info couldn\'t be fetched',
(response) => {
res => {
console.warn("All session info has been fetched");
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -197,7 +260,7 @@ function forceDisconnect() {
connectionId: document.getElementById('forceValue').value
},
'Connection couldn\'t be closed',
(response) => {
res => {
console.warn("Connection has been closed");
}
);
@ -211,14 +274,14 @@ function forceUnpublish() {
streamId: document.getElementById('forceValue').value
},
'Stream couldn\'t be closed',
(response) => {
res => {
console.warn("Stream has been closed");
}
);
}
function httpRequest(method, url, body, errorMsg, callback) {
$('#text-area').text('');
$('#textarea-http').text('');
var http = new XMLHttpRequest();
http.open(method, url, true);
http.setRequestHeader('Content-type', 'application/json');
@ -236,16 +299,16 @@ function httpRequest(method, url, body, errorMsg, callback) {
} else {
console.warn(errorMsg + ' (' + http.status + ')');
console.warn(http.responseText);
$('#text-area').text(errorMsg + ": HTTP " + http.status + " (" + http.responseText + ")");
$('#textarea-http').text(errorMsg + ": HTTP " + http.status + " (" + http.responseText + ")");
}
}
}
}
function startRecording() {
var outputMode = document.querySelector('input[name="outputMode"]:checked').value;
var hasAudio = !!document.querySelector("#has-audio-checkbox:checked");
var hasVideo = !!document.querySelector("#has-video-checkbox:checked");
var outputMode = $('input[name=outputMode]:checked').val();
var hasAudio = $('#has-audio-checkbox').prop('checked');
var hasVideo = $('#has-video-checkbox').prop('checked');
httpRequest(
'POST',
'api/recording/start', {
@ -255,11 +318,11 @@ function startRecording() {
hasVideo: hasVideo
},
'Start recording WRONG',
(response) => {
console.log(response);
document.getElementById('forceRecordingId').value = response.id;
res => {
console.log(res);
document.getElementById('forceRecordingId').value = res.id;
checkBtnsRecordings();
$('#text-area').text(JSON.stringify(response, null, "\t"));
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -272,9 +335,9 @@ function stopRecording() {
recording: forceRecordingId
},
'Stop recording WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -287,9 +350,9 @@ function deleteRecording() {
recording: forceRecordingId
},
'Delete recording WRONG',
() => {
res => {
console.log("DELETE ok");
$('#text-area').text("DELETE ok");
$('#textarea-http').text("DELETE ok");
}
);
}
@ -300,9 +363,9 @@ function getRecording() {
'GET',
'api/recording/get/' + forceRecordingId, {},
'Get recording WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -312,9 +375,9 @@ function listRecordings() {
'GET',
'api/recording/list', {},
'List recordings WRONG',
(response) => {
console.log(response);
$('#text-area').text(JSON.stringify(response, null, "\t"));
res => {
console.log(res);
$('#textarea-http').text(JSON.stringify(res, null, "\t"));
}
);
}
@ -325,6 +388,8 @@ function listRecordings() {
/* APPLICATION BROWSER METHODS */
events = '';
window.onbeforeunload = function () { // Gracefully leave session
if (session) {
removeUser();
@ -373,4 +438,18 @@ function checkBtnsRecordings() {
}
}
function pushEvent(event) {
events += (!events ? '' : '\n') + event.type;
$('#textarea-events').text(events);
}
function clearHttpTextarea() {
$('#textarea-http').text('');
}
function clearEventsTextarea() {
$('#textarea-events').text('');
events = '';
}
/* APPLICATION BROWSER METHODS */

View File

@ -60,7 +60,7 @@
<input class="form-control" type="text" id="sessionName" value="SessionA" required>
</p>
<p class="text-center">
<button class="btn btn-lg btn-success" onclick="joinSession()">Join!</button>
<button class="btn btn-lg btn-success" id="join-btn" onclick="joinSession()">Join!</button>
</p>
</form>
<hr>
@ -89,10 +89,10 @@
<input class="btn btn-md" type="button" id="buttonStartRecording" onmouseup="startRecording()" value="Start recording">
<form>
<label class="radio-inline">
<input type="radio" name="outputMode" value="COMPOSED" checked>COMPOSED
<input type="radio" name="outputMode" value="COMPOSED" id="radio-composed" checked>COMPOSED
</label>
<label class="radio-inline">
<input type="radio" name="outputMode" value="INDIVIDUAL">INDIVIDUAL
<input type="radio" name="outputMode" value="INDIVIDUAL" id="radio-individual">INDIVIDUAL
</label>
</form>
<form>
@ -115,7 +115,16 @@
disabled>
<input class="form-control" id="forceRecordingId" type="text" onkeyup="checkBtnsRecordings()">
</div>
<textarea id="text-area" readonly="true" class="form-control" name="comment">HTTP responses...</textarea>
<div class="textarea-container" id="textarea-http-container">
<button type="button" class="btn btn-outline-secondary" id="clear-http-btn" onclick="clearHttpTextarea()">Clear</button>
<span>HTTP responses</span>
<textarea id="textarea-http" readonly="true" class="form-control" name="textarea-http"></textarea>
</div>
<div class="textarea-container" id="textarea-events-container">
<button type="button" class="btn btn-outline-secondary" id="clear-events-btn" onclick="clearEventsTextarea()">Clear</button>
<span>OpenVidu events</span>
<textarea id="textarea-events" readonly="true" class="form-control" name="textarea-events"></textarea>
</div>
</div>
</div>
</div>

View File

@ -250,21 +250,21 @@ a:hover .demo-logo {
#video-container {
width: 100%;
max-height: 45%;
display: inline-block;
max-height: 42%;
display: block;
overflow: hidden;
}
#video-container video.two {
width: 50%;
max-width: 50%;
}
#video-container video.three {
width: 33.33%;
max-width: 33.33%;
}
#video-container video.four {
width: 25%;
max-width: 25%;
}
#video-container div {
@ -294,10 +294,10 @@ a:hover .demo-logo {
}
video {
width: 100%;
height: auto;
max-height: 100%;
object-fit: cover;
width: auto;
height: auto;
max-height: 100%;
object-fit: scale-down;
}
#session {
@ -353,11 +353,11 @@ table i {
}
#recording-btns {
display: flow-root;
padding-left: 15px;
padding-top: 20px;
width: 100%;
height: 40%;
display: inline-block;
padding-left: 15px;
padding-top: 20px;
width: 100%;
height: 40%;
}
#recording-btns .btns {
@ -371,19 +371,57 @@ table i {
#recording-btns .btns form {
display: inline;
margin-left: 5px;
}
#recording-btns #text-area {
display: inline;
width: 100%;
#recording-btns textarea {
height: 100%;
}
.textarea-container {
position: relative;
display: inline-block;
height: 74%;
margin-top: 20px;
resize: none;
}
#textarea-http-container {
width: 69%;
}
#textarea-events-container {
width: 29%;
}
.textarea-container button {
position: absolute;
top: 1px;
right: 1px;
z-index: 1;
}
.textarea-container span {
position: absolute;
bottom: 1px;
right: 1px;
padding: 3px;
border-bottom-right-radius: 4px;
z-index: 1;
color: #a5a5a5;
background-color: #ededee;
font-weight: 600;
}
.textarea-container textarea {
height: 100%;
resize: none;
}
.vertical-separator-bottom {
width: 2px;
height: 34px;
display: inline;
display: inline-block;
background-color: #cbcbcb;
margin: 0 8px 0 8px;
margin-bottom: -12px;

View File

@ -1,9 +1,7 @@
/* CONFIGURATION */
var OpenVidu = require('openvidu-node-client').OpenVidu;
var Session = require('openvidu-node-client').Session;
var OpenViduRole = require('openvidu-node-client').OpenViduRole;
var TokenOptions = require('openvidu-node-client').TokenOptions;
// Check launch arguments: must receive openvidu-server URL and the secret
if (process.argv.length != 4) {
@ -93,41 +91,50 @@ app.post('/api/get-token', function (req, res) {
})
.catch(error => {
console.error(error);
if (error.message === "404") {
delete mapSessions[sessionName];
delete mapSessionNamesTokens[sessionName];
newSession(sessionName, tokenOptions, res);
}
});
} else {
// New session
console.log('New session ' + sessionName);
// Create a new OpenVidu Session asynchronously
OV.createSession()
.then(session => {
// Store the new Session in the collection of Sessions
mapSessions[sessionName] = session;
// Store a new empty array in the collection of tokens
mapSessionNamesTokens[sessionName] = [];
// Generate a new token asynchronously with the recently created tokenOptions
session.generateToken(tokenOptions)
.then(token => {
// Store the new token in the collection of tokens
mapSessionNamesTokens[sessionName].push(token);
// Return the Token to the client
res.status(200).send({
0: token
});
})
.catch(error => {
console.error(error);
});
})
.catch(error => {
console.error(error);
});
newSession(sessionName, tokenOptions, res);
}
});
function newSession(sessionName, tokenOptions, res) {
// New session
console.log('New session ' + sessionName);
// Create a new OpenVidu Session asynchronously
OV.createSession()
.then(session => {
// Store the new Session in the collection of Sessions
mapSessions[sessionName] = session;
// Store a new empty array in the collection of tokens
mapSessionNamesTokens[sessionName] = [];
// Generate a new token asynchronously with the recently created tokenOptions
session.generateToken(tokenOptions)
.then(token => {
// Store the new token in the collection of tokens
mapSessionNamesTokens[sessionName].push(token);
// Return the Token to the client
res.status(200).send({
0: token
});
})
.catch(error => {
console.error(error);
});
})
.catch(error => {
console.error(error);
});
}
// Remove user from session
app.post('/api/remove-user', function (req, res) {
// Retrieve params from POST body