哔哩哔哩(B站, bilibili)播放界面和部分操作优化

B站播放器速度自定义(0.25 ~ 3), 支持快捷键(z:正常, x:减少速度, c:增加速度), 鼠标中键切换全屏等

  1. // ==UserScript==
  2. // @name 哔哩哔哩(B站, bilibili)播放界面和部分操作优化
  3. // @description B站播放器速度自定义(0.25 ~ 3), 支持快捷键(z:正常, x:减少速度, c:增加速度), 鼠标中键切换全屏等
  4. // @namespace bili
  5. // @version 1.6.29
  6. // @author vizo
  7. // @license MIT
  8. // @include *bilibili.com/video/*
  9. // @include *bilibili.com/bangumi/play*
  10. // @include *bilibili.com/medialist/*
  11. // @include *bilibili.com/cheese/play*
  12. // @include *search.bilibili.com*
  13. // @grant GM_addStyle
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @run-at document-end
  17. // @require https://unpkg.com/jquery@3.6.0/dist/jquery.min.js
  18. // @require https://unpkg.com/vio-utils@2.7.8/index.js
  19. // @require https://unpkg.com/@vizoy/tmk-utils@1.2.18/index.js
  20. // @noframes
  21. // ==/UserScript==
  22.  
  23.  
  24. GM_addStyle(`
  25. html {
  26. overflow-y: scroll;
  27. }
  28. html.hideScroll {
  29. overflow: hidden;
  30. margin-left: -3px;
  31. }
  32. body::-webkit-scrollbar {
  33. width: 6px;
  34. }
  35. body::-webkit-scrollbar-corner,
  36. body::-webkit-scrollbar-track {
  37. background-color: #f8f8f8;
  38. }
  39. body::-webkit-scrollbar-thumb {
  40. background: #c5c5c5;
  41. }
  42. #bilibili-player {
  43. position: relative;
  44. }
  45. #spsy_msg {
  46. width: 105px;
  47. height: 42px;
  48. text-align: center;
  49. line-height: 42px;
  50. border-radius: 4px;
  51. background: rgba(255,255,255,.8);
  52. color: #222;
  53. font-size: 16px;
  54. position: absolute;
  55. top: -80px;
  56. right: 0;
  57. bottom: 0;
  58. left: 0;
  59. margin: auto;
  60. z-index: 999888;
  61. display: none;
  62. }
  63. #bl_info_xz {
  64. height: 25px;
  65. line-height: 25px;
  66. font-size: 14px;
  67. padding: 0 5px;
  68. color: #00a1d6;
  69. position: absolute;
  70. top: -25px;
  71. right: 0;
  72. z-index: 2;
  73. }
  74. .bilibili-player-video-btn-speed {
  75. opacity: 0.4;
  76. pointer-events: none;
  77. }
  78. .bilibili-player-video-btn-speed.on {
  79. opacity: 1;
  80. pointer-events: auto;
  81. }
  82. .bilibili-player-volumeHint,
  83. .bpx-player-volume-hint {
  84. display: none !important;
  85. }
  86. .so-fg5r .inp {
  87. width: 50px;
  88. color: #666;
  89. border: 1px solid #0ad;
  90. border-radius: 2px;
  91. padding: 2px 5px;
  92. outline: none;
  93. }
  94. .so-fg5r .inp:focus {
  95. border-color: #0ad;
  96. }
  97. .so-fg5r span {
  98. color: #666;
  99. }
  100. .video-item.hide_7s {
  101. display: none !important;
  102. }
  103. `)
  104.  
  105. Object.assign(TMK, vio)
  106.  
  107. const pks = ['video', 'bangumi', 'medialist']
  108. if (
  109. pks.some(v => location.pathname.includes(v))
  110. && !location.host.includes('search')
  111. && document.documentElement.scrollTop === 0
  112. ) {
  113. document.documentElement.classList.add('hideScroll')
  114. }
  115.  
  116. const G = {
  117. timer2s: 0,
  118. timer3s: 0,
  119. focusChangeTime: 0,
  120. g2URL: '',
  121. }
  122.  
  123. async function vdoWp() {
  124. const sltor = location.pathname.includes('bangumi') ?
  125. '.bpx-player-video-area' :
  126. '#bilibiliPlayer'
  127. return (await TMK.loadEl(sltor))
  128. }
  129. async function appendMsgLay() {
  130. let wp = await vdoWp()
  131. let msg = document.getElementById('spsy_msg')
  132. if (!wp.contains(msg)) {
  133. wp.insertAdjacentHTML('beforeend', `<div id="spsy_msg"></div>`)
  134. }
  135. }
  136. async function appendAxInfo() {
  137. let wp = await vdoWp()
  138. let inf = document.getElementById('bl_info_xz')
  139. if (!wp.contains(inf)) {
  140. wp.insertAdjacentHTML('beforeend', `<div id="bl_info_xz"></div>`)
  141. }
  142. }
  143. function setAxInfo() {
  144. let inf = $('#bl_info_xz')
  145. let vol = Math.trunc(getGMvolume() * 100)
  146. let speed = getGMspeed()
  147. if (inf.length) {
  148. speed = speed === 1 ? speed : `<span style="color:#f33;">${speed}</span>`
  149. inf.html(`<span>速度: ${speed} &nbsp; 音量: ${vol}</span>`)
  150. }
  151. }
  152. function toggleVideoFullscreen() {
  153. try {
  154. $('.bilibili-player-video-btn-fullscreen')[0].click()
  155. } catch (err) {}
  156. try {
  157. $('.squirtle-video-fullscreen > div')[0].click()
  158. } catch (err) {}
  159. }
  160. function setGMspeed(val) {
  161. return GM_setValue('--> bl_player_speed', val)
  162. }
  163. function setGMvolume(val) {
  164. GM_setValue('--> bl_player_volume', val)
  165. }
  166. function getGMspeed() {
  167. return +GM_getValue('--> bl_player_speed') || 1
  168. }
  169. function getGMvolume() {
  170. let vol = GM_getValue('--> bl_player_volume')
  171. return vol !== undefined ? vol : 0.5
  172. }
  173. // 判断是否全屏
  174. function isFullScreen() {
  175. return document.isFullScreen || document.mozIsFullScreen || document.webkitIsFullScreen
  176. }
  177. // 显示信息
  178. function showSpMsg(msg, type = '速度') {
  179. let mp = $('#spsy_msg')
  180. clearTimeout(G.timer2s)
  181. mp.fadeIn(180)
  182. mp.text(`${type} ${msg}`)
  183. G.timer2s = setTimeout(() => {
  184. mp.fadeOut(350)
  185. }, 800)
  186. }
  187. // 设置播放器播放速度
  188. async function setPlayerSpeed(speedVal = getGMspeed()) {
  189. const vd = await vdo()
  190. if (vd) {
  191. vd.playbackRate = speedVal
  192. }
  193. if (vd.nodeName === 'bwp-video' || vd?.nodeName?.includes('bwp-video')) {
  194. $('.bilibili-player-video-btn-speed').addClass('on')
  195. }
  196. setGMspeed(speedVal)
  197. setAxInfo()
  198. }
  199. // 设置音量
  200. async function setVolume(vol = getGMvolume()) {
  201. const vd = await vdo()
  202. if (vd) {
  203. vd.volume = vol
  204. }
  205. setGMvolume(vol)
  206. setAxInfo()
  207. }
  208.  
  209. async function vdo() {
  210. const node = await TMK.loadEl('video, bwp-video')
  211. return node.length > 1 ? node[0] : node
  212. }
  213.  
  214. function setVdoCfg() {
  215. appendMsgLay()
  216. appendAxInfo()
  217. setPlayerSpeed()
  218. setVolume()
  219. }
  220.  
  221. function runVdoTimer() {
  222. setVdoCfg()
  223. setTimeout(runVdoTimer, 2000)
  224. }
  225.  
  226. async function regWpMouseEvt() {
  227. const wwp = $('#playerWrap')
  228. const wp = wwp.length ? wwp : await vdoWp()
  229. $(wp).on('mouseenter', () => {
  230. $('html').addClass('hideScroll')
  231. })
  232. $(wp).on('mouseleave', () => {
  233. $('html').removeClass('hideScroll')
  234. })
  235. }
  236.  
  237. function watchUrlFunc() {
  238. if (G.g2URL !== location.href) {
  239. G.g2URL = location.href
  240. eachVideoList()
  241. }
  242. setTimeout(watchUrlFunc, 500)
  243. }
  244.  
  245. function tryAppendSoMod() {
  246. if ($('.so-fg5r').length) return
  247. const html = `
  248. <div class="so-fg5r">
  249. <input class="inp inp-s" type="text">
  250. <span> - </span>
  251. <input class="inp inp-e" type="text">
  252. <span>分钟</span>
  253. </div>
  254. `
  255. $('ul.filter-type.duration').append(html)
  256. }
  257.  
  258. function watchContains() {
  259. TMK.watchDom('.contain', () => {
  260. tryAppendSoMod()
  261. })
  262. }
  263.  
  264. function eachVideoList() {
  265. const min = $('.inp-s').val() || 0
  266. const max = $('.inp-e').val() || 1e9
  267. if (!min && !max) {
  268. $('.video-list > li.video-item').each((i, v) => {
  269. $(v).removeClass('hide_7s')
  270. })
  271. return
  272. }
  273. $('.video-list > li.video-item').each((i, v) => {
  274. let tis = $(v)
  275. tis.removeClass('hide_7s')
  276. let sTime = tis.find('.so-imgTag_rb').text()
  277. let mth = sTime.match(/\d{2}(?=(\:\d{2}){2})/)
  278. let hour = mth ? mth[0] : 0
  279. let minute = sTime.replace(/(?:\d{2}\:)?(\d{2})\:\d{2}/, '$1')
  280. let total = Number(hour) * 60 + Number(minute)
  281. if (total < Number(min) || total > Number(max)) {
  282. tis.closest('.video-item').addClass('hide_7s')
  283. }
  284. })
  285. microScroll()
  286. }
  287.  
  288. function microScroll() {
  289. clearTimeout(G.timer3s)
  290. G.timer3s = setTimeout(async () => {
  291. const st = document.documentElement.scrollTop
  292. window.scrollTo(0, st + 1)
  293. await vio.timeout(100)
  294. window.scrollTo(0, st)
  295. }, 500)
  296. }
  297.  
  298. function regPageKey() {
  299. $('body').on('keydown', function(e) {
  300. try {
  301. if (/left/i.test(e.key)) {
  302. $('.page-item.prev > button')[0].click()
  303. }
  304. if (/right/i.test(e.key)) {
  305. $('.page-item.next > button')[0].click()
  306. }
  307. } catch(e) {}
  308. })
  309. }
  310. // 筛选搜索结果
  311. $('body').on('input', '.inp-s, .inp-e', function() {
  312. eachVideoList()
  313. })
  314.  
  315. async function initVolumeLabel(val = getGMspeed()) {
  316. const vArr = [0.5, 0.75, 1, 1.25, 1.5, 2]
  317. if (!vArr.includes(val)) return
  318. const lbs = await TMK.loadEl('.bilibili-player-video-btn-speed-menu-list')
  319. $(lbs).each((i, v) => {
  320. const tis = $(v)
  321. const vol = tis.attr('data-value')
  322. if (vol && vol === String(val)) {
  323. v.click()
  324. }
  325. })
  326. }
  327.  
  328.  
  329. function regKbEvt() {
  330. window.addEventListener('keydown', async e => {
  331. if (e.target.nodeName !== 'BODY') return
  332. if (/^[zxc]$/.test(e.key) && !e.altKey && !e.ctrlKey) {
  333. await TMK.timeout(100)
  334. if (Date.now() - G.focusChangeTime < 1000) return
  335. let val = getGMspeed()
  336. if (e.key === 'z') {
  337. val = 1
  338. }
  339. if (e.key === 'x') {
  340. val = Math.max(val - 0.25, 0.25)
  341. }
  342. if (e.key === 'c') {
  343. val = Math.min(val + 0.25, 3)
  344. }
  345. setPlayerSpeed(val)
  346. showSpMsg(val)
  347. initVolumeLabel(val)
  348. }
  349. if (/up|down/i.test(e.key)) {
  350. let vl = getGMvolume()
  351. let vol = e.key.includes('Up') ? Math.min(vl + 0.03, 1) : Math.max(vl - 0.03, 0)
  352. setVolume(TMK.floor(vol))
  353. showSpMsg(Math.trunc(vol * 100), '音量')
  354. }
  355. })
  356. // ctrl和alt按下后短时间内阻止zxc
  357. $('body').on('keydown', function(e) {
  358. if (/up|down/i.test(e.key)) {
  359. e.stopPropagation()
  360. e.preventDefault()
  361. }
  362. })
  363. }
  364.  
  365. function regMouseEvt() {
  366. window.addEventListener('wheel', async (e) => {
  367. const wp = await vdoWp()
  368. const isContains = wp.contains(e.target)
  369. if (!isContains) return
  370. let vol = getGMvolume()
  371. $('video').each((i, v) => {
  372. if ($(v).attr('src')) {
  373. v.muted = false
  374. }
  375. })
  376. if (e.deltaY > 0) {
  377. // 减少音量
  378. vol = Math.max(vol - (e.altKey ? 0.1 : 0.03), 0)
  379. } else {
  380. // 增加音量
  381. vol = Math.min(vol + (e.altKey ? 0.1 : 0.03), 1)
  382. }
  383. vol = TMK.floor(vol)
  384. setVolume(vol)
  385. let pVol = Math.trunc(vol * 100)
  386. showSpMsg(pVol, '音量')
  387. })
  388. // 滚轮中键点击(滚轮点击)切换全屏
  389. $('body').on('mousedown', '.bilibili-player-video-wrap,.bpx-player-video-area', function(e) {
  390. if (e.button === 1) {
  391. e.preventDefault()
  392. toggleVideoFullscreen()
  393. }
  394. })
  395. }
  396.  
  397. async function initFunc() {
  398. ['focus', 'blur'].forEach(v => {
  399. window.addEventListener(v, e => {
  400. G.focusChangeTime = Date.now()
  401. })
  402. })
  403. if (location.host.includes('search.bilibili.com')) {
  404. watchContains()
  405. watchUrlFunc()
  406. regPageKey()
  407. } else {
  408. runVdoTimer()
  409. regWpMouseEvt()
  410. regKbEvt()
  411. regMouseEvt()
  412. initVolumeLabel()
  413. }
  414. }
  415.  
  416. initFunc()
  417.  

QingJ © 2025

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