openvidu-test-e2e: new Pro class. Parent class AbstractOpenViduTestAppE2eTest

pull/550/head
pabloFuente 2020-10-07 12:12:28 +02:00
parent 2e919b69e6
commit 63a6261d88
3 changed files with 812 additions and 572 deletions

View File

@ -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<MyUser> 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<String> 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<MyUser> 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<Boolean> waitForVideoDuration(WebElement element, int durationInSeconds) {
return new ExpectedCondition<Boolean>() {
@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<String, Long> 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<String, Long> 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<String, Long> 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<Map<String, Long>, 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<String, Long> 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<File> 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<Map<String, Long>, 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<String, Long> colorMap = this.averageColor(image);
log.info("Thumbnail map color: {}", colorMap.toString());
isFine = colorCheckFunction.apply(colorMap);
return isFine;
}
protected Map<String, Long> 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<String, Long> 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 {}");
}
}

View File

@ -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<String> 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/<SESSION_ID>/connection/<CONNECTION_ID>
**/
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"));
}
}

View File

@ -18,23 +18,11 @@
package io.openvidu.test.e2e; package io.openvidu.test.e2e;
import static org.junit.Assert.fail; 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.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.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -42,21 +30,9 @@ import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; 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.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.Assert;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -65,30 +41,21 @@ import org.openqa.selenium.Alert;
import org.openqa.selenium.By; import org.openqa.selenium.By;
import org.openqa.selenium.Dimension; import org.openqa.selenium.Dimension;
import org.openqa.selenium.Keys; import org.openqa.selenium.Keys;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement; import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.ExpectedConditions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.junit.jupiter.SpringExtension;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.google.gson.stream.JsonReader;
import com.mashape.unirest.http.HttpMethod; import com.mashape.unirest.http.HttpMethod;
import io.github.bonigarcia.wdm.WebDriverManager;
import io.openvidu.java.client.Connection; import io.openvidu.java.client.Connection;
import io.openvidu.java.client.KurentoOptions; import io.openvidu.java.client.KurentoOptions;
import io.openvidu.java.client.MediaMode; import io.openvidu.java.client.MediaMode;
import io.openvidu.java.client.OpenVidu; import io.openvidu.java.client.OpenVidu;
import io.openvidu.java.client.OpenViduHttpException; import io.openvidu.java.client.OpenViduHttpException;
import io.openvidu.java.client.OpenViduJavaClientException;
import io.openvidu.java.client.OpenViduRole; import io.openvidu.java.client.OpenViduRole;
import io.openvidu.java.client.Publisher; import io.openvidu.java.client.Publisher;
import io.openvidu.java.client.Recording; 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.Session;
import io.openvidu.java.client.SessionProperties; import io.openvidu.java.client.SessionProperties;
import io.openvidu.java.client.TokenOptions; 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.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.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.layout.CustomLayoutHandler;
import io.openvidu.test.browsers.utils.webhook.CustomWebhook; import io.openvidu.test.browsers.utils.webhook.CustomWebhook;
@ -120,195 +80,7 @@ import io.openvidu.test.browsers.utils.webhook.CustomWebhook;
@Tag("e2e") @Tag("e2e")
@DisplayName("E2E tests for OpenVidu TestApp") @DisplayName("E2E tests for OpenVidu TestApp")
@ExtendWith(SpringExtension.class) @ExtendWith(SpringExtension.class)
public class OpenViduTestAppE2eTest { public class OpenViduTestAppE2eTest extends AbstractOpenViduTestAppE2eTest {
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<MyUser> 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<String> 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<MyUser> 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);
}
@Test @Test
@DisplayName("One2One Chrome [Video + Audio]") @DisplayName("One2One Chrome [Video + Audio]")
@ -1583,46 +1355,6 @@ public class OpenViduTestAppE2eTest {
gracefullyLeaveParticipants(2); 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 @Test
@DisplayName("Record cross-browser audio-only and video-only") @DisplayName("Record cross-browser audio-only and video-only")
void audioOnlyVideoOnlyRecordTest() throws Exception { void audioOnlyVideoOnlyRecordTest() throws Exception {
@ -3062,12 +2794,11 @@ public class OpenViduTestAppE2eTest {
**/ **/
body = "{'customSessionId': 'CUSTOM_SESSION_ID'}"; body = "{'customSessionId': 'CUSTOM_SESSION_ID'}";
restClient.rest(HttpMethod.POST, "/openvidu/api/sessions", body, HttpStatus.SC_OK); 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); res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK);
final String tokenAConnectionId = res.get("connectionId").getAsString(); final String tokenAConnectionId = res.get("connectionId").getAsString();
final String tokenA = res.get("token").getAsString(); final String tokenA = res.get("token").getAsString();
res = restClient.rest(HttpMethod.POST, "/openvidu/api/tokens", body, HttpStatus.SC_OK); 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(); final String tokenBConnectionId = res.get("connectionId").getAsString();
user.getDriver().findElement(By.id("one2one-btn")).click(); user.getDriver().findElement(By.id("one2one-btn")).click();
@ -3079,15 +2810,6 @@ public class OpenViduTestAppE2eTest {
tokenInput.clear(); tokenInput.clear();
tokenInput.sendKeys(tokenA); 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(); user.getDriver().findElement(By.id("save-btn")).click();
Thread.sleep(1000); Thread.sleep(1000);
@ -3095,7 +2817,7 @@ public class OpenViduTestAppE2eTest {
restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenAConnectionId, restClient.rest(HttpMethod.DELETE, "/openvidu/api/sessions/CUSTOM_SESSION_ID/connection/" + tokenAConnectionId,
HttpStatus.SC_NO_CONTENT); 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); user.getDriver().findElement(By.cssSelector("#openvidu-instance-0 .join-btn")).sendKeys(Keys.ENTER);
try { try {
user.getWaiter().until(ExpectedConditions.alertIsPresent()); 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<Boolean> waitForVideoDuration(WebElement element, int durationInSeconds) {
return new ExpectedCondition<Boolean>() {
@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<String, Long> 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<String, Long> 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<String, Long> 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<Map<String, Long>, 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<String, Long> 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<File> 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<Map<String, Long>, 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<String, Long> colorMap = this.averageColor(image);
log.info("Thumbnail map color: {}", colorMap.toString());
isFine = colorCheckFunction.apply(colorMap);
return isFine;
}
private Map<String, Long> 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<String, Long> 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 {}");
}
} }