Youtube Better Player

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

目前为 2021-07-30 提交的版本。查看 最新版本

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