网页视频控速工具使用指南

网页视频控速工具使用指南

前言
#

在实际观看网页视频时,我们经常需要调整视频播放速度或添加循环播放功能。本文将介绍如何在Edge浏览器中安装油猴插件(Tampermonkey),并使用自定义脚本来实现视频控制功能。

安装油猴插件
#

开启开发者模式
#

  1. 打开Edge浏览器
  2. 点击右上角的"…“菜单
  3. 选择"扩展"选项
  4. 在扩展页面左侧找到并打开"开发人员模式"开关

安装Tampermonkey
#

  1. 下载Tampermonkey插件文件点击下载Tampermonkey
  2. 将下载好的.crx文件直接拖拽到Edge浏览器的扩展页面
  3. 在弹出的安装确认对话框中点击"添加扩展”
  4. 安装完成后,浏览器右上角会出现Tampermonkey图标

添加视频控制脚本
#

创建新脚本
#

  1. 点击浏览器右上角的Tampermonkey图标
  2. 选择"创建新脚本"
  3. 在编辑器中粘贴以下代码:
// ==UserScript==
// @name         小杜视频控速助手(油猴版)
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  支持HTML5视频加速播放,支持任意倍速
// @author       小杜
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // 添加一个检测视频/音频的函数
    function checkMediaElements() {
        const videos = document.getElementsByTagName('video');
        const audios = document.getElementsByTagName('audio');
        return videos.length > 0 || audios.length > 0;
    }

    // 创建或显示控制面板
    function showControlPanel() {
        let panel = document.getElementById('speed-control-tampermonkey');
        if (!panel) {
            createControlPanel();
        } else {
            panel.style.display = 'block';
        }
    }

    // 隐藏控制面板
    function hideControlPanel() {
        const panel = document.getElementById('speed-control-tampermonkey');
        if (panel) {
            panel.style.display = 'none';
        }
    }

    // 创建控制面板UI
    function createControlPanel() {
        // 避免重复创建
        if (document.getElementById('speed-control-tampermonkey')) {
            return;
        }

        const panel = document.createElement('div');
        panel.innerHTML = `
            <div id="speed-control-tampermonkey" style="
                position: fixed;
                top: ${panelPosition.top};
                right: ${panelPosition.right};
                background: #1a1a1a;
                padding: 12px;
                border-radius: 8px;
                z-index: 999999;
                color: white;
                font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
                width: 180px;
                transition: all 0.3s;
                cursor: move;
            ">
                <!-- 标题栏 -->
                <div style="
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 8px;
                ">
                    <span style="font-size: 14px; color: #888;">视频控速助手</span>
                    <button id="toggle-mini" style="
                        padding: 2px 6px;
                        border: none;
                        border-radius: 3px;
                        background: #333;
                        color: #888;
                        font-size: 12px;
                        cursor: pointer;
                    ">迷你模式</button>
                </div>

                <!-- 速度显示 -->
                <div style="
                    text-align: center;
                    margin: 8px 0;
                    font-size: 24px;
                    font-weight: 600;
                    color: #00ff9d;
                    font-family: 'Monaco', monospace;
                ">
                    <span id="speed-display">${currentSpeed.toFixed(2)}x</span>
                </div>

                <!-- 滑块控制 -->
                <input type="range" id="speed-slider"
                    min="0.5"
                    max="16"
                    step="0.5"
                    value="${currentSpeed}"
                    style="
                        width: 100%;
                        height: 4px;
                        -webkit-appearance: none;
                        background: #333;
                        border-radius: 2px;
                        margin: 12px 0;
                    "
                >

                <!-- 预设速度按钮 -->
                <div style="
                    display: grid;
                    grid-template-columns: repeat(4, 1fr);
                    gap: 4px;
                    margin: 8px 0;
                ">
                    ${presetSpeeds.map(speed => `
                        <button class="preset-speed" data-speed="${speed}" style="
                            padding: 4px 0;
                            border: none;
                            border-radius: 3px;
                            background: #333;
                            color: #888;
                            font-size: 12px;
                            cursor: pointer;
                        ">${speed}x</button>
                    `).join('')}
                </div>

                <!-- 自定义速度 -->
                <div style="display: flex; gap: 4px; margin: 8px 0;">
                    <input type="number" id="custom-speed"
                        style="
                            flex: 1;
                            padding: 4px;
                            border: none;
                            border-radius: 3px;
                            background: #333;
                            color: white;
                            font-size: 12px;
                            text-align: center;
                        "
                        step="0.5"
                        min="0.5"
                        max="16"
                        value="${currentSpeed}"
                    >
                    <button id="set-speed" style="
                        padding: 4px 8px;
                        border: none;
                        border-radius: 3px;
                        background: #00ff9d;
                        color: #000;
                        font-size: 12px;
                        cursor: pointer;
                    ">设置</button>
                </div>

                <!-- 功能区 -->
                <div style="
                    display: grid;
                    grid-template-columns: repeat(3, 1fr);
                    gap: 4px;
                    margin: 8px 0;
                ">
                    <button id="pip-mode" class="function-btn">画中画</button>
                    <button id="screenshot" class="function-btn">截图</button>
                    <button id="manage-presets" class="function-btn">预设</button>
                </div>

                <!-- 开关区 -->
                <div style="
                    display: flex;
                    justify-content: space-between;
                    margin-top: 8px;
                    padding-top: 8px;
                    border-top: 1px solid #333;
                ">
                    <label style="display: flex; align-items: center; gap: 4px;">
                        <input type="checkbox" id="enable-control" checked style="accent-color: #00ff9d;">
                        <span style="font-size: 12px; color: #888;">启用控速</span>
                    </label>
                    <label style="display: flex; align-items: center; gap: 4px;">
                        <input type="checkbox" id="enable-fake-time" style="accent-color: #00ff9d;">
                        <span style="font-size: 12px; color: #888;">模拟进度</span>
                    </label>
                </div>

                <!-- 快捷键提示 -->
                <div style="
                    font-size: 11px;
                    color: #666;
                    text-align: center;
                    margin-top: 8px;
                ">
                    快捷键: [+]加速 [-]减速 [R]重置
                </div>
            </div>
        `;

        document.body.appendChild(panel);

        // 获取控制元素
        const controlPanel = document.getElementById('speed-control-tampermonkey');
        const enableControl = document.getElementById('enable-control');
        const speedSlider = document.getElementById('speed-slider');
        const speedDisplay = document.getElementById('speed-display');
        const customSpeed = document.getElementById('custom-speed');
        const setSpeedBtn = document.getElementById('set-speed');
        const enableFakeTime = document.getElementById('enable-fake-time');
        const pipButton = document.getElementById('pip-mode');
        const screenshotButton = document.getElementById('screenshot');
        const managePresetsButton = document.getElementById('manage-presets');
        const miniModeButton = document.getElementById('toggle-mini');

        // 添加拖拽支持
        addDragSupport(controlPanel);

        // 绑定事件
        enableControl.addEventListener('change', function() {
            isEnabled = this.checked;
            if (isEnabled) {
                setVideoSpeed(currentSpeed);
            } else {
                const videos = document.getElementsByTagName('video');
                for (let video of videos) {
                    if (video._speedProtected) {
                        delete video.playbackRate;
                        video._speedProtected = false;
                    }
                    Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate')
                        .set.call(video, 1.0);
                }
            }
        });

        speedSlider.addEventListener('input', function() {
            currentSpeed = parseFloat(this.value);
            updateSpeed(currentSpeed);
        });

        setSpeedBtn.addEventListener('click', function() {
            const speed = parseFloat(customSpeed.value);
            if(speed >= 0.5 && speed <= 16) {
                currentSpeed = speed;
                updateSpeed(speed);
            }
        });

        // 绑定预设速度按钮事件
        document.querySelectorAll('.preset-speed').forEach(btn => {
            btn.addEventListener('click', function() {
                const speed = parseFloat(this.dataset.speed);
                currentSpeed = speed;
                updateSpeed(speed);
            });
        });

        // 绑定功能按钮事件
        pipButton.addEventListener('click', togglePiP);
        screenshotButton.addEventListener('click', takeScreenshot);
        managePresetsButton.addEventListener('click', managePresets);
        miniModeButton.addEventListener('click', toggleMiniMode);

        enableFakeTime.addEventListener('change', function() {
            isFaking = this.checked;
            const videos = document.getElementsByTagName('video');
            for (let video of videos) {
                if (isFaking) {
                    enableFakeProgress(video);
                } else {
                    if (video._fakeProgressEnabled) {
                        delete video.currentTime;
                        video._fakeProgressEnabled = false;
                        if (originalTimeUpdate) {
                            video.ontimeupdate = originalTimeUpdate;
                        }
                    }
                }
            }
        });

        // 恢复上次的迷你模式状态
        const wasMini = localStorage.getItem('miniMode') === 'true';
        if (wasMini) {
            toggleMiniMode();
        }

        // 在createControlPanel函数中添加样式部分
        const style = document.createElement('style');
        style.textContent = `
            #speed-control-tampermonkey input[type="range"]::-webkit-slider-thumb {
                -webkit-appearance: none;
                width: 12px;
                height: 12px;
                border-radius: 50%;
                background: #00ff9d;
                cursor: pointer;
                border: none;
            }

            #speed-control-tampermonkey input[type="range"]::-webkit-slider-runnable-track {
                background: #333;
                height: 4px;
                border-radius: 2px;
            }

            .function-btn {
                padding: 4px 0;
                border: none;
                border-radius: 3px;
                background: #333;
                color: #888;
                font-size: 12px;
                cursor: pointer;
                transition: all 0.2s;
            }

            .function-btn:hover,
            .preset-speed:hover,
            #toggle-mini:hover {
                background: #444;
                color: #fff;
            }

            #speed-control-tampermonkey.mini-mode {
                width: 120px;
            }

            #speed-control-tampermonkey.mini-mode .hide-in-mini {
                display: none;
            }
        `;

        document.head.appendChild(style);

        // 添加迷你模式的样式
        style.textContent += `
            #speed-control-tampermonkey.mini-mode {
                padding: 12px;
            }
            #speed-control-tampermonkey.mini-mode #speed-display {
                font-size: 18px;
            }
            #speed-control-tampermonkey.mini-mode #speed-slider {
                margin: 8px 0;
            }
        `;
    }

    // 保存当前速度的全局变量
    let currentSpeed = parseFloat(localStorage.getItem('lastSpeed')) || 1.0;
    let isFaking = false;
    let animationFrameId = null;
    let isEnabled = true;
    let observerTimeout;
    let panelPosition = JSON.parse(localStorage.getItem('speedControlPosition')) || { top: '160px', right: '20px' };
    let presetSpeeds = JSON.parse(localStorage.getItem('speedPresets')) || [2, 4, 8, 16];
    let originalTimeUpdate = null;
    let fakeCurrentTime = 0;
    let lastRealTime = 0;

    // 设置视频速度的函数
    function setVideoSpeed(speed) {
        const videos = document.getElementsByTagName('video');
        for (let video of videos) {
            try {
                if (!isEnabled) {
                    if (video._speedProtected) {
                        delete video.playbackRate;
                        video._speedProtected = false;
                    }
                    return;
                }

                // 启用时间模拟
                if (isFaking) {
                    enableFakeProgress(video);
                }

                video.playbackRate = speed;

                if (video._speedProtected) {
                    delete video.playbackRate;
                    video._speedProtected = false;
                }

                Object.defineProperty(video, 'playbackRate', {
                    get: function() {
                        return speed;
                    },
                    set: function(newValue) {
                        if (isEnabled) {
                            Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate')
                                .set.call(this, speed);
                        } else {
                            Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate')
                                .set.call(this, newValue);
                        }
                    },
                    configurable: true
                });

                video._speedProtected = true;

            } catch(e) {
                console.error('设置视频速度失败:', e);
            }
        }
    }

    // 添加视频事件监听
    function addVideoEventListeners(video) {
        if(video._eventsAdded) return;
        video._eventsAdded = true;

        video.addEventListener('ratechange', function(e) {
            if(isEnabled && this.playbackRate !== currentSpeed) {
                Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate')
                    .set.call(this, currentSpeed);
            }
        });

        video.addEventListener('play', function() {
            if(isEnabled) {
                Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate')
                    .set.call(this, currentSpeed);
            }
        });
    }

    // 监听新加入的视频
    const observer = new MutationObserver(function(mutations) {
        clearTimeout(observerTimeout);
        observerTimeout = setTimeout(() => {
            if (checkMediaElements()) {
                showControlPanel();
                const videos = document.getElementsByTagName('video');
                for(let video of videos) {
                    if(!video._eventsAdded) {
                        addVideoEventListeners(video);
                    }
                    if(video.playbackRate !== currentSpeed && isEnabled) {
                        setVideoSpeed(currentSpeed);
                    }
                }
            } else {
                hideControlPanel();
            }
        }, 200);
    });

    // 开始观察DOM变化
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true
    });

    // 修改页面加载事件监听
    let panelCreated = false; // 添加标志位

    document.addEventListener('DOMContentLoaded', function() {
        if (!panelCreated && checkMediaElements()) {
            createControlPanel();
            panelCreated = true;
            const videos = document.getElementsByTagName('video');
            for(let video of videos) {
                addVideoEventListeners(video);
                if(isEnabled) {
                    setVideoSpeed(currentSpeed);
                }
            }
        }
    });

    function addDragSupport(panel) {
        let isDragging = false;
        let currentX;
        let currentY;
        let initialX;
        let initialY;

        panel.addEventListener('mousedown', dragStart);
        document.addEventListener('mousemove', drag);
        document.addEventListener('mouseup', dragEnd);

        function dragStart(e) {
            initialX = e.clientX - panel.offsetLeft;
            initialY = e.clientY - panel.offsetTop;
            if (e.target === panel) {
                isDragging = true;
            }
        }

        function drag(e) {
            if (isDragging) {
                e.preventDefault();
                currentX = e.clientX - initialX;
                currentY = e.clientY - initialY;

                // 更新位置
                panel.style.top = currentY + 'px';
                panel.style.right = (window.innerWidth - currentX - panel.offsetWidth) + 'px';

                // 保存位置到localStorage
                panelPosition = {
                    top: panel.style.top,
                    right: panel.style.right
                };
                localStorage.setItem('speedControlPosition', JSON.stringify(panelPosition));
            }
        }

        function dragEnd() {
            isDragging = false;
        }
    }

    document.addEventListener('keydown', function(e) {
        if (!isEnabled) return;

        switch(e.key) {
            case '+':
            case '=':
                e.preventDefault();
                currentSpeed = Math.min(currentSpeed + 0.5, 16);
                updateSpeed(currentSpeed);
                break;
            case '-':
                e.preventDefault();
                currentSpeed = Math.max(currentSpeed - 0.5, 0.5);
                updateSpeed(currentSpeed);
                break;
            case 'r':
            case 'R':
                e.preventDefault();
                currentSpeed = 1.0;
                updateSpeed(currentSpeed);
                break;
        }
    });

    function updateSpeed(speed) {
        const speedDisplay = document.getElementById('speed-display');
        const speedSlider = document.getElementById('speed-slider');
        const customSpeed = document.getElementById('custom-speed');

        if (speedDisplay) speedDisplay.textContent = speed.toFixed(2) + 'x';
        if (speedSlider) speedSlider.value = speed;
        if (customSpeed) customSpeed.value = speed;

        setVideoSpeed(speed);
        localStorage.setItem('lastSpeed', speed);
    }

    // 添加模拟播放时间的函数
    function enableFakeProgress(video) {
        if (video._fakeProgressEnabled) return;
        video._fakeProgressEnabled = true;

        // 保存原始的currentTime
        let realCurrentTime = video.currentTime;
        fakeCurrentTime = realCurrentTime;
        lastRealTime = Date.now();

        // 保存原始的timeupdate事件
        originalTimeUpdate = video.ontimeupdate;

        // 重写currentTime的getter和setter
        Object.defineProperty(video, 'currentTime', {
            get: function() {
                if (!isEnabled || !isFaking) return realCurrentTime;
                return fakeCurrentTime;
            },
            set: function(value) {
                realCurrentTime = value;
                fakeCurrentTime = value;
                lastRealTime = Date.now();
            },
            configurable: true
        });

        // 模拟timeupdate事件
        function fakeTimeUpdate() {
            if (isEnabled && isFaking && !video.paused) {
                const now = Date.now();
                const deltaTime = (now - lastRealTime) / 1000;
                lastRealTime = now;

                fakeCurrentTime += deltaTime * currentSpeed;

                // 触发timeupdate事件
                const event = new Event('timeupdate');
                video.dispatchEvent(event);

                // 如果有原始的timeupdate处理函数,调用它
                if (originalTimeUpdate) {
                    originalTimeUpdate.call(video);
                }
            }

            // 继续下一帧
            animationFrameId = requestAnimationFrame(fakeTimeUpdate);
        }

        // 开始模拟
        fakeTimeUpdate();
    }

    // 修改预设管理功能
    function managePresets() {
        const savedPresets = JSON.parse(localStorage.getItem('speedPresets')) || presetSpeeds;
        const newPreset = prompt('请输入新的预设速度(多个请用逗号分隔):', savedPresets.join(','));
        if (newPreset) {
            const presets = newPreset.split(',')
                .map(x => parseFloat(x))
                .filter(x => !isNaN(x) && x >= 0.5 && x <= 16)
                .sort((a, b) => a - b);

            if (presets.length > 0) {
                localStorage.setItem('speedPresets', JSON.stringify(presets));
                presetSpeeds = presets; // 更新当前预设

                // 更新预设按钮
                const presetContainer = document.querySelector('#speed-control-tampermonkey .preset-speed').parentElement;
                presetContainer.innerHTML = presets.map(speed => `
                    <button class="preset-speed" data-speed="${speed}" style="
                        padding: 4px 0;
                        border: none;
                        border-radius: 3px;
                        background: #333;
                        color: #888;
                        font-size: 12px;
                        cursor: pointer;
                    ">${speed}x</button>
                `).join('');

                // 重新绑定预设按钮事件
                document.querySelectorAll('.preset-speed').forEach(btn => {
                    btn.addEventListener('click', function() {
                        const speed = parseFloat(this.dataset.speed);
                        currentSpeed = speed;
                        updateSpeed(speed);
                    });
                });
            }
        }
    }

    // 修改迷你模式切换功能
    function toggleMiniMode() {
        const panel = document.getElementById('speed-control-tampermonkey');
        const isMini = panel.classList.toggle('mini-mode');
        localStorage.setItem('miniMode', isMini);

        if (isMini) {
            // 隐藏不需要的元素
            panel.querySelectorAll('.function-btn, #manage-presets, #custom-speed, #set-speed').forEach(el => {
                el.style.display = 'none';
            });
            // 调整面板宽度
            panel.style.width = '120px';
        } else {
            // 显示所有元素
            panel.querySelectorAll('.function-btn, #manage-presets, #custom-speed, #set-speed').forEach(el => {
                el.style.display = '';
            });
            // 恢复面板宽度
            panel.style.width = '180px';
        }
    }

    // 添加画中画功能
    async function togglePiP() {
        const videos = document.getElementsByTagName('video');
        if (videos.length > 0) {
            try {
                if (document.pictureInPictureElement) {
                    await document.exitPictureInPicture();
                } else {
                    await videos[0].requestPictureInPicture();
                }
            } catch (error) {
                console.error('画中画模式切换失败:', error);
            }
        }
    }

    // 添加截图功能
    function takeScreenshot() {
        const videos = document.getElementsByTagName('video');
        if (videos.length > 0) {
            const video = videos[0];
            const canvas = document.createElement('canvas');
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            canvas.getContext('2d').drawImage(video, 0, 0);

            const link = document.createElement('a');
            link.download = `截图_${new Date().getTime()}.png`;
            link.href = canvas.toDataURL();
            link.click();
        }
    }

    // 添加循环播放功能
    function toggleLoop(enabled) {
        const videos = document.getElementsByTagName('video');
        for (let video of videos) {
            video.loop = enabled;
        }
    }

})();

保存并启用脚本
#

  1. 点击编辑器左上角的"文件"→“保存”,或使用快捷键Ctrl+S
  2. 确保脚本在Tampermonkey的管理面板中处于启用状态

使用说明
#

安装完成后,当你访问包含视频的网页时,会在页面右上角出现控制面板,包含以下功能:

  1. 循环播放开关

    • 点击按钮切换循环播放状态
    • 开启后视频将自动循环播放
  2. 倍速播放选择

    • 提供0.5x到16.0x的播放速度选项
    • 选择后立即生效

注意事项
#

  1. 脚本兼容性

    • 部分网站可能因为安全限制而无法使用该脚本
    • 如遇到不兼容情况,请检查网站是否允许脚本注入
  2. 性能考虑

    • 脚本会定期检查页面中的视频元素
    • 如果页面性能出现问题,可以尝试禁用脚本
  3. 使用建议

    • 建议根据实际需求选择合适的播放速度
    • 不建议长时间使用过快的播放速度

总结
#

通过安装Tampermonkey插件并使用自定义脚本,我们可以方便地控制网页视频的播放方式。这个工具不仅提高了观看视频的效率,还为用户提供了更好的视频控制体验。希望这个指南能帮助你更好地使用网页视频控速工具。

Weidong's Blok
Weidong’s Blok
欢迎访问我的技术博客,记录工具、踩坑、系统运维经验。