bili自定义播放速率

哔哩哔哩自定义视频播放速度,shift1~9快捷键更改速度。插入样式比较违和,只在普通视频下才有自定义按钮,番剧等页面只能通过快捷键更改速度。

目前为 2024-07-21 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name bili自定义播放速率
  3. // @namespace Violentmonkey Scripts
  4. // @match https://www.bilibili.com/*
  5. // @grant GM_addStyle
  6. // @version 1.1
  7. // @author vurses
  8. // @license GPL
  9. // @description 哔哩哔哩自定义视频播放速度,shift1~9快捷键更改速度。插入样式比较违和,只在普通视频下才有自定义按钮,番剧等页面只能通过快捷键更改速度。
  10. // ==/UserScript==
  11. (async () => {
  12. // 在video页面下能使用额外的一些按钮,在其它页面只能使用shift1~9快捷键
  13. // 每次currentEle必须由筛选的下标获取到dom元素,无需本地存储,知道了默认播放速度就知道了数据列表下标,就知道了选中的元素(但此dom元素也不一定存在),不必要存储
  14. // 速度列表listproxy,当前速度信息对象objproxy,视频video当前速度pbRList
  15. // 切换自定义的倍速按钮触发objproxy代理=>更改样式同时触发video的ratechange事件=>
  16. // 用户切换速度的方式有多种,所以ratechange改变时也需要更改一次样式,该元素通过listproxy过滤出第一个对应速度index可知
  17. // 点击新增按钮=>更改listproxy触发代理=>重新渲染自定义的所有倍速按钮
  18.  
  19. // 只有/video下的视频dom结构会优先加载
  20. let video = document.querySelector("video");
  21. const waitForVideo = new Promise((resolve, reject) => {
  22. let checkInterval;
  23. let timeout;
  24. function checkVideoExists() {
  25. video = document.querySelector("video");
  26. if (video) {
  27. clearInterval(checkInterval);
  28. clearTimeout(timeout);
  29. resolve("video加载成功");
  30. }
  31. }
  32. function stopChecking() {
  33. clearInterval(checkInterval);
  34. reject("video获取超时..."); // 8秒内不存在,reject Promise
  35. }
  36. // 每100ms检查一次a是否存在
  37. checkInterval = setInterval(checkVideoExists, 100);
  38.  
  39. // 8秒后停止检查
  40. timeout = setTimeout(stopChecking, 8000);
  41. });
  42. // 8秒内未加载出video抛出异常中断代码执行
  43. await waitForVideo;
  44.  
  45. // 保证有video元素的情况下快捷键能使用
  46. // shift+1~9快捷键修改速度
  47. document.addEventListener("keydown", function (event) {
  48. // 检查是否按下了Shift键
  49. if (event.shiftKey && event.keyCode >= 51 && event.keyCode <= 57) {
  50. video.playbackRate = event.which - 48;
  51. }
  52. });
  53. // 以评论区dom的加载为标志
  54. const commentContainer = document.querySelector("#comment");
  55.  
  56. // 用定时器也行
  57. // 创建观察者实例,bili的dom结构有部分异步加载,避免页面出现问题脚本的所有操作得在dom完全加载之后才执行
  58. // 只要能标志dom结构完全加载,选择观察哪个dom元素的容器都无所谓
  59. const domLoadedSignalObserver = new MutationObserver(
  60. (mutationsList, observer) => {
  61. // 监听回调
  62. scriptCallback();
  63. // 移除监听
  64. domLoadedSignalObserver.disconnect();
  65. }
  66. );
  67. // 在/video下才加载样式和脚本,避免出错。其它页面与/video不同,插入一些样式比较违和
  68. if (window.location.href.includes("video")) {
  69. domLoadedSignalObserver.observe(commentContainer, {
  70. //监听异步加载dom时,childList变更以触发observe
  71. childList: true,
  72. });
  73. } else {
  74. return;
  75. }
  76. // 样式
  77. const link = document.createElement("link");
  78. link.rel = "stylesheet";
  79. // bulma.css用require引入会报错
  80. // link.href =
  81. // "https://cdn.bootcdn.net/ajax/libs/bulma/1.0.1/css/bulma.min.css";
  82. // 无语,bulma默认暗黑就算了,还有样式污染,还得一个个改类名前缀
  83. // link.href =
  84. // "https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/versions/bulma-prefixed.min.css";
  85. // jsdelivr太慢了
  86. link.href =
  87. "https://unpkg.com/bulma@1.0.0/css/versions/bulma-prefixed.min.css";
  88. document.head.appendChild(link);
  89. // bulma带来的样式污染(ˉ▽ˉ;)...如果样式污染问题太严重得重构
  90. const styleConflictPatch = document.createElement("style");
  91. styleConflictPatch.rel = "text/css";
  92. // 高能进度条svg,一键三连弹出框,视频底部控件样式
  93. styleConflictPatch.innerHTML = `div.bpx-player-pbp.pin svg{
  94. width:100% !important;
  95. height:100% !important;
  96. }
  97. .bili-guide-all *{
  98. box-sizing:content-box !important;
  99. }
  100. .bpx-player-control-bottom > div {
  101. box-sizing:content-box !important;
  102. }
  103. `;
  104. document.head.appendChild(styleConflictPatch);
  105. // bulma样式默认黑暗模式很蛋疼
  106. const script = document.createElement("script");
  107. script.innerHTML = `document.documentElement.setAttribute("data-bulma-theme", "light")`;
  108. document.head.appendChild(script);
  109. // 一些额外的样式
  110. (() => {
  111. const css = `.script-box {
  112. width: 100%;
  113. display: flex;
  114. }
  115. .tag-box a:nth-of-type(1):hover {
  116. background-color: skyblue;
  117. }
  118. .tag-box a:nth-of-type(2):hover {
  119. background-color: rgb(243, 142, 140);
  120. }
  121. .new-tag-box {
  122. width: 70px;
  123. }
  124. .bulma-is-blue{
  125. color: white;
  126. background-color: lightskyblue;
  127. }
  128. `;
  129. GM_addStyle(css);
  130. })();
  131.  
  132. // 将脚本所有操作放一个函数里供observer回调
  133. const scriptCallback = () => {
  134. /*********** 函数定义 ***************/
  135. // 给每一个倍速按钮设置的监听器,用css也能实现,但有一些样式问题还是选择用js
  136. const setListeners = () => {
  137. const mouseenterListeners = new WeakMap();
  138. const mouseleaveListeners = new WeakMap();
  139. const mouseenterCallback = function (e) {
  140. const deleteTag = document.createElement("a");
  141. deleteTag.className = "bulma-tag bulma-is-delete";
  142. // <div class="tags has-addons">
  143. this.children[0].append(deleteTag);
  144. };
  145. const mouseleaveCallback = function (e) {
  146. // <div class="tags has-addons">
  147. this.children[0].lastElementChild.remove();
  148. };
  149. Array.from(document.querySelectorAll(".bulma-control")).forEach(
  150. (element) => {
  151. // weakmap保存监听函数防止内存泄漏
  152. mouseenterListeners.set(element, mouseenterCallback);
  153. mouseleaveListeners.set(element, mouseleaveCallback);
  154. element.addEventListener(
  155. "mouseenter",
  156. mouseenterListeners.get(element)
  157. );
  158. element.addEventListener(
  159. "mouseleave",
  160. mouseleaveListeners.get(element)
  161. );
  162. }
  163. );
  164. };
  165. // 播放速度数组的渲染
  166. const tagRender = (list) => {
  167. return list
  168. .map((value, index) => {
  169. return `<div class="bulma-control" data-index=${index} style="width:70px">
  170. <div class="bulma-tags bulma-has-addons">
  171. <a class="bulma-tag" style="width:40px">${value.toFixed(
  172. 2
  173. )}x</a>
  174. </div>
  175. </div>`;
  176. })
  177. .join("");
  178. };
  179. // 计算当前速度对应下标和元素对象
  180. const computeRateObj = (pbRList, curpbRate) => {
  181. const index = pbRList.findIndex((value, index) => {
  182. return value === curpbRate;
  183. });
  184. const element =
  185. index === -1
  186. ? null
  187. : document.querySelector(`.bulma-field div:nth-child(${index + 1})`)
  188. .children[0].children[0];
  189. return { index, element };
  190. };
  191. /*********** 用户界面构建 ***************/
  192. // 用户操作的容器
  193. const scriptBox = document.createElement("div");
  194. scriptBox.className = "script-box pb-1";
  195. document.querySelector("#viewbox_report").style.height = "auto";
  196. document.querySelector("#viewbox_report").append(scriptBox);
  197. document.querySelector(".script-box").innerHTML = `
  198. <div class="tag-box">
  199. <div class="bulma-field bulma-is-grouped bulma-is-grouped-multiline">
  200. </div>
  201. </div>
  202. <div class="new-tag-box">
  203. <button class="bulma-button bulma-is-small" style="height: 21px">+ Rate</button>
  204. <input class="bulma-input bulma-is-small bulma-is-info" type="number" style="width: 60px;height: 21px; display: none;" />
  205. </div>`;
  206.  
  207. // 播放速度列表数组
  208. const playbackRateList = JSON.parse(
  209. localStorage.getItem("PLAYBACK_RATE_LIST_GREASYFORK") ||
  210. "[0.5, 0.75, 1, 1.25, 1.5, 2, 3]"
  211. );
  212.  
  213. // 首次渲染
  214. document.querySelector(".tag-box .bulma-field").innerHTML =
  215. tagRender(playbackRateList);
  216.  
  217. // 为每个按钮添加监听器控制样式
  218. setListeners();
  219.  
  220. /*********** 响应式,劫持数据操作驱动页面变化 ***************/
  221. // 默认播放速度
  222. const currentPlaybackRate = JSON.parse(
  223. localStorage.getItem("CURRENT_PLAYBACK_RATE_GREASYFORK") || "1"
  224. );
  225. // 将视频速度修改为默认速度
  226. video.playbackRate = currentPlaybackRate;
  227.  
  228. // 记录默认播放速度(当前播放速度)和当前被选tag的对象
  229. const { index: currentRateIndex, element: currentRateEle } = computeRateObj(
  230. playbackRateList,
  231. currentPlaybackRate
  232. );
  233. // 高亮倍速按钮
  234. if (currentRateEle) currentRateEle.classList.add("bulma-is-blue");
  235.  
  236. const playbackRateObj = {
  237. currentRate: currentPlaybackRate,
  238. currentRateElement: currentRateEle,
  239. };
  240.  
  241. // 代理播放速度数组的set操作
  242. const playbackRateListProxy = new Proxy(playbackRateList, {
  243. set: (target, key, value) => {
  244. target[key] = value;
  245. localStorage.setItem(
  246. "PLAYBACK_RATE_LIST_GREASYFORK",
  247. JSON.stringify(target)
  248. );
  249. tagRender(target);
  250. /* 性能可优化 */
  251. // 重复渲染
  252. document.querySelector(".bulma-field").innerHTML =
  253. tagRender(playbackRateList);
  254. const { element } = computeRateObj(
  255. playbackRateList,
  256. playbackRateObjProxy.currentRate
  257. );
  258. // 移除元素高亮
  259. playbackRateObjProxy.currentRateElement = element;
  260. // 添加元素高亮
  261. if (element) element.classList.add("bulma-is-blue");
  262. // 重复添加监听器
  263. setListeners();
  264. return Reflect.set(target, key, value);
  265. },
  266. });
  267. // 代理持久化播放速度对象属性的set操作
  268. const playbackRateObjProxy = new Proxy(playbackRateObj, {
  269. set: (target, key, value) => {
  270. // 需要存在符合条件的tag再移除样式
  271. key === "currentRateElement" &&
  272. target[key] &&
  273. target[key] !== value &&
  274. target[key].classList.remove("bulma-is-blue");
  275. key === "currentRate" &&
  276. (video.playbackRate = value) &&
  277. localStorage.setItem("CURRENT_PLAYBACK_RATE_GREASYFORK", value);
  278. target[key] = value;
  279. return Reflect.set(target, key, value);
  280. },
  281. });
  282.  
  283. // 速度变化监听
  284. video.addEventListener("ratechange", function () {
  285. // 默认速度持久化
  286. playbackRateObjProxy.currentRate = video.playbackRate;
  287. const { element } = computeRateObj(
  288. playbackRateList,
  289. playbackRateObjProxy.currentRate
  290. );
  291. // 移除元素高亮
  292. playbackRateObjProxy.currentRateElement = element;
  293. // 添加元素高亮
  294. if (element) element.classList.add("bulma-is-blue");
  295. });
  296.  
  297. // 根元素事件委托,处理各种事件
  298. document
  299. .querySelector(".script-box")
  300. .addEventListener("click", function (e) {
  301. switch (e.target.tagName) {
  302. case "BUTTON":
  303. const _this = this;
  304. const newTagBox = _this.children[1];
  305. const button = newTagBox.children[0];
  306. const input = newTagBox.children[1];
  307. button.style.display = "none";
  308. // 只创建一次回调函数, [0]button || [1]input
  309. if (!input.onblur) {
  310. input.onblur = function () {
  311. button.style.display = "";
  312. input.style.display = "none";
  313. // 数字过滤
  314. this.value &&
  315. this.value >= 0 &&
  316. (this.value >= 16
  317. ? playbackRateListProxy.push(16)
  318. : playbackRateListProxy.push(Number(this.value)));
  319. this.value = "";
  320. button.focus();
  321. };
  322. input.onkeydown = function (e) {
  323. // 两个Enter键
  324. if (e.keyCode === 108 || e.keyCode === 13) {
  325. this.onblur();
  326. }
  327. // Esc键
  328. if (e.keyCode === 27) {
  329. this.value = "";
  330. this.blur();
  331. button.blur();
  332. }
  333. };
  334. }
  335. // 显示并聚焦input
  336. input.style.display = "";
  337. input.focus();
  338. break;
  339. case "A":
  340. // type:string
  341. const index =
  342. e.target.parentElement.parentElement.getAttribute("data-index");
  343. // 数据变更驱动dom更新
  344. if (Array.from(e.target.classList).includes("bulma-is-delete")) {
  345. // index of tags
  346. playbackRateListProxy.splice(index, 1);
  347. } else {
  348. playbackRateObjProxy.currentRate = playbackRateListProxy[index];
  349. e.target.classList.add("bulma-is-blue");
  350. // tags样式互斥,每次存入当前选中的tag方便下次移除样式
  351. playbackRateObjProxy.currentRateElement = e.target;
  352. }
  353. break;
  354. }
  355. });
  356. // 当前页面切换视频引起video属性变换
  357. const videoChangeObserver = new MutationObserver(
  358. (mutationsList, observer) => {
  359. // 避免视频速度被初始化
  360. video.playbackRate = playbackRateObjProxy.currentRate;
  361. }
  362. );
  363. videoChangeObserver.observe(video, {
  364. attributes: true,
  365. });
  366. };
  367. })();

QingJ © 2025

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