com.kurento.kmf.test.client.BrowserClient.java Source code

Java tutorial

Introduction

Here is the source code for com.kurento.kmf.test.client.BrowserClient.java

Source

/*
 * (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.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 boolean remoteTest;
    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.remoteTest = builder.remoteTest;
        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 (remoteTest) {
                    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");

                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 (remoteTest) {
                    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) {
            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 boolean remoteTest;
        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.remoteTest = false;

            // 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 remoteTest() {
            this.remoteTest = true;
            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);
        }
    }
}