Youtube Better Player

Scroll wheel volume, "Are you there" popup bypass, Volume percent display, Infinite autoplay, Volume save

安装此脚本?
作者推荐脚本

你可能也喜欢 Twitch Scroll Wheel Volume

安装此脚本
  1. // ==UserScript==
  2. // @name Youtube Better Player
  3. // @description Scroll wheel volume, "Are you there" popup bypass, Volume percent display, Infinite autoplay, Volume save
  4. // @include /^https:\/\/www\.youtube(-nocookie)?\.com\/(?!(live_chat\?.*|ytscframe)$).*$/
  5. // @run-at document-idle
  6. // @allFrames true
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @grant GM_addStyle
  10. // @version 0.0.1.20240506140158
  11. // @namespace https://greasyfork.org/users/286737
  12. // ==/UserScript==
  13.  
  14. class Player {
  15. constructor() {
  16. this.volumeSk = 'volume'
  17. }
  18.  
  19. async init(isEmbed) {
  20. const api = this.api = await this.getApi(isEmbed)
  21. this.volume = Number(GM_getValue(this.volumeSk))
  22.  
  23. if (isNaN(this.volume)) {
  24. this.volume = Math.floor(api.getVolume())
  25. GM_setValue(this.volumeSk, this.volume)
  26. }
  27. else {
  28. api.unMute()
  29. api.setVolume(this.volume)
  30. }
  31.  
  32. const {$player, $video, $eventCatcher, $volumeArea, $volumeBar} = this.getEls()
  33.  
  34. this.$volumeText = this.buildVolumeText($volumeArea)
  35.  
  36. const onVolumeChange = this.onVolumeChange.bind(this)
  37. $video.addEventListener('volumechange', onVolumeChange)
  38.  
  39. new WheelVolume(this, api, $volumeBar, $player).init($eventCatcher)
  40.  
  41. if (!isEmbed) new RealAutoPlay(api).init($video)
  42. }
  43.  
  44. async getApi(isEmbed) {
  45. if (isEmbed) {
  46. let api
  47. while (!(api = unsafeWindow.movie_player)) await wait(200)
  48.  
  49. return api
  50. }
  51.  
  52. let $el, api
  53.  
  54. while (!($el = unsafeWindow['ytd-player'])) await wait(1000)
  55. while (!(api = $el.player_)) await wait(200)
  56. while (!api.isReady()) await wait(200)
  57.  
  58. return api
  59. }
  60.  
  61. getEls() {
  62. const $player = $('#movie_player')
  63. const $video = $('.html5-main-video', $player)
  64. const $eventCatcher = $player.parentElement
  65. const $volumeArea = $('.ytp-volume-area', $player)
  66. const $volumeBar = $('.ytp-volume-slider', $volumeArea)
  67.  
  68. return {$player, $video, $eventCatcher, $volumeArea, $volumeBar}
  69. }
  70.  
  71. buildVolumeText($volumeArea) {
  72. const $volumeText = document.createElement('span')
  73. $volumeText.classList.add('ytbp-volume-text')
  74. $volumeText.textContent = this.volume
  75. GM_addStyle(volumeTextStyle)
  76.  
  77. $volumeArea.insertAdjacentElement('beforeend', $volumeText)
  78.  
  79. return $volumeText
  80. }
  81.  
  82. onVolumeChange() {
  83. this.volume = this.$volumeText.textContent = Math.floor(this.api.getVolume())
  84.  
  85. clearTimeout(this.saveTimeout)
  86.  
  87. this.saveTimeout = setTimeout(() => GM_setValue(this.volumeSk, this.volume), 1000)
  88. }
  89. }
  90.  
  91. class WheelVolume {
  92. constructor(player, api, $volumeBar, $player) {
  93. this.player = player
  94. this.api = api
  95. this.$volumeBar = $volumeBar
  96. this.$player = $player
  97.  
  98. this.events = {
  99. mouseover: new Event('mouseover', {bubbles: true}),
  100. mouseout: new Event('mouseout', {bubbles: true}),
  101. mousemove: new Event('mousemove')
  102. }
  103. }
  104.  
  105. init($eventCatcher) {
  106. const onWheel = this.onWheel.bind(this)
  107. const onClick = this.onClick.bind(this)
  108.  
  109. $eventCatcher.addEventListener('wheel', onWheel)
  110. $eventCatcher.addEventListener('mousedown', onClick)
  111. }
  112.  
  113. onWheel(e) {
  114. e.preventDefault()
  115. e.stopImmediatePropagation()
  116.  
  117. this.show()
  118.  
  119. const api = this.api
  120. const now = Date.now(), since = now - this.prevScrollDate
  121. const step = (e.deltaY < 0 ? 1 : -1) * (since < 50 ? 4 : 1)
  122.  
  123. if (api.isMuted()) api.unMute()
  124.  
  125. api.setVolume(this.player.volume + step)
  126.  
  127. this.prevScrollDate = now
  128. }
  129.  
  130. onClick(e) {
  131. if (e.which != 2) return
  132.  
  133. e.preventDefault()
  134.  
  135. this.show()
  136.  
  137. const api = this.api
  138.  
  139. if (api.isMuted()) {
  140. api.unMute()
  141. api.setVolume(this.player.volume)
  142. }
  143. else api.mute()
  144. }
  145.  
  146. show() {
  147. const $volumeBar = this.$volumeBar, events = this.events
  148.  
  149. this.$player.dispatchEvent(events.mousemove)
  150.  
  151. clearTimeout(this.showTimeout)
  152.  
  153. $volumeBar.dispatchEvent(events.mouseover)
  154.  
  155. this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
  156. }
  157. }
  158.  
  159. class RealAutoPlay {
  160. constructor(api) {
  161. this.api = api
  162.  
  163. this.popupName = 'yt-confirm-dialog-renderer'
  164.  
  165. const popupEl = $('ytd-popup-container', unsafeWindow.document)
  166. this.popupContainer = popupEl.polymerController ?? popupEl.inst
  167. }
  168.  
  169. init($video) {
  170. const $autonavToggleButton = $('.ytp-autonav-toggle-button')
  171.  
  172. this.autoNavEnabled = $autonavToggleButton.ariaChecked
  173.  
  174. $autonavToggleButton.addEventListener('click', onToggleAutoNav)
  175.  
  176. const bypassPopup = this.bypassPopup.bind(this)
  177. const forceNextVideo = this.forceNextVideo.bind(this)
  178. const onToggleAutoNav = this.onToggleAutoNav.bind(this)
  179.  
  180. $video.addEventListener('pause', bypassPopup)
  181. $video.addEventListener('waiting', bypassPopup)
  182. $video.addEventListener('ended', forceNextVideo)
  183. }
  184.  
  185. bypassPopup() {
  186. const popup = this.popupContainer.popups_?.[this.popupName]
  187.  
  188. if (!popup) return
  189.  
  190. this.api.playVideo()
  191.  
  192. popup.popup.remove()
  193. delete this.popupContainer.popups_[this.popupName]
  194. }
  195.  
  196. forceNextVideo() {
  197. if (this.autoNavEnabled && !document.hasFocus()) this.api.nextVideo()
  198. }
  199.  
  200. onToggleAutoNav() {
  201. this.autoNavEnabled = !this.autoNavEnabled
  202. }
  203. }
  204.  
  205. const init = async () => {
  206. const isEmbed = location.pathname.startsWith('/embed/')
  207.  
  208. if (isEmbed) await new Promise(r => $('.html5-main-video').addEventListener('canplay', r, {once: true}))
  209.  
  210. new Player().init(isEmbed)
  211. }
  212.  
  213. const volumeTextStyle = `
  214. .ytbp-volume-text {
  215. position: relative;
  216. top: -0.5px;
  217. width: 0;
  218. text-indent: 2px;
  219. overflow: hidden;
  220. color: #ddd;
  221. font-size: 109%;
  222. text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
  223. transition: width .2s
  224. }
  225. .ytbp-volume-text:after { content: '%' }
  226. .ytp-volume-control-hover:not([aria-valuenow="0"], [aria-valuenow="100"]) + .ytbp-volume-text {
  227. width: 2.5em
  228. }
  229. `
  230.  
  231. const $ = (sel, el = document) => el.querySelector(sel)
  232.  
  233. const wait = (ms) => new Promise(r => setTimeout(r, ms))
  234.  
  235.  
  236. init()