From 42ec6f0810332904fbb22d1d3dd87a8ce442c655 Mon Sep 17 00:00:00 2001
From: kapodamy <kapo.damy@yahoo.com.ar>
Date: Tue, 14 Jan 2020 00:00:45 -0300
Subject: [PATCH 01/10] ttml to srt conversion rewrite SubtitleConverter (use
 JSoup library instead, remove unused methods)

---
 .../newpipe/download/DownloadDialog.java      |   1 -
 .../newpipe/streams/SrtFromTtmlWriter.java    |  95 +++++
 .../newpipe/streams/SubtitleConverter.java    | 369 ------------------
 .../giga/postprocessing/TtmlConverter.java    |  32 +-
 4 files changed, 100 insertions(+), 397 deletions(-)
 create mode 100644 app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
 delete mode 100644 app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java

diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
index 853bb6d68..44966744b 100644
--- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java
@@ -832,7 +832,6 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
                     psArgs = new String[]{
                             selectedStream.getFormat().getSuffix(),
                             "false",// ignore empty frames
-                            "false",// detect youtube duplicate lines
                     };
                 }
                 break;
diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
new file mode 100644
index 000000000..75e16edad
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
@@ -0,0 +1,95 @@
+package org.schabi.newpipe.streams;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.nodes.Node;
+import org.jsoup.nodes.TextNode;
+import org.jsoup.parser.Parser;
+import org.jsoup.select.Elements;
+import org.schabi.newpipe.streams.io.SharpStream;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.text.ParseException;
+
+/**
+ * @author kapodamy
+ */
+public class SrtFromTtmlWriter {
+    private static final String NEW_LINE = "\r\n";
+
+    private SharpStream out;
+    private boolean ignoreEmptyFrames;
+    private final Charset charset = Charset.forName("utf-8");
+
+    private int frameIndex = 0;
+
+    public SrtFromTtmlWriter(SharpStream out, boolean ignoreEmptyFrames) {
+        this.out = out;
+        this.ignoreEmptyFrames = true;
+    }
+
+    private static String getTimestamp(Element frame, String attr) {
+        return frame
+                .attr(attr)
+                .replace('.', ',');// Str uses comma as decimal separator
+    }
+
+    private void writeFrame(String begin, String end, StringBuilder text) throws IOException {
+        writeString(String.valueOf(frameIndex++));
+        writeString(NEW_LINE);
+        writeString(begin);
+        writeString(" --> ");
+        writeString(end);
+        writeString(NEW_LINE);
+        writeString(text.toString());
+        writeString(NEW_LINE);
+        writeString(NEW_LINE);
+    }
+
+    private void writeString(String text) throws IOException {
+        out.write(text.getBytes(charset));
+    }
+
+    public void build(SharpStream ttml) throws IOException {
+        /*
+         * TTML parser with BASIC support
+         * multiple CUE is not supported
+         * styling is not supported
+         * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
+         * also TimestampTagOption enum is not applicable
+         * Language parsing is not supported
+         */
+
+        // parse XML
+        byte[] buffer = new byte[(int) ttml.available()];
+        ttml.read(buffer);
+        Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser());
+
+        StringBuilder text = new StringBuilder(128);
+        Elements paragraph_list = doc.select("body>div>p");
+
+        // check if has frames
+        if (paragraph_list.size() < 1) return;
+
+        for (Element paragraph : paragraph_list) {
+            text.setLength(0);
+
+            for (Node children : paragraph.childNodes()) {
+                if (children instanceof TextNode)
+                    text.append(((TextNode) children).text());
+                else if (children instanceof Element && ((Element) children).tagName().equalsIgnoreCase("br"))
+                    text.append(NEW_LINE);
+            }
+
+            if (ignoreEmptyFrames && text.length() < 1) continue;
+
+            String begin = getTimestamp(paragraph, "begin");
+            String end = getTimestamp(paragraph, "end");
+
+            writeFrame(begin, end, text);
+        }
+    }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java b/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java
deleted file mode 100644
index c41db4373..000000000
--- a/app/src/main/java/org/schabi/newpipe/streams/SubtitleConverter.java
+++ /dev/null
@@ -1,369 +0,0 @@
-package org.schabi.newpipe.streams;
-
-import org.schabi.newpipe.streams.io.SharpStream;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-import org.xml.sax.SAXException;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.text.ParseException;
-import java.util.Locale;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPathExpressionException;
-
-/**
- * @author kapodamy
- */
-public class SubtitleConverter {
-    private static final String NEW_LINE = "\r\n";
-
-    public void dumpTTML(SharpStream in, final SharpStream out, final boolean ignoreEmptyFrames, final boolean detectYoutubeDuplicateLines
-    ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
-
-        final FrameWriter callback = new FrameWriter() {
-            int frameIndex = 0;
-            final Charset charset = Charset.forName("utf-8");
-
-            @Override
-            public void yield(SubtitleFrame frame) throws IOException {
-                if (ignoreEmptyFrames && frame.isEmptyText()) {
-                    return;
-                }
-                out.write(String.valueOf(frameIndex++).getBytes(charset));
-                out.write(NEW_LINE.getBytes(charset));
-                out.write(getTime(frame.start, true).getBytes(charset));
-                out.write(" --> ".getBytes(charset));
-                out.write(getTime(frame.end, true).getBytes(charset));
-                out.write(NEW_LINE.getBytes(charset));
-                out.write(frame.text.getBytes(charset));
-                out.write(NEW_LINE.getBytes(charset));
-                out.write(NEW_LINE.getBytes(charset));
-            }
-        };
-
-        read_xml_based(in, callback, detectYoutubeDuplicateLines,
-                "tt", "xmlns", "http://www.w3.org/ns/ttml",
-                new String[]{"timedtext", "head", "wp"},
-                new String[]{"body", "div", "p"},
-                "begin", "end", true
-        );
-    }
-
-    private void read_xml_based(SharpStream source, FrameWriter callback, boolean detectYoutubeDuplicateLines,
-                                String root, String formatAttr, String formatVersion, String[] cuePath, String[] framePath,
-                                String timeAttr, String durationAttr, boolean hasTimestamp
-    ) throws IOException, ParseException, SAXException, ParserConfigurationException, XPathExpressionException {
-        /*
-         * XML based subtitles parser with BASIC support
-         * multiple CUE is not supported
-         * styling is not supported
-         * tag timestamps (in auto-generated subtitles) are not supported, maybe in the future
-         * also TimestampTagOption enum is not applicable
-         * Language parsing is not supported
-         */
-
-        byte[] buffer = new byte[(int) source.available()];
-        source.read(buffer);
-
-        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-        factory.setNamespaceAware(true);
-        DocumentBuilder builder = factory.newDocumentBuilder();
-        Document xml = builder.parse(new ByteArrayInputStream(buffer));
-
-        String attr;
-
-        // get the format version or namespace
-        Element node = xml.getDocumentElement();
-
-        if (node == null) {
-            throw new ParseException("Can't get the format version. ¿wrong namespace?", -1);
-        } else if (!node.getNodeName().equals(root)) {
-            throw new ParseException("Invalid root", -1);
-        }
-
-        if (formatAttr.equals("xmlns")) {
-            if (!node.getNamespaceURI().equals(formatVersion)) {
-                throw new UnsupportedOperationException("Expected xml namespace: " + formatVersion);
-            }
-        } else {
-            attr = node.getAttributeNS(formatVersion, formatAttr);
-            if (attr == null) {
-                throw new ParseException("Can't get the format attribute", -1);
-            }
-            if (!attr.equals(formatVersion)) {
-                throw new ParseException("Invalid format version : " + attr, -1);
-            }
-        }
-
-        NodeList node_list;
-
-        int line_break = 0;// Maximum characters per line if present (valid for TranScript v3)
-
-        if (!hasTimestamp) {
-            node_list = selectNodes(xml, cuePath, formatVersion);
-
-            if (node_list != null) {
-                // if the subtitle has multiple CUEs, use the highest value
-                for (int i = 0; i < node_list.getLength(); i++) {
-                    try {
-                        int tmp = Integer.parseInt(((Element) node_list.item(i)).getAttributeNS(formatVersion, "ah"));
-                        if (tmp > line_break) {
-                            line_break = tmp;
-                        }
-                    } catch (Exception err) {
-                    }
-                }
-            }
-        }
-
-        // parse every frame
-        node_list = selectNodes(xml, framePath, formatVersion);
-
-        if (node_list == null) {
-            return;// no frames detected
-        }
-
-        int fs_ff = -1;// first timestamp of first frame
-        boolean limit_lines = false;
-
-        for (int i = 0; i < node_list.getLength(); i++) {
-            Element elem = (Element) node_list.item(i);
-            SubtitleFrame obj = new SubtitleFrame();
-            obj.text = elem.getTextContent();
-
-            attr = elem.getAttribute(timeAttr);// ¡this cant be null!
-            obj.start = hasTimestamp ? parseTimestamp(attr) : Integer.parseInt(attr);
-
-            attr = elem.getAttribute(durationAttr);
-            if (obj.text == null || attr == null) {
-                continue;// normally is a blank line (on auto-generated subtitles) ignore
-            }
-
-            if (hasTimestamp) {
-                obj.end = parseTimestamp(attr);
-
-                if (detectYoutubeDuplicateLines) {
-                    if (limit_lines) {
-                        int swap = obj.end;
-                        obj.end = fs_ff;
-                        fs_ff = swap;
-                    } else {
-                        if (fs_ff < 0) {
-                            fs_ff = obj.end;
-                        } else {
-                            if (fs_ff < obj.start) {
-                                limit_lines = true;// the subtitles has duplicated lines
-                            } else {
-                                detectYoutubeDuplicateLines = false;
-                            }
-                        }
-                    }
-                }
-            } else {
-                obj.end = obj.start + Integer.parseInt(attr);
-            }
-
-            if (/*node.getAttribute("w").equals("1") &&*/line_break > 1 && obj.text.length() > line_break) {
-
-                // implement auto line breaking (once)
-                StringBuilder text = new StringBuilder(obj.text);
-                obj.text = null;
-
-                switch (text.charAt(line_break)) {
-                    case ' ':
-                    case '\t':
-                        putBreakAt(line_break, text);
-                        break;
-                    default:// find the word start position
-                        for (int j = line_break - 1; j > 0; j--) {
-                            switch (text.charAt(j)) {
-                                case ' ':
-                                case '\t':
-                                    putBreakAt(j, text);
-                                    j = -1;
-                                    break;
-                                case '\r':
-                                case '\n':
-                                    j = -1;// long word, just ignore
-                                    break;
-                            }
-                        }
-                        break;
-                }
-
-                obj.text = text.toString();// set the processed text
-            }
-
-            callback.yield(obj);
-        }
-    }
-
-    private static NodeList selectNodes(Document xml, String[] path, String namespaceUri) {
-        Element ref = xml.getDocumentElement();
-
-        for (int i = 0; i < path.length - 1; i++) {
-            NodeList nodes = ref.getChildNodes();
-            if (nodes.getLength() < 1) {
-                return null;
-            }
-
-            Element elem;
-            for (int j = 0; j < nodes.getLength(); j++) {
-                if (nodes.item(j).getNodeType() == Node.ELEMENT_NODE) {
-                    elem = (Element) nodes.item(j);
-                    if (elem.getNodeName().equals(path[i]) && elem.getNamespaceURI().equals(namespaceUri)) {
-                        ref = elem;
-                        break;
-                    }
-                }
-            }
-        }
-
-        return ref.getElementsByTagNameNS(namespaceUri, path[path.length - 1]);
-    }
-
-    private static int parseTimestamp(String multiImpl) throws NumberFormatException, ParseException {
-        if (multiImpl.length() < 1) {
-            return 0;
-        } else if (multiImpl.length() == 1) {
-            return Integer.parseInt(multiImpl) * 1000;// ¡this must be a number in seconds!
-        }
-
-        // detect wallclock-time
-        if (multiImpl.startsWith("wallclock(")) {
-            throw new UnsupportedOperationException("Parsing wallclock timestamp is not implemented");
-        }
-
-        // detect offset-time
-        if (multiImpl.indexOf(':') < 0) {
-            int multiplier = 1000;
-            char metric = multiImpl.charAt(multiImpl.length() - 1);
-            switch (metric) {
-                case 'h':
-                    multiplier *= 3600000;
-                    break;
-                case 'm':
-                    multiplier *= 60000;
-                    break;
-                case 's':
-                    if (multiImpl.charAt(multiImpl.length() - 2) == 'm') {
-                        multiplier = 1;// ms
-                    }
-                    break;
-                default:
-                    if (!Character.isDigit(metric)) {
-                        throw new NumberFormatException("Invalid metric suffix found on : " + multiImpl);
-                    }
-                    metric = '\0';
-                    break;
-            }
-            try {
-                String offset_time = multiImpl;
-
-                if (multiplier == 1) {
-                    offset_time = offset_time.substring(0, offset_time.length() - 2);
-                } else if (metric != '\0') {
-                    offset_time = offset_time.substring(0, offset_time.length() - 1);
-                }
-
-                double time_metric_based = Double.parseDouble(offset_time);
-                if (Math.abs(time_metric_based) <= Double.MAX_VALUE) {
-                    return (int) (time_metric_based * multiplier);
-                }
-            } catch (Exception err) {
-                throw new UnsupportedOperationException("Invalid or not implemented timestamp on: " + multiImpl);
-            }
-        }
-
-        // detect clock-time
-        int time = 0;
-        String[] units = multiImpl.split(":");
-
-        if (units.length < 3) {
-            throw new ParseException("Invalid clock-time timestamp", -1);
-        }
-
-        time += Integer.parseInt(units[0]) * 3600000;// hours
-        time += Integer.parseInt(units[1]) * 60000;//minutes
-        time += Float.parseFloat(units[2]) * 1000f;// seconds and milliseconds (if present)
-
-        // frames and sub-frames are ignored (not implemented)
-        // time += units[3] * fps;
-        return time;
-    }
-
-    private static void putBreakAt(int idx, StringBuilder str) {
-        // this should be optimized at compile time
-
-        if (NEW_LINE.length() > 1) {
-            str.delete(idx, idx + 1);// remove after replace
-            str.insert(idx, NEW_LINE);
-        } else {
-            str.setCharAt(idx, NEW_LINE.charAt(0));
-        }
-    }
-
-    private static String getTime(int time, boolean comma) {
-        // cast every value to integer to avoid auto-round in ToString("00").
-        StringBuilder str = new StringBuilder(12);
-        str.append(numberToString(time / 1000 / 3600, 2));// hours
-        str.append(':');
-        str.append(numberToString(time / 1000 / 60 % 60, 2));// minutes
-        str.append(':');
-        str.append(numberToString(time / 1000 % 60, 2));// seconds
-        str.append(comma ? ',' : '.');
-        str.append(numberToString(time % 1000, 3));// miliseconds
-
-        return str.toString();
-    }
-
-    private static String numberToString(int nro, int pad) {
-        return String.format(Locale.ENGLISH, "%0".concat(String.valueOf(pad)).concat("d"), nro);
-    }
-
-
-    /******************
-     * helper classes *
-     ******************/
-
-    private interface FrameWriter {
-
-        void yield(SubtitleFrame frame) throws IOException;
-    }
-
-    private static class SubtitleFrame {
-        //Java no support unsigned int
-
-        public int end;
-        public int start;
-        public String text = "";
-
-        private boolean isEmptyText() {
-            if (text == null) {
-                return true;
-            }
-
-            for (int i = 0; i < text.length(); i++) {
-                switch (text.charAt(i)) {
-                    case ' ':
-                    case '\t':
-                    case '\r':
-                    case '\n':
-                        break;
-                    default:
-                        return false;
-                }
-            }
-
-            return true;
-        }
-    }
-
-}
diff --git a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java
index 5a5b687f7..8ed0dfae5 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/TtmlConverter.java
@@ -2,15 +2,10 @@ package us.shandian.giga.postprocessing;
 
 import android.util.Log;
 
-import org.schabi.newpipe.streams.SubtitleConverter;
+import org.schabi.newpipe.streams.SrtFromTtmlWriter;
 import org.schabi.newpipe.streams.io.SharpStream;
-import org.xml.sax.SAXException;
 
 import java.io.IOException;
-import java.text.ParseException;
-
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.xpath.XPathExpressionException;
 
 /**
  * @author kapodamy
@@ -27,33 +22,16 @@ class TtmlConverter extends Postprocessing {
     int process(SharpStream out, SharpStream... sources) throws IOException {
         // check if the subtitle is already in srt and copy, this should never happen
         String format = getArgumentAt(0, null);
+        boolean ignoreEmptyFrames = getArgumentAt(1, "true").equals("true");
 
         if (format == null || format.equals("ttml")) {
-            SubtitleConverter ttmlDumper = new SubtitleConverter();
+            SrtFromTtmlWriter writer = new SrtFromTtmlWriter(out, ignoreEmptyFrames);
 
             try {
-                ttmlDumper.dumpTTML(
-                        sources[0],
-                        out,
-                        getArgumentAt(1, "true").equals("true"),
-                        getArgumentAt(2, "true").equals("true")
-                );
+                writer.build(sources[0]);
             } catch (Exception err) {
                 Log.e(TAG, "subtitle parse failed", err);
-
-                if (err instanceof IOException) {
-                    return 1;
-                } else if (err instanceof ParseException) {
-                    return 2;
-                } else if (err instanceof SAXException) {
-                    return 3;
-                } else if (err instanceof ParserConfigurationException) {
-                    return 4;
-                } else if (err instanceof XPathExpressionException) {
-                    return 7;
-                }
-
-                return 8;
+                return err instanceof IOException ? 1 : 8;
             }
 
             return OK_RESULT;

From 00eddcb2378c6788e1ef00d375ce9ef49747e4b5 Mon Sep 17 00:00:00 2001
From: kapodamy <kapo.damy@yahoo.com.ar>
Date: Wed, 8 Jan 2020 14:36:31 -0300
Subject: [PATCH 02/10] android 5 (lollipop) fixup this commit attempts to fix
 the pickAvailableTemporalDir() method in ROMS that not are CTS compliant.

---
 .../giga/postprocessing/Postprocessing.java   | 10 +++++---
 .../giga/service/DownloadManager.java         | 23 ++++++++++++++-----
 2 files changed, 24 insertions(+), 9 deletions(-)

diff --git a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
index 773ff92d1..e93e83a87 100644
--- a/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
+++ b/app/src/main/java/us/shandian/giga/postprocessing/Postprocessing.java
@@ -80,7 +80,7 @@ public abstract class Postprocessing implements Serializable {
 
     private transient DownloadMission mission;
 
-    private File tempFile;
+    private transient File tempFile;
 
     Postprocessing(boolean reserveSpace, boolean worksOnSameFile, String algorithmName) {
         this.reserveSpace = reserveSpace;
@@ -95,8 +95,12 @@ public abstract class Postprocessing implements Serializable {
 
     public void cleanupTemporalDir() {
         if (tempFile != null && tempFile.exists()) {
-            //noinspection ResultOfMethodCallIgnored
-            tempFile.delete();
+            try {
+                //noinspection ResultOfMethodCallIgnored
+                tempFile.delete();
+            } catch (Exception e) {
+                // nothing to do
+            }
         }
     }
 
diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java
index e8bc468e9..994c6ee63 100644
--- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java
+++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java
@@ -139,6 +139,9 @@ public class DownloadManager {
             Log.d(TAG, "Loading pending downloads from directory: " + mPendingMissionsDir.getAbsolutePath());
         }
 
+        File tempDir = pickAvailableTemporalDir(ctx);
+        Log.i(TAG, "using '" + tempDir + "' as temporal directory");
+
         for (File sub : subs) {
             if (!sub.isFile()) continue;
             if (sub.getName().equals(".tmp")) continue;
@@ -184,7 +187,7 @@ public class DownloadManager {
 
             if (mis.psAlgorithm != null) {
                 mis.psAlgorithm.cleanupTemporalDir();
-                mis.psAlgorithm.setTemporalDir(pickAvailableTemporalDir(ctx));
+                mis.psAlgorithm.setTemporalDir(tempDir);
             }
 
             mis.metadata = sub;
@@ -513,13 +516,21 @@ public class DownloadManager {
     }
 
     static File pickAvailableTemporalDir(@NonNull Context ctx) {
-        if (isDirectoryAvailable(ctx.getExternalFilesDir(null)))
-            return ctx.getExternalFilesDir(null);
-        else if (isDirectoryAvailable(ctx.getFilesDir()))
-            return ctx.getFilesDir();
+        File dir = ctx.getExternalFilesDir(null);
+        if (isDirectoryAvailable(dir)) return dir;
+
+        dir = ctx.getFilesDir();
+        if (isDirectoryAvailable(dir)) return dir;
 
         // this never should happen
-        return ctx.getDir("tmp", Context.MODE_PRIVATE);
+        dir = ctx.getDir("muxing_tmp", Context.MODE_PRIVATE);
+        if (isDirectoryAvailable(dir)) return dir;
+
+        // fallback to cache dir
+        dir = ctx.getCacheDir();
+        if (isDirectoryAvailable(dir)) return dir;
+
+        throw new RuntimeException("Not temporal directories are available");
     }
 
     @Nullable

From 570dded8d601ea86ec92038283a65774fed8fb92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Jakse?= <raphael.github@jakse.fr>
Date: Thu, 16 Jan 2020 20:44:08 +0100
Subject: [PATCH 03/10] Add field START_PAUSED to the Player Intent

This allows fixing spurious playback resume when minimizing to the background player.
---
 .../org/schabi/newpipe/player/BackgroundPlayerActivity.java | 5 ++++-
 app/src/main/java/org/schabi/newpipe/player/BasePlayer.java | 4 +++-
 .../java/org/schabi/newpipe/player/MainVideoPlayer.java     | 6 ++++--
 .../java/org/schabi/newpipe/player/PopupVideoPlayer.java    | 5 +++--
 .../org/schabi/newpipe/player/PopupVideoPlayerActivity.java | 5 ++++-
 .../org/schabi/newpipe/player/ServicePlayerActivity.java    | 6 +++++-
 .../main/java/org/schabi/newpipe/util/NavigationHelper.java | 6 ++++--
 7 files changed, 27 insertions(+), 10 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
index 761b50d85..59f6e1e6d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
@@ -57,7 +57,10 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
 
             this.player.setRecovery();
             getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-            getApplicationContext().startService(getSwitchIntent(PopupVideoPlayer.class));
+            getApplicationContext().startService(
+                getSwitchIntent(PopupVideoPlayer.class)
+                    .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
+            );
             return true;
         }
         return false;
diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index 6452a9850..cd1ec07f9 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -150,6 +150,8 @@ public abstract class BasePlayer implements
     @NonNull
     public static final String RESUME_PLAYBACK = "resume_playback";
     @NonNull
+    public static final String START_PAUSED = "start_paused";
+    @NonNull
     public static final String SELECT_ON_APPEND = "select_on_append";
 
     /*//////////////////////////////////////////////////////////////////////////
@@ -304,7 +306,7 @@ public abstract class BasePlayer implements
         }
         // Good to go...
         initPlayback(queue, repeatMode, playbackSpeed, playbackPitch, playbackSkipSilence,
-                /*playOnInit=*/true);
+                /*playOnInit=*/!intent.getBooleanExtra(START_PAUSED, false));
     }
 
     protected void initPlayback(@NonNull final PlayQueue queue,
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 7a3e60c66..9ccf5b9d3 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -614,7 +614,8 @@ public final class MainVideoPlayer extends AppCompatActivity
                     this.getPlaybackPitch(),
                     this.getPlaybackSkipSilence(),
                     this.getPlaybackQuality(),
-                    false
+                    false,
+                    !isPlaying()
             );
             context.startService(intent);
 
@@ -637,7 +638,8 @@ public final class MainVideoPlayer extends AppCompatActivity
                     this.getPlaybackPitch(),
                     this.getPlaybackSkipSilence(),
                     this.getPlaybackQuality(),
-                    false
+                    false,
+                    !isPlaying()
             );
             context.startService(intent);
 
diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
index 969c47990..70fb77060 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayer.java
@@ -567,7 +567,8 @@ public final class PopupVideoPlayer extends Service {
                     this.getPlaybackPitch(),
                     this.getPlaybackSkipSilence(),
                     this.getPlaybackQuality(),
-                    false
+                    false,
+                    !isPlaying()
             );
             intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
             context.startActivity(intent);
@@ -1123,4 +1124,4 @@ public final class PopupVideoPlayer extends Service {
             return distanceFromCloseButton(popupMotionEvent) <= getClosingRadius();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
index 44fcdb8dd..5000d07e2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
@@ -50,7 +50,10 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
         if (item.getItemId() == R.id.action_switch_background) {
             this.player.setRecovery();
             getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-            getApplicationContext().startService(getSwitchIntent(BackgroundPlayer.class));
+            getApplicationContext().startService(
+                getSwitchIntent(BackgroundPlayer.class)
+                    .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
+            );
             return true;
         }
         return false;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index 2207808ac..a7b3006a1 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -167,7 +167,10 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
             case R.id.action_switch_main:
                 this.player.setRecovery();
                 getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-                getApplicationContext().startActivity(getSwitchIntent(MainVideoPlayer.class));
+                getApplicationContext().startActivity(
+                    getSwitchIntent(MainVideoPlayer.class)
+                        .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
+                );
                 return true;
         }
         return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
@@ -189,6 +192,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
                 this.player.getPlaybackPitch(),
                 this.player.getPlaybackSkipSilence(),
                 null,
+                false,
                 false
         ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
     }
diff --git a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
index e2b03c8e8..a19aa92ae 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -109,12 +109,14 @@ public class NavigationHelper {
                                          final float playbackPitch,
                                          final boolean playbackSkipSilence,
                                          @Nullable final String playbackQuality,
-                                         final boolean resumePlayback) {
+                                         final boolean resumePlayback,
+                                         final boolean startPaused) {
         return getPlayerIntent(context, targetClazz, playQueue, playbackQuality, resumePlayback)
                 .putExtra(BasePlayer.REPEAT_MODE, repeatMode)
                 .putExtra(BasePlayer.PLAYBACK_SPEED, playbackSpeed)
                 .putExtra(BasePlayer.PLAYBACK_PITCH, playbackPitch)
-                .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence);
+                .putExtra(BasePlayer.PLAYBACK_SKIP_SILENCE, playbackSkipSilence)
+                .putExtra(BasePlayer.START_PAUSED, startPaused);
     }
 
     public static void playOnMainPlayer(final Context context, final PlayQueue queue, final boolean resumePlayback) {

From ef90493c27af898d8e79adbb5e8d449ef5d0ae35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Jakse?= <raphael.github@jakse.fr>
Date: Thu, 16 Jan 2020 20:46:11 +0100
Subject: [PATCH 04/10] Deduplicate code switching to another player into a
 function

---
 .../player/BackgroundPlayerActivity.java       |  8 +-------
 .../player/PopupVideoPlayerActivity.java       |  8 +-------
 .../newpipe/player/ServicePlayerActivity.java  | 18 ++++++++++--------
 3 files changed, 12 insertions(+), 22 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
index 59f6e1e6d..1b5b5d07c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BackgroundPlayerActivity.java
@@ -55,13 +55,7 @@ public final class BackgroundPlayerActivity extends ServicePlayerActivity {
                 return true;
             }
 
-            this.player.setRecovery();
-            getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-            getApplicationContext().startService(
-                getSwitchIntent(PopupVideoPlayer.class)
-                    .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
-            );
-            return true;
+            return switchTo(PopupVideoPlayer.class);
         }
         return false;
     }
diff --git a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
index 5000d07e2..b2af6d9d8 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PopupVideoPlayerActivity.java
@@ -48,13 +48,7 @@ public final class PopupVideoPlayerActivity extends ServicePlayerActivity {
     @Override
     public boolean onPlayerOptionSelected(MenuItem item) {
         if (item.getItemId() == R.id.action_switch_background) {
-            this.player.setRecovery();
-            getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-            getApplicationContext().startService(
-                getSwitchIntent(BackgroundPlayer.class)
-                    .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
-            );
-            return true;
+            return switchTo(BackgroundPlayer.class);
         }
         return false;
     }
diff --git a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
index a7b3006a1..b5a697d05 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ServicePlayerActivity.java
@@ -165,13 +165,7 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
                 startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
                 return true;
             case R.id.action_switch_main:
-                this.player.setRecovery();
-                getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
-                getApplicationContext().startActivity(
-                    getSwitchIntent(MainVideoPlayer.class)
-                        .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying())
-                );
-                return true;
+                return switchTo(MainVideoPlayer.class);
         }
         return onPlayerOptionSelected(item) || super.onOptionsItemSelected(item);
     }
@@ -194,7 +188,15 @@ public abstract class ServicePlayerActivity extends AppCompatActivity
                 null,
                 false,
                 false
-        ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        .putExtra(BasePlayer.START_PAUSED, !this.player.isPlaying());
+    }
+
+    protected boolean switchTo(final Class clazz) {
+        this.player.setRecovery();
+        getApplicationContext().sendBroadcast(getPlayerShutdownIntent());
+        getApplicationContext().startActivity(getSwitchIntent(clazz));
+        return true;
     }
 
     ////////////////////////////////////////////////////////////////////////////

From 7dbb2b206c438078676f668a9a779537730143fe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Rapha=C3=ABl=20Jakse?= <raphael.github@jakse.fr>
Date: Thu, 16 Jan 2020 20:46:52 +0100
Subject: [PATCH 05/10] Simplify an if expression

---
 app/src/main/java/org/schabi/newpipe/player/BasePlayer.java | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
index cd1ec07f9..46ca3921d 100644
--- a/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/BasePlayer.java
@@ -946,10 +946,10 @@ public abstract class BasePlayer implements
     public void onPlayPause() {
         if (DEBUG) Log.d(TAG, "onPlayPause() called");
 
-        if (!isPlaying()) {
-            onPlay();
-        } else {
+        if (isPlaying()) {
             onPause();
+        } else {
+            onPlay();
         }
     }
 

From 845767e2f8f62dc34452e54723a2dd3670c93ff7 Mon Sep 17 00:00:00 2001
From: kapodamy <kapo.damy@yahoo.com.ar>
Date: Sat, 18 Jan 2020 00:43:38 -0300
Subject: [PATCH 06/10] StandardCharsets.UTF_8 instead of
 Charset.forName("utf-8")

---
 .../main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
index 75e16edad..696f24d05 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
@@ -22,7 +22,7 @@ public class SrtFromTtmlWriter {
 
     private SharpStream out;
     private boolean ignoreEmptyFrames;
-    private final Charset charset = Charset.forName("utf-8");
+    private final Charset charset = StandardCharsets.UTF_8;
 
     private int frameIndex = 0;
 

From a2d3e2c7e0799fbc9fba717d7cc586dbf63ac241 Mon Sep 17 00:00:00 2001
From: kapodamy <kapo.damy@yahoo.com.ar>
Date: Sat, 18 Jan 2020 01:10:25 -0300
Subject: [PATCH 07/10] 2 typo fixup * add missing namespace of
 StandardCharsets * use an unused constructor argument

---
 .../java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
index 696f24d05..e20b06352 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
@@ -12,7 +12,7 @@ import org.schabi.newpipe.streams.io.SharpStream;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.charset.Charset;
-import java.text.ParseException;
+import java.nio.charset.StandardCharsets;
 
 /**
  * @author kapodamy
@@ -28,7 +28,7 @@ public class SrtFromTtmlWriter {
 
     public SrtFromTtmlWriter(SharpStream out, boolean ignoreEmptyFrames) {
         this.out = out;
-        this.ignoreEmptyFrames = true;
+        this.ignoreEmptyFrames = ignoreEmptyFrames;
     }
 
     private static String getTimestamp(Element frame, String attr) {

From afc362d2b613773df1896dbb1a3f6179f2d7beb6 Mon Sep 17 00:00:00 2001
From: kapodamy <kapo.damy@yahoo.com.ar>
Date: Mon, 20 Jan 2020 23:33:30 -0300
Subject: [PATCH 08/10] readability changes

---
 .../java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
index e20b06352..6f1cceeed 100644
--- a/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
+++ b/app/src/main/java/org/schabi/newpipe/streams/SrtFromTtmlWriter.java
@@ -34,7 +34,7 @@ public class SrtFromTtmlWriter {
     private static String getTimestamp(Element frame, String attr) {
         return frame
                 .attr(attr)
-                .replace('.', ',');// Str uses comma as decimal separator
+                .replace('.', ',');// SRT subtitles uses comma as decimal separator
     }
 
     private void writeFrame(String begin, String end, StringBuilder text) throws IOException {
@@ -69,7 +69,7 @@ public class SrtFromTtmlWriter {
         Document doc = Jsoup.parse(new ByteArrayInputStream(buffer), "UTF-8", "", Parser.xmlParser());
 
         StringBuilder text = new StringBuilder(128);
-        Elements paragraph_list = doc.select("body>div>p");
+        Elements paragraph_list = doc.select("body > div > p");
 
         // check if has frames
         if (paragraph_list.size() < 1) return;

From 7f7bf8474e0f8cdbc730cfaab80d3a0ffce4dfbe Mon Sep 17 00:00:00 2001
From: Tobias Groza <TobiGr@users.noreply.github.com>
Date: Sat, 25 Jan 2020 21:35:07 +0100
Subject: [PATCH 09/10] Add link to FAQ in README

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f78725338..987327ab8 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
 </p>
 <hr>
 <p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#description">Description</a> &bull; <a href="#features">Features</a> &bull; <a href="#updates">Updates</a> &bull; <a href="#contribution">Contribution</a> &bull; <a href="#donate">Donate</a> &bull; <a href="#license">License</a></p>
-<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a>  &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
+<p align="center"><a href="https://newpipe.schabi.org">Website</a> &bull; <a href="https://newpipe.schabi.org/blog/">Blog</a> &bull; <a href="https://newpipe.schabi.org/FAQ/">FAQ</a> &bull; <a href="https://newpipe.schabi.org/press/">Press</a></p>
 <hr>
 
 <b>WARNING: THIS IS A BETA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.</b>

From 94403a9c3c137d3911fa205f8d039527f64d8f12 Mon Sep 17 00:00:00 2001
From: Christophe <contact2@c-henry.fr>
Date: Thu, 25 Apr 2019 20:47:39 +0200
Subject: [PATCH 10/10] Add send to Kodi button to player next to share button

---
 .../fragments/detail/VideoDetailFragment.java | 13 ++---------
 .../newpipe/player/MainVideoPlayer.java       | 21 +++++++++++++++++
 .../org/schabi/newpipe/util/KoreUtil.java     | 23 +++++++++++++++++++
 .../activity_main_player.xml                  | 21 ++++++++++++++++-
 .../main/res/layout/activity_main_player.xml  | 21 ++++++++++++++++-
 5 files changed, 86 insertions(+), 13 deletions(-)
 create mode 100644 app/src/main/java/org/schabi/newpipe/util/KoreUtil.java

diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
index 14e989625..f59cfaef0 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java
@@ -79,6 +79,7 @@ import org.schabi.newpipe.util.Constants;
 import org.schabi.newpipe.util.ExtractorHelper;
 import org.schabi.newpipe.util.ImageDisplayConstants;
 import org.schabi.newpipe.util.InfoCache;
+import org.schabi.newpipe.util.KoreUtil;
 import org.schabi.newpipe.util.ListHelper;
 import org.schabi.newpipe.util.Localization;
 import org.schabi.newpipe.util.NavigationHelper;
@@ -624,7 +625,7 @@ public class VideoDetailFragment
                             url.replace("https", "http")));
                 } catch (Exception e) {
                     if (DEBUG) Log.i(TAG, "Failed to start kore", e);
-                    showInstallKoreDialog(activity);
+                    KoreUtil.showInstallKoreDialog(activity);
                 }
                 return true;
             default:
@@ -632,16 +633,6 @@ public class VideoDetailFragment
         }
     }
 
-    private static void showInstallKoreDialog(final Context context) {
-        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
-        builder.setMessage(R.string.kore_not_found)
-                .setPositiveButton(R.string.install, (DialogInterface dialog, int which) ->
-                        NavigationHelper.installKore(context))
-                .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
-                });
-        builder.create().show();
-    }
-
     private void setupActionBarOnError(final String url) {
         if (DEBUG) Log.d(TAG, "setupActionBarHandlerOnError() called with: url = [" + url + "]");
         Log.e("-----", "missing code");
diff --git a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
index 9ccf5b9d3..1153c41fd 100644
--- a/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
+++ b/app/src/main/java/org/schabi/newpipe/player/MainVideoPlayer.java
@@ -28,6 +28,7 @@ import android.database.ContentObserver;
 import android.graphics.Color;
 import android.graphics.drawable.ColorDrawable;
 import android.media.AudioManager;
+import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
@@ -75,6 +76,7 @@ import org.schabi.newpipe.player.playqueue.PlayQueueItemTouchCallback;
 import org.schabi.newpipe.player.resolver.MediaSourceTag;
 import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
 import org.schabi.newpipe.util.AnimationUtils;
+import org.schabi.newpipe.util.KoreUtil;
 import org.schabi.newpipe.util.ListHelper;
 import org.schabi.newpipe.util.NavigationHelper;
 import org.schabi.newpipe.util.PermissionHelper;
@@ -435,6 +437,7 @@ public final class MainVideoPlayer extends AppCompatActivity
         private boolean queueVisible;
 
         private ImageButton moreOptionsButton;
+        private ImageButton kodiButton;
         private ImageButton shareButton;
         private ImageButton toggleOrientationButton;
         private ImageButton switchPopupButton;
@@ -471,6 +474,7 @@ public final class MainVideoPlayer extends AppCompatActivity
 
             this.moreOptionsButton = rootView.findViewById(R.id.moreOptionsButton);
             this.secondaryControls = rootView.findViewById(R.id.secondaryControls);
+            this.kodiButton = rootView.findViewById(R.id.kodi);
             this.shareButton = rootView.findViewById(R.id.share);
             this.toggleOrientationButton = rootView.findViewById(R.id.toggleOrientation);
             this.switchBackgroundButton = rootView.findViewById(R.id.switchBackground);
@@ -482,6 +486,9 @@ public final class MainVideoPlayer extends AppCompatActivity
 
             titleTextView.setSelected(true);
             channelTextView.setSelected(true);
+            boolean showKodiButton = PreferenceManager.getDefaultSharedPreferences(this.context).getBoolean(
+					this.context.getString(R.string.show_play_with_kodi_key), false);
+            kodiButton.setVisibility(showKodiButton ? View.VISIBLE : View.GONE);
 
             getRootView().setKeepScreenOn(true);
         }
@@ -518,6 +525,7 @@ public final class MainVideoPlayer extends AppCompatActivity
             closeButton.setOnClickListener(this);
 
             moreOptionsButton.setOnClickListener(this);
+            kodiButton.setOnClickListener(this);
             shareButton.setOnClickListener(this);
             toggleOrientationButton.setOnClickListener(this);
             switchBackgroundButton.setOnClickListener(this);
@@ -588,6 +596,17 @@ public final class MainVideoPlayer extends AppCompatActivity
             finish();
         }
 
+        public void onKodiShare() {
+            onPause();
+            try {
+                NavigationHelper.playWithKore(this.context, Uri.parse(
+                        playerImpl.getVideoUrl().replace("https", "http")));
+            } catch (Exception e) {
+                if (DEBUG) Log.i(TAG, "Failed to start kore", e);
+                KoreUtil.showInstallKoreDialog(this.context);
+            }
+        }
+
         /*//////////////////////////////////////////////////////////////////////////
         // Player Overrides
         //////////////////////////////////////////////////////////////////////////*/
@@ -688,6 +707,8 @@ public final class MainVideoPlayer extends AppCompatActivity
             } else if (v.getId() == closeButton.getId()) {
                 onPlaybackShutdown();
                 return;
+            } else if (v.getId() == kodiButton.getId()) {
+            	onKodiShare();
             }
 
             if (getCurrentState() != STATE_COMPLETED) {
diff --git a/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java
new file mode 100644
index 000000000..2ed3c698d
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/KoreUtil.java
@@ -0,0 +1,23 @@
+package org.schabi.newpipe.util;
+
+
+import android.content.Context;
+import android.content.DialogInterface;
+import androidx.appcompat.app.AlertDialog;
+
+import org.schabi.newpipe.R;
+
+
+public class KoreUtil {
+    private KoreUtil() { }
+
+    public static void showInstallKoreDialog(final Context context) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setMessage(R.string.kore_not_found)
+               .setPositiveButton(R.string.install,
+                   (DialogInterface dialog, int which) -> NavigationHelper.installKore(context))
+               .setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> {
+               });
+        builder.create().show();
+    }
+}
diff --git a/app/src/main/res/layout-large-land/activity_main_player.xml b/app/src/main/res/layout-large-land/activity_main_player.xml
index b535db2b8..98017b132 100644
--- a/app/src/main/res/layout-large-land/activity_main_player.xml
+++ b/app/src/main/res/layout-large-land/activity_main_player.xml
@@ -305,7 +305,7 @@
                     tools:text="English" />
 
                 <ImageButton
-                    android:id="@+id/share"
+                    android:id="@+id/kodi"
                     android:layout_width="30dp"
                     android:layout_height="30dp"
                     android:layout_marginLeft="4dp"
@@ -316,6 +316,25 @@
                     android:focusable="true"
                     android:padding="5dp"
                     android:scaleType="fitXY"
+                    android:src="@drawable/ic_cast_white_24dp"
+                    android:background="?attr/selectableItemBackground"
+                    android:contentDescription="@string/play_with_kodi_title"
+                    tools:ignore="RtlHardcoded"
+                    android:visibility="visible"/>
+
+                <ImageButton
+                    android:id="@+id/share"
+                    android:layout_width="30dp"
+                    android:layout_height="30dp"
+                    android:layout_marginLeft="4dp"
+                    android:layout_marginRight="2dp"
+                    android:layout_toLeftOf="@id/kodi"
+                    android:layout_alignWithParentIfMissing="true"
+                    android:layout_centerVertical="true"
+                    android:clickable="true"
+                    android:focusable="true"
+                    android:padding="5dp"
+                    android:scaleType="fitXY"
                     android:src="@drawable/ic_share_white_24dp"
                     android:background="?attr/selectableItemBackground"
                     android:contentDescription="@string/share"
diff --git a/app/src/main/res/layout/activity_main_player.xml b/app/src/main/res/layout/activity_main_player.xml
index f2b204b75..ab9eb8a61 100644
--- a/app/src/main/res/layout/activity_main_player.xml
+++ b/app/src/main/res/layout/activity_main_player.xml
@@ -303,7 +303,7 @@
                     tools:text="English" />
 
                 <ImageButton
-                    android:id="@+id/share"
+                    android:id="@+id/kodi"
                     android:layout_width="30dp"
                     android:layout_height="30dp"
                     android:layout_marginLeft="4dp"
@@ -314,6 +314,25 @@
                     android:focusable="true"
                     android:padding="5dp"
                     android:scaleType="fitXY"
+                    android:src="@drawable/ic_cast_white_24dp"
+                    android:background="?attr/selectableItemBackground"
+                    android:contentDescription="@string/play_with_kodi_title"
+                    tools:ignore="RtlHardcoded"
+                    android:visibility="visible"/>
+
+                <ImageButton
+                    android:id="@+id/share"
+                    android:layout_width="30dp"
+                    android:layout_height="30dp"
+                    android:layout_marginLeft="4dp"
+                    android:layout_marginRight="2dp"
+                    android:layout_toLeftOf="@id/kodi"
+                    android:layout_alignWithParentIfMissing="true"
+                    android:layout_centerVertical="true"
+                    android:clickable="true"
+                    android:focusable="true"
+                    android:padding="5dp"
+                    android:scaleType="fitXY"
                     android:src="@drawable/ic_share_white_24dp"
                     android:background="?attr/selectableItemBackground"
                     android:contentDescription="@string/share"