/* * (C) Copyright 2014 Kurento (http://kurento.org/) * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl-2.1.html * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * */ package com.kurento.kmf.test.client; import static com.kurento.kmf.common.PropertiesManager.getProperty; import java.awt.Color; import java.io.Closeable; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.SystemUtils; import org.openqa.selenium.By; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import org.openqa.selenium.firefox.FirefoxDriver; import org.openqa.selenium.firefox.FirefoxProfile; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.kurento.kmf.media.WebRtcEndpoint; import com.kurento.kmf.media.factory.KmfMediaApiProperties; import com.kurento.kmf.test.base.GridBrowserMediaApiTest; import com.kurento.kmf.test.services.AudioChannel; import com.kurento.kmf.test.services.KurentoServicesTestHelper; import com.kurento.kmf.test.services.Node; import com.kurento.kmf.test.services.Recorder; /** * Class that models the video tag (HTML5) in a web browser; it uses Selenium to * launch the real browser. * * @author Micael Gallego (micael.gallego@gmail.com) * @author Boni Garcia (bgarcia@gsyc.es) * @since 4.2.3 * @see <a href="http://www.seleniumhq.org/">Selenium</a> */ public class BrowserClient implements Closeable { public Logger log = LoggerFactory.getLogger(BrowserClient.class); private List<Thread> callbackThreads = new ArrayList<>(); private Map<String, CountDownLatch> countDownLatchEvents; private WebDriver driver; private String videoUrl; private int timeout; // seconds private double maxDistance; private String video; private String audio; private int serverPort; private Client client; private Browser browser; private boolean usePhysicalCam; private Node remoteNode; private int recordAudio; private int audioSampleRate; private AudioChannel audioChannel; private BrowserClient(Builder builder) { this.video = builder.video; this.audio = builder.audio; this.serverPort = builder.serverPort; this.client = builder.client; this.browser = builder.browser; this.usePhysicalCam = builder.usePhysicalCam; this.remoteNode = builder.remoteNode; this.recordAudio = builder.recordAudio; this.audioSampleRate = builder.audioSampleRate; this.audioChannel = builder.audioChannel; countDownLatchEvents = new HashMap<>(); timeout = 60; // default (60 seconds) maxDistance = 60.0; // default distance (for color comparison) String hostAddress = KmfMediaApiProperties.getThriftKmfAddress() .getHost(); // Setup Selenium initDriver(hostAddress); // Launch Browser driver.manage().timeouts(); driver.get("http://" + hostAddress + ":" + serverPort + client.toString()); } private void initDriver(String hostAddress) { Class<? extends WebDriver> driverClass = browser.getDriverClass(); int hubPort = getProperty("test.hub.port", GridBrowserMediaApiTest.DEFAULT_HUB_PORT); try { if (driverClass.equals(FirefoxDriver.class)) { FirefoxProfile profile = new FirefoxProfile(); // This flag avoids granting the access to the camera profile.setPreference("media.navigator.permission.disabled", true); if (remoteNode != null) { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability(FirefoxDriver.PROFILE, profile); capabilities.setBrowserName(DesiredCapabilities.firefox() .getBrowserName()); driver = new RemoteWebDriver(new URL("http://" + hostAddress + ":" + hubPort + "/wd/hub"), capabilities); } else { driver = new FirefoxDriver(profile); } if (!usePhysicalCam && video != null) { launchFakeCam(); } } else if (driverClass.equals(ChromeDriver.class)) { String chromedriver = null; if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_LINUX) { chromedriver = "chromedriver"; } else if (SystemUtils.IS_OS_WINDOWS) { chromedriver = "chromedriver.exe"; } System.setProperty("webdriver.chrome.driver", new File( "target/webdriver/" + chromedriver).getAbsolutePath()); ChromeOptions options = new ChromeOptions(); // This flag avoids grant the camera options.addArguments("--use-fake-ui-for-media-stream"); // This flag avoids warning in chrome. See: // https://code.google.com/p/chromedriver/issues/detail?id=799 options.addArguments("--test-type"); if (!usePhysicalCam) { // This flag makes using a synthetic video (green with // spinner) in webrtc. Or it is needed to combine with // use-file-for-fake-video-capture to use a file faking the // cam options.addArguments("--use-fake-device-for-media-stream"); if (video != null) { options.addArguments("--use-file-for-fake-video-capture=" + video); // Alternative: lauch fake cam also in Chrome // launchFakeCam(); } } if (remoteNode != null) { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability(ChromeOptions.CAPABILITY, options); capabilities.setBrowserName(DesiredCapabilities.chrome() .getBrowserName()); driver = new RemoteWebDriver(new URL("http://" + hostAddress + ":" + hubPort + "/wd/hub"), capabilities); } else { driver = new ChromeDriver(options); } } driver.manage().timeouts() .setScriptTimeout(timeout, TimeUnit.SECONDS); } catch (MalformedURLException e) { log.error("MalformedURLException in BrowserClient.initDriver", e); } } private void launchFakeCam() { FakeCam.getSingleton().launchCam(video); } public void setURL(String videoUrl) { this.videoUrl = videoUrl; } public void resetEvents() { driver.findElement(By.id("status")).clear(); } public void setColorCoordinates(int x, int y) { driver.findElement(By.id("x")).clear(); driver.findElement(By.id("y")).clear(); driver.findElement(By.id("x")).sendKeys(String.valueOf(x)); driver.findElement(By.id("y")).sendKeys(String.valueOf(y)); } public void subscribeEvents(String... eventType) { for (final String e : eventType) { CountDownLatch latch = new CountDownLatch(1); countDownLatchEvents.put(e, latch); this.addEventListener(e, new EventListener() { @Override public void onEvent(String event) { log.info("Event: {}", event); countDownLatchEvents.get(e).countDown(); } }); } } public boolean waitForEvent(final String eventType) throws InterruptedException { if (!countDownLatchEvents.containsKey(eventType)) { // We cannot wait for an event without previous subscription return false; } boolean result = countDownLatchEvents.get(eventType).await(timeout, TimeUnit.SECONDS); // Record local audio when playing event reaches the browser if (eventType.equalsIgnoreCase("playing") && recordAudio > 0) { if (remoteNode != null) { Recorder.recordRemote(remoteNode, recordAudio, audioSampleRate, audioChannel); } else { Recorder.record(recordAudio, audioSampleRate, audioChannel); } } countDownLatchEvents.remove(eventType); return result; } public void addEventListener(final String eventType, final EventListener eventListener) { Thread t = new Thread() { public void run() { ((JavascriptExecutor) driver) .executeScript("video.addEventListener('" + eventType + "', videoEvent, false);"); (new WebDriverWait(driver, timeout)) .until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return d.findElement(By.id("status")) .getAttribute("value") .equalsIgnoreCase(eventType); } }); eventListener.onEvent(eventType); } }; callbackThreads.add(t); t.setDaemon(true); t.start(); } public void start() { if (driver instanceof JavascriptExecutor) { ((JavascriptExecutor) driver).executeScript("play('" + videoUrl + "', false);"); } } public void showSpinners() { if (driver instanceof JavascriptExecutor) { ((JavascriptExecutor) driver) .executeScript("showSpinner('local');"); ((JavascriptExecutor) driver) .executeScript("showSpinner('video');"); } } public void stop() { if (driver instanceof JavascriptExecutor) { ((JavascriptExecutor) driver).executeScript("terminate();"); } } public void startRcvOnly() { if (driver instanceof JavascriptExecutor) { ((JavascriptExecutor) driver).executeScript("play('" + videoUrl + "', true);"); } } @SuppressWarnings("deprecation") public void close() { for (Thread t : callbackThreads) { t.stop(); } driver.quit(); driver = null; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public double getCurrentTime() { log.debug("getCurrentTime() called"); double currentTime = Double.parseDouble(driver.findElement( By.id("currentTime")).getAttribute("value")); log.debug("getCurrentTime() result: {}", currentTime); return currentTime; } public boolean color(Color expectedColor, final double seconds, int x, int y) { // Wait to be in the right time (new WebDriverWait(driver, timeout)) .until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { double time = Double.parseDouble(d.findElement( By.id("currentTime")).getAttribute("value")); return time > seconds; } }); setColorCoordinates(x, y); // Guard time to wait JavaScript function to detect the color (otherwise // race conditions could appear) try { Thread.sleep(200); } catch (InterruptedException e) { log.trace("InterruptedException in guard condition ({})", e.getMessage()); } return colorSimilarTo(expectedColor); } public boolean colorSimilarTo(Color expectedColor) { String[] realColor = driver.findElement(By.id("color")) .getAttribute("value").split(","); int red = Integer.parseInt(realColor[0]); int green = Integer.parseInt(realColor[1]); int blue = Integer.parseInt(realColor[2]); double distance = Math.sqrt((red - expectedColor.getRed()) * (red - expectedColor.getRed()) + (green - expectedColor.getGreen()) * (green - expectedColor.getGreen()) + (blue - expectedColor.getBlue()) * (blue - expectedColor.getBlue())); log.info("Color comparision: real {}, expected {}, distance {}", realColor, expectedColor, distance); return distance <= getMaxDistance(); } public double getMaxDistance() { return maxDistance; } public void setMaxDistance(double maxDistance) { this.maxDistance = maxDistance; } public void connectToWebRtcEndpoint(WebRtcEndpoint webRtcEndpoint, WebRtcChannel channel) { if (driver instanceof JavascriptExecutor) { String getSdpOffer = "getSdpOffer(" + channel.getAudio() + "," + channel.getVideo(); if (audio != null) { getSdpOffer += ",'" + audio + "');"; } else { getSdpOffer += ");"; } ((JavascriptExecutor) driver).executeScript(getSdpOffer); // Wait to valid sdpOffer (new WebDriverWait(driver, timeout)) .until(new ExpectedCondition<Boolean>() { public Boolean apply(WebDriver d) { return ((JavascriptExecutor) driver) .executeScript("return sdpOffer;") != null; } }); String sdpOffer = (String) ((JavascriptExecutor) driver) .executeScript("return sdpOffer;"); String sdpAnswer = webRtcEndpoint.processOffer(sdpOffer); // Encode to base64 to avoid parsing error in Javascript due to // break lines sdpAnswer = new String(Base64.encodeBase64(sdpAnswer.getBytes())); ((JavascriptExecutor) driver).executeScript("setSdpAnswer('" + sdpAnswer + "');"); } } public static class Builder { private String video; private String audio; private int serverPort; private Client client; private Browser browser; private boolean usePhysicalCam; private Node remoteNode; private int recordAudio; // seconds private int audioSampleRate; // samples per seconds (e.g. 8000, 16000) private AudioChannel audioChannel; // stereo, mono public Builder() { this.serverPort = KurentoServicesTestHelper.getAppHttpPort(); // By default physical camera will not be used; instead synthetic // videos will be used for testing this.usePhysicalCam = false; // By default is not a remote test this.remoteNode = null; // By default, not recording audio (0 seconds) this.recordAudio = 0; } public Builder(int serverPort) { this.serverPort = serverPort; } public Builder video(String video) { this.video = video; return this; } public Builder client(Client client) { this.client = client; return this; } public Builder browser(Browser browser) { this.browser = browser; return this; } public Builder usePhysicalCam() { this.usePhysicalCam = true; return this; } public Builder remoteNode(Node remoteNode) { this.remoteNode = remoteNode; return this; } public Builder audio(String audio, int recordAudio, int audioSampleRate, AudioChannel audioChannel) { this.audio = audio; this.recordAudio = recordAudio; this.audioSampleRate = audioSampleRate; this.audioChannel = audioChannel; return this; } public BrowserClient build() { return new BrowserClient(this); } } }