PiPifier

PiPifier is an extension that lets you use every HTML5 video in Picture in Picture mode

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         PiPifier
// @namespace    https://github.com/Willian-Zhang/PiPifier
// @version      0.3
// @description  PiPifier is an extension that lets you use every HTML5 video in Picture in Picture mode
// @author       @arno_app <https://twitter.com/arno_app>, @Cacauu_de <https://twitter.com/Cacauu_de>, @Willian <https://github.com/willian-zhang>
// @match        */*
// @grant        none
// ==/UserScript==

//image URLs
var whiteSVG_Icon = `data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8"?>
<svg width="671px" height="441px" viewBox="0 0 671 441" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
    <title>PiP_Toolbar_Icon_white</title>
    <desc>Created with Sketch.</desc>
    <defs>
        <polyline id="path-1" points="617.200445 188.359322 617.200445 0 0 0 0 366.254237 252.55902 366.254237"></polyline>
        <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="617.200445" height="366.254237" fill="white">
            <use xlink:href="#path-1"></use>
        </mask>
    </defs>
    <g id="UI" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="PiP_Toolbar_Icon_white" transform="translate(-15.000000, -130.000000)">
            <g id="Toolbar-Icon" transform="translate(15.000000, 130.000000)">
                <use id="Combined-Shape" stroke="#FFFFFF" mask="url(#mask-2)" stroke-width="54" xlink:href="#path-1"></use>
                <rect id="Rectangle" fill="#FFFFFF" x="263.020045" y="197.328814" width="407.979955" height="243.671186"></rect>
                <path d="M166.149412,103.362155 L166.149412,245.485858 L131.742911,245.485858 L131.742911,103.706611 L89.1651635,115.230311 L149.582508,5.46510893 L209.999853,115.230311 L166.149412,103.362155 Z" id="Combined-Shape" fill="#FFFFFF" transform="translate(149.582508, 125.475483) rotate(-236.000000) translate(-149.582508, -125.475483) "></path>
            </g>
        </g>
    </g>
</svg>`;
var blackSVG_Icon = `data:image/svg+xml;utf8,<?xml version="1.0" encoding="UTF-8"?>
<svg width="701px" height="701px" viewBox="0 0 701 701" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <!-- Generator: Sketch 40.3 (33839) - http://www.bohemiancoding.com/sketch -->
    <title>PiP_Toolbar_Icon</title>
    <desc>Created with Sketch.</desc>
    <defs>
        <polyline id="path-1" points="617.200445 188.359322 617.200445 0 0 0 0 366.254237 252.55902 366.254237"></polyline>
        <mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="617.200445" height="366.254237" fill="white">
            <use xlink:href="#path-1"></use>
        </mask>
    </defs>
    <g id="UI" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
        <g id="PiP_Toolbar_Icon">
            <g id="Toolbar-Icon" transform="translate(15.000000, 130.000000)">
                <use id="Combined-Shape" stroke="#000000" mask="url(#mask-2)" stroke-width="54" xlink:href="#path-1"></use>
                <rect id="Rectangle" fill="#000000" x="263.020045" y="197.328814" width="407.979955" height="243.671186"></rect>
                <path d="M166.149412,103.362155 L166.149412,245.485858 L131.742911,245.485858 L131.742911,103.706611 L89.1651635,115.230311 L149.582508,5.46510893 L209.999853,115.230311 L166.149412,103.362155 Z" id="Combined-Shape" fill="#000000" transform="translate(149.582508, 125.475483) rotate(-236.000000) translate(-149.582508, -125.475483) "></path>
            </g>
        </g>
    </g>
</svg>`;

//safari.self.addEventListener("message", messageHandler); // Message recieved from Swift code
window.onfocus = function() {
    previousResult = null;
    checkForVideo();
}; // Tab selected
new MutationObserver(checkForVideo).observe(document, {subtree: true, childList: true}); // DOM changed

//function dispatchMessage(messageName, parameters) {
//    safari.extension.dispatchMessage(messageName, parameters);
//}

function messageHandler(event) {
    if (event.name === "enablePiP" && getVideo() != null) {
        enablePiP();
    } else if (event.name === "addCustomPiPButtonToPlayer") {
        addCustomPiPButtonToPlayer(event.message)
    }
}
function addCustomPiPButtonToPlayer(message){
    message.callback();
}

var previousResult = null;
var videoCheck = {found: true}
function checkForVideo() {
    if (getVideo() != null) {
        addCustomPiPButtons();
        if (previousResult === null || previousResult === false) {
            //dispatchMessage("videoCheck", {found: true});
            console.warn("videoCheck", {found: true})
            videoCheck = {found: true}
            // enablePiP();
        }
        previousResult = true;
    } else if (window == window.top) {
        if (previousResult === null || previousResult === true) {
            //dispatchMessage("videoCheck", {found: false});
            console.warn("videoCheck", {found: false})
            videoCheck = {found: false}
        }
        previousResult = false;
    }
}

function getVideo() {
    return document.getElementsByTagName('video')[0];
}

async function action(video) {
    if (video.hasAttribute('__pip__')) {
        await document.exitPictureInPicture();
    } else {
        await video.requestPictureInPicture();
        video.setAttribute('__pip__', true);
        video.addEventListener('leavepictureinpicture', event => {
            video.removeAttribute('__pip__');
        }, {
            once: true
        });
    }
}

function enablePiP() {
    let video = getVideo()
    if(video.webkitSetPresentationMode){
        // safari
        video.webkitSetPresentationMode('picture-in-picture');
    }else{
        //chrome
        action(video);
    }
}

//----------------- Custom Button Methods -----------------

var players = [
               {name: "YouTube", shouldAddButton: shouldAddYouTubeButton, addButton: addYouTubeButton},
               {name: "VideoJS", shouldAddButton: shouldAddVideoJSButton, addButton: addVideoJSButton},
               {name: "Netflix", shouldAddButton: shouldAddNetflixButton, addButton: addNetflixButton},
               {name: "Wistia", shouldAddButton: shouldAddWistiaButton, addButton: addWistiaButton},
               //TODO: add other players here
               ];

let pipCheck = function pipCheck(message){
    addCustomPiPButtonToPlayer(message);
}
function addCustomPiPButtons() {
    for (const player of players) {
        if (player.shouldAddButton()) {
            //dispatchMessage("pipCheck", {callback: player.addButton.name}) //Sets the callback to the player's addButton
            pipCheck({callback: player.addButton});
        }
    }
}

//----------------- Player Implementations -------------------------

function shouldAddYouTubeButton() {
    //check if on youtube or player is embedded
    return (location.hostname.match(/^(www\.)?youtube\.com$/)
            || document.getElementsByClassName("ytp-right-controls").length > 0)
    && document.getElementsByClassName('PiPifierButton').length == 0;
}

function addYouTubeButton() {
    if (!shouldAddYouTubeButton()) return;
    var button = document.createElement("button");
    button.className = "ytp-button PiPifierButton";
    button.title = "PiP (by PiPifier)";
    button.onclick = enablePiP;
    //TODO add style
    //button.style.backgroundImage = 'url('+ whiteSVG_Icon + ')';
    var buttonImage = document.createElement("img");
    buttonImage.src = whiteSVG_Icon;
    buttonImage.width = 22;
    buttonImage.height = 36;
    button.appendChild(buttonImage);

    document.getElementsByClassName("ytp-right-controls")[0].appendChild(button);
}


function shouldAddVideoJSButton() {
    return document.getElementsByClassName('vjs-control-bar').length > 0
    && document.getElementsByClassName('PiPifierButton').length == 0;
}


function addVideoJSButton() {
    if (!shouldAddVideoJSButton()) return;
    var button = document.createElement("button");
    button.className = "PiPifierButton vjs-control vjs-button";
    button.title = "PiP (by PiPifier)";
    button.onclick = enablePiP;
    var buttonImage = document.createElement("img");
    buttonImage.src = whiteSVG_Icon;
    buttonImage.width = 16;
    buttonImage.height = 30;
    button.appendChild(buttonImage);
    var fullscreenButton = document.getElementsByClassName("vjs-fullscreen-control")[0];
    fullscreenButton.parentNode.insertBefore(button, fullscreenButton);
}

function shouldAddWistiaButton() {
    return document.getElementsByClassName('wistia_playbar').length > 0
    && document.getElementsByClassName('PiPifierButton').length == 0;
}

function addWistiaButton() {
    if (!shouldAddWistiaButton()) return;
    var button = document.createElement("button");
    button.className = "PiPifierButton w-control w-control--fullscreen w-is-visible";
    button.alt = "Picture in Picture";
    button.title = "PiP (by PiPifier)";
    button.onclick = enablePiP;
    var buttonImage = document.createElement("img");
    buttonImage.src = whiteSVG_Icon;
    buttonImage.width = 28;
    buttonImage.height = 18;
    buttonImage.style.verticalAlign = "middle";
    button.appendChild(buttonImage);
    document.getElementsByClassName("w-control-bar__region--airplay")[0].appendChild(button);
}


function shouldAddNetflixButton() {
    return location.hostname.match('netflix')
    && document.getElementsByClassName('PiPifierButton').length == 0;
}

function addNetflixButton(timeOutCounter) {
    if (!shouldAddNetflixButton()) return;
    if (timeOutCounter == null) timeOutCounter = 0;
    var button = document.createElement("button");
    button.className = "PiPifierButton";
    button.title = "PiP (by PiPifier)";
    button.onclick = enablePiP;
    button.style.backgroundColor = "transparent";
    button.style.border = "none";
    button.style.maxHeight = "inherit";
    button.style.width = "70px";
    button.style.marginRight = "2px";
    var buttonImage = document.createElement("img");
    buttonImage.src = whiteSVG_Icon;
    buttonImage.style.verticalAlign = "middle";
    buttonImage.style.maxHeight = "40%";
    button.appendChild(buttonImage);
    var playerStatusDiv = document.getElementsByClassName("player-status")[0];
    if (playerStatusDiv == null && timeOutCounter < 3) {
        //this is needed because the div is sometimes not reachable on the first load
        //also necessary to count up and stop at some time to avoid endless loop on main netflix page
        setTimeout(function() {addNetflixButton(timeOutCounter+1);}, 3000);
        return;
    }
    playerStatusDiv.insertBefore(button, playerStatusDiv.firstChild);
}