视频倍速播放(追剧学习神器)

全网视频倍速播放,看视频播太慢,这能忍?直接倍速播放,最高速度20倍【食用方法】①调节右上角加速框右侧上下按钮即可调节倍率 ②在右上角的加速框内输入加速倍率,如2、4、8、16等。【快捷键】:①单手快捷键:“x”,“c” 恢复正常播放:“t”或“z” ②双手快捷键:ctrl + 左右箭头

目前为 2023-08-31 提交的版本。查看 最新版本

// ==UserScript==
// @name         视频倍速播放(追剧学习神器)
// @namespace    http://tampermonkey.net/
// @icon         https://img-blog.csdnimg.cn/20181221195058594.gif
// @version      1.3.1.1
// @description  全网视频倍速播放,看视频播太慢,这能忍?直接倍速播放,最高速度20倍【食用方法】①调节右上角加速框右侧上下按钮即可调节倍率 ②在右上角的加速框内输入加速倍率,如2、4、8、16等。【快捷键】:①单手快捷键:“x”,“c” 恢复正常播放:“t”或“z”  ②双手快捷键:ctrl + 左右箭头
// @author       wll
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/sweetalert2.all.min.js
// @require      https://gf.qytechs.cn/scripts/471299-toastify-js/code/toastifyjs.js?version=1222923
// @resource     css https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_registerMenuCommand
// @match        *://*/*
// @note         增加支持网站:	依照规则增加@match所在标签即可
// @note         郑重声明:	本脚本只做学习交流使用,未经作者允许,禁止转载,不得使用与非法用途,一经发现,追责到底
// @note         授权联系:	[email protected]
// @note         版本更新	20-12-26 1.0.0	初版发布视频倍速播放
// @note         版本更新	21-02-04 1.0.1 	优化用户体验
// @note         版本更新	21-02-04 1.0.2 	优化标题,优化简介
// @note         版本更新	21-06-18 1.0.3 	增加新的倍速网址,ehuixue.cn/index/study,ehuixue.cn/index/study,chaoxing.com
// @note         版本更新	21-06-25 1.0.4 	增加新的倍速网址,douyin.com
// @note         版本更新	21-06-26 1.0.5 	增加新的倍速网址,pan.baidu.com,youku.com
// @note         版本更新	21-07-09 1.0.6 	修正哔哩哔哩网站无法暂停问题
// @note         版本更新	21-10-11 1.0.7 	由于百度云视频倍速播放收费,一时无法解决,暂时停用百度相关加速*://*.pan.baidu.com/*
// @note         版本更新	21-12-11 1.0.8  感谢用户“何佳林”,提供建议,增加快捷键控制倍速 ctrl + ->  ctrl + <-
// @note         版本更新	21-12-13 1.0.9  增加cctv支持,增加倍速控件悬浮不跟随滑动
// @note         版本更新	21-12-14 1.1.0  增加倍率记忆功能,防止页面刷新倍率重新计算
// @note         版本更新	21-12-19 1.1.1  1、增加单手快捷键: “x” 、“c”, 2、增加寄存器倍率存储,浏览器全局使用 3、增加倍速框自动聚焦
// @note         版本更新	21-12-20 1.1.2  代码脚本优化
// @note         版本更新	21-12-20 1.1.3  增加全网倍速支持,让倍速不再有障碍
// @note         版本更新	21-12-21 1.1.4  增加快捷键d,用于恢复正常播放速度
// @note         版本更新	21-12-22 1.1.5  更改快捷键t,用于恢复正常播放速度
// @note         版本更新	21-12-23 1.1.6  修正倍速无法回到正常播放问题,感谢大佬“我不想上班”提供技术支持
// @note         版本更新	21-12-24 1.1.7  修正插件自带寄存器存储赋值失效问题
// @note         版本更新	21-12-24 1.1.8  修正bilibili自动下一集倍速失效问题
// @note         版本更新	21-12-28 1.1.9  增加大写快捷键支持
// @note         版本更新	22-01-06 1.2.0  增加倍率改变提示
// @note         版本更新	22-11-30 1.2.1  增加自动播放支持,增加代码优化
// @note         版本更新	22-12-03 1.2.2  增加浏览器菜单-可以:开启/关闭“倍速框”
// @note         版本更新	22-12-03 1.2.3  优化页面倍速框,倍速药丸不能停 O(∩_∩)O哈哈~
// @note         版本更新	22-12-05 1.2.4  优化倍速框样式
// @note         版本更新	23-01-18 1.2.5  @include *:* @match *://*/*
// @note         版本更新	23-06-08 1.2.6  增加触屏支持
// @note         版本更新	23-06-08 1.2.7  增加快速还原1.0
// @note         版本更新	23-07-21 1.2.8  替换提示,去除双手快捷键
// @note         版本更新	23-08-04 1.2.9  增加滑动支持
// @note         版本更新	23-08-04 1.3.0  滑动支持优化
// @note         版本更新	23-08-04 1.3.1  增加兼容性,增加跳过片头片尾功能

// ==/UserScript==

(function() {
    'use strict';

    // 自定义样式
    function addStyle() {
        let customCss=`
            #rangeId{z-index:99999999;position:fixed;top:100px;right:100px;width:60px;background-color:#E3EDCD;display:inline-block;text-align:center;padding:0 6px 0 7px;height:16px;line-height:16px;border-radius:9px;border:1px solid var(--brand_pink);outline: none;color:var(--brand_pink);font-size:12px;margin-right:4px;transition:background 0.3s,color 0.3s;flex-shrink:0;filter: opacity(1);}
            #rangeId:hover{filter: opacity(1);}
            .slider-container {display: flex;align-items: center;justify-content: center;}
        `;
        GM_addStyle(customCss);
    }

    // 自定义节点
    function addDocument(){

        $("body").prepend('<input id="rangeId" type="number" step="0.1" min="0.1" max="20" autofocus="autofocus" value=""  />');

        let element = document.getElementById('rangeId');
        element.style.opacity = 0.5;

        element.addEventListener('change', function () {
            // 在这里执行 change 事件的处理逻辑
            element.style.opacity = 1;
            addToast(element.value);

            // 设置一个定时器,在一秒后将opacity设置为0.5
            // setTimeout(function() {
            //     element.style.opacity = 0.5;
            // }, 1000);
        });
        element.addEventListener('mouseover', function() {
            element.style.opacity = 1;
        });
        element.addEventListener('mouseout', function() {
            element.style.opacity = 0.5;
        });
    }

    // 监听快捷键
    document.addEventListener("keypress", handleKeyPress);

    function handleKeyPress(e) {
        log.info("--->e.key:" + e.key);
        let videos = document.querySelectorAll("video").length;
        if (videos > 0) {
            switch (e.key.toLowerCase()) {
                case "x":
                    speedFun("-");
                    break;
                case "c":
                    speedFun("+");
                    break;
                case "t":
                case "z":
                    speedFun("1");
                    break;
            }
        }
    }

    /* PC端滑动处理 */
    var isMouseDown = false;
    var startX, startY;
    $(document).on('mousedown', function(event) {
        isMouseDown = true;
        startX = event.pageX;
        startY = event.pageY;
    });
    $(document).on('mousemove', function(event) {
        if (isMouseDown) {
            var currentX = event.pageX;
            var currentY = event.pageY;

            var distanceX = currentX - startX;
            var distanceY = currentY - startY;

            var times = Math.abs(distanceY) / 600;
            for (var i = 0; i < times; i++) {
                if (distanceY > 0) {
                    // speedFun("-");
                } else {
                    // speedFun("+");
                }
            }
        }
    });
    $(document).on('mouseup', function(event) {
        isMouseDown = false;
    });


    /* 移动端滑动处理 */
    var lastY = 0;
    var direction = ""; // 保存方向信息

    $(document).on('touchstart', function(e) {
        lastY = e.originalEvent.touches[0].clientY;
    });

    $(document).on('touchmove', function(e) {
        var currentY = e.originalEvent.touches[0].clientY;
        var deltaY = currentY - lastY;
        var times = Math.abs(deltaY) / 100;

        if (deltaY > 0) { direction = "down";} else { direction = "up";}

        for (var i = 0; i < times; i++) {
            log.info(direction);
            if (direction = "down") { speedFun("-"); }
            if (direction = "up") { speedFun("+"); }
        }

        lastY = currentY;
    });

    // 更改倍速
    function speedFun(spee) {

        log.info("this speedFun is spee:" + spee);
        controlVideoProperty('playbackRate', spee);  // 调用函数,设置播放速度为2.0

        if ("+" == spee) {
            let numVal = parseFloat(parseFloat($("#rangeId").val()) + 0.1 > 20 ? 20 : parseFloat($("#rangeId").val()) + 0.1).toFixed(1);
            addToast(numVal);
            $("#rangeId").val(numVal).trigger("change");
            return;
        }
        if ("-" == spee) {
            let numVal = parseFloat(parseFloat($("#rangeId").val()) - 0.1 < 0.1 ? 0.1 : parseFloat($("#rangeId").val()) - 0.1).toFixed(1);
            addToast(numVal);
            $("#rangeId").val(numVal).trigger("change");
            return;
        }
        if ("1" == spee) {
            $("#rangeId").val(1.0);
            addToast(1.0);
            localUtil.setSValue("speedStepKey", null);
            return;
        }

    }

    // 消息提示
    function addToast(msgText) {
        showVideoMessage("当前倍速:" + msgText);
        showtoastMessage("当前倍速:" + msgText);
    }

    /**
     * 在页面右下角显示
     * @param msgText
     */
    function showtoastMessage(msgText){
        GM_addStyle(GM_getResourceText("css"));
        Toastify({
            text: msgText,
            duration: 1500,
            newWindow: false,
            gravity: "bottom", // `top` or `bottom`
            position: "right", // `left`, `center` or `right`
            style: {
                background: "linear-gradient(to right, #00b09b, #96c93d)",
            }
        }).showToast();
    }

    /**
     * 在视频左上角显示
     * @param msgText
     */
    function showVideoMessage(msgText) {
        var messageElement = document.createElement('div');
        messageElement.style.position = 'absolute';
        messageElement.style.top = '10px';
        messageElement.style.left = '10px';
        messageElement.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
        messageElement.style.color = 'white';
        messageElement.style.padding = '10px';
        messageElement.style.fontFamily = 'Arial, sans-serif';
        messageElement.style.fontSize = '16px';
        messageElement.innerText = msgText;

        findNodeWithSelector('video', nodei => {
            if (nodei) {
                nodei.parentNode.appendChild(messageElement);
            }
        });

        var hideMessage = function() {
            messageElement.style.display = 'none';
        };

        var showMessage = function() {
            messageElement.style.display = 'block';
            setTimeout(hideMessage, 1000); // 一秒后隐藏消息
        };

        showMessage(); // 显示消息框
    }

    /**
     * 数值转换为分钟
     * @param value
     * @returns {string}
     */
    function convertToMinutes(value) {
        const minutes = Math.floor(value / 60);
        const seconds = value % 60;
        const paddedMinutes = minutes.toString().padStart(2, "0");
        const paddedSeconds = seconds.toString().padStart(2, "0");
        return `${paddedMinutes}:${paddedSeconds}`;
    }

    /**
     * 初始化跳过片头片尾
     */
    function initStartEnd() {
        var speed_skip_start = localUtil.getSValue("speed_skip_start") || 0;
        var speed_skip_end = localUtil.getSValue("speed_skip_end") || 0;

        if (parseInt(speed_skip_start) >= 0 || parseInt(speed_skip_end) >= 0) {
            toRunCurrentTime(speed_skip_start, speed_skip_end);
        }
    }

    /**
     * 执行引擎
     */
    function initRun(){
        var step = document.getElementById("rangeId").value;
        log.info("倍速播放方法启动,当前倍率为....." + step);
        var speedStepKey = localUtil.getSValue("speedStepKey");
        if ((step == null || step == '') && speedStepKey == null) {
            changeSpeend(1);
            return;
        }
        if ((step == null || step == '') && speedStepKey != null) {
            changeSpeend(speedStepKey);
            return;
        }
        if ((step != null && step != '' && step != speedStepKey) || (step == speedStepKey)) {
            changeSpeend(step);
            return;
        }
    }

    /**
     * 更改倍速
     * @param speed
     */
    function changeSpeend(speed) {

        document.getElementById("rangeId").value = speed;

        findNodeWithSelector('video', nodei => {
            if (nodei) {
                nodei.playbackRate = speed;
            }
        });

        // findNodeWithSelector('video', function (nodei) {
        //     if (nodei) {
        //         nodei.playbackRate = speed;
        //     }
        // });

        localUtil.setSValue("speedStepKey", speed);

    }

    /**
     * 通用查找节点方法
     * @param selector
     * @param callback
     */
    function findNodeWithSelector(selector, callback) {

        // 遍历页面中的所有节点并执行回调
        if (document.querySelectorAll(selector)) {
            document.querySelectorAll(selector).forEach(node => {
                callback(node);
            });
        }

        // 遍历页面中的所有 <iframe> 节点
        if (document.querySelectorAll("iframe")) {
            document.querySelectorAll("iframe").forEach(iframe => {
                // 确保 <iframe> 加载完成后再访问其内容
                iframe.addEventListener("load", () => {
                    try {
                        // 获取 <iframe> 的文档对象
                        let iframeDoc = iframe.contentDocument || iframe.contentWindow.document;

                        // 在 <iframe> 的文档中查找节点并执行回调
                        if (iframeDoc.querySelectorAll(selector)) {
                            iframeDoc.querySelectorAll(selector).forEach(iframeNode => {
                                callback(iframeNode);
                            });
                        }
                    } catch (error) {
                        log.error("Error occurred while accessing iframe content:", error);
                    }

                });
            });
        }

    }

    // 创建一个函数来覆盖对象的指定属性的setter方法
    function overrideSetter(object, property, desiredValue) {
        // 保存原始的setter方法
        var originalSetter = Object.getOwnPropertyDescriptor(object, property).set;

        // 覆盖setter方法
        Object.defineProperty(object, property, {
            set: function(value) {
                originalSetter.call(this, value);
            }
        });
    }

    function controlVideoProperty(propertyName, desiredValue) {

        findNodeWithSelector('video', nodei => {
            if (nodei) {
                // 使用overrideSetter函数来覆盖HTMLMediaElement.prototype的指定属性的setter方法
                overrideSetter(HTMLMediaElement.prototype, propertyName, desiredValue);

                // 创建一个MutationObserver实例来监听指定属性的变化
                var observer = new MutationObserver(function(mutations) {
                    mutations.forEach(function(mutation) {
                        if (mutation.type == 'attributes' && mutation.attributeName == propertyName && nodei[propertyName] != desiredValue) {
                            nodei[propertyName] = desiredValue;  // 更改属性的值
                        }
                    });
                });

                // 配置观察器
                var config = { attributes: true };

                // 开始观察
                observer.observe(nodei, config);
            }
        });

    }

    /**
     * 初始化菜单
     */
    function mokInitMenu(){

        const toggleSpeedStep = () => {
            let speedStepKeyInput = localUtil.getSValue("speedStepKeyInput") || "0.5";
            log.info('speedStepKeyInput waiting...' + speedStepKeyInput);
            if (speedStepKeyInput == "0.5") {
                speedStepKeyInput = "0";
            } else {
                speedStepKeyInput = "0.5";
            }
            $('#rangeId').css("opacity", speedStepKeyInput);
            localUtil.setSValue("speedStepKeyInput", speedStepKeyInput);
        };

        GM_registerMenuCommand('倍速框开启/关闭', toggleSpeedStep);

        GM_registerMenuCommand("跳过片头片尾", function() {
            Swal.fire({
                title: "跳过片头片尾",
                html:`
                        <div>
                            <div class="slider-container">
                                <span>跳过片头</span>&nbsp;&nbsp;
                                <p id="sliderValue1">00:00</p>&nbsp;&nbsp;
                                <input type="range" id="slider1" min="0" max="360" step="1" value="0"/>
                            </div>
                        </div>
                        <br>
                        <div>
                            <div class="slider-container">
                                <span>跳过片尾</span>&nbsp;&nbsp;
                                <p id="sliderValue2">00:00</p> &nbsp;&nbsp;
                                <input type="range" id="slider2" min="0" max="360" step="1" value="0"/>
                            </div>
                        </div>
                    `,
                showConfirmButton: false,
                showCloseButton: true,
                didOpen: () => {
                    // 选择要修改宽度的 input 元素
                    let rangeInput = $('input[type="range"]');
                    // 设置宽度为 260px
                    rangeInput.css('width', '260px');

                    const sliders = document.querySelectorAll(".slider-container");

                    sliders.forEach(function(sliderContainer) {
                        const slider = sliderContainer.querySelector("input[type='range']");
                        const sliderValue = sliderContainer.querySelector("p");

                        // 从 localUtil 中读取滑块的值,如果存在则更新滑块和滑块值
                        const storedData = localUtil.getSValue(slider.id);
                        if (storedData) {
                            const {value,textContent} = JSON.parse(storedData);
                            slider.value = value;
                            sliderValue.textContent = textContent;

                            toSendCurrentTime(slider.id, value);
                        }

                        // 添加一个 "input" 事件监听器来更新滑块值并将其存储到 localUtil 中
                        slider.addEventListener("input", function() {
                            const value = this.value;
                            const textContent = convertToMinutes(value);
                            sliderValue.textContent = textContent;
                            const data = {value,textContent};

                            localUtil.setSValue(this.id, JSON.stringify(data));
                            toSendCurrentTime(this.id, value);
                        });
                    });
                }
            });
        });
    }

    /**
     * 运行至当前时间
     * @param speed_skip_start
     * @param speed_skip_end
     */
    function toRunCurrentTime(speed_skip_start,speed_skip_end){

        findNodeWithSelector('video', video => {
            if (video) {
                // 如果视频的长度大于跳过的开始和结束时间
                if (video.duration > parseInt(speed_skip_start) + parseInt(speed_skip_end)) {

                    // 已经过了片头,则不进行跳过
                    if (video.currentTime > parseInt(speed_skip_start)) {
                        return;
                    }
                    // 跳过视频的开始
                    video.currentTime = speed_skip_start;
                    addToast("跳过片头:" + convertToMinutes(speed_skip_start));

                    // 当视频时间更新时
                    video.ontimeupdate = function () {
                        // 如果视频在跳过结束时间内
                        if (video.duration - video.currentTime <= speed_skip_end) {
                            // 跳转到视频末尾
                            video.currentTime = video.duration;
                            addToast("跳过片尾:" + convertToMinutes(speed_skip_end));
                        }
                    };
                }
            }
        });

        // 保存设置的视频进度
        localUtil.setSValue("speed_skip_start", speed_skip_start);
        localUtil.setSValue("speed_skip_end", speed_skip_end);
    }

    /**
     * 运行至当前时间
     * @param speed_skip_start
     * @param speed_skip_end
     */
    function toSendCurrentTime(saveId,value){
        let speed_skip_start = 0;
        let speed_skip_end = 0;

        if (saveId == "slider1") {speed_skip_start = value;}
        if (saveId == "slider2") {speed_skip_end = value;}

        if (speed_skip_start >= 0 || speed_skip_end >= 0) {
            toRunCurrentTime(speed_skip_start,speed_skip_end);
        }
    }

    // 日志打印封装
    var log = {
        log: function (msg) {
            if (localStorage.getItem("debug") == "true") {console.log(msg);}
        },
        info: function (msg) {
            if (localStorage.getItem("debug") == "true") {console.info(msg);}
        },
        warn: function (msg) {
            if (localStorage.getItem("debug") == "true") {console.warn(msg);}
        },
        error: function (msg) {
            if (localStorage.getItem("debug") == "true") {console.error(msg);}
        }
    };

    // 本地存储封装
    var localUtil = {
        getSValue(name) {
            return window.localStorage.getItem(name);
        },
        setSValue(name, value) {
            window.localStorage.setItem(name, value);
        },
        getGValue(name) {
            return window.GM_getValue(name);
        },
        setGValue(name, value) {
            window.GM_setValue(name, value);
        }
    }

    var main = {
        init() {
            mokInitMenu();
            addStyle();
            addDocument();
            initStartEnd();
        },
        run() {
            initRun();
        }
    }

    window.onload = function() {
        localStorage.setItem("debug", "false");
        var startStamp = new Date().getTime();
        window.initTimer = setInterval(() => {
            var videos = document.querySelectorAll("video").length;
            var nowStamp = new Date().getTime();
            if (videos > 0) {
                clearInterval(initTimer);
                main.init();
                window.setInterval(function() {main.run();}, 1000);
            } else if ((nowStamp - startStamp) > 30 * 1000) {
                clearInterval(initTimer);
                log.error('search video is long to stop...');
            } else {
                log.error('search video waiting...');
            }
        }, 1000);

    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址