mirror of https://github.com/OpenVidu/openvidu.git
openvidu-test-e2e: new Pro class. Parent class AbstractOpenViduTestAppE2eTest
parent
2e919b69e6
commit
63a6261d88
|
@ -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 {}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {}");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue