diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java new file mode 100644 index 00000000..3634ad71 --- /dev/null +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/AbstractOpenViduTestAppE2eTest.java @@ -0,0 +1,546 @@ +package io.openvidu.test.e2e; + +import static org.openqa.selenium.OutputType.BASE64; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.math.RoundingMode; +import java.nio.file.Path; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.imageio.ImageIO; + +import org.apache.commons.io.FileUtils; +import org.jcodec.api.FrameGrab; +import org.jcodec.api.JCodecException; +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.junit.Assert; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.TakesScreenshot; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; + +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.test.browsers.BrowserUser; +import io.openvidu.test.browsers.ChromeAndroidUser; +import io.openvidu.test.browsers.ChromeUser; +import io.openvidu.test.browsers.FirefoxUser; +import io.openvidu.test.browsers.OperaUser; +import io.openvidu.test.browsers.utils.CommandLineExecutor; +import io.openvidu.test.browsers.utils.MultimediaFileMetadata; +import io.openvidu.test.browsers.utils.Unzipper; + +public class AbstractOpenViduTestAppE2eTest { + + protected static String OPENVIDU_SECRET = "MY_SECRET"; + protected static String OPENVIDU_URL = "https://localhost:4443/"; + protected static String APP_URL = "http://localhost:4200/"; + protected static String EXTERNAL_CUSTOM_LAYOUT_URL = "http://localhost:5555"; + protected static String EXTERNAL_CUSTOM_LAYOUT_PARAMS = "sessionId,CUSTOM_LAYOUT_SESSION,secret,MY_SECRET"; + protected static Exception ex = null; + protected final Object lock = new Object(); + + protected static final Logger log = LoggerFactory.getLogger(OpenViduTestAppE2eTest.class); + protected static final CommandLineExecutor commandLine = new CommandLineExecutor(); + protected static final String RECORDING_IMAGE = "openvidu/openvidu-recording"; + + protected MyUser user; + protected Collection otherUsers = new ArrayList<>(); + protected volatile static boolean isRecordingTest; + protected volatile static boolean isKurentoRestartTest; + protected static OpenVidu OV; + + @BeforeAll() + protected static void setupAll() { + + String ffmpegOutput = commandLine.executeCommand("which ffmpeg"); + if (ffmpegOutput == null || ffmpegOutput.isEmpty()) { + log.error("ffmpeg package is not installed in the host machine"); + Assert.fail(); + return; + } else { + log.info("ffmpeg is installed and accesible"); + } + + WebDriverManager.chromedriver().setup(); + WebDriverManager.firefoxdriver().setup(); + + String appUrl = System.getProperty("APP_URL"); + if (appUrl != null) { + APP_URL = appUrl; + } + log.info("Using URL {} to connect to openvidu-testapp", APP_URL); + + String externalCustomLayoutUrl = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_URL"); + if (externalCustomLayoutUrl != null) { + EXTERNAL_CUSTOM_LAYOUT_URL = externalCustomLayoutUrl; + } + log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_URL); + + String externalCustomLayoutParams = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_PARAMS"); + if (externalCustomLayoutParams != null) { + // Parse external layout parameters and build a URL formatted params string + List params = Stream.of(externalCustomLayoutParams.split(",", -1)).collect(Collectors.toList()); + if (params.size() % 2 != 0) { + log.error( + "Wrong configuration property EXTERNAL_CUSTOM_LAYOUT_PARAMS. Must be a comma separated list with an even number of elements. e.g: EXTERNAL_CUSTOM_LAYOUT_PARAMS=param1,value1,param2,value2"); + Assert.fail(); + return; + } else { + EXTERNAL_CUSTOM_LAYOUT_PARAMS = ""; + for (int i = 0; i < params.size(); i++) { + if (i % 2 == 0) { + // Param name + EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i) + "="; + } else { + // Param value + EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i); + if (i < params.size() - 1) { + EXTERNAL_CUSTOM_LAYOUT_PARAMS += "&"; + } + } + } + } + } + log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_PARAMS); + + 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); + + 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); + } + + protected void setupBrowser(String browser) { + + BrowserUser browserUser; + + switch (browser) { + case "chrome": + browserUser = new ChromeUser("TestUser", 50, false); + break; + case "firefox": + browserUser = new FirefoxUser("TestUser", 50); + break; + case "opera": + browserUser = new OperaUser("TestUser", 50); + break; + case "chromeAndroid": + browserUser = new ChromeAndroidUser("TestUser", 50); + break; + case "chromeAlternateScreenShare": + browserUser = new ChromeUser("TestUser", 50, "OpenVidu TestApp", false); + break; + case "chromeAsRoot": + browserUser = new ChromeUser("TestUser", 50, true); + break; + default: + browserUser = new ChromeUser("TestUser", 50, false); + } + + this.user = new MyUser(browserUser); + + user.getDriver().get(APP_URL); + + WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); + urlInput.clear(); + urlInput.sendKeys(OPENVIDU_URL); + WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); + secretInput.clear(); + secretInput.sendKeys(OPENVIDU_SECRET); + + user.getEventManager().startPolling(); + } + + protected void setupChromeWithFakeVideo(Path videoFileLocation) { + this.user = new MyUser(new ChromeUser("TestUser", 50, videoFileLocation)); + user.getDriver().get(APP_URL); + WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); + urlInput.clear(); + urlInput.sendKeys(OPENVIDU_URL); + WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); + secretInput.clear(); + secretInput.sendKeys(OPENVIDU_SECRET); + user.getEventManager().startPolling(); + } + + @AfterEach + protected void dispose() { + if (user != null) { + user.dispose(); + } + Iterator it = otherUsers.iterator(); + while (it.hasNext()) { + MyUser other = it.next(); + other.dispose(); + it.remove(); + } + 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 e) { + log.error("Error closing session: {}", e.getMessage()); + } catch (OpenViduHttpException e) { + log.error("Error closing session: {}", e.getMessage()); + } + }); + if (isRecordingTest) { + removeAllRecordingContiners(); + try { + FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); + } catch (IOException e) { + log.error(e.getMessage()); + } + isRecordingTest = false; + } + if (isKurentoRestartTest) { + this.restartKms(); + isKurentoRestartTest = false; + } + OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); + } + + protected void listEmptyRecordings() { + // List existing recordings (empty) + user.getDriver().findElement(By.id("list-recording-btn")).click(); + user.getWaiter() + .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Recording list []")); + } + + protected ExpectedCondition waitForVideoDuration(WebElement element, int durationInSeconds) { + return new ExpectedCondition() { + @Override + public Boolean apply(WebDriver input) { + return element.getAttribute("duration").matches( + durationInSeconds - 1 + "\\.[5-9][0-9]{0,5}|" + durationInSeconds + "\\.[0-5][0-9]{0,5}"); + } + }; + } + + protected static boolean checkVideoAverageRgbGreen(Map rgb) { + // GREEN color: {r < 15, g > 130, b <15} + return (rgb.get("r") < 15) && (rgb.get("g") > 130) && (rgb.get("b") < 15); + } + + protected static boolean checkVideoAverageRgbGray(Map rgb) { + // GRAY color: {r < 50, g < 50, b < 50} and the absolute difference between them + // not greater than 2 + return (rgb.get("r") < 50) && (rgb.get("g") < 50) && (rgb.get("b") < 50) + && (Math.abs(rgb.get("r") - rgb.get("g")) <= 2) && (Math.abs(rgb.get("r") - rgb.get("b")) <= 2) + && (Math.abs(rgb.get("b") - rgb.get("g")) <= 2); + } + + protected static boolean checkVideoAverageRgbRed(Map rgb) { + // RED color: {r > 240, g < 15, b <15} + return (rgb.get("r") > 240) && (rgb.get("g") < 15) && (rgb.get("b") < 15); + } + + protected void gracefullyLeaveParticipants(int numberOfParticipants) throws Exception { + int accumulatedConnectionDestroyed = 0; + for (int j = 1; j <= numberOfParticipants; j++) { + user.getDriver().findElement(By.id("remove-user-btn")).sendKeys(Keys.ENTER); + user.getEventManager().waitUntilEventReaches("sessionDisconnected", j); + accumulatedConnectionDestroyed = (j != numberOfParticipants) + ? (accumulatedConnectionDestroyed + numberOfParticipants - j) + : (accumulatedConnectionDestroyed); + user.getEventManager().waitUntilEventReaches("connectionDestroyed", accumulatedConnectionDestroyed); + } + } + + protected String getBase64Screenshot(MyUser user) throws Exception { + String screenshotBase64 = ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64); + return "data:image/png;base64," + screenshotBase64; + } + + protected boolean recordedFileFine(File file, Recording recording, + Function, Boolean> colorCheckFunction) throws IOException { + this.checkMultimediaFile(file, recording.hasAudio(), recording.hasVideo(), recording.getDuration(), + recording.getResolution(), "aac", "h264", true); + + boolean isFine = false; + Picture frame; + try { + // Get a frame at 75% duration and check that it has the expected color + frame = FrameGrab.getFrameAtSec(file, (double) (recording.getDuration() * 0.75)); + BufferedImage image = AWTUtil.toBufferedImage(frame); + Map colorMap = this.averageColor(image); + + String realResolution = image.getWidth() + "x" + image.getHeight(); + Assert.assertEquals( + "Resolution (" + recording.getResolution() + + ") of recording entity is not equal to real video resolution (" + realResolution + ")", + recording.getResolution(), realResolution); + + log.info("Recording map color: {}", colorMap.toString()); + log.info("Recording frame below"); + System.out.println(bufferedImageToBase64PngString(image)); + isFine = colorCheckFunction.apply(colorMap); + } catch (IOException | JCodecException e) { + log.warn("Error getting frame from video recording: {}", e.getMessage()); + isFine = false; + } + return isFine; + } + + protected boolean recordedGreenFileFine(File file, Recording recording) throws IOException { + return this.recordedFileFine(file, recording, OpenViduTestAppE2eTest::checkVideoAverageRgbGreen); + } + + protected boolean recordedRedFileFine(File file, Recording recording) throws IOException { + return this.recordedFileFine(file, recording, OpenViduTestAppE2eTest::checkVideoAverageRgbRed); + } + + protected String bufferedImageToBase64PngString(BufferedImage image) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String imageString = null; + try { + ImageIO.write(image, "png", bos); + byte[] imageBytes = bos.toByteArray(); + imageString = "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes); + bos.close(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + return imageString; + } + + protected void checkIndividualRecording(String recPath, Recording recording, int numberOfVideoFiles, + String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { + + // Should be only 2 files: zip and metadata + File folder = new File(recPath); + Assert.assertEquals("There are more than 2 files (ZIP and metadata) inside individual recording folder " + + recPath + ": " + Arrays.toString(folder.listFiles()), 2, folder.listFiles().length); + + File file1 = new File(recPath + recording.getName() + ".zip"); + File file2 = new File(recPath + ".recording." + recording.getId()); + + Assert.assertTrue("File " + file1.getAbsolutePath() + " does not exist or is empty", + file1.exists() && file1.length() > 0); + Assert.assertTrue("File " + file2.getAbsolutePath() + " does not exist or is empty", + file2.exists() && file2.length() > 0); + + List unzippedWebmFiles = new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); + + Assert.assertEquals("Expecting " + numberOfVideoFiles + " videos inside ZIP file but " + + unzippedWebmFiles.size() + " found: " + unzippedWebmFiles.toString(), numberOfVideoFiles, + unzippedWebmFiles.size()); + + File jsonSyncFile = new File(recPath + recording.getName() + ".json"); + Assert.assertTrue("JSON sync file " + jsonSyncFile.getAbsolutePath() + "does not exist or is empty", + jsonSyncFile.exists() && jsonSyncFile.length() > 0); + + JsonObject jsonSyncMetadata; + try { + Gson gson = new Gson(); + JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); + jsonSyncMetadata = gson.fromJson(reader, JsonObject.class); + } catch (Exception e) { + log.error("Cannot read JSON sync metadata file from {}. Error: {}", jsonSyncFile.getAbsolutePath(), + e.getMessage()); + Assert.fail("Cannot read JSON sync metadata file from " + jsonSyncFile.getAbsolutePath()); + return; + } + + long totalFileSize = 0; + JsonArray syncArray = jsonSyncMetadata.get("files").getAsJsonArray(); + for (File webmFile : unzippedWebmFiles) { + totalFileSize += webmFile.length(); + + Assert.assertTrue("WEBM file " + webmFile.getAbsolutePath() + " does not exist or is empty", + webmFile.exists() && webmFile.length() > 0); + + double durationInSeconds = 0; + boolean found = false; + for (int i = 0; i < syncArray.size(); i++) { + JsonObject j = syncArray.get(i).getAsJsonObject(); + if (webmFile.getName().contains(j.get("streamId").getAsString())) { + durationInSeconds = (double) (j.get("endTimeOffset").getAsDouble() + - j.get("startTimeOffset").getAsDouble()) / 1000; + found = true; + break; + } + } + + Assert.assertTrue("Couldn't find in JSON sync object information for webm file " + webmFile.getName(), + found); + + log.info("Duration of {} according to sync metadata json file: {} s", webmFile.getName(), + durationInSeconds); + this.checkMultimediaFile(webmFile, recording.hasAudio(), recording.hasVideo(), durationInSeconds, + recording.getResolution(), audioDecoder, videoDecoder, checkAudio); + webmFile.delete(); + } + + Assert.assertEquals("Size of recording entity (" + recording.getSessionId() + + ") is not equal to real file size (" + totalFileSize + ")", recording.getSize(), totalFileSize); + + jsonSyncFile.delete(); + } + + protected void checkMultimediaFile(File file, boolean hasAudio, boolean hasVideo, double duration, + String resolution, String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { + // Check tracks, duration, resolution, framerate and decoders + MultimediaFileMetadata metadata = new MultimediaFileMetadata(file.getAbsolutePath()); + + if (hasVideo) { + if (checkAudio) { + if (hasAudio) { + Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have audio", + metadata.hasAudio() && metadata.hasVideo()); + Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); + } else { + Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have video", + metadata.hasVideo()); + Assert.assertFalse(metadata.hasAudio()); + } + } + if (resolution != null) { + Assert.assertEquals(resolution, metadata.getVideoWidth() + "x" + metadata.getVideoHeight()); + } + Assert.assertTrue(metadata.getVideoDecoder().toLowerCase().contains(videoDecoder)); + } else if (hasAudio && checkAudio) { + Assert.assertTrue(metadata.hasAudio()); + Assert.assertFalse(metadata.hasVideo()); + Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); + } else { + Assert.fail("Cannot check a file witho no audio and no video"); + } + // Check duration with 1 decimal precision + DecimalFormat df = new DecimalFormat("#0.0"); + df.setRoundingMode(RoundingMode.UP); + log.info("Duration of {} according to ffmpeg: {} s", file.getName(), metadata.getDuration()); + log.info("Duration of {} according to 'duration' property: {} s", file.getName(), duration); + log.info("Difference in s duration: {}", Math.abs(metadata.getDuration() - duration)); + final double difference = 10; + Assert.assertTrue( + "Difference between recording entity duration (" + duration + ") and real video duration (" + + metadata.getDuration() + ") is greater than " + difference + " in file " + file.getName(), + Math.abs((metadata.getDuration() - duration)) < difference); + } + + protected boolean thumbnailIsFine(File file, Function, Boolean> colorCheckFunction) { + boolean isFine = false; + BufferedImage image = null; + try { + image = ImageIO.read(file); + } catch (IOException e) { + log.error(e.getMessage()); + return false; + } + log.info("Recording thumbnail dimensions: {}x{}", image.getWidth(), image.getHeight()); + Map colorMap = this.averageColor(image); + log.info("Thumbnail map color: {}", colorMap.toString()); + isFine = colorCheckFunction.apply(colorMap); + return isFine; + } + + protected Map averageColor(BufferedImage bi) { + int x0 = 0; + int y0 = 0; + int w = bi.getWidth(); + int h = bi.getHeight(); + int x1 = x0 + w; + int y1 = y0 + h; + long sumr = 0, sumg = 0, sumb = 0; + for (int x = x0; x < x1; x++) { + for (int y = y0; y < y1; y++) { + Color pixel = new Color(bi.getRGB(x, y)); + sumr += pixel.getRed(); + sumg += pixel.getGreen(); + sumb += pixel.getBlue(); + } + } + int num = w * h; + Map colorMap = new HashMap<>(); + colorMap.put("r", (long) (sumr / num)); + colorMap.put("g", (long) (sumg / num)); + colorMap.put("b", (long) (sumb / num)); + return colorMap; + } + + protected void startKms() { + log.info("Starting KMS"); + commandLine.executeCommand("/usr/bin/kurento-media-server &>> /kms.log &"); + } + + protected void stopKms() { + log.info("Stopping KMS"); + commandLine.executeCommand("kill -9 $(pidof kurento-media-server)"); + } + + protected void restartKms() { + this.stopKms(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + this.startKms(); + } + + protected void checkDockerContainerRunning(String imageName, int amount) { + int number = Integer.parseInt(commandLine.executeCommand("docker ps | grep " + imageName + " | wc -l")); + Assert.assertEquals("Wrong number of Docker containers for image " + imageName + " running", amount, number); + } + + protected void removeAllRecordingContiners() { + commandLine.executeCommand("docker ps -a | awk '{ print $1,$2 }' | grep " + RECORDING_IMAGE + + " | awk '{print $1 }' | xargs -I {} docker rm -f {}"); + } + +} diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java new file mode 100644 index 00000000..00774e0f --- /dev/null +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduProTestAppE2eTest.java @@ -0,0 +1,263 @@ +package io.openvidu.test.e2e; + +import java.io.File; +import java.io.FileReader; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.http.HttpStatus; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.Alert; +import org.openqa.selenium.By; +import org.openqa.selenium.Keys; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import com.mashape.unirest.http.HttpMethod; + +import io.openvidu.java.client.OpenVidu; +import io.openvidu.java.client.Recording; +import io.openvidu.test.browsers.utils.CustomHttpClient; +import io.openvidu.test.browsers.utils.Unzipper; + +public class OpenViduProTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { + + @Test + @DisplayName("Individual dynamic record") + void individualDynamicRecordTest() throws Exception { + isRecordingTest = true; + + setupBrowser("chrome"); + + log.info("Individual dynamic record"); + + CustomHttpClient restClient = new CustomHttpClient(OpenViduTestAppE2eTest.OPENVIDU_URL, "OPENVIDUAPP", + OpenViduTestAppE2eTest.OPENVIDU_SECRET); + + // Connect 3 users. Record only the first one + for (int i = 0; i < 3; i++) { + user.getDriver().findElement(By.id("add-user-btn")).click(); + if (i > 0) { + user.getDriver().findElement(By.id("session-settings-btn-" + i)).click(); + Thread.sleep(1000); + user.getDriver().findElement(By.id("record-checkbox")).click(); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + } + } + + String sessionName = "TestSession"; + + user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); + user.getEventManager().waitUntilEventReaches("streamPlaying", 9); + + // Start the recording for one of the not recorded users + JsonObject sessionInfo = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/" + sessionName, + HttpStatus.SC_OK); + JsonArray connections = sessionInfo.get("connections").getAsJsonObject().get("content").getAsJsonArray(); + String connectionId1 = null; + String streamId1 = null; + // Get connectionId and streamId + for (JsonElement connection : connections) { + if (connection.getAsJsonObject().get("record").getAsBoolean()) { + connectionId1 = connection.getAsJsonObject().get("connectionId").getAsString(); + streamId1 = connection.getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject() + .get("streamId").getAsString(); + break; + } + } + + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", + "{'session':'" + sessionName + "','outputMode':'INDIVIDUAL'}", HttpStatus.SC_OK); + user.getEventManager().waitUntilEventReaches("recordingStarted", 3); + Thread.sleep(1000); + + // Start the recording for one of the not recorded users + sessionInfo = restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/" + sessionName, HttpStatus.SC_OK); + connections = sessionInfo.get("connections").getAsJsonObject().get("content").getAsJsonArray(); + String connectionId2 = null; + String streamId2 = null; + // Get connectionId and streamId + for (JsonElement connection : connections) { + if (!connection.getAsJsonObject().get("record").getAsBoolean()) { + connectionId2 = connection.getAsJsonObject().get("connectionId").getAsString(); + streamId2 = connection.getAsJsonObject().get("publishers").getAsJsonArray().get(0).getAsJsonObject() + .get("streamId").getAsString(); + break; + } + } + + // Generate 3 total recordings of 1 second length for this same stream + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':false}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/" + sessionName + "/connection/" + connectionId2, + "{'record':true}", HttpStatus.SC_OK); + Thread.sleep(1000); + + restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/" + sessionName, HttpStatus.SC_OK); + user.getEventManager().waitUntilEventReaches("recordingStopped", 3); + + gracefullyLeaveParticipants(3); + + String recPath = "/opt/openvidu/recordings/" + sessionName + "/"; + Recording recording = new OpenVidu(OpenViduTestAppE2eTest.OPENVIDU_URL, OpenViduTestAppE2eTest.OPENVIDU_SECRET) + .getRecording(sessionName); + checkIndividualRecording(recPath, recording, 4, "opus", "vp8", true); + + // Analyze INDIVIDUAL recording metadata + new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); + File jsonSyncFile = new File(recPath + recording.getName() + ".json"); + JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); + JsonObject jsonMetadata = new Gson().fromJson(reader, JsonObject.class); + JsonArray syncArray = jsonMetadata.get("files").getAsJsonArray(); + int count1 = 0; + int count2 = 0; + List names = Stream.of(streamId2 + ".webm", streamId2 + "-1.webm", streamId2 + "-2.webm") + .collect(Collectors.toList()); + for (JsonElement fileJson : syncArray) { + JsonObject file = fileJson.getAsJsonObject(); + String fileStreamId = file.get("streamId").getAsString(); + if (fileStreamId.equals(streamId1)) { + // Normal recorded user + Assert.assertEquals("Wrong connectionId file metadata property", connectionId1, + file.get("connectionId").getAsString()); + long msDuration = file.get("endTimeOffset").getAsLong() - file.get("startTimeOffset").getAsLong(); + Assert.assertTrue("Wrong recording duration of individual file. Difference: " + (msDuration - 4000), + msDuration - 4000 < 750); + count1++; + } else if (fileStreamId.equals(streamId2)) { + // Dynamically recorded user + Assert.assertEquals("Wrong connectionId file metadata property", connectionId2, + file.get("connectionId").getAsString()); + long msDuration = file.get("endTimeOffset").getAsLong() - file.get("startTimeOffset").getAsLong(); + Assert.assertTrue( + "Wrong recording duration of individual file. Difference: " + Math.abs(msDuration - 1000), + Math.abs(msDuration - 1000) < 100); + Assert.assertTrue("File name not found among " + names.toString(), + names.remove(file.get("name").getAsString())); + count2++; + } else { + Assert.fail("Metadata file element does not belong to a known stream (" + fileStreamId + ")"); + } + } + Assert.assertEquals("Wrong number of recording files for stream " + streamId1, 1, count1); + Assert.assertEquals("Wrong number of recording files for stream " + streamId2, 3, count2); + Assert.assertTrue("Some expected file name didn't existed: " + names.toString(), names.isEmpty()); + } + + @Test + @DisplayName("REST API PRO test") + void restApiProTest() throws Exception { + + setupBrowser("chrome"); + + log.info("REST API PRO test"); + + CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); + + /** + * PATCH /openvidu/api/sessions//connection/ + **/ + String body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; + restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); + body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'SUBSCRIBER'}"; + JsonObject res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK); + final String token = res.get("token").getAsString(); + final String tokenConnectionId = res.get("connectionId").getAsString(); + + user.getDriver().findElement(By.id("add-user-btn")).click(); + user.getDriver().findElement(By.id("session-settings-btn-0")).click(); + Thread.sleep(1000); + + // Set token + WebElement tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); + tokenInput.clear(); + tokenInput.sendKeys(token); + // Force publishing even SUBSCRIBER + user.getDriver().findElement(By.id("force-publishing-checkbox")).click(); + user.getDriver().findElement(By.id("save-btn")).click(); + Thread.sleep(1000); + + user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).sendKeys(Keys.ENTER); + + user.getEventManager().waitUntilEventReaches("connectionCreated", 1); + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + + try { + user.getWaiter().until(ExpectedConditions.alertIsPresent()); + Alert alert = user.getDriver().switchTo().alert(); + Assert.assertTrue("Alert does not contain expected text", + alert.getText().equals("OPENVIDU_PERMISSION_DENIED: You don't have permissions to publish")); + alert.accept(); + } catch (Exception e) { + Assert.fail("Alert exception"); + } + Thread.sleep(500); + + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'role':false}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'record':123}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'role':'PUBLISHER',record:'WRONG'}", HttpStatus.SC_BAD_REQUEST); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'role':'SUBSCRIBER','record':true}", HttpStatus.SC_NO_CONTENT); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'role':'PUBLISHER'}", HttpStatus.SC_OK); + restClient.rest(HttpMethod.PATCH, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenConnectionId, + "{'role':'PUBLISHER'}", HttpStatus.SC_NO_CONTENT); + + user.getEventManager().resetEventThread(); + + user.getWaiter().until(ExpectedConditions.elementToBeClickable(By.cssSelector(".republish-error-btn"))); + user.getDriver().findElement(By.cssSelector(".republish-error-btn")).click(); + + user.getEventManager().waitUntilEventReaches("accessAllowed", 1); + user.getEventManager().waitUntilEventReaches("streamPlaying", 1); + user.getEventManager().waitUntilEventReaches("streamCreated", 1); + + // connectionId should be equal to the one brought by the token + Assert.assertEquals("Wrong connectionId", tokenConnectionId, + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_OK) + .get("connections").getAsJsonObject().get("content").getAsJsonArray().get(0).getAsJsonObject() + .get("connectionId").getAsString()); + + restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID", HttpStatus.SC_NO_CONTENT); + + // GET /openvidu/api/sessions should return empty again + restClient.rest(HttpMethod.GET, "/openvidu/api/sessions", null, HttpStatus.SC_OK, true, + ImmutableMap.of("numberOfElements", new Integer(0), "content", new JsonArray())); + + /** GET /openvidu/api/config **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/config", null, HttpStatus.SC_OK, true, + "{'VERSION':'STR','DOMAIN_OR_PUBLIC_IP':'STR','HTTPS_PORT':0,'OPENVIDU_PUBLICURL':'STR','OPENVIDU_CDR':false,'OPENVIDU_STREAMS_VIDEO_MAX_RECV_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_RECV_BANDWIDTH':0," + + "'OPENVIDU_STREAMS_VIDEO_MAX_SEND_BANDWIDTH':0,'OPENVIDU_STREAMS_VIDEO_MIN_SEND_BANDWIDTH':0,'OPENVIDU_SESSIONS_GARBAGE_INTERVAL':0,'OPENVIDU_SESSIONS_GARBAGE_THRESHOLD':0," + + "'OPENVIDU_RECORDING':false,'OPENVIDU_RECORDING_VERSION':'STR','OPENVIDU_RECORDING_PATH':'STR','OPENVIDU_RECORDING_PUBLIC_ACCESS':false,'OPENVIDU_RECORDING_NOTIFICATION':'STR'," + + "'OPENVIDU_RECORDING_CUSTOM_LAYOUT':'STR','OPENVIDU_RECORDING_AUTOSTOP_TIMEOUT':0,'OPENVIDU_WEBHOOK':false,'OPENVIDU_WEBHOOK_ENDPOINT':'STR','OPENVIDU_WEBHOOK_HEADERS':[]," + + "'OPENVIDU_WEBHOOK_EVENTS':[],'OPENVIDU_SERVER_DEPENDENCY_VERSION':'STR','KMS_URIS':[],'OPENVIDU_PRO_STATS_MONITORING_INTERVAL':0,'OPENVIDU_PRO_STATS_WEBRTC_INTERVAL':0,'OPENVIDU_PRO_CLUSTER_ID':'STR'," + + "'OPENVIDU_PRO_CLUSTER_ENVIRONMENT':'STR','OPENVIDU_PRO_CLUSTER_MEDIA_NODES':0,'OPENVIDU_PRO_CLUSTER_PATH':'STR','OPENVIDU_PRO_CLUSTER_AUTOSCALING':false,'OPENVIDU_PRO_NETWORK_STAT':false," + + "'OPENVIDU_PRO_ELASTICSEARCH':false,'OPENVIDU_PRO_KIBANA':false,'OPENVIDU_PRO_RECORDING_STORAGE':'STR'}"); + + /** GET /openvidu/api/health **/ + restClient.rest(HttpMethod.GET, "/openvidu/api/health", null, HttpStatus.SC_OK, true, + ImmutableMap.of("status", "UP")); + } + +} diff --git a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java index f90eaa56..665e1d44 100644 --- a/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java +++ b/openvidu-test-e2e/src/test/java/io/openvidu/test/e2e/OpenViduTestAppE2eTest.java @@ -18,23 +18,11 @@ package io.openvidu.test.e2e; import static org.junit.Assert.fail; -import static org.openqa.selenium.OutputType.BASE64; -import java.awt.Color; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.math.RoundingMode; -import java.nio.file.Path; import java.nio.file.Paths; -import java.text.DecimalFormat; -import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; -import java.util.Collection; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -42,21 +30,9 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.imageio.ImageIO; - -import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; -import org.jcodec.api.FrameGrab; -import org.jcodec.api.JCodecException; -import org.jcodec.common.model.Picture; -import org.jcodec.scale.AWTUtil; 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.Tag; import org.junit.jupiter.api.Test; @@ -65,30 +41,21 @@ import org.openqa.selenium.Alert; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.Keys; -import org.openqa.selenium.TakesScreenshot; -import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.ExpectedConditions; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.google.common.collect.ImmutableMap; -import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.google.gson.stream.JsonReader; import com.mashape.unirest.http.HttpMethod; -import io.github.bonigarcia.wdm.WebDriverManager; import io.openvidu.java.client.Connection; import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.OpenVidu; import io.openvidu.java.client.OpenViduHttpException; -import io.openvidu.java.client.OpenViduJavaClientException; import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.Publisher; import io.openvidu.java.client.Recording; @@ -99,15 +66,8 @@ import io.openvidu.java.client.RecordingProperties; import io.openvidu.java.client.Session; import io.openvidu.java.client.SessionProperties; import io.openvidu.java.client.TokenOptions; -import io.openvidu.test.browsers.BrowserUser; -import io.openvidu.test.browsers.ChromeAndroidUser; -import io.openvidu.test.browsers.ChromeUser; import io.openvidu.test.browsers.FirefoxUser; -import io.openvidu.test.browsers.OperaUser; -import io.openvidu.test.browsers.utils.CommandLineExecutor; import io.openvidu.test.browsers.utils.CustomHttpClient; -import io.openvidu.test.browsers.utils.MultimediaFileMetadata; -import io.openvidu.test.browsers.utils.Unzipper; import io.openvidu.test.browsers.utils.layout.CustomLayoutHandler; import io.openvidu.test.browsers.utils.webhook.CustomWebhook; @@ -120,195 +80,7 @@ import io.openvidu.test.browsers.utils.webhook.CustomWebhook; @Tag("e2e") @DisplayName("E2E tests for OpenVidu TestApp") @ExtendWith(SpringExtension.class) -public class OpenViduTestAppE2eTest { - - static String OPENVIDU_SECRET = "MY_SECRET"; - static String OPENVIDU_URL = "https://localhost:4443/"; - static String APP_URL = "http://localhost:4200/"; - static String EXTERNAL_CUSTOM_LAYOUT_URL = "http://localhost:5555"; - static String EXTERNAL_CUSTOM_LAYOUT_PARAMS = "sessionId,CUSTOM_LAYOUT_SESSION,secret,MY_SECRET"; - static Exception ex = null; - private final Object lock = new Object(); - - private static final Logger log = LoggerFactory.getLogger(OpenViduTestAppE2eTest.class); - private static final CommandLineExecutor commandLine = new CommandLineExecutor(); - private static final String RECORDING_IMAGE = "openvidu/openvidu-recording"; - - MyUser user; - Collection otherUsers = new ArrayList<>(); - volatile static boolean isRecordingTest; - volatile static boolean isKurentoRestartTest; - private static OpenVidu OV; - - @BeforeAll() - static void setupAll() { - - String ffmpegOutput = commandLine.executeCommand("which ffmpeg"); - if (ffmpegOutput == null || ffmpegOutput.isEmpty()) { - log.error("ffmpeg package is not installed in the host machine"); - Assert.fail(); - return; - } else { - log.info("ffmpeg is installed and accesible"); - } - - WebDriverManager.chromedriver().setup(); - WebDriverManager.firefoxdriver().setup(); - - String appUrl = System.getProperty("APP_URL"); - if (appUrl != null) { - APP_URL = appUrl; - } - log.info("Using URL {} to connect to openvidu-testapp", APP_URL); - - String externalCustomLayoutUrl = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_URL"); - if (externalCustomLayoutUrl != null) { - EXTERNAL_CUSTOM_LAYOUT_URL = externalCustomLayoutUrl; - } - log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_URL); - - String externalCustomLayoutParams = System.getProperty("EXTERNAL_CUSTOM_LAYOUT_PARAMS"); - if (externalCustomLayoutParams != null) { - // Parse external layout parameters and build a URL formatted params string - List params = Stream.of(externalCustomLayoutParams.split(",", -1)).collect(Collectors.toList()); - if (params.size() % 2 != 0) { - log.error( - "Wrong configuration property EXTERNAL_CUSTOM_LAYOUT_PARAMS. Must be a comma separated list with an even number of elements. e.g: EXTERNAL_CUSTOM_LAYOUT_PARAMS=param1,value1,param2,value2"); - Assert.fail(); - return; - } else { - EXTERNAL_CUSTOM_LAYOUT_PARAMS = ""; - for (int i = 0; i < params.size(); i++) { - if (i % 2 == 0) { - // Param name - EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i) + "="; - } else { - // Param value - EXTERNAL_CUSTOM_LAYOUT_PARAMS += params.get(i); - if (i < params.size() - 1) { - EXTERNAL_CUSTOM_LAYOUT_PARAMS += "&"; - } - } - } - } - } - log.info("Using URL {} to connect to external custom layout", EXTERNAL_CUSTOM_LAYOUT_PARAMS); - - 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); - - 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); - } - - void setupBrowser(String browser) { - - BrowserUser browserUser; - - switch (browser) { - case "chrome": - browserUser = new ChromeUser("TestUser", 50, false); - break; - case "firefox": - browserUser = new FirefoxUser("TestUser", 50); - break; - case "opera": - browserUser = new OperaUser("TestUser", 50); - break; - case "chromeAndroid": - browserUser = new ChromeAndroidUser("TestUser", 50); - break; - case "chromeAlternateScreenShare": - browserUser = new ChromeUser("TestUser", 50, "OpenVidu TestApp", false); - break; - case "chromeAsRoot": - browserUser = new ChromeUser("TestUser", 50, true); - break; - default: - browserUser = new ChromeUser("TestUser", 50, false); - } - - this.user = new MyUser(browserUser); - - user.getDriver().get(APP_URL); - - WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); - urlInput.clear(); - urlInput.sendKeys(OPENVIDU_URL); - WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); - secretInput.clear(); - secretInput.sendKeys(OPENVIDU_SECRET); - - user.getEventManager().startPolling(); - } - - void setupChromeWithFakeVideo(Path videoFileLocation) { - this.user = new MyUser(new ChromeUser("TestUser", 50, videoFileLocation)); - user.getDriver().get(APP_URL); - WebElement urlInput = user.getDriver().findElement(By.id("openvidu-url")); - urlInput.clear(); - urlInput.sendKeys(OPENVIDU_URL); - WebElement secretInput = user.getDriver().findElement(By.id("openvidu-secret")); - secretInput.clear(); - secretInput.sendKeys(OPENVIDU_SECRET); - user.getEventManager().startPolling(); - } - - @AfterEach - void dispose() { - if (user != null) { - user.dispose(); - } - Iterator it = otherUsers.iterator(); - while (it.hasNext()) { - MyUser other = it.next(); - other.dispose(); - it.remove(); - } - 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 e) { - log.error("Error closing session: {}", e.getMessage()); - } catch (OpenViduHttpException e) { - log.error("Error closing session: {}", e.getMessage()); - } - }); - if (isRecordingTest) { - removeAllRecordingContiners(); - try { - FileUtils.cleanDirectory(new File("/opt/openvidu/recordings")); - } catch (IOException e) { - log.error(e.getMessage()); - } - isRecordingTest = false; - } - if (isKurentoRestartTest) { - this.restartKms(); - isKurentoRestartTest = false; - } - OV = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET); - } +public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest { @Test @DisplayName("One2One Chrome [Video + Audio]") @@ -1583,46 +1355,6 @@ public class OpenViduTestAppE2eTest { gracefullyLeaveParticipants(2); } - @Test - @DisplayName("Individual dynamic record") - void individualDynamicRecordTest() throws Exception { - isRecordingTest = true; - - setupBrowser("chrome"); - - log.info("Individual dynamic record"); - - // Connect 3 users. Two of them not recorded - for (int i = 0; i < 3; i++) { - user.getDriver().findElement(By.id("add-user-btn")).click(); - if (i < 2) { - user.getDriver().findElement(By.id("session-settings-btn-" + i)).click(); - Thread.sleep(1000); - user.getDriver().findElement(By.id("record-checkbox")).click(); - user.getDriver().findElement(By.id("save-btn")).click(); - Thread.sleep(1000); - } - } - - String sessionName = "TestSession"; - - user.getDriver().findElements(By.className("join-btn")).forEach(el -> el.sendKeys(Keys.ENTER)); - user.getEventManager().waitUntilEventReaches("streamPlaying", 6); - - CustomHttpClient restClient = new CustomHttpClient(OPENVIDU_URL, "OPENVIDUAPP", OPENVIDU_SECRET); - restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/start", - "{'session':'" + sessionName + "','outputMode':'INDIVIDUAL'}", HttpStatus.SC_OK); - user.getEventManager().waitUntilEventReaches("recordingStarted", 3); - Thread.sleep(2000); - restClient.rest(HttpMethod.POST, "/openvidu/api/recordings/stop/" + sessionName, HttpStatus.SC_OK); - user.getEventManager().waitUntilEventReaches("recordingStopped", 3); - - String recPath = "/opt/openvidu/recordings/" + sessionName + "/"; - Recording recording = new OpenVidu(OPENVIDU_URL, OPENVIDU_SECRET).getRecording(sessionName); - this.checkIndividualRecording(recPath, recording, 1, "opus", "vp8", true); - - } - @Test @DisplayName("Record cross-browser audio-only and video-only") void audioOnlyVideoOnlyRecordTest() throws Exception { @@ -3062,12 +2794,11 @@ public class OpenViduTestAppE2eTest { **/ body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); - body = "{'session': 'CUSTOM_SESSION_ID'}"; + body = "{'session': 'CUSTOM_SESSION_ID', 'role': 'SUBSCRIBER'}"; res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK); final String tokenAConnectionId = res.get("connectionId").getAsString(); final String tokenA = res.get("token").getAsString(); res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK); - final String tokenB = res.get("token").getAsString(); final String tokenBConnectionId = res.get("connectionId").getAsString(); user.getDriver().findElement(By.id("one2one-btn")).click(); @@ -3079,15 +2810,6 @@ public class OpenViduTestAppE2eTest { tokenInput.clear(); tokenInput.sendKeys(tokenA); - user.getDriver().findElement(By.id("save-btn")).click(); - Thread.sleep(1000); - user.getDriver().findElement(By.id("session-settings-btn-1")).click(); - Thread.sleep(1000); - - // Set token 2 - tokenInput = user.getDriver().findElement(By.cssSelector("#custom-token-div input")); - tokenInput.clear(); - tokenInput.sendKeys(tokenB); user.getDriver().findElement(By.id("save-btn")).click(); Thread.sleep(1000); @@ -3095,7 +2817,7 @@ public class OpenViduTestAppE2eTest { restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenAConnectionId, HttpStatus.SC_NO_CONTENT); - // First user should pop up invalid token + // User should pop up invalid token user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).sendKeys(Keys.ENTER); try { user.getWaiter().until(ExpectedConditions.alertIsPresent()); @@ -3694,295 +3416,4 @@ public class OpenViduTestAppE2eTest { } } - private void listEmptyRecordings() { - // List existing recordings (empty) - user.getDriver().findElement(By.id("list-recording-btn")).click(); - user.getWaiter() - .until(ExpectedConditions.attributeToBe(By.id("api-response-text-area"), "value", "Recording list []")); - } - - private ExpectedCondition waitForVideoDuration(WebElement element, int durationInSeconds) { - return new ExpectedCondition() { - @Override - public Boolean apply(WebDriver input) { - return element.getAttribute("duration").matches( - durationInSeconds - 1 + "\\.[5-9][0-9]{0,5}|" + durationInSeconds + "\\.[0-5][0-9]{0,5}"); - } - }; - } - - private static boolean checkVideoAverageRgbGreen(Map rgb) { - // GREEN color: {r < 15, g > 130, b <15} - return (rgb.get("r") < 15) && (rgb.get("g") > 130) && (rgb.get("b") < 15); - } - - private static boolean checkVideoAverageRgbGray(Map rgb) { - // GRAY color: {r < 50, g < 50, b < 50} and the absolute difference between them - // not greater than 2 - return (rgb.get("r") < 50) && (rgb.get("g") < 50) && (rgb.get("b") < 50) - && (Math.abs(rgb.get("r") - rgb.get("g")) <= 2) && (Math.abs(rgb.get("r") - rgb.get("b")) <= 2) - && (Math.abs(rgb.get("b") - rgb.get("g")) <= 2); - } - - private static boolean checkVideoAverageRgbRed(Map rgb) { - // RED color: {r > 240, g < 15, b <15} - return (rgb.get("r") > 240) && (rgb.get("g") < 15) && (rgb.get("b") < 15); - } - - private void gracefullyLeaveParticipants(int numberOfParticipants) throws Exception { - int accumulatedConnectionDestroyed = 0; - for (int j = 1; j <= numberOfParticipants; j++) { - user.getDriver().findElement(By.id("remove-user-btn")).sendKeys(Keys.ENTER); - user.getEventManager().waitUntilEventReaches("sessionDisconnected", j); - accumulatedConnectionDestroyed = (j != numberOfParticipants) - ? (accumulatedConnectionDestroyed + numberOfParticipants - j) - : (accumulatedConnectionDestroyed); - user.getEventManager().waitUntilEventReaches("connectionDestroyed", accumulatedConnectionDestroyed); - } - } - - private String getBase64Screenshot(MyUser user) throws Exception { - String screenshotBase64 = ((TakesScreenshot) user.getDriver()).getScreenshotAs(BASE64); - return "data:image/png;base64," + screenshotBase64; - } - - private boolean recordedFileFine(File file, Recording recording, - Function, Boolean> colorCheckFunction) throws IOException { - this.checkMultimediaFile(file, recording.hasAudio(), recording.hasVideo(), recording.getDuration(), - recording.getResolution(), "aac", "h264", true); - - boolean isFine = false; - Picture frame; - try { - // Get a frame at 75% duration and check that it has the expected color - frame = FrameGrab.getFrameAtSec(file, (double) (recording.getDuration() * 0.75)); - BufferedImage image = AWTUtil.toBufferedImage(frame); - Map colorMap = this.averageColor(image); - - String realResolution = image.getWidth() + "x" + image.getHeight(); - Assert.assertEquals( - "Resolution (" + recording.getResolution() - + ") of recording entity is not equal to real video resolution (" + realResolution + ")", - recording.getResolution(), realResolution); - - log.info("Recording map color: {}", colorMap.toString()); - log.info("Recording frame below"); - System.out.println(bufferedImageToBase64PngString(image)); - isFine = colorCheckFunction.apply(colorMap); - } catch (IOException | JCodecException e) { - log.warn("Error getting frame from video recording: {}", e.getMessage()); - isFine = false; - } - return isFine; - } - - private boolean recordedGreenFileFine(File file, Recording recording) throws IOException { - return this.recordedFileFine(file, recording, OpenViduTestAppE2eTest::checkVideoAverageRgbGreen); - } - - private boolean recordedRedFileFine(File file, Recording recording) throws IOException { - return this.recordedFileFine(file, recording, OpenViduTestAppE2eTest::checkVideoAverageRgbRed); - } - - private String bufferedImageToBase64PngString(BufferedImage image) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - String imageString = null; - try { - ImageIO.write(image, "png", bos); - byte[] imageBytes = bos.toByteArray(); - imageString = "data:image/png;base64," + Base64.getEncoder().encodeToString(imageBytes); - bos.close(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - return imageString; - } - - private void checkIndividualRecording(String recPath, Recording recording, int numberOfVideoFiles, - String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { - - // Should be only 2 files: zip and metadata - File folder = new File(recPath); - Assert.assertEquals("There are more than 2 files (ZIP and metadata) inside individual recording folder " - + recPath + ": " + Arrays.toString(folder.listFiles()), 2, folder.listFiles().length); - - File file1 = new File(recPath + recording.getName() + ".zip"); - File file2 = new File(recPath + ".recording." + recording.getId()); - - Assert.assertTrue("File " + file1.getAbsolutePath() + " does not exist or is empty", - file1.exists() && file1.length() > 0); - Assert.assertTrue("File " + file2.getAbsolutePath() + " does not exist or is empty", - file2.exists() && file2.length() > 0); - - List unzippedWebmFiles = new Unzipper().unzipFile(recPath, recording.getName() + ".zip"); - - Assert.assertEquals("Expecting " + numberOfVideoFiles + " videos inside ZIP file but " - + unzippedWebmFiles.size() + " found: " + unzippedWebmFiles.toString(), numberOfVideoFiles, - unzippedWebmFiles.size()); - - File jsonSyncFile = new File(recPath + recording.getName() + ".json"); - Assert.assertTrue("JSON sync file " + jsonSyncFile.getAbsolutePath() + "does not exist or is empty", - jsonSyncFile.exists() && jsonSyncFile.length() > 0); - - JsonObject jsonSyncMetadata; - try { - Gson gson = new Gson(); - JsonReader reader = new JsonReader(new FileReader(jsonSyncFile)); - jsonSyncMetadata = gson.fromJson(reader, JsonObject.class); - } catch (Exception e) { - log.error("Cannot read JSON sync metadata file from {}. Error: {}", jsonSyncFile.getAbsolutePath(), - e.getMessage()); - Assert.fail("Cannot read JSON sync metadata file from " + jsonSyncFile.getAbsolutePath()); - return; - } - - long totalFileSize = 0; - JsonArray syncArray = jsonSyncMetadata.get("files").getAsJsonArray(); - for (File webmFile : unzippedWebmFiles) { - totalFileSize += webmFile.length(); - - Assert.assertTrue("WEBM file " + webmFile.getAbsolutePath() + " does not exist or is empty", - webmFile.exists() && webmFile.length() > 0); - - double durationInSeconds = 0; - boolean found = false; - for (int i = 0; i < syncArray.size(); i++) { - JsonObject j = syncArray.get(i).getAsJsonObject(); - if (webmFile.getName().contains(j.get("streamId").getAsString())) { - durationInSeconds = (double) (j.get("endTimeOffset").getAsDouble() - - j.get("startTimeOffset").getAsDouble()) / 1000; - found = true; - break; - } - } - - Assert.assertTrue("Couldn't find in JSON sync object information for webm file " + webmFile.getName(), - found); - - log.info("Duration of {} according to sync metadata json file: {} s", webmFile.getName(), - durationInSeconds); - this.checkMultimediaFile(webmFile, recording.hasAudio(), recording.hasVideo(), durationInSeconds, - recording.getResolution(), audioDecoder, videoDecoder, checkAudio); - webmFile.delete(); - } - - Assert.assertEquals("Size of recording entity (" + recording.getSessionId() - + ") is not equal to real file size (" + totalFileSize + ")", recording.getSize(), totalFileSize); - - jsonSyncFile.delete(); - } - - private void checkMultimediaFile(File file, boolean hasAudio, boolean hasVideo, double duration, String resolution, - String audioDecoder, String videoDecoder, boolean checkAudio) throws IOException { - // Check tracks, duration, resolution, framerate and decoders - MultimediaFileMetadata metadata = new MultimediaFileMetadata(file.getAbsolutePath()); - - if (hasVideo) { - if (checkAudio) { - if (hasAudio) { - Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have audio", - metadata.hasAudio() && metadata.hasVideo()); - Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); - } else { - Assert.assertTrue("Media file " + file.getAbsolutePath() + " should have video", - metadata.hasVideo()); - Assert.assertFalse(metadata.hasAudio()); - } - } - if (resolution != null) { - Assert.assertEquals(resolution, metadata.getVideoWidth() + "x" + metadata.getVideoHeight()); - } - Assert.assertTrue(metadata.getVideoDecoder().toLowerCase().contains(videoDecoder)); - } else if (hasAudio && checkAudio) { - Assert.assertTrue(metadata.hasAudio()); - Assert.assertFalse(metadata.hasVideo()); - Assert.assertTrue(metadata.getAudioDecoder().toLowerCase().contains(audioDecoder)); - } else { - Assert.fail("Cannot check a file witho no audio and no video"); - } - // Check duration with 1 decimal precision - DecimalFormat df = new DecimalFormat("#0.0"); - df.setRoundingMode(RoundingMode.UP); - log.info("Duration of {} according to ffmpeg: {} s", file.getName(), metadata.getDuration()); - log.info("Duration of {} according to 'duration' property: {} s", file.getName(), duration); - log.info("Difference in s duration: {}", Math.abs(metadata.getDuration() - duration)); - final double difference = 10; - Assert.assertTrue( - "Difference between recording entity duration (" + duration + ") and real video duration (" - + metadata.getDuration() + ") is greater than " + difference + " in file " + file.getName(), - Math.abs((metadata.getDuration() - duration)) < difference); - } - - private boolean thumbnailIsFine(File file, Function, Boolean> colorCheckFunction) { - boolean isFine = false; - BufferedImage image = null; - try { - image = ImageIO.read(file); - } catch (IOException e) { - log.error(e.getMessage()); - return false; - } - log.info("Recording thumbnail dimensions: {}x{}", image.getWidth(), image.getHeight()); - Map colorMap = this.averageColor(image); - log.info("Thumbnail map color: {}", colorMap.toString()); - isFine = colorCheckFunction.apply(colorMap); - return isFine; - } - - private Map averageColor(BufferedImage bi) { - int x0 = 0; - int y0 = 0; - int w = bi.getWidth(); - int h = bi.getHeight(); - int x1 = x0 + w; - int y1 = y0 + h; - long sumr = 0, sumg = 0, sumb = 0; - for (int x = x0; x < x1; x++) { - for (int y = y0; y < y1; y++) { - Color pixel = new Color(bi.getRGB(x, y)); - sumr += pixel.getRed(); - sumg += pixel.getGreen(); - sumb += pixel.getBlue(); - } - } - int num = w * h; - Map colorMap = new HashMap<>(); - colorMap.put("r", (long) (sumr / num)); - colorMap.put("g", (long) (sumg / num)); - colorMap.put("b", (long) (sumb / num)); - return colorMap; - } - - private void startKms() { - log.info("Starting KMS"); - commandLine.executeCommand("/usr/bin/kurento-media-server &>> /kms.log &"); - } - - private void stopKms() { - log.info("Stopping KMS"); - commandLine.executeCommand("kill -9 $(pidof kurento-media-server)"); - } - - private void restartKms() { - this.stopKms(); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - this.startKms(); - } - - private void checkDockerContainerRunning(String imageName, int amount) { - int number = Integer.parseInt(commandLine.executeCommand("docker ps | grep " + imageName + " | wc -l")); - Assert.assertEquals("Wrong number of Docker containers for image " + imageName + " running", amount, number); - } - - private void removeAllRecordingContiners() { - commandLine.executeCommand("docker ps -a | awk '{ print $1,$2 }' | grep " + RECORDING_IMAGE - + " | awk '{print $1 }' | xargs -I {} docker rm -f {}"); - } - }