BiliBili倍速插件,支持自定义速度、记忆上一次速度、快捷键调速。
// ==UserScript== // @name BiliBili 高级倍速功能 // @namespace cec8225d12878f0fc33997eb79a69894 // @version 1.8 // @description BiliBili倍速插件,支持自定义速度、记忆上一次速度、快捷键调速。 // @author TheBszk // @match https://www.bilibili.com/video/* // @match https://www.bilibili.com/list/* // @match https://www.bilibili.com/bangumi/play/* // @match https://www.bilibili.com/cheese/play/* // @match https://www.bilibili.com/festival/* // @icon https://www.bilibili.com/favicon.ico // @license AGPL // ==/UserScript== (function () { "use strict"; const CUSTOM_RATE_ARRAY = "custom_rate_array"; const CUSTOM_RATE = "custom_rate"; const CUSTOM_ShowTimeState = "custom_showtimestate"; const CUSTOM_ArrowRightSpeed = "custom_arrowrightspeed"; const CUSTOM_SwitchCustomSpeed = "custom_switchcustomspeed"; const CUSTOM_DefaultWideScreen = "custom_defaultwidescreen"; const CUSTOM_Volume = "custom_volume"; const CUSTOM_GlobalVolumeAdjustment = "custom_globalvolumeadjustment"; if (!localStorage.getItem(CUSTOM_ArrowRightSpeed)) { localStorage.setItem(CUSTOM_ArrowRightSpeed, "2x"); //设置默认值 } function getPageType() { const path = window.location.pathname; if (path.startsWith("/video/")) { return "video"; } else if (path.startsWith("/list/")) { return "list"; } else if (path.startsWith("/bangumi/play/")) { return "bangumi"; } else if (path.startsWith("/cheese/play/")) { return "cheese"; } else if (path.startsWith("/festival/")) { return "festival"; } else { return "unknown"; } } const pageType = getPageType(); if (pageType == "video" || pageType == "list" || pageType == "bangumi" || pageType == "cheese" || pageType == "festival") { var MENUCLASS = "bpx-player-ctrl-playbackrate-menu"; var MENUCLASS_ITEM = "bpx-player-ctrl-playbackrate-menu-item"; var MENUCLASS_ACTIVE = "bpx-state-active"; } else { return; } function getRate() { let rate = localStorage.getItem(CUSTOM_RATE); if (rate <= 0) { rate = 1; } return rate; } function getRateArray() { let storageData = localStorage.getItem(CUSTOM_RATE_ARRAY); let rates; if (storageData == null) { rates = []; } else { rates = storageData.split(","); } if (rates.length === 0) { //如果没有,则初始化一个默认的 rates = [0.5, 1.0, 1.5, 2, 2.5, 3.0, 4.0]; localStorage.setItem(CUSTOM_RATE_ARRAY, rates.join(",")); } return rates; } // 创建显示元素 function createTip() { var elem = document.createElement("div"); elem.style.display = "none"; elem.style.position = "absolute"; elem.style.backgroundColor = "rgba(255, 255, 255, 0.2)"; elem.style.color = "white"; elem.style.padding = "5px"; elem.style.borderRadius = "5px"; elem.style.zIndex = "1000"; elem.style.fontSize = "22px"; return elem; } var timeDisplay = createTip(); timeDisplay.style.top = "20px"; timeDisplay.style.right = "20px"; let _showtime; function setShowTimeState(state) { localStorage.setItem(CUSTOM_ShowTimeState, state); if (state == true) { timeDisplay.style.display = "block"; if (!_showtime) { _showtime = setInterval(FlashShowTime, 1000); } } else { timeDisplay.style.display = "none"; if (_showtime) { clearInterval(_showtime); _showtime = 0; } } } var tipDisplay = createTip(); tipDisplay.style.right = "20px"; let hideTimer; let bpx_player_control_entity = document.querySelector(".bpx-player-control-entity"); let bpx_player_control_wrap = document.querySelector(".bpx-player-control-wrap"); function showTip(title, time) { let h; if (bpx_player_control_entity.getAttribute("data-shadow-show") == "true") { h = 0; } else { h = bpx_player_control_wrap.clientHeight; } tipDisplay.style.bottom = h + 20 + "px"; tipDisplay.textContent = title; tipDisplay.style.display = "block"; if (!hideTimer) { clearTimeout(hideTimer); } hideTimer = setTimeout(function () { tipDisplay.style.display = "none"; }, time); } function showPlayRate(rate) { showTip(`速度: ${rate}x`, 1200); } function showVolume(volume) { showTip(`音量: ${volume}%`, 1200); } class SettingPopup { popup_dragend_move(e) { this.popup.style.left = e.clientX - this.offsetX + this.startX + "px"; this.popup.style.top = e.clientY - this.offsetY + this.startY + "px"; } constructor() { this.speedlist = getRateArray().join(","); this.ArrowRightTime = localStorage.getItem(CUSTOM_ArrowRightSpeed); this.SwitchCustomSpeed = localStorage.getItem(CUSTOM_SwitchCustomSpeed) == "true" ? true : false; // this.GlobalVolumeAdjustment = localStorage.getItem(CUSTOM_GlobalVolumeAdjustment) == "true" ? true : false; this.GlobalVolumeAdjustment = false;//不再开启 this.DefaultWideScreen = localStorage.getItem(CUSTOM_DefaultWideScreen) == "true" ? true : false; let v = localStorage.getItem(CUSTOM_Volume); if (v == null || v == "") { this.volume = -1; } else { this.volume = parseInt(v); } } create(handle) { this.popup = document.createElement("div"); this.popup.innerHTML = ` <div class="popup-title" id="popupTitle"> <span>BiliBili 高级倍速功能</span> <button class="close-button">×</button> </div> <div class="popup-content"> <label for="SpeedList">自定义倍速列表:</label> <input type="text" id="SpeedList" placeholder="以英文逗号隔开" /> <label for="ArrowRightSpeed">长按右光标键速度:</label> <input type="text" id="ArrowRightSpeed" placeholder="例: 2 为固定二倍速, 2x 为当前速度两倍" /> <label title="默认为对应速度(如 按2为2倍速、按3为3倍速)"><input type="checkbox" id="SwitchCustomSpeed" /> 0~9/Ctrl+0~9 快捷键切换自定义列表速度</label> <br /> <label title="默认宽屏"><input type="checkbox" id="DefaultWideScreen" /> 默认宽屏</label> <br /> <label title="[!]此功能放弃维护,因此不再可以使用\n\n↑ / ↓ 全局调整音量 ; 优化滚轮调整音量体验\n支持 0 ~ 500%(过高会有轻微失真)\n仅支持快捷键与鼠标滚轮调整\n\n[!] 本项修改需要刷新网页后生效"><input type="checkbox" id="GlobalVolumeAdjustment" disabled/> 接管音量控制</label> <br /> <label title="暂不支持取消"><input type="checkbox" disabled checked /> 增加快捷键: 字幕切换(Z)</label> <br /> <label title="暂不支持取消"><input type="checkbox" disabled checked /> 增加快捷键: 网页全屏(G)</label> <br /> <label title="暂不支持取消"><input type="checkbox" disabled checked /> 增加快捷键: 宽屏模式(H)</label> <br /> <label title="暂不支持取消"><input type="checkbox" disabled checked /> 双击字幕复制内容</label> </div> <div id="popup-tips">关闭设置窗口自动保存 | 鼠标停留查看更多信息</div> `; this.popup.classList.add("popup-container"); this.popupcss = document.createElement("style"); this.popupcss.innerHTML = ` .popup-container { width: 330px; position: absolute; z-index: 999999; background-color: #fff; border: 1px solid #ccc; border-radius: 8px; box-shadow: 0 0 10px rgba(33,150,243,0.5); } .popup-container .popup-title { position: relative; background-color: #3498db; color: #fff; padding: 10px; cursor: move; border-top-left-radius: 8px; border-top-right-radius: 8px; user-select: none; } .popup-container .popup-content { padding: 20px; } .popup-container .close-button { position: absolute; top: 0px; right: 0px; height: 100%; background-color: #3498db; color: #fff; border: none; padding: 0px 13px; font-size: 24px; cursor: pointer; border-top-right-radius: 8px; transition: background-color 0.3s ease, transform 0.3s ease; } .popup-container .close-button:hover { background-color: #e74c3c; } .popup-container label { font-size: 14px; } .popup-container #popup-tips { color: #555555; font-size: 14px; padding: 4px 0px 4px 10px; border-top: 1px solid #ccc; } .popup-container .button { display: block; padding: 10px; background-color: #3498db; color: #fff; text-align: center; text-decoration: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease; border: none; } .popup-container .button:hover { background-color: #2980b9; } .popup-container select, input[type="text"] { display: block; margin-bottom: 10px; padding: 8px; border: 1px solid #ccc; border-radius: 4px; width: 100%; box-sizing: border-box; outline: none; } .popup-container input[type="text"]:focus { border: 1px solid #2980b9; } .popup-container input[type="radio"] { margin-right: 5px; }`; document.body.appendChild(this.popup); document.head.appendChild(this.popupcss); this.popup_dragend_move = this.popup_dragend_move.bind(this); this.popup.querySelector("#popupTitle").addEventListener("mousedown", (e) => { this.offsetX = e.clientX; this.offsetY = e.clientY; this.startX = parseInt(this.popup.style.left); this.startY = parseInt(this.popup.style.top); document.addEventListener("mousemove", this.popup_dragend_move); document.addEventListener("mouseup", (e) => { document.removeEventListener("mousemove", this.popup_dragend_move); }); }); this.popup.querySelector(".close-button").addEventListener("click", (e) => { this.close(); }); this.handle = handle; } show() { this.popup.querySelector("#SpeedList").value = this.speedlist; this.popup.querySelector("#ArrowRightSpeed").value = this.ArrowRightTime; this.popup.querySelector("#SwitchCustomSpeed").checked = this.SwitchCustomSpeed; this.popup.querySelector("#GlobalVolumeAdjustment").checked = this.GlobalVolumeAdjustment; this.popup.querySelector("#DefaultWideScreen").checked = this.DefaultWideScreen; this.popup.style.display = "block"; let left = (window.innerWidth - this.popup.offsetWidth) / 2; let top = (window.innerHeight - this.popup.offsetHeight) / 2; this.popup.style.left = left + "px"; this.popup.style.top = top + "px"; } close() { let sl, ars; // 读取元素的值 sl = this.popup.querySelector("#SpeedList").value; ars = this.popup.querySelector("#ArrowRightSpeed").value; this.SwitchCustomSpeed = this.popup.querySelector("#SwitchCustomSpeed").checked; this.GlobalVolumeAdjustment = this.popup.querySelector("#GlobalVolumeAdjustment").checked; this.DefaultWideScreen = this.popup.querySelector("#DefaultWideScreen").checked; let sl_ = null, ars_ = null; //进行处理 //自定义速度列表 if (!(sl === null || sl.trim() === "")) { let rates = sl .split(",") .map((s) => s.trim()) .filter((s) => s); if (rates.length > 0) { // 检查输入是否全部为有效数字 if (rates.every((s) => isFinite(s))) { localStorage.setItem(CUSTOM_RATE_ARRAY, rates.join(",")); this.speedlist = sl; sl_ = rates; } } } //右光标键速度 if (parseInt(ars) > 0) { localStorage.setItem(CUSTOM_ArrowRightSpeed, ars); this.ArrowRightTime = ars; ars_ = ars; } localStorage.setItem(CUSTOM_SwitchCustomSpeed, this.SwitchCustomSpeed); localStorage.setItem(CUSTOM_GlobalVolumeAdjustment, this.GlobalVolumeAdjustment); localStorage.setItem(CUSTOM_DefaultWideScreen, this.DefaultWideScreen); this.handle(sl_, ars_); this.popup.remove(); } } let setting = new SettingPopup(); class PlayRateMenu { init(menu) { this.videoObj = document.querySelector("video"); if (this.videoObj) { if (setting.GlobalVolumeAdjustment) { let context = new (window.AudioContext || window.webkitAudioContext)(); this.gain = context.createGain(); context.createMediaElementSource(this.videoObj).connect(this.gain); this.gain.connect(context.destination); this.volumeNumElem = document.querySelector(".bpx-player-ctrl-volume-number"); if (setting.volume != -1) { this.setVolume(setting.volume / 100, false); } this.videoObj.addEventListener("volumechange", () => { if (this.gain) { this.gain.gain.value = 1; } }); } } else { this.videoObj = document.querySelector("bwp-video"); //b站自研wasm软解视频播放器 } if (!this.videoObj) { return false; } this.saveSetting = this.saveSetting.bind(this); this.menu = menu; this.rates = getRateArray(); this.videoObj.addEventListener("loadedmetadata", () => { this.setRate(getRate()); }); if (setting.DefaultWideScreen) { document.querySelector("#bilibili-player .bpx-player-ctrl-wide span").click(); } return true; } insertRate(rateValue) { this.rates.push(rateValue); this.render(); } insertItem(content, rate, event) { const item = document.createElement("li"); item.textContent = content; item.classList.add(MENUCLASS_ITEM); item.setAttribute("data-value", rate); item.addEventListener("click", event); this.menu.appendChild(item); } saveSetting(sl, ars) { if (sl != null) { this.rates = sl; this.render(); let nowRate = getRate(); if (this.rates.indexOf(nowRate) === -1) { this.setRate(1); } else { this.setRate(nowRate); } } } render() { this.menu.innerHTML = ""; this.rates.sort((a, b) => b - a); //排序 this.rates.forEach((rate) => { this.insertItem(rate % 1 == 0 ? rate + ".0x" : rate + "x", rate, (e) => { e.stopPropagation(); const rateValue = e.target.getAttribute("data-value"); this.setVideoRate(rateValue); this.setActiveRate(rateValue); localStorage.setItem(CUSTOM_RATE, rateValue); }); }); //插入一个设置按钮 this.insertItem("设置", 0, (e) => { e.stopPropagation(); setting.create(this.saveSetting); setting.show(); }); } setActiveRate(rateValue) { const items = this.menu.querySelectorAll(`.${MENUCLASS_ITEM}`); items.forEach((item) => { const value = item.getAttribute("data-value"); if (value === rateValue) { item.classList.add(MENUCLASS_ACTIVE); } else { item.classList.remove(MENUCLASS_ACTIVE); } }); } getDuration() { return this.videoObj.duration; } getCurrentTime() { return this.videoObj.currentTime; } setVideoRate(rate) { this.videoObj.playbackRate = parseFloat(rate); } getVideoRate() { return this.videoObj.playbackRate; } //使用此函数前提:速度列表必须存在该速度值 setRate(rate) { const item = document.querySelector(`.${MENUCLASS_ITEM}[data-value="${rate}"]`); if (item) { item.classList.add(MENUCLASS_ACTIVE); item.click(); } else { console.error("未找到匹配元素"); } } changeRate(up) { let nowRate = getRate(); let index = this.rates.indexOf(nowRate); if ((index == 0 && up) || (index == this.rates.length && !up)) { return nowRate; } else { index += up ? -1 : 1; this.setRate(this.rates[index]); return this.rates[index]; } } getVolume() { if (this.videoObj.volume == 1.0) { return this.gain.gain.value; } else { return this.videoObj.volume; } } setVolume(volume, show) { if (!this.gain && volume > 1.0) { volume = 1.0; } if (volume <= 1.0) { this.videoObj.volume = volume; if (this.gain) { this.gain.gain.value = 1; } } else { this.videoObj.volume = 1; this.gain.gain.value = volume; } let sv = (volume * 100).toFixed(0); localStorage.setItem(CUSTOM_Volume, sv); this.volumeNumElem.textContent = sv; if (show == true) { showVolume(sv); } } } let menu = new PlayRateMenu(); let _interval = setInterval(function () { let element = document.querySelector(`.${MENUCLASS}`); if (element) { if (menu.init(element)) { menu.render(); menu.setRate(getRate()); let bpx_player_video_warp = document.querySelector(".bpx-player-video-wrap"); bpx_player_video_warp.appendChild(tipDisplay); bpx_player_video_warp.appendChild(timeDisplay); if (setting.GlobalVolumeAdjustment) { bpx_player_video_warp.addEventListener("mousewheel", (e) => { e.preventDefault(); e.stopImmediatePropagation(); let volume = menu.getVolume() + parseInt(e.wheelDelta / 120) * 0.05; if (volume > 5.0) { volume = 5.0; } else if (volume < 0) { volume = 0; } menu.setVolume(volume, true); }); } setShowTimeState(localStorage.getItem(CUSTOM_ShowTimeState) == "true"); clearInterval(_interval); } else { console.warn("获取视频元素失败!"); } //双击复制字幕内容 let subtitle_panel = document.querySelector(".bpx-player-subtitle-panel-major-group"); if (subtitle_panel) { subtitle_panel.addEventListener("dblclick", function () { let text = document.querySelector(".bpx-player-subtitle-panel-major-group span").textContent; //如果是歌词会存在音乐符号,要清除 let musicSymbol = "♪"; if (text.startsWith(musicSymbol)) { text = text.slice(musicSymbol.length); if (text.endsWith(musicSymbol)) { text = text.slice(0, -musicSymbol.length); } } navigator.clipboard.writeText(text); }); } } }, 500); let ArrowRightTime = 0; let OldRate = 0; document.addEventListener( "keydown", function (e) { e = e || window.event; if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName == "BILI-COMMENTS" || e.target.isContentEditable) { return; } if (e.ctrlKey == true && e.code == "ArrowUp") { let rate = menu.changeRate(true); showPlayRate(rate); } else if (e.ctrlKey == true && e.code == "ArrowDown") { let rate = menu.changeRate(false); showPlayRate(rate); } else if (e.code == "ArrowRight" && !e.ctrlKey && !e.shiftKey && !e.altKey) { if (ArrowRightTime == 0) { ArrowRightTime = e.timeStamp; } else { if (e.timeStamp - ArrowRightTime > 500) { if (OldRate == 0) { OldRate = getRate(); if (typeof setting.ArrowRightTime === "string" && setting.ArrowRightTime.indexOf("x") != -1) { menu.setVideoRate(OldRate * parseInt(setting.ArrowRightTime)); showPlayRate(OldRate * parseInt(setting.ArrowRightTime)); } else { menu.setVideoRate(parseInt(setting.ArrowRightTime)); showPlayRate(parseInt(setting.ArrowRightTime)); } } } } } else if ("0" <= e.key && e.key <= "9") { e.preventDefault(); e.stopImmediatePropagation(); let num = parseInt(e.key - "0"); let speed; if (setting.SwitchCustomSpeed) { if (!(1 <= num && num <= menu.rates.length)) { return; } speed = menu.rates[menu.rates.length - num]; } else { if (num == 0) { speed = 0.5; } else { speed = num; } } if (e.ctrlKey) { menu.setVideoRate(speed); menu.setActiveRate(speed); showPlayRate(speed); localStorage.setItem(CUSTOM_RATE, speed); } else { if (OldRate == 0) { OldRate = getRate(); menu.setVideoRate(speed); showPlayRate(speed); } } } else if (e.code == "KeyZ" && !e.ctrlKey && !e.shiftKey && !e.altKey) { let subtitle_btn = document.querySelector("#bilibili-player .bpx-player-ctrl-subtitle span"); if (subtitle_btn) { subtitle_btn.click(); } } else if (e.code == "KeyG" && !e.ctrlKey && !e.shiftKey && !e.altKey) { document.querySelector("#bilibili-player .bpx-player-ctrl-web span").click(); } else if (e.code == "KeyH" && !e.ctrlKey && !e.shiftKey && !e.altKey) { document.querySelector("#bilibili-player .bpx-player-ctrl-wide span").click(); } else if ((e.code == "ArrowUp" || e.code == "ArrowDown") && !e.ctrlKey && !e.shiftKey && !e.altKey) { if (setting.GlobalVolumeAdjustment) { e.preventDefault(); e.stopImmediatePropagation(); let volume = menu.getVolume(); if (e.code == "ArrowUp") { volume = volume + 0.1; if (volume > 5.0) { volume = 5.0; } } else { volume = volume - 0.1; if (volume < 0) { volume = 0; } } menu.setVolume(volume, true); } } }, true ); document.addEventListener("keyup", function (e) { if (e.code == "ArrowRight" || ("0" <= e.key && e.key <= "9")) { ArrowRightTime = 0; if (OldRate != 0) { menu.setVideoRate(OldRate); showPlayRate(OldRate); OldRate = 0; e.preventDefault(); } } else if (e.code == "F2") { setShowTimeState(localStorage.getItem(CUSTOM_ShowTimeState) == "false"); } }); window.addEventListener("focus", function () { menu.setRate(getRate()); setShowTimeState(localStorage.getItem(CUSTOM_ShowTimeState) == "true"); if (setting.GlobalVolumeAdjustment) { let volume = localStorage.getItem(CUSTOM_Volume); if (volume != -1) { menu.setVolume(volume / 100, false); } } }); function formatTime(s) { var m = parseInt(s / 60); var ss = parseInt(s % 60); return (m > 9 ? `${m}` : `0${m}`) + ":" + (ss > 9 ? `${ss}` : `0${ss}`); } function FlashShowTime() { var rate = menu.getVideoRate(); timeDisplay.textContent = formatTime(menu.getCurrentTime() / rate) + "/" + formatTime(menu.getDuration() / rate); } })();