Video Speed Buttons

Add speed buttons to any HTML5 <video> element. Comes with a loader for YouTube and Vimeo

当前为 2017-09-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Video Speed Buttons
// @description  Add speed buttons to any HTML5 <video> element. Comes with a loader for YouTube and Vimeo
// @namespace    bradenscode
// @version      1.0.2
// @copyright    2017, Braden Best
// @run-at       document-end
// @grant        none
//
// @match        *://*.youtube.com/*
// @match        *://*.vimeo.com/*
// ==/UserScript==

// To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom

function video_speed_buttons(anchor, video_el){
    if(!anchor || !video_el)
        return null;

    const COLOR_SELECTED = "black",
        COLOR_NORMAL = "grey",
        BUTTON_SIZE = "100%",
        DEFAULT_SPEED = 1.0,
        LABEL_TEXT = "Video Speed: ";

    const BUTTON_TEMPLATES = [
        ["25%",    0.25],
        ["50%",    0.5],
        ["Normal", 1],
        ["1.5x",   1.5],
        ["2x",     2],
        ["3x",     3],
        ["4x",     4],
        ["8x",     8],
        ["16x",    16]
    ];

    const buttons = {
        head:      null,
        selected:  null,
        last:      null
    };

    const keyboard_controls = [
        [",", "Speed Down", function(ev){
            if(is_comment_box(ev.target))
                return false;

            (buttons.selected || buttons.head)
                .getprev()
                .el
                .dispatchEvent(new MouseEvent("click"));
        }],
        [".", "Speed Up", function(ev){
            if(is_comment_box(ev.target))
                return false;

            (buttons.selected || buttons.head)
                .getnext()
                .el
                .dispatchEvent(new MouseEvent("click"));
        }],
        ["?", "Show Help", function(ev){
            var infobox;

            if(is_comment_box(ev.target))
                return false;

            (infobox = Infobox(container))
                .log("Keyboard Controls (click to close)<br>");

            keyboard_controls.forEach(function([key, description]){
                infobox.log("    [.s]  .s<br>"
                        .replace(".s", key)
                        .replace(".s", description)); });
        }]
    ];

    const container = (function(){
        var div = document.createElement("div");
        var prev_node = null;

        div.className = "vsb-container";
        div.style.borderBottom = "1px solid #ccc";
        div.style.marginBottom = "10px";
        div.style.paddingBottom = "10px";
        div.appendChild(document.createTextNode(LABEL_TEXT));

        BUTTON_TEMPLATES.forEach(function(button){
            var speedButton = SpeedButton(...button, div);

            if(buttons.head === null)
                buttons.head = speedButton;

            if(prev_node !== null){
                speedButton.prev = prev_node;
                prev_node.next = speedButton;
            }

            prev_node = speedButton;

            if(speedButton.speed == DEFAULT_SPEED)
                speedButton.select();
        });

        return div;
    })();

    function is_comment_box(el){
        return el === document.querySelector(".comment-simplebox-text");
    }

    function Infobox(parent){
        var el = document.createElement("pre");

        el.style.font = "1em monospace";
        el.style.borderTop = "1px solid #ccc";
        el.style.marginTop = "10px";
        el.style.paddingTop = "10px";

        el.addEventListener("click", function(){
            parent.removeChild(el);
        });

        parent.appendChild(el);

        function log(msg){
            el.innerHTML += msg;
        }

        return {
            el,
            log
        };
    }

    function setPlaybackRate(el, rate){
        if(el)
            el.playbackRate = rate;
        else
            console.log("setPlaybackRate: video element is null or undefined");
    }

    function SpeedButton(text, speed, parent){
        var el = document.createElement("span");
        var self;

        el.innerHTML = text;
        el.style.marginRight = "10px";
        el.style.fontWeight = "bold";
        el.style.fontSize = BUTTON_SIZE;
        el.style.color = COLOR_NORMAL;
        el.style.cursor = "pointer";

        el.addEventListener("click", function(){
            setPlaybackRate(video_el, speed);
            self.select();
        });

        parent.appendChild(el);

        function select(){
            if(buttons.last !== null)
                buttons.last.el.style.color = COLOR_NORMAL;

            buttons.last = self;
            buttons.selected = self;
            el.style.color = COLOR_SELECTED;
        }

        function getprev(){
            if(self.prev === null)
                return self;

            return buttons.selected = self.prev;
        }

        function getnext(){
            if(self.next === null)
                return self;

            return buttons.selected = self.next;
        }

        return self = {
            el,
            text,
            speed,
            prev:  null,
            next:  null,
            select,
            getprev,
            getnext
        };
    }

    function kill(){
        anchor.removeChild(container);
        document.body.removeEventListener("keydown", ev_keyboard);
    }

    function ev_keyboard(ev){
        let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
        let callback = (match || {2: ()=>null})[2];

        callback(ev);
    }

    setPlaybackRate(video_el, DEFAULT_SPEED);
    anchor.insertBefore(container, anchor.firstChild);
    document.body.addEventListener("keydown", ev_keyboard);

    return {
        controls: keyboard_controls,
        buttons,
        kill,
        SpeedButton,
        Infobox,
        setPlaybackRate,
        is_comment_box
    };
}

video_speed_buttons.from_query = function(anchor_q, video_q){
    return video_speed_buttons(
            document.querySelector(anchor_q),
            document.querySelector(video_q));
}

// Multi-purpose Loader (defaults to floating on top right)
const loader_data = {
    container_candidates: [
        // YouTube
        "div#container.ytd-video-primary-info-renderer",
        "div#watch-header",
        "div#watch7-headline",
        "div#watch-headline-title",
        // Vimeo
        ".clip_info-wrapper",
    ],

    css_div: [
        "position:    fixed",
        "top:         0",
        "right:       0",
        "zIndex:      100",
        "background:  rgba(0, 0, 0, 0.8)",
        "color:       #eeeeee",
        "padding:     10px"
    ].map(rule => rule.split(/: */)),

    css_vsb_container: [
        "borderBottom:    none",
        "marginBottom:    0",
        "paddingBottom:   0",
    ].map(rule => rule.split(/: */))
};

setInterval(function(){
    let vsbc = () => document.querySelector(".vsb-container");
    let candidate;
    let default_candidate;

    if(document.readyState !== "complete" || vsbc() !== null)
        return;

    candidate = loader_data
        .container_candidates
        .map(candidate => document.querySelector(candidate))
        .find(candidate => candidate !== null);

    default_candidate = (function(){
        let el = document.createElement("div");

        loader_data.css_div.forEach(function([name, value]){
            el.style[name] = value; });

        document.body.appendChild(el);
        return el;
    }());

    video_speed_buttons(candidate || default_candidate, document.querySelector("video"));

    if(!candidate)
        loader_data.css_vsb_container.forEach(function([name, value]){
            vsbc().style[name] = value; });
}, 100);