Picture In Picture

Provide picture in picture functionality to HTML5 videos on supported browsers

// ==UserScript==
// @name               Picture In Picture
// @name:zh-CN         HTML5画中画
// @namespace          http://github.com/eternal-flame-AD/picture-in-picture/
// @version            0.2
// @homepage           http://github.com/eternal-flame-AD/picture-in-picture/
// @description        Provide picture in picture functionality to HTML5 videos on supported browsers
// @description:zh-CN  向兼容浏览器中的HTML5视频添加画中画按钮
// @compatible         chrome 70+
// @author             eternal-flame-AD
// @include            *
// @grant              GM_info
// @license            Apache-2.0
// ==/UserScript==

(function() {
    'use strict';

    if (!("pictureInPictureEnabled" in document)) {
        console.log("Your browser does not support picture in picture. Exiting...")
        return
    }
    if (!document.pictureInPictureEnabled) {
        console.log("Picture in picture is disabled. Exiting...")
        return
    }

    window["PIP_URL_BASE"] = "https://cdn.jsdelivr.net/gh/eternal-flame-AD/picture-in-picture/";

    function formatURL(url) {
        return (!/^(https?:)?\/\//.test(url))?(window['PIP_URL_BASE'] + url):url;
    }

    function createStylesheet(url) {
        url = formatURL(url);
        let elt = document.createElement('link');
        elt.rel = 'stylesheet';
        elt.href = url;
        document.documentElement.appendChild(elt);
    };

    const locateVideoElement = function _self(DOM=document.body) {
        let video = DOM.querySelector("video")
        if (video) return video
        let iframes = DOM.querySelectorAll("iframe")
        for (let iframe of iframes) {
            video = _self(iframe.contentDocument.body)
            if (video) return video
        }
        return null
    }

    let loaded = false;
    const load = function(target) {
        if (loaded) return
        loaded = true;
        createStylesheet('style.css');
        class PIP {
            constructor(target) {
                this.target = target || locateVideoElement(document.body);
                this.outside = false
    
                let holder = document.createElement('div')
                holder.className = 'pip-indicator-holder pip-off'
            
                let img = document.createElement('img');
                img.className = 'pip-indicator-logo'
                img.src = formatURL("logo.svg")
                holder.appendChild(img)
            
                document.body.appendChild(holder)
                
                this.off = this.off.bind(this)
                this.on = this.on.bind(this)
                this.updateTarget = this.updateTarget.bind(this)
                
                holder.onclick = () => {
                    (this.outside?this.off:this.on)().catch(console.error)
                }
                this.elem = holder;
            }
    
            updateTarget() {
                if (!this.target.isConnected) this.target = locateVideoElement(document.body);
                let _this = this;
                this.target.addEventListener('enterpictureinpicture', function _self() {
                    if (!_this.target.isConnected) {
                        _this.target.removeEventListener('enterpictureinpicture', _self)
                        return
                    }
                    _this.outside = true
                    _this.elem.classList.replace("pip-off","pip-on")
                });
            
                this.target.addEventListener('leavepictureinpicture', function _self() {
                    if (!_this.target.isConnected) {
                        _this.target.removeEventListener('leavepictureinpicture', _self)
                        return
                    }
                    _this.outside = false
                    _this.elem.classList.replace("pip-on","pip-off")
                });
            }
    
            async on() {
                this.updateTarget()
                await this.target.requestPictureInPicture()
            }
    
            async off() {
                await document.exitPictureInPicture()
            }
            
        }
        
        //new PIP(target)
        new PIP() // Fixed ad problem
    }

    {
        let observers = []
        let observe = function _self(DOM=document.body, _window=window, debug=false) {
            const gotVideo = function(video) {
                if (video.disablePictureInPicture) return;
                console.log("Got video element! Loading...")
                load(video)
                observers.forEach(obs=>{
                    obs.disconnect()
                })
            }
            
            {
                // Existing video
                let video = locateVideoElement(DOM)
                if (video) gotVideo(video)
            }
            
            {
                // Existing iframe
                DOM.querySelectorAll("iframe").forEach(iframe=>{
                    iframe.contentWindow.onload = function() {
                        iframe.contentWindow["PIP_OBSERVING"] = true;
                        _self(iframe.contentDocument.body,iframe.contentWindow)
                    }
                })
            }
            
            console.log("No video elements. Watching for changes...")

            let callback = function(mutationList) {
                mutationList.forEach((mutation)=>{
                    mutation.addedNodes.forEach((node)=>{
                        if (debug) console.log(node)
                        const search = function (node,tag) {
                            if (node.tagName && node.tagName.toUpperCase()==tag.toUpperCase()) {
                                return node
                            }
                            return node.querySelector && node.querySelector(tag)
                        }
                        
                        {
                            // Dynamically added video
                            let video = search(node, "video")
                            if (video) gotVideo(video)
                        }

                        {
                            // Dynamically added iframe
                            let iframe = search(node, "iframe")
                            if (iframe) {
                                iframe.contentWindow.onload = function() {
                                    if (iframe.contentWindow["PIP_OBSERVING"] == true) return;
                                    iframe.contentWindow["PIP_OBSERVING"] = true;
                                    _self(iframe.contentDocument.body,iframe.contentWindow)
                                }
                            }
                        }

                    })
                })
            }
            let observer = new _window.MutationObserver(callback)
            observer.observe(DOM,{
                childList: true,
                attributes: false,
                subtree: true
            })
            observers.push(observer)
            window.observers = observers;
        }
        observe(document.body)
    }

})();