// ==UserScript== // @name 哔哩哔哩视频倍速扩展 3x/4x // @tag 哔哩哔哩 bilibili B站 // @namespace https://github.com/ // @version 3.5.0 // @description 为哔哩哔哩原生倍速菜单添加3倍速和4倍速档位;无定时器,几乎零开销。 // @author deran // @match *://www.bilibili.com/video/* // @match *://www.bilibili.com/bangumi/play/* // @match *://www.bilibili.com/list/watchlater* // @grant none // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; /********** 配置项 **********/ const EXTRA_SPEEDS = [3, 4]; const VIDEO_SELECTOR = 'video'; const MENU_SELECTORS = [ '.bpx-player-ctrl-playbackrate-menu', '.bpx-player-ctrl-speed-menu' ]; const SPEED_BTN_SELECTOR = '.bpx-player-ctrl-playbackrate'; /********** DOM工具 **********/ function waitForElement(selector, parent = document.body) { return new Promise(resolve => { const target = parent.querySelector(selector); if (target) return resolve(target); const observer = new MutationObserver(() => { const target = parent.querySelector(selector); if (target) { observer.disconnect(); resolve(target); } }); observer.observe(parent, { childList: true, subtree: true }); }); } function getMenuEl() { for (const sel of MENU_SELECTORS) { const el = document.querySelector(sel); if (el?.children.length) return el; } return null; } /********** 菜单注入 **********/ function injectMenuOptions() { const menu = getMenuEl(); if (!menu || menu.dataset.customInjected) return; // 收集原生已有倍速,自动去重 const existing = new Set(); const items = menu.querySelectorAll('li, .bpx-player-ctrl-playbackrate-menu-item'); items.forEach(item => { const v = parseFloat(item.dataset.value); if (!isNaN(v)) existing.add(v); }); // 过滤并排序新增档位 const speeds = EXTRA_SPEEDS.filter(r => !existing.has(r)).sort((a, b) => b - a); if (speeds.length === 0) { menu.dataset.customInjected = 'true'; return; } // 批量注入选项 const frag = document.createDocumentFragment(); speeds.forEach(rate => { const li = document.createElement('li'); if (items[0]) li.className = items[0].className; li.dataset.value = rate; li.textContent = `${rate}.0x`; li.addEventListener('click', () => { const video = document.querySelector(VIDEO_SELECTOR); if (video) video.playbackRate = rate; }); frag.appendChild(li); }); menu.insertBefore(frag, menu.firstChild); menu.dataset.customInjected = 'true'; } /********** SPA路由适配 **********/ function onRouteChange() { setTimeout(injectMenuOptions, 0); } const _push = history.pushState; history.pushState = function(...args) { _push.apply(this, args); onRouteChange(); }; const _replace = history.replaceState; history.replaceState = function(...args) { _replace.apply(this, args); onRouteChange(); }; window.addEventListener('popstate', onRouteChange); // 菜单懒加载兼容:鼠标悬停倍速按钮时检查注入 async function initHoverFallback() { const btn = await waitForElement(SPEED_BTN_SELECTOR); if (!btn || btn.dataset.fallbackBound) return; btn.dataset.fallbackBound = 'true'; btn.addEventListener('mouseenter', injectMenuOptions); } /********** 初始化 **********/ initHoverFallback(); injectMenuOptions(); })();