video speed

try to take over the world!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         video speed
// @namespace    http://tampermonkey.net/
// @version      20240114.1
// @description  try to take over the world!
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

class KeepVideoTime {
    constructor(props) {
        this.key = "_video_cache_";
        this.obj = this.get() || {};
        this.id = null;
    }
    set(key, time) {
        this.obj[key] = time;
        this.store();
    }
    store() {
        localStorage.setItem(this.key, JSON.stringify(this.obj));
    }

    get() {
        let data = localStorage.getItem(this.key);
        if (!data) {
            return null;
        }
        try {
            data = JSON.parse(data)
        } catch (e) {
            return null;
        } finally {
            return data;
        }
    }
    keep(getUrlId, video) {
        if (!this.id) {
            this.id = setInterval(() => {
                let vid = getUrlId();
                if (vid) {
                    this.set(vid, video.currentTime);
                }
            }, 500);
        }
    }
    stop() {
        if (this.id) {
            clearInterval(this.id);
            this.id = null;
        }
    }
}
let KeepVideoTimeInstance = null;
let onceMap = {};
let clickDomOnce = function (selector, key) {
    if (key in onceMap) {
        return;
    }
    onceMap[key] = true;
    let id = setInterval(() => {
        let d = selector();
        if (d.length) {
            d[0].click();
            clearInterval(id);
            delete onceMap[key];
        }
    }, 500);
}
let DefaultAction = {
    'default'(video, cache = new Cache()) {
        return [
            {
                group: "重置",
                name: "重置",
                run: false,
                action() {
                    cache.clear();
                }
            },
            {
                group: "全屏自动隐藏",
                name: "全屏自动隐藏",
                run: false,
                action() {
                    // cache.obj.fullScreenToHide = !cache.obj.fullScreenToHide;
                }
            }
        ]
    },
    "bilibili.com"(video) {
        return [
            {
                group: "屏幕操作",
                name: "宽屏",
                run: false,
                action() {
                    clickDomOnce(() => document.getElementsByClassName('bpx-player-ctrl-web-enter'), "bpx-player-ctrl-web-enter");
                }
            },
            {
                group: "屏幕操作",
                name: "全屏",
                run: false,
                action() {
                    clickDomOnce(() => document.getElementsByClassName('bpx-player-ctrl-full'), "bpx-player-ctrl-full");
                }
            },
            {
                group: "屏幕操作",
                name: "undo",
                run: true,
                action() {
                    console.log("undo");
                }
            }
        ]
    },
    'youtube.com'(videoInstance) {
        return [
            {
                group: "屏幕操作",
                name: "宽屏",
                run: false,
                action() {
                    let video = videoInstance.getVideo();
                    if (video.width < window.innerWidth * .8) {
                        clickDomOnce(() => document.getElementsByClassName('ytp-size-button'), "ytp-size-button");
                    }
                }
            },
            {
                group: "屏幕操作",
                name: "全屏",
                run: false,
                action() {
                    clickDomOnce(() => document.getElementsByClassName('ytp-fullscreen-button'), "ytp-fullscreen-button");
                }
            },
            {
                group: "屏幕操作",
                name: "undo",
                run: true,
                action() {
                    console.log("undo");
                }
            },
            {
                name: "记住最后播放时间",
                action(run, cache) {
                    let video = videoInstance.getVideo();
                    let getVid = () => {
                        let vid = (location.search.split('?')[1] || "").split('&').map(_=>_.split('=')).filter(_=>_[0]==='v');
                        if (vid.length === 1) {
                            vid = vid[0][1];
                        }
                        return vid;
                    }
                    let vid = getVid();
                    if (!vid || vid.length < 3) {
                        return;
                    }
                    if (!KeepVideoTimeInstance) {
                        KeepVideoTimeInstance = new KeepVideoTime();
                        if (vid && vid in KeepVideoTimeInstance.obj) {
                            if (video.currentTime < KeepVideoTimeInstance.obj[vid]) {
                                video.currentTime = KeepVideoTimeInstance.obj[vid];
                            }
                        }
                    }
                    if (run) {
                        KeepVideoTimeInstance.keep(getVid, video);
                    } else {
                        KeepVideoTimeInstance.stop();
                    }
                },
                run: true,
                replay: true,
            }
        ]
    }
};
function checkWindow() {
    return window === parent.window;
};

class DraggableDom {
    static Style = {
        // width: 100px;
        // height: 100px;
        backgroundColor: "lightblue",
        border: "2px solid #4CAF50",
        margin: "10px",
        padding: "10px",
        cursor: "move",
        position: "fixed",
        zIndex: 100000,
        borderRadius: "5px",
    }
    constructor(cache, parentDom = document.body) {
        this.cache = cache;
        this.toggleKey = this.cache.obj.toggleKey;
        this.show = cache.obj.show;
        this.id = `DD_${new Date().getTime()}`;
        this.dom = null;
        this.size = {
            width: 0,
            height: 0,
        };
        this.mark = null;
        if (this.cache.obj.top > window.innerHeight) {
            this.cache.set("top", 0);
        }
        if (this.cache.obj.left > window.innerWidth) {
            this.cache.set("left", 0);
        }
        this.style = {
            ...DraggableDom.Style,
            width: `${cache.obj.width}px`,
            height: `${cache.obj.height}px`,
            top: `${cache.obj.top}px`,
            left: `${cache.obj.left}px`,
            opacity: cache.obj.opacity ? cache.obj.opacity : 1,
            display: cache.obj.show ? 'unset' : 'none',
        };
        this.createDom(parentDom);
        this.dom.id = this.id;
        this.bindEvent();
        setTimeout(() => {
            this.size.width = this.dom.scrollWidth + 20;
            this.size.height = this.dom.scrollHeight + 20;
        }, 500);
    }
    toggleShow(v) {
        if (typeof v === 'boolean') {
            this.show = v;
        } else {
            this.show = !this.show;
        }
        this.dom.style.display = this.show ? 'unset' : 'none';
    }
    getDom() {
        if (this.dom) {
            let dom = this.dom.getElementsByClassName('dom');
            if (dom.length) {
                return dom[0];
            }
        }
        return null;
    }
    createDom(parentDom) {
        // <button accessKey="T" onClick="clickSubmit()">提交按钮</button>
        let btn = document.createElement('button');
        btn.style.display = 'none';
        btn.setAttribute("accessKey", this.toggleKey);
        btn.onclick = () => {
            if (this.dom) {
                this.toggleShow();
            }
        };
        this.mark = document.createElement('div');
        this.mark.style.width = "100vw";
        this.mark.style.height = "100vh";
        this.mark.style.position = "fixed";
        this.mark.style.left = "0";
        this.mark.style.top = "0";
        this.mark.style.display = "none";
        this.mark.style.zIndex = this.style.zIndex - 1;
        this.dom = document.createElement('div');
        this.dom .setAttribute('draggable', "true");
        this.dom.innerHTML = `<div>按下 &lt;ALT+${this.toggleKey}&gt; 可以[隐藏/显示]当前选项框</div><div class="dom"></div>`
        for (let i in this.style) {
            this.dom.style[i] = this.style[i];
        }
        parentDom.append(this.dom);
        parentDom.append(this.mark);
        parentDom.append(btn);
    }
    bindEvent() {
        // JavaScript 代码,实现拖拽功能
        let draggableElement = this.dom;
        let $this = this;
        draggableElement.ondragstart = function(event) {
            event.dataTransfer.setData('text/plain', event.target.id);
            $this.mark.style.display = 'block';
        }
        draggableElement.ondragend = function(event) {
            $this.mark.style.display = 'none';
        }

        document.body.ondragover = function(event) {
            event.preventDefault();
        };

        document.body.ondrop = function(event) {
            event.preventDefault();
            var data = event.dataTransfer.getData('text/plain');
            if (!data) {
                return;
            }
            var draggedElement = document.getElementById(data);

            // 获取鼠标位置
            var mouseX = event.clientX;
            var mouseY = event.clientY;

            // 设置元素的新位置
            draggedElement.style.left = mouseX - draggedElement.offsetWidth / 2 + 'px';
            draggedElement.style.top = mouseY - draggedElement.offsetHeight / 2 + 'px';
            $this.cache.set("top", parseFloat(draggedElement.style.top));
            $this.cache.set("left", parseFloat(draggedElement.style.left));
        };
    }
};

class Cache {
    static Reg = {
        Float: /^[0-9]+.[0-9]+$/,
        Int: /^[0-9]+$/,
    }
    static Default = {
        toggleKey: "X",
        top: 0,
        left: 0,
        width: 'unset',
        height: 'unset',
        speed: 1,
        Action: {},
        opacity: 1,
        show: true,
        // fullScreenToHide: false,
    };
    constructor(action) {
        let $this = this;
        this.key = "_video_speed_cache_";
        this.objType = {
            toggleKey(r) {
                r = r.toString();
                if (r.length > 1) {
                    return r[0];
                } else if (r.length < 1) {
                    return "X";
                } else {
                    return r;
                }
            },
            opacity(r) {
                r = $this.toNumberFloat(r);
                return r <= 0 ? 0.1 : (r > 1 ? 1 : r);
            },
            top: $this.toNumberFloat,
            left: $this.toNumberFloat,
            // width: 'unset',
            // height: 'unset',
            speed(r) {
                return $this.toNumberFloat(r, 1)
            },
        };
        this.obj = this.get() || {};
        for (let i in Cache.Default) {
            if (!(i in this.obj)) {
                this.obj[i] = Cache.Default[i];
            }
        }
    }
    toNumberFloat(r, dv) {
        r = parseFloat(r.toString());
        return Cache.Reg.Float.test(r.toFixed(1)) ? r : (dv||0);
    }
    toNumberInt(r, dv) {
        r = parseInt(r.toString());
        return Cache.Reg.Int.test(r) ? r : (dv||0);
    }
    set(key, value) {
        if (key in this.objType) {
            this.obj[key] = this.objType[key](value);
        } else {
            this.obj[key] = value;
        }
        this.store();
        return this;
    }
    store() {
        localStorage.setItem(this.key, JSON.stringify(this.obj));
    }
    checkData(data) {
        for (let i in this.objType) {
            data[i] = this.objType[i](data[i]);
        }
        return data;
    }
    get() {
        let data = localStorage.getItem(this.key);
        if (!data) {
            return null;
        }
        try {
            data = JSON.parse(data)
        } catch (e) {
            return null;
        } finally {
            return this.checkData(data);
        }
    }
    clear() {
        setTimeout(() => {
            localStorage.removeItem(this.key);
        }, 1000);
    }
}

class PlayDom {
    constructor(video, videoInstance, actions = [], cache = new Cache()) {
        this.video = video;
        this.cache = cache;
        this.draggableDom = new DraggableDom(this.cache, videoInstance.getParentDomMethod());
        this.action = {};
        this.videoInstance = videoInstance;
        this.loopActionRun = false;

        let ret = this.buildAction(actions);

        this.createDom(ret.actionListForDom);
        ret.promise().then(() => {
            this.loopAction();
        });
    }
    addListen() {
        let $this = this;
        document.addEventListener('fullscreenchange', handleFullscreenChange);
        let video = null;

        function handleFullscreenChange() {
            if (!video) {
                video = $this.videoInstance.getVideo();
            }
            if (!document.fullscreenElement) {
                if ($this.cache.obj.Action['全屏自动隐藏'] === '全屏自动隐藏') {
                    $this.draggableDom.toggleShow(true);
                }
                return;
            }
            let isVideoFullscreen = false;
            if (video === document.fullscreenElement) {
                isVideoFullscreen = true;
            } else {
                isVideoFullscreen = !!new Array(...document.fullscreenElement.getElementsByTagName($this.videoInstance.baseDom)).filter(v => v=== video).length;
                if (isVideoFullscreen) {
                    video = document.fullscreenElement;
                }
            }
            if ($this.cache.obj.Action['全屏自动隐藏'] === '全屏自动隐藏') {
                $this.draggableDom.toggleShow(false);
            }
        }
    }
    loopAction() {
        if (this.loopActionRun) return;
        this.loopActionRun = true;
        this.addListen();
        let fn = () => {
            let v = this.videoInstance.getVideo();

            let update = false;
            let windowWidth = window.innerWidth;
            let windowHeight = window.innerHeight;
            if (windowWidth < this.cache.obj.left) {
                this.cache.obj.left = windowWidth - (this.draggableDom.size.width || windowWidth);
                update = true;
            }
            if (windowHeight < this.cache.obj.top) {
                this.cache.obj.top = windowHeight - (this.draggableDom.size.height || windowHeight);
                update = true;
            }

            v.playbackRate = this.cache.obj.speed;
            if (update) {
                this.draggableDom.dom.style.left = `${this.cache.obj.left}px`;
                this.draggableDom.dom.style.top = `${this.cache.obj.top}px`;
            }

            this.videoInstance.LoopAction();
        }
        fn();
        setInterval(() => {
            fn();
        },500);
    }

    buildAction(actions) {
        let cacheAction = this.cache.obj.Action;
        let actionList = [];
        let actionListForDom = {};
        let actionListForDom_ = [];
        actions.forEach(act => {
            this.action[act.name] = {
                action:()=>{},
                replay: false,
            }
            // act = { name: "", action() {}, group: "", run: false }
            if (!act.group) {
                act.group = act.name;
            }
            if (!(act.group in actionListForDom)) {
                actionListForDom[act.group] = [];
            }
            if (!(act.group in cacheAction)) {
                cacheAction[act.group] = "";
            } else if (cacheAction[act.group] === "") {
                cacheAction[act.group] = "none_" + new Date().getTime();
            }
            if (act.name === cacheAction[act.group]) {
                // 该主当前希望运行改方法
                actionList.push(act.action);
                cacheAction[act.group] = act.name;
                act.run = true;
                if (act.replay) {
                    this.action[act.name] = {
                        action: act.action,
                        replay: act.replay,
                    };
                }
            } else {
                if (act.run) {
                    if (cacheAction[act.group] === "") {
                        cacheAction[act.group] = act.name;
                        actionList.push(act.action);
                        if (act.replay) {
                            this.action[act.name] = {
                                action: act.action,
                                replay: act.replay,
                            };
                        }
                    } else {
                        act.run = false;
                        this.action[act.name] = {
                            action: act.action,
                            replay: false,
                        };
                    }
                } else {
                    this.action[act.name] = {
                        action: act.action,
                        replay: false,
                    };
                }
            }
            if (!act.replay) {
                this.action[act.name].replay = true;
            }
            actionListForDom[act.group].push({
                name: act.name,
                run: act.run
            });
        });
        for (let i in actionListForDom) {
            actionListForDom_.push({
                group: i,
                actions: actionListForDom[i]
            });
        }
        this.cache.obj.Action = Object.assign(this.cache.obj.Action, cacheAction);
        this.cache.store();
        return {
            promise: (function (actionLis, cache) {
                return new Promise(s => s()).then(() => {
                    actionList.forEach(act => act(true, cache));
                    return true;
                });
            }).bind(null,actionList,this.cache.obj),
            actionListForDom: actionListForDom_
        };
    }
    createDom(actionListForDom) {
        let styleDom = document.createElement('style');
        styleDom.innerHTML = `.play_dom_box {
        display: flex;
        padding: 5px;
    }
    .play_dom_btn {
        background: gainsboro;
        padding: 2px;
        margin-bottom: 2px;
        border-radius: 4px;
        width: 40px;
        text-align: center;
        cursor: pointer;
    }
    .play_dom_text {
        flex: 1;
        line-height: 80px;
        text-align: center;
    }`;
        let boxDom = document.createElement('div');
        boxDom.innerHTML = `
<div class="play_dom_box">
    <div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">-0.1</div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">-0.2</div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">-0.5</div>
    </div>
    <div class="play_dom_text play_dom_text_speed">${this.cache.obj.speed}</div>
    <div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">+0.1</div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">+0.2</div>
        <div class="play_dom_btn play_dom_btn_speed" tag="speed">+0.5</div>
    </div>
</div>
<div style="display: flex;">透明 【<div class="play_dom_text_opacity">${this.cache.obj.opacity}</div>】 <div class="play_dom_btn play_dom_btn_opacity" tag="speed">-0.1</div>
&nbsp;&nbsp;&nbsp;<div class="play_dom_btn play_dom_btn_opacity" tag="speed">+0.1</div></div>
<div>
${actionListForDom.map(act => {
            // {
            //      group: "",
            //      actions: [{name: '', run: ''}]
            // }
            if (act.actions.length > 1) {
                let dom = act.actions.map(a => {
                    return `<input type="radio" tag="${a.name}" name="${act.group}" ${a.run ? 'checked' : ''} class="play_dom_check"/> ${a.name}`;
                }).join('');
                return `<div>${act.group} | ${dom}</div>`
            } else {
                let acti = act.actions[0];
                return `<div>${acti.name} <input tag="${acti.name}" type="checkbox" name="${act.group}" ${acti.run ? 'checked' : ''} class="play_dom_check"/></div>`
            }
        }).join(' ')}
</div>`;
        let dom = this.draggableDom.getDom();
        dom.append(styleDom);
        dom.append(boxDom);
        let $this = this;
        let speedTextDom = boxDom.getElementsByClassName('play_dom_text_speed')[0];
        let opacityTextDom = boxDom.getElementsByClassName('play_dom_text_opacity')[0];

        new Array(...boxDom.getElementsByClassName('play_dom_btn_opacity')).forEach(btn => {
            btn.onclick = function () {
                let sp = +this.innerText;
                $this.cache.set('opacity', (+$this.cache.obj.opacity + sp).toFixed(1));
                opacityTextDom.innerText = $this.cache.obj.opacity;
                $this.draggableDom.dom.style.opacity = $this.cache.obj.opacity;
            };
        });
        new Array(...boxDom.getElementsByClassName('play_dom_btn_speed')).forEach(btn => {
            btn.onclick = function () {
                let sp = +this.innerText;
                $this.cache.set('speed', (+$this.cache.obj.speed + sp).toFixed(2));
                speedTextDom.innerText = $this.cache.obj.speed;
                // $this.video.playbackRate = $this.cache.obj.speed;
            };
        });
        new Array(...boxDom.getElementsByClassName('play_dom_check')).forEach(btn => {
            btn.onchange = function () {
                let group = this.getAttribute('name');
                let actionName = this.getAttribute('tag');
                let currentAction = $this.action[actionName];
                if (this.checked) {
                    $this.cache.obj.Action[group] = actionName;
                    if (currentAction.replay) {
                        currentAction.action(true, $this.cache.obj);
                    } else {
                        currentAction.action(true, $this.cache.obj);
                        $this.action[actionName].action = () => {};
                    }
                } else {
                    $this.cache.obj.Action[group] = "";
                    if (currentAction.replay) {
                        currentAction.action(false, $this.cache.obj);
                    }
                }
                $this.cache.store();
            };
        });
    }
}

class Video {
    static getVideoMethod = {
        default() {
            let videos = document.getElementsByTagName('video');
            let video = false;
            if (videos.length) {
                if (videos.length === 1) {
                    video = videos[0];
                } else {
                    let ww = parseInt(window.innerWidth * 0.4);
                    let v = videos.filter(v => v.width > ww);
                    if (v.length) {
                        video = v[0];
                    }
                }
            }
            return video;
        },
        'youtube.com'() {
            let videos = document.getElementsByTagName('video');
            return videos.length ? videos[0] : false;
        },
        'bilibili.com'() {
            let bwpVideos = document.getElementsByTagName('bwp-video');
            let trVideos = document.getElementsByTagName('video');
            return bwpVideos.length ? bwpVideos[0] : (trVideos.length ? trVideos[0] : false);
        },
    }
    static getParentDomMethod = {
        'bilibili.com'() {
            let dom = document.getElementsByClassName('bpx-player-video-area');
            if (dom.length) {
                return dom[0];
            }
            window.loopActionBili = true;
            return document.body;
        }
    }
    static LoopAction = {
        'bilibili.com'() {
            if (!window.loopActionBili) {
                window.loopActionBili = setInterval(() => {
                    new Array(...document.getElementsByClassName('fixed-header')).forEach(ele => ele.style.zIndex = 1);
                }, 500);
            }
        }
    }
    static RetryTime = {
        'default': {
            time: 60,
            sleepTime: 1000,
        },
        'youtube.com': {
            time: 1e5,
            sleepTime: 3000,
        },
    }
    constructor() {
        this.baseDom = null;
        this.host =null;
        this.getVideoMethod = () => false;
        this.getParentDomMethod = () => document.body;
        this.LoopAction = () => {};
    }
    getVideo() {
        return this.getVideoMethod();
    }
    init() {
        return new Promise(s => {
            setTimeout(() => {
                this.host = location.hostname.split('.').reverse().slice(0, 2).reverse().join('.');
                let RetryTime = this.host in Video.RetryTime ? Video.RetryTime[this.host] : Video.RetryTime['default'];
                if (this.host in Video.getVideoMethod) {
                    this.getVideoMethod = Video.getVideoMethod[this.host];
                }
                if (this.host in Video.getParentDomMethod) {
                    this.getParentDomMethod = Video.getParentDomMethod[this.host];
                }
                if (this.host in Video.LoopAction) {
                    this.LoopAction = Video.LoopAction[this.host];
                }
                // 尝试推荐方法2次后使用默认方法不断尝试,超过一分钟后停止所有尝试
                let times = 2;
                let nexter = () => {
                    this.getVideoMethod = Video.getVideoMethod["default"];
                    times = RetryTime.time;
                    let id = setInterval(() => {
                        let video = this.getVideoMethod();
                        if (video) {
                            clearInterval(id);
                            this.baseDom = video.tagName;
                            s();
                        } else {
                            times--;
                            if (!times) {
                                clearInterval(id);
                                console.log("放弃,没有希望了");
                            }
                        }
                    }, RetryTime.sleepTime);
                };

                let id = setInterval(() => {
                    let video = this.getVideoMethod();
                    if (video) {
                        clearInterval(id);
                        this.baseDom = video.tagName;
                        s();
                    } else {
                        times--;
                        if (!times) {
                            clearInterval(id);
                            nexter();
                        }
                    }
                }, 500);
            }, 1000);
        })
    }
    getParentDom() {
        return this.getParentDomMethod();
    }
}
(function() {
    'use strict';
    // window.customElements.defineOld = window.customElements.define;
    // window.customElements.define = function (name, clz) {
    //     clz.prototype.attachShadowOld = clz.prototype.attachShadow;
    //     clz.prototype.attachShadow = function (obj) {
    //         return this.attachShadowOld({...obj||{},mode: 'open'})
    //     }
    //     return window.customElements.defineOld(name, clz);
    // }
    if (checkWindow()) {
        let video = new Video();
        video.init().then(() => {
            let v = video.getVideo();
            let cache = new Cache();
            if (v) {
                console.log("play dom");
                window.playDom = new PlayDom(v, video, [
                    ...(DefaultAction[video.host](video, cache) || []),
                    ...DefaultAction['default'](video, cache),
                ],cache);
            }
        });
    }
    // Your code here...
})();