org.kurento.test.base.BrowserTest.java Source code

Java tutorial

Introduction

Here is the source code for org.kurento.test.base.BrowserTest.java

Source

/*
 * (C) Copyright 2014 Kurento (http://kurento.org/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.kurento.test.base;

import static org.bytedeco.javacpp.lept.pixDestroy;
import static org.bytedeco.javacpp.lept.pixReadMem;
import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.config.TestConfiguration.TEST_URL_TIMEOUT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_URL_TIMEOUT_PROPERTY;

import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.HttpURLConnection;
import java.net.SocketException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.cert.X509Certificate;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import javax.imageio.ImageIO;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.lept.PIX;
import org.bytedeco.javacpp.tesseract.TessBaseAPI;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.kurento.commons.exception.KurentoException;
import org.kurento.test.browser.Browser;
import org.kurento.test.browser.WebPage;
import org.kurento.test.config.BrowserConfig;
import org.kurento.test.config.TestScenario;
import org.kurento.test.internal.AbortableCountDownLatch;
import org.kurento.test.lifecycle.FailedTest;
import org.kurento.test.utils.Shell;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Multimap;
import com.google.common.collect.Table;

/**
 * Base for Kurento tests that use browsers.
 *
 * @author Boni Garcia (bgarcia@gsyc.es)
 * @author Micael Gallego (micael.gallego@gmail.com)
 * @since 4.2.3
 */
public abstract class BrowserTest<W extends WebPage> extends KurentoTest {

    public static Logger log = LoggerFactory.getLogger(BrowserTest.class);
    public static final Color CHROME_VIDEOTEST_COLOR = new Color(0, 135, 0);
    public static final int OCR_TIME_THRESHOLD_MS = 300;
    public static final int OCR_COLOR_THRESHOLD = 180;
    public static final String LATENCY_KEY = "E2ELatencyMs";
    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("H:mm:ss:S");
    public static final double FPS = 30;
    public static final int BLOCKSIZE = 1;
    public static final String SSIM_KEY = "avg_ssim";
    public static final String PSNR_KEY = "avg_psnr";
    public static final String PNG = ".png";
    public static final String Y4M = ".y4m";

    private static Map<String, LogEntries> browserLogs = new ConcurrentHashMap<>();

    private final Map<String, W> pages = new ConcurrentHashMap<>();

    @Before
    public void setupBrowserTest() throws InterruptedException {
        if (testScenario != null && testScenario.getBrowserMap() != null
                && testScenario.getBrowserMap().size() > 0) {
            ExecutorService executor = Executors.newFixedThreadPool(testScenario.getBrowserMap().size());
            final AbortableCountDownLatch latch = new AbortableCountDownLatch(testScenario.getBrowserMap().size());
            for (final String browserKey : testScenario.getBrowserMap().keySet()) {

                executor.execute(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            Browser browser = testScenario.getBrowserMap().get(browserKey);

                            int timeout = getProperty(TEST_URL_TIMEOUT_PROPERTY, TEST_URL_TIMEOUT_DEFAULT);

                            URL url = browser.getUrl();
                            if (!testScenario.getUrlList().contains(url)) {
                                waitForHostIsReachable(url, timeout);
                                testScenario.getUrlList().add(url);
                            }
                            initBrowser(browserKey, browser);
                            latch.countDown();
                        } catch (Throwable t) {
                            latch.abort("Exception setting up test. A browser could not be initialised", t);
                            t.printStackTrace();
                        }
                    }
                });
            }

            latch.await();
        }
    }

    private void initBrowser(String browserKey, Browser browser) throws IOException {
        browser.setId(browserKey);
        browser.setName(getTestMethodName());
        browser.init();
        browser.injectKurentoTestJs();
    }

    @After
    public void teardownBrowserTest() {
        if (testScenario != null) {
            for (Browser browser : testScenario.getBrowserMap().values()) {
                try {
                    if (browser.getWebDriver() != null) {
                        browserLogs.put(browser.getId(),
                                browser.getWebDriver().manage().logs().get(LogType.BROWSER));
                    } else {
                        log.warn("It was not possible to recover logs for {} "
                                + "since browser is no longer available (maybe "
                                + "it has been closed manually or crashed)", browser.getId());
                    }
                } catch (Exception e) {
                    log.warn("Exception getting logs {}", browser.getId(), e);
                }
                try {
                    browser.close();
                } catch (Exception e) {
                    log.warn("Exception closing browser {}", browser.getId(), e);
                }
            }
        }
    }

    @FailedTest
    public static void storeBrowsersLogs() {
        List<String> lines = new ArrayList<>();
        for (String browserKey : browserLogs.keySet()) {
            for (LogEntry logEntry : browserLogs.get(browserKey)) {
                lines.add(logEntry.toString());
            }

            File file = new File(getDefaultOutputTestPath() + browserKey + ".log");

            try {
                FileUtils.writeLines(file, lines);
            } catch (IOException e) {
                log.error("Error while writing browser log to a file", e);
            }
        }
    }

    public TestScenario getTestScenario() {
        return testScenario;
    }

    public void addBrowser(String browserKey, Browser browser) throws IOException {
        testScenario.getBrowserMap().put(browserKey, browser);
        initBrowser(browserKey, browser);
    }

    public W getPage(String browserKey) {
        return assertAndGetPage(browserKey);
    }

    public W getPage() {
        try {
            return assertAndGetPage(BrowserConfig.BROWSER);

        } catch (RuntimeException e) {
            if (testScenario.getBrowserMap().isEmpty()) {
                throw new RuntimeException("Empty test scenario: no available browser to run tests!");
            } else {
                String browserKey = testScenario.getBrowserMap().entrySet().iterator().next().getKey();
                log.debug(BrowserConfig.BROWSER + " is not registered in test scenarario, instead"
                        + " using first browser in the test scenario, i.e. " + browserKey);

                return getOrCreatePage(browserKey);
            }
        }
    }

    public W getPage(int index) {
        return assertAndGetPage(BrowserConfig.BROWSER + index);
    }

    public W getPresenter() {
        return assertAndGetPage(BrowserConfig.PRESENTER);
    }

    public W getPresenter(int index) {
        return assertAndGetPage(BrowserConfig.PRESENTER + index);
    }

    public W getViewer() {
        return assertAndGetPage(BrowserConfig.VIEWER);
    }

    public W getViewer(int index) {
        return assertAndGetPage(BrowserConfig.VIEWER + index);
    }

    private W assertAndGetPage(String browserKey) {
        if (!testScenario.getBrowserMap().keySet().contains(browserKey)) {
            throw new RuntimeException(browserKey + " is not registered as browser in the test scenario");
        }
        return getOrCreatePage(browserKey);
    }

    private synchronized W getOrCreatePage(String browserKey) {
        W webPage;
        if (pages.containsKey(browserKey)) {
            webPage = pages.get(browserKey);
            webPage.setBrowser(testScenario.getBrowserMap().get(browserKey));
        } else {
            webPage = createWebPage();
            webPage.setBrowser(testScenario.getBrowserMap().get(browserKey));
            pages.put(browserKey, webPage);
        }

        return webPage;
    }

    @SuppressWarnings("unchecked")
    protected W createWebPage() {

        Class<?> testClientClass = getParamType(this.getClass());

        try {
            return (W) testClientClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException("Exception creating an instance of class " + testClientClass.getName(), e);
        }
    }

    public static Class<?> getParamType(Class<?> testClass) {

        Type genericSuperclass = testClass.getGenericSuperclass();

        if (genericSuperclass != null) {

            if (genericSuperclass instanceof Class) {
                return getParamType((Class<?>) genericSuperclass);
            }

            ParameterizedType paramClass = (ParameterizedType) genericSuperclass;

            return (Class<?>) paramClass.getActualTypeArguments()[0];
        }

        throw new RuntimeException("Unable to obtain the type paramter of KurentoTest");
    }

    public void waitForHostIsReachable(URL url, int timeout) {
        long timeoutMillis = TimeUnit.MILLISECONDS.convert(timeout, TimeUnit.SECONDS);
        long endTimeMillis = System.currentTimeMillis() + timeoutMillis;

        log.debug("Waiting for {} to be reachable (timeout {} seconds)", url, timeout);

        try {
            TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
                @Override
                public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                    return null;
                }

                @Override
                public void checkClientTrusted(X509Certificate[] certs, String authType) {
                }

                @Override
                public void checkServerTrusted(X509Certificate[] certs, String authType) {
                }
            } };

            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new java.security.SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

            HostnameVerifier allHostsValid = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };
            HttpsURLConnection.setDefaultHostnameVerifier(allHostsValid);

            int responseCode = 0;
            while (true) {
                try {
                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                    connection.setConnectTimeout((int) timeoutMillis);
                    connection.setReadTimeout((int) timeoutMillis);
                    connection.setRequestMethod("HEAD");
                    responseCode = connection.getResponseCode();

                    break;
                } catch (SSLHandshakeException | SocketException e) {
                    log.warn("Error {} waiting URL {}, trying again in 1 second", e.getMessage(), url);
                    // Polling to wait a consistent SSL state
                    Thread.sleep(1000);
                }
                if (System.currentTimeMillis() > endTimeMillis) {
                    break;
                }
            }

            if (responseCode != HttpURLConnection.HTTP_OK) {
                log.warn("URL " + url + " not reachable. Response code=" + responseCode);
            }
        } catch (Exception e) {
            e.printStackTrace();
            Assert.fail("URL " + url + " not reachable in " + timeout + " seconds (" + e.getClass().getName() + ", "
                    + e.getMessage() + ")");
        }

        log.debug("URL {} already reachable", url);
    }

    public void waitSeconds(long waitTime) {
        waitMilliSeconds(TimeUnit.SECONDS.toMillis(waitTime));
    }

    public void waitMilliSeconds(long waitTime) {
        try {
            Thread.sleep(waitTime);
        } catch (InterruptedException e) {
            log.warn("InterruptedException waiting {} milliseconds", waitTime, e);
        }
    }

    public void syncTimeForOcr(final W[] webpages, final String[] videoTagsId, final String[] peerConnectionsId)
            throws InterruptedException {
        int webpagesLength = webpages.length;
        int videoTagsLength = videoTagsId.length;
        if (webpagesLength != videoTagsLength) {
            throw new KurentoException("The size of webpage arrays (" + webpagesLength
                    + "}) must be the same as videoTags (" + videoTagsLength + ")");
        }

        final ExecutorService service = Executors.newFixedThreadPool(webpagesLength);
        final CountDownLatch latch = new CountDownLatch(webpagesLength);

        for (int i = 0; i < webpagesLength; i++) {
            final int j = i;
            service.execute(new Runnable() {
                @Override
                public void run() {
                    webpages[j].syncTimeForOcr(videoTagsId[j], peerConnectionsId[j]);
                    latch.countDown();
                }
            });
        }
        latch.await();
        service.shutdown();
    }

    public void serializeObject(Object object, String file) throws IOException {
        FileOutputStream fos = new FileOutputStream(file);
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(object);
        oos.close();
        fos.close();
    }

    public Table<Integer, Integer, String> processOcrAndStats(final Map<String, Map<String, Object>> presenter,
            final Map<String, Map<String, Object>> viewer) throws InterruptedException, IOException {

        log.debug("Processing OCR and stats");
        log.trace("Presenter {} : {}", presenter.size(), presenter.keySet());
        log.trace("Viewer {} : {}", viewer.size(), viewer.keySet());

        final Table<Integer, Integer, String> resultTable = HashBasedTable.create();
        final int numRows = presenter.size();
        final int threadPoolSize = Runtime.getRuntime().availableProcessors();
        final ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);
        final CountDownLatch latch = new CountDownLatch(numRows);
        final Iterator<String> iteratorPresenter = presenter.keySet().iterator();

        // Process OCR (in parallel)
        for (int i = 0; i < numRows; i++) {
            final int j = i;
            final String key = iteratorPresenter.next();

            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        String matchKey = containSimilarDate(key, viewer.keySet());
                        if (matchKey != null) {
                            String presenterBase64 = presenter.get(key).get(LATENCY_KEY).toString();
                            String viewerBase64 = viewer.get(matchKey).get(LATENCY_KEY).toString();
                            String presenterDateStr = ocr(presenterBase64);
                            String viewerDateStr = ocr(viewerBase64);
                            String latency = String.valueOf(
                                    processOcr(presenterDateStr, viewerDateStr, presenterBase64, viewerBase64));
                            synchronized (resultTable) {
                                if (!resultTable.row(0).containsValue(LATENCY_KEY)) {
                                    resultTable.put(0, 0, LATENCY_KEY);
                                }
                                resultTable.put(j + 1, 0, latency);
                            }
                        }
                    } finally {
                        latch.countDown();
                    }
                }
            });
        }
        latch.await();
        executor.shutdown();

        // Process statistics
        processStats(presenter, resultTable);
        processStats(viewer, resultTable);

        log.debug("OCR + Stats results: {}", resultTable);

        return resultTable;
    }

    public synchronized long processOcr(String presenterDateStr, String viewerDateStr, String presenterBase64,
            String viewerBase64) {
        long latency = -1;
        try {
            Date presenterDate = DATE_FORMAT.parse(presenterDateStr);
            Date viewerDate = DATE_FORMAT.parse(viewerDateStr);
            latency = presenterDate.getTime() - viewerDate.getTime();
        } catch (Exception e) {
            log.warn(
                    "Unparseable date(s) (presenter: '{}' - viewer: '{}')" + "\nBase64 presenter: {}"
                            + "\nBase64 viewer: {}",
                    presenterDateStr, viewerDateStr, presenterBase64, viewerBase64, e);
        }
        log.debug("--> Latency {} ms (presenter: '{}' - viewer: '{}')", latency, presenterDateStr, viewerDateStr);

        // Debug trace for latencies over 1 second (or lower than -1)
        if (latency > 1000 || latency < -1) {
            log.trace(
                    ">>> Bad latency measurement: {} ms (presenter: '{}' - viewer: '{}')" + "\nBase64 presenter: {}"
                            + "\nBase64 viewer: {}",
                    latency, presenterDateStr, viewerDateStr, presenterBase64, viewerBase64);
        }
        return latency;
    }

    public void processStats(Map<String, Map<String, Object>> stats, Table<Integer, Integer, String> resultTable) {
        Iterator<String> iterator = stats.keySet().iterator();
        for (int i = 0; i < stats.size(); i++) {
            String mapKey = iterator.next();
            Map<String, Object> entryStat = stats.get(mapKey);
            for (String key : entryStat.keySet()) {
                if (key.equalsIgnoreCase(LATENCY_KEY)) {
                    continue;
                }
                if (!resultTable.row(0).containsValue(key)) {
                    int columnCount = resultTable.columnKeySet().size();
                    resultTable.put(0, columnCount, key);
                    resultTable.put(1 + i, columnCount, entryStat.get(key).toString());
                    log.trace("Inserting new header for stat: {} on column {}", key, columnCount);
                    log.trace("Inserting first value for stat: {} on row {} column {}", entryStat.get(key), (1 + i),
                            columnCount);
                } else {
                    int columnIndex = getKeyOfValue(resultTable.row(0), key);
                    resultTable.put(1 + i, columnIndex, entryStat.get(key).toString());
                    log.trace("Inserting value for stat: {} on row {} column {}", entryStat.get(key), (1 + i),
                            columnIndex);
                }
            }
        }
    }

    public void writeCSV(String outputFile, Table<Integer, Integer, String> resultTable) throws IOException {
        FileWriter writer = new FileWriter(outputFile);
        for (Integer row : resultTable.rowKeySet()) {
            boolean first = true;
            for (Integer column : resultTable.columnKeySet()) {
                if (!first) {
                    writer.append(',');
                }
                String value = resultTable.get(row, column);
                if (value != null) {
                    writer.append(value);
                }
                first = false;
            }
            writer.append('\n');
        }
        writer.flush();
        writer.close();
    }

    public void writeCSV(String outputFile, Multimap<String, Object> multimap, boolean orderKeys)
            throws IOException {
        FileWriter writer = new FileWriter(outputFile);

        // Header
        boolean first = true;
        Set<String> keySet = orderKeys ? new TreeSet<String>(multimap.keySet()) : multimap.keySet();
        for (String key : keySet) {
            if (!first) {
                writer.append(',');
            }
            writer.append(key);
            first = false;
        }
        writer.append('\n');

        // Values
        int i = 0;
        boolean moreValues;
        do {
            moreValues = false;
            first = true;
            for (String key : keySet) {
                Object[] array = multimap.get(key).toArray();
                moreValues = i < array.length;
                if (moreValues) {
                    if (!first) {
                        writer.append(',');
                    }
                    writer.append(array[i].toString());
                }
                first = false;
            }
            i++;
            if (moreValues) {
                writer.append('\n');
            }
        } while (moreValues);

        writer.flush();
        writer.close();
    }

    public Integer getKeyOfValue(Map<Integer, String> map, String value) {
        Integer key = null;
        for (Integer i : map.keySet()) {
            if (map.get(i).equalsIgnoreCase(value)) {
                key = i;
                break;
            }
        }
        return key;
    }

    public String containSimilarDate(String key, Set<String> keySet) {
        long minDiff = 0;
        for (String k : keySet) {
            long diff = Math.abs(Long.parseLong(key) - Long.parseLong(k));
            if (diff < OCR_TIME_THRESHOLD_MS) {
                return k;
            }
            if (minDiff == 0) {
                minDiff = diff;
            } else if (diff < minDiff) {
                minDiff = diff;
            }
        }
        log.warn("Not matching key for {} [min difference {}]", key, minDiff);
        return null;
    }

    public String ocr(String imgBase64) {
        // Base64 to BufferedImage
        BufferedImage imgBuff = null;
        try {
            imgBuff = ImageIO.read(new ByteArrayInputStream(
                    Base64.decodeBase64(imgBase64.substring(imgBase64.lastIndexOf(",") + 1))));
        } catch (IOException e) {
            log.warn("IOException converting image to buffer", e);
        }
        return ocr(imgBuff);
    }

    public String ocr(BufferedImage imgBuff) {
        String parsedOut = null;

        try {
            // Color image to pure black and white
            for (int x = 0; x < imgBuff.getWidth(); x++) {
                for (int y = 0; y < imgBuff.getHeight(); y++) {
                    Color color = new Color(imgBuff.getRGB(x, y));
                    int red = color.getRed();
                    int green = color.getBlue();
                    int blue = color.getGreen();
                    if (red + green + blue > OCR_COLOR_THRESHOLD) {
                        red = green = blue = 0; // Black
                    } else {
                        red = green = blue = 255; // White
                    }
                    Color col = new Color(red, green, blue);
                    imgBuff.setRGB(x, y, col.getRGB());
                }
            }

            // OCR recognition
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ImageIO.write(imgBuff, "png", baos);
            byte[] imageBytes = baos.toByteArray();

            TessBaseAPI api = new TessBaseAPI();
            api.Init(null, "eng");
            ByteBuffer imgBB = ByteBuffer.wrap(imageBytes);

            PIX image = pixReadMem(imgBB, imageBytes.length);
            api.SetImage(image);

            // Get OCR result
            BytePointer outText = api.GetUTF8Text();

            // Destroy used object and release memory
            api.End();
            api.close();
            outText.deallocate();
            pixDestroy(image);

            // OCR corrections
            parsedOut = outText.getString().replaceAll("l", "1").replaceAll("Z", "2").replaceAll("O", "0")
                    .replaceAll("B", "8").replaceAll("G", "6").replaceAll("S", "8").replaceAll("'", "")
                    .replaceAll("", "").replaceAll("\\.", ":").replaceAll("E", "8").replaceAll("o", "0")
                    .replaceAll("", "0").replaceAll("?", "6").replaceAll("", "5").replaceAll("I", "1")
                    .replaceAll("T", "7").replaceAll("", "").replaceAll("U", "0").replaceAll("D", "0");
            if (parsedOut.length() > 7) {
                parsedOut = parsedOut.substring(0, 7) + ":" + parsedOut.substring(8, parsedOut.length());
            }
            parsedOut = parsedOut.replaceAll("::", ":");

            // Remove last part (number of frames)
            int iSpace = parsedOut.lastIndexOf(" ");
            if (iSpace != -1) {
                parsedOut = parsedOut.substring(0, iSpace);
            }
        } catch (IOException e) {
            log.warn("IOException in OCR", e);
        }
        return parsedOut;
    }

    public File convertToRaw(File inputFile, File tmpFolder, double fps) {
        File y4m = new File(tmpFolder.toString() + File.separator + inputFile.getName() + Y4M);
        String[] ffmpegCommand = { "ffmpeg", "-i", inputFile.toString(), "-f", "yuv4mpegpipe", "-r", parseFps(fps),
                y4m.toString() };
        log.debug("Running command to convert to raw: {}", Arrays.toString(ffmpegCommand));
        Shell.runAndWait(ffmpegCommand);
        return y4m;
    }

    public Multimap<String, Object> getVideoQuality(File inputFile1, File inputFile2, String videoAlgorithm,
            double fps, int blocksize) throws IOException {
        String qpsnrCommand = "qpsnr -a " + videoAlgorithm + " -o blocksize=" + blocksize + ":fpa=" + parseFps(fps)
                + " -r " + inputFile1.getAbsolutePath() + " " + inputFile2.getAbsolutePath();
        log.debug("Running qpsnr to calcule video quality ({}): {}", videoAlgorithm, qpsnrCommand);

        String outputShell[] = Shell.runAndWait("sh", "-c", qpsnrCommand).split("\\r?\\n");
        Multimap<String, Object> outputMap = ArrayListMultimap.create();
        boolean insertValues = false;
        for (String s : outputShell) {
            if (s.startsWith("Sample,")) {
                insertValues = true;
                continue;
            }
            if (insertValues) {
                outputMap.put(videoAlgorithm, s.split(",")[1]);
            }
        }
        return outputMap;
    }

    public String parseFps(double fps) {
        DecimalFormat df = new DecimalFormat("0");
        return df.format(fps);
    }

    public File cutVideo(File inputFile, File tmpFolder, int cutFrame, double fps) {
        double cutTime = cutFrame / fps;
        DecimalFormat df = new DecimalFormat("0.00");
        File cutVideoFile = new File(tmpFolder.toString() + File.separator + "cut-" + inputFile.getName());
        String[] command = { "ffmpeg", "-i", inputFile.getAbsolutePath(), "-ss", df.format(cutTime),
                cutVideoFile.getAbsolutePath() };
        log.debug("Running command to cut video: {}", Arrays.toString(command));
        Shell.runAndWait(command);
        return cutVideoFile;
    }

    public File cutAndTranscodeVideo(File inputFile, File tmpFolder, int cutFrame, double fps) {
        double cutTime = cutFrame / fps;
        DecimalFormat df = new DecimalFormat("0.00");
        File cutVideoFile = new File(tmpFolder.toString() + File.separator + "cut-" + inputFile.getName());
        String[] command = { "ffmpeg", "-i", inputFile.getAbsolutePath(), "-ss", df.format(cutTime), "-acodec",
                "copy", "-codec:v", "libvpx", cutVideoFile.getAbsolutePath() };
        log.debug("Running command to cut video: {}", Arrays.toString(command));
        Shell.runAndWait(command);
        return cutVideoFile;
    }

    public File transcodeVideo(File inputFile, File tmpFolder, double fps) {
        File transVideoFile = new File(tmpFolder.toString() + File.separator + "trans-" + inputFile.getName());
        String[] command = { "ffmpeg", "-i", inputFile.getAbsolutePath(), "-acodec", "copy", "-codec:v", "libvpx",
                transVideoFile.getAbsolutePath() };
        log.debug("Running command to transcode video: {}", Arrays.toString(command));
        Shell.runAndWait(command);
        return transVideoFile;
    }

    public int getCutFrame(final File inputFile1, final File inputFile2, File tmpFolder) throws IOException {
        // Filters to distinguish frames from file1 to file2
        FilenameFilter fileNameFilter1 = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.contains(inputFile1.getName()) && name.endsWith(PNG);
            }
        };
        FilenameFilter fileNameFilter2 = new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return name.contains(inputFile2.getName()) && name.endsWith(PNG);
            }
        };

        File[] ls1 = tmpFolder.listFiles(fileNameFilter1);
        File[] ls2 = tmpFolder.listFiles(fileNameFilter2);
        Arrays.sort(ls1);
        Arrays.sort(ls2);

        List<String> ocrList1 = new ArrayList<>();
        List<String> ocrList2 = new ArrayList<>();
        int i = 0;
        for (; i < Math.min(ls1.length, ls2.length); i++) {
            String ocr1 = this.ocr(ImageIO.read(ls1[i]));
            String ocr2 = this.ocr(ImageIO.read(ls2[i]));
            ocrList1.add(ocr1);
            ocrList2.add(ocr2);

            log.trace("---> Time comparsion to find cut frame: {} vs {}", ocr1, ocr2);
            if (ocrList2.contains(ocr1)) {
                log.debug("Found OCR match {} at position {}", ocr1, i);
                // TODO Hack here: if the first video should be cut (presenter), the result is negative.
                // Otherwise the result is positive (cut the second video, i.e. the viewer)
                i *= -1;
                break;
            } else if (ocrList1.contains(ocr2)) {
                log.debug("Found OCR match {} at position {}", ocr2, i);
                break;
            }
        }
        return i;
    }

    public void getFrames(final File inputFile, final File tmpFolder) {
        Thread t = new Thread() {
            @Override
            public void run() {
                String[] command = { "ffmpeg", "-i", inputFile.getAbsolutePath(),
                        tmpFolder.toString() + File.separator + inputFile.getName() + "-%03d" + PNG };
                log.debug("Running command to get frames: {}", Arrays.toString(command));
                Shell.runAndWait(command);
            }
        };
        t.start();
        waitMilliSeconds(500);
        t.interrupt();
    }

    public Multimap<String, Object> getSsim(File inputFile1, File inputFile2) throws IOException {
        return getVideoQuality(inputFile1, inputFile2, SSIM_KEY, FPS, BLOCKSIZE);
    }

    public Multimap<String, Object> getPsnr(File inputFile1, File inputFile2) throws IOException {
        return getVideoQuality(inputFile1, inputFile2, PSNR_KEY, FPS, BLOCKSIZE);
    }

    public void waitForFilesInFolder(String folder, final String ext, int expectedFilesNumber) {
        File dir = new File(folder);
        File[] files = null;
        do {
            if (files != null) {
                waitMilliSeconds(500); // polling
            }
            files = dir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return name.toLowerCase().endsWith(ext);
                }
            });
            log.debug("Number of files with extension {} in {} = {} (expected {})", ext, folder, files.length,
                    expectedFilesNumber);
        } while (files.length != expectedFilesNumber);
    }

    public void addColumnsToTable(Table<Integer, Integer, String> table, Multimap<String, Object> column,
            int columnKey) {
        for (String key : column.keySet()) {
            shiftTable(table, columnKey);
            table.put(0, columnKey, key);
            Collection<Object> content = column.get(key);
            Iterator<Object> iterator = content.iterator();
            log.debug("Adding columun {} ({} elements) to table in position {}", key, content.size(), columnKey);
            for (int i = 0; i < content.size(); i++) {
                table.put(i + 1, columnKey, iterator.next().toString());
            }
            columnKey++;
        }
    }

    private void shiftTable(Table<Integer, Integer, String> table, int columnKey) {
        for (int i = table.columnKeySet().size() - 1; i >= columnKey; i--) {
            Map<Integer, String> column = table.column(i);
            for (int j : column.keySet()) {
                table.put(j, i + 1, column.get(j));
            }
        }
        table.column(columnKey).clear();
    }

}