diff --git a/openvidu-recording-java/.gitignore b/openvidu-recording-java/.gitignore index 950fc085..6c57dbc8 100644 --- a/openvidu-recording-java/.gitignore +++ b/openvidu-recording-java/.gitignore @@ -22,3 +22,4 @@ hs_err_pid* target/ +.vscode/* diff --git a/openvidu-recording-java/pom.xml b/openvidu-recording-java/pom.xml index 145a1974..cf2e68a3 100644 --- a/openvidu-recording-java/pom.xml +++ b/openvidu-recording-java/pom.xml @@ -1,4 +1,5 @@ - 4.0.0 @@ -13,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 1.4.1.RELEASE + 2.1.2.RELEASE @@ -21,6 +22,11 @@ 1.8 io.openvidu.recording.java.App openvidu + + + 5.0.3 + 1.3.2 + 3.1.0 @@ -36,7 +42,7 @@ org.springframework springloaded - 1.2.6.RELEASE + 2.1.2.RELEASE @@ -48,35 +54,11 @@ ${start-class} - - - com.spotify - docker-maven-plugin - 0.2.3 - - ${docker.image.prefix}/${project.artifactId} - src/main/docker - - - / - ${project.build.directory} - ${project.build.finalName}.war - - - - - - - junit - junit - test - - org.springframework.boot spring-boot-starter-web @@ -86,17 +68,59 @@ org.springframework.boot spring-boot-devtools - - - com.googlecode.json-simple - json-simple - - io.openvidu openvidu-java-client 2.8.1 + + org.junit.jupiter + junit-jupiter-api + test + + + io.github.bonigarcia + selenium-jupiter + ${selenium-jupiter.version} + test + + + io.github.bonigarcia + webdrivermanager + 2.2.0 + test + + + org.junit.platform + junit-platform-runner + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + com.google.guava + guava + 23.0 + test + + + + + ws.schild + jave-all-deps + 2.4.5 + + + ws.schild + jave-core + 2.4.5 + + diff --git a/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java b/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java index 7ce2f00b..53fa5f25 100644 --- a/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java +++ b/openvidu-recording-java/src/main/java/io/openvidu/recording/java/MyRestController.java @@ -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); } } diff --git a/openvidu-recording-java/src/main/resources/static/app.js b/openvidu-recording-java/src/main/resources/static/app.js index 0de93909..8fd38e5b 100644 --- a/openvidu-recording-java/src/main/resources/static/app.js +++ b/openvidu-recording-java/src/main/resources/static/app.js @@ -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 */ \ No newline at end of file diff --git a/openvidu-recording-java/src/main/resources/static/index.html b/openvidu-recording-java/src/main/resources/static/index.html index 0cd0b071..ae86e6ed 100644 --- a/openvidu-recording-java/src/main/resources/static/index.html +++ b/openvidu-recording-java/src/main/resources/static/index.html @@ -60,7 +60,7 @@

- +


@@ -89,10 +89,10 @@
@@ -115,7 +115,16 @@ disabled> - +
+ + HTTP responses + +
+
+ + OpenVidu events + +
diff --git a/openvidu-recording-java/src/main/resources/static/style.css b/openvidu-recording-java/src/main/resources/static/style.css index abac6a79..232ae9be 100644 --- a/openvidu-recording-java/src/main/resources/static/style.css +++ b/openvidu-recording-java/src/main/resources/static/style.css @@ -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; diff --git a/openvidu-recording-java/src/test/java/io/openvidu/js/java/test/AppTest.java b/openvidu-recording-java/src/test/java/io/openvidu/js/java/test/AppTest.java deleted file mode 100644 index 0ba99ec8..00000000 --- a/openvidu-recording-java/src/test/java/io/openvidu/js/java/test/AppTest.java +++ /dev/null @@ -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 ); - } -} diff --git a/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTest.java b/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTest.java new file mode 100644 index 00000000..ec34aa6b --- /dev/null +++ b/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTest.java @@ -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); + } + +} diff --git a/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTestE2e.java b/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTestE2e.java new file mode 100644 index 00000000..d4f5060b --- /dev/null +++ b/openvidu-recording-java/src/test/java/io/openvidu/recording/java/test/AppTestE2e.java @@ -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 eventsToBe(String... events) { + final Map 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() { + @Override + public Boolean apply(WebDriver driver) { + boolean eventsCorrect = true; + String events = driver.findElement(By.id("textarea-events")).getText(); + + for (Entry 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; + } + +} diff --git a/openvidu-recording-java/src/test/resources/logback.xml b/openvidu-recording-java/src/test/resources/logback.xml new file mode 100644 index 00000000..08a8eb48 --- /dev/null +++ b/openvidu-recording-java/src/test/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + [%p] %d [%.12t] %c \(%M\) - %msg%n + + + + + + + \ No newline at end of file diff --git a/openvidu-recording-node/public/app.js b/openvidu-recording-node/public/app.js index 0de93909..8fd38e5b 100644 --- a/openvidu-recording-node/public/app.js +++ b/openvidu-recording-node/public/app.js @@ -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 */ \ No newline at end of file diff --git a/openvidu-recording-node/public/index.html b/openvidu-recording-node/public/index.html index 7798501f..08c02a81 100644 --- a/openvidu-recording-node/public/index.html +++ b/openvidu-recording-node/public/index.html @@ -60,7 +60,7 @@

- +


@@ -89,10 +89,10 @@
@@ -115,7 +115,16 @@ disabled> - +
+ + HTTP responses + +
+
+ + OpenVidu events + +
diff --git a/openvidu-recording-node/public/style.css b/openvidu-recording-node/public/style.css index abac6a79..232ae9be 100644 --- a/openvidu-recording-node/public/style.css +++ b/openvidu-recording-node/public/style.css @@ -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; diff --git a/openvidu-recording-node/server.js b/openvidu-recording-node/server.js index 271e3f68..f2162e69 100644 --- a/openvidu-recording-node/server.js +++ b/openvidu-recording-node/server.js @@ -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