您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
哔哩哔哩自定义视频播放速度,shift1~9快捷键更改速度。插入样式比较违和,只在普通视频下才有自定义按钮,番剧等页面只能通过快捷键更改速度。
// ==UserScript== // @name bili自定义播放速率 // @namespace Violentmonkey Scripts // @match https://www.bilibili.com/* // @exclude https://www.bilibili.com/correspond/* // @grant GM_addStyle // @version 1.3.1 // @author vurses // @license GPL // @description 哔哩哔哩自定义视频播放速度,shift1~9快捷键更改速度。插入样式比较违和,只在普通视频下才有自定义按钮,番剧等页面只能通过快捷键更改速度。 // ==/UserScript== (async () => { // 在video页面下能使用额外的一些按钮,在其它页面只能使用shift1~9快捷键 // 每次currentEle必须由筛选的下标获取到dom元素,无需本地存储,知道了默认播放速度就知道了数据列表下标,就知道了选中的元素(但此dom元素也不一定存在),不必要存储 // 速度列表listproxy,当前速度信息对象objproxy,视频video当前速度pbRList // 切换自定义的倍速按钮触发objproxy代理=>更改样式同时触发video的ratechange事件=> // 用户切换速度的方式有多种,所以ratechange改变时也需要更改一次样式,该元素通过listproxy过滤出第一个对应速度index可知 // 点击新增按钮=>更改listproxy触发代理=>重新渲染自定义的所有倍速按钮 // 只有/video下的视频dom结构会优先加载 let video = document.querySelector("video"); const waitForVideo = new Promise((resolve, reject) => { let checkInterval; let timeout; function checkVideoExists() { video = document.querySelector("video"); if (video) { clearInterval(checkInterval); clearTimeout(timeout); resolve("video加载成功"); } } function stopChecking() { clearInterval(checkInterval); reject("video获取超时..."); // 8秒内不存在,reject Promise } // 每100ms检查一次a是否存在 checkInterval = setInterval(checkVideoExists, 100); // 8秒后停止检查 timeout = setTimeout(stopChecking, 8000); }); // 8秒内未加载出video抛出异常中断代码执行 await waitForVideo; // 保证有video元素的情况下快捷键能使用 // shift+1~9快捷键修改速度 document.addEventListener("keydown", function (event) { // 检查是否按下了Shift键 if (event.shiftKey && event.keyCode >= 51 && event.keyCode <= 57) { video.playbackRate = event.which - 48; } }); // 以评论区dom的加载为标志 const commentContainer = document.querySelector("#commentapp"); // 用定时器也行 // 创建观察者实例,bili的dom结构有部分异步加载,避免页面出现问题脚本的所有操作得在dom完全加载之后才执行 // 只要能标志dom结构完全加载,选择观察哪个dom元素的容器都无所谓 const domLoadedSignalObserver = new MutationObserver( (mutationsList, observer) => { // 监听回调 scriptCallback(); // 移除监听 domLoadedSignalObserver.disconnect(); } ); // 在/video下才加载样式和脚本,避免出错。其它页面与/video不同,插入一些样式比较违和 if (window.location.href.includes("video")) { domLoadedSignalObserver.observe(commentContainer, { //监听异步加载dom时,childList变更以触发observe childList: true, }); } else { return; } // 样式 const link = document.createElement("link"); link.rel = "stylesheet"; // bulma.css用require引入会报错 // link.href = // "https://cdn.bootcdn.net/ajax/libs/bulma/1.0.1/css/bulma.min.css"; // 无语,bulma默认暗黑就算了,还有样式污染,还得一个个改类名前缀 // link.href = // "https://cdn.jsdelivr.net/npm/[email protected]/css/versions/bulma-prefixed.min.css"; // jsdelivr太慢了 link.href = "https://unpkg.com/[email protected]/css/versions/bulma-prefixed.min.css"; document.head.appendChild(link); // bulma带来的样式污染(ˉ▽ˉ;)...如果样式污染问题太严重得重构 const styleConflictPatch = document.createElement("style"); styleConflictPatch.rel = "text/css"; // 高能进度条svg,一键三连弹出框,视频底部控件样式 styleConflictPatch.innerHTML = `div.bpx-player-pbp svg{ width:100% !important; height:100% !important; } .bili-danmaku-x-guide-three *{ box-sizing:content-box !important; } .bpx-player-control-bottom > div { box-sizing:content-box !important; } `; document.head.appendChild(styleConflictPatch); // bulma样式默认黑暗模式很蛋疼 const script = document.createElement("script"); script.innerHTML = `document.documentElement.setAttribute("data-bulma-theme", "light")`; document.head.appendChild(script); // 一些额外的样式 (() => { const css = `.script-box { width: 100%; display: flex; } .tag-box a:nth-of-type(1):hover { background-color: skyblue; } .tag-box a:nth-of-type(2):hover { background-color: rgb(243, 142, 140); } .new-tag-box { width: 70px; } .bulma-is-blue{ color: white; background-color: lightskyblue; } `; GM_addStyle(css); })(); // 将脚本所有操作放一个函数里供observer回调 const scriptCallback = () => { /*********** 函数定义 ***************/ // 给每一个倍速按钮设置的监听器,用css也能实现,但有一些样式问题还是选择用js const setListeners = () => { const mouseenterListeners = new WeakMap(); const mouseleaveListeners = new WeakMap(); const mouseenterCallback = function (e) { const deleteTag = document.createElement("a"); deleteTag.className = "bulma-tag bulma-is-delete"; // <div class="tags has-addons"> this.children[0].append(deleteTag); }; const mouseleaveCallback = function (e) { // <div class="tags has-addons"> this.children[0].lastElementChild.remove(); }; Array.from(document.querySelectorAll(".bulma-control")).forEach( (element) => { // weakmap保存监听函数防止内存泄漏 mouseenterListeners.set(element, mouseenterCallback); mouseleaveListeners.set(element, mouseleaveCallback); element.addEventListener( "mouseenter", mouseenterListeners.get(element) ); element.addEventListener( "mouseleave", mouseleaveListeners.get(element) ); } ); }; // 播放速度数组的渲染 const tagRender = (list) => { return list .map((value, index) => { return `<div class="bulma-control" data-index=${index} style="width:70px"> <div class="bulma-tags bulma-has-addons"> <a class="bulma-tag" style="width:40px">${value.toFixed( 2 )}x</a> </div> </div>`; }) .join(""); }; // 计算当前速度对应下标和元素对象 const computeRateObj = (pbRList, curpbRate) => { const index = pbRList.findIndex((value, index) => { return value === curpbRate; }); const element = index === -1 ? null : document.querySelector(`.bulma-field div:nth-child(${index + 1})`) .children[0].children[0]; return { index, element }; }; /*********** 用户界面构建 ***************/ // 用户操作的容器 const scriptBox = document.createElement("div"); scriptBox.className = "script-box pb-1"; document.querySelector("#viewbox_report").style.height = "auto"; document.querySelector("#viewbox_report").append(scriptBox); document.querySelector(".script-box").innerHTML = ` <div class="tag-box"> <div class="bulma-field bulma-is-grouped bulma-is-grouped-multiline"> </div> </div> <div class="new-tag-box"> <button class="bulma-button bulma-is-small" style="height: 21px">+ Rate</button> <input class="bulma-input bulma-is-small bulma-is-info" type="number" style="width: 60px;height: 21px; display: none;" /> </div>`; // 播放速度列表数组 const playbackRateList = JSON.parse( localStorage.getItem("PLAYBACK_RATE_LIST_GREASYFORK") || "[0.5, 0.75, 1, 1.25, 1.5, 2, 3]" ); // 首次渲染 document.querySelector(".tag-box .bulma-field").innerHTML = tagRender(playbackRateList); // 为每个按钮添加监听器控制样式 setListeners(); /*********** 响应式,劫持数据操作驱动页面变化 ***************/ // 默认播放速度 const currentPlaybackRate = JSON.parse( localStorage.getItem("CURRENT_PLAYBACK_RATE_GREASYFORK") || "1" ); // 将视频速度修改为默认速度 video.playbackRate = currentPlaybackRate; // 记录默认播放速度(当前播放速度)和当前被选tag的对象 const { index: currentRateIndex, element: currentRateEle } = computeRateObj( playbackRateList, currentPlaybackRate ); // 高亮倍速按钮 if (currentRateEle) currentRateEle.classList.add("bulma-is-blue"); const playbackRateObj = { currentRate: currentPlaybackRate, currentRateElement: currentRateEle, }; // 代理播放速度数组的set操作 const playbackRateListProxy = new Proxy(playbackRateList, { set: (target, key, value) => { target[key] = value; localStorage.setItem( "PLAYBACK_RATE_LIST_GREASYFORK", JSON.stringify(target) ); tagRender(target); /* 性能可优化 */ // 重复渲染 document.querySelector(".bulma-field").innerHTML = tagRender(playbackRateList); const { element } = computeRateObj( playbackRateList, playbackRateObjProxy.currentRate ); // 移除元素高亮 playbackRateObjProxy.currentRateElement = element; // 添加元素高亮 if (element) element.classList.add("bulma-is-blue"); // 重复添加监听器 setListeners(); return Reflect.set(target, key, value); }, }); // 代理持久化播放速度对象属性的set操作 const playbackRateObjProxy = new Proxy(playbackRateObj, { set: (target, key, value) => { // 需要存在符合条件的tag再移除样式 key === "currentRateElement" && target[key] && target[key] !== value && target[key].classList.remove("bulma-is-blue"); key === "currentRate" && (video.playbackRate = value) && localStorage.setItem("CURRENT_PLAYBACK_RATE_GREASYFORK", value); target[key] = value; return Reflect.set(target, key, value); }, }); // 速度变化监听 video.addEventListener("ratechange", function () { // 默认速度持久化 playbackRateObjProxy.currentRate = video.playbackRate; const { element } = computeRateObj( playbackRateList, playbackRateObjProxy.currentRate ); // 移除元素高亮 playbackRateObjProxy.currentRateElement = element; // 添加元素高亮 if (element) element.classList.add("bulma-is-blue"); }); // 根元素事件委托,处理各种事件 document .querySelector(".script-box") .addEventListener("click", function (e) { switch (e.target.tagName) { case "BUTTON": const _this = this; const newTagBox = _this.children[1]; const button = newTagBox.children[0]; const input = newTagBox.children[1]; button.style.display = "none"; // 只创建一次回调函数, [0]button || [1]input if (!input.onblur) { input.onblur = function () { button.style.display = ""; input.style.display = "none"; // 数字过滤 this.value && this.value >= 0 && (this.value >= 16 ? playbackRateListProxy.push(16) : playbackRateListProxy.push(Number(this.value))); this.value = ""; button.focus(); }; input.onkeydown = function (e) { // 两个Enter键 if (e.keyCode === 108 || e.keyCode === 13) { this.onblur(); } // Esc键 if (e.keyCode === 27) { this.value = ""; this.blur(); button.blur(); } }; } // 显示并聚焦input input.style.display = ""; input.focus(); break; case "A": // type:string const index = e.target.parentElement.parentElement.getAttribute("data-index"); // 数据变更驱动dom更新 if (Array.from(e.target.classList).includes("bulma-is-delete")) { // index of tags playbackRateListProxy.splice(index, 1); } else { playbackRateObjProxy.currentRate = playbackRateListProxy[index]; e.target.classList.add("bulma-is-blue"); // tags样式互斥,每次存入当前选中的tag方便下次移除样式 playbackRateObjProxy.currentRateElement = e.target; } break; } }); // 当前页面切换视频引起video属性变换 const videoChangeObserver = new MutationObserver( (mutationsList, observer) => { // 避免视频速度被初始化 video.playbackRate = playbackRateObjProxy.currentRate; } ); videoChangeObserver.observe(video, { attributes: true, }); }; })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址