Youtube Better Player

Scroll Wheel volume control, no autoplay interruptions, save volume

目前为 2021-01-27 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Youtube Better Player
  3. // @description Scroll Wheel volume control, no autoplay interruptions, save volume
  4. // @match https://www.youtube.com/*
  5. // @grant GM_getValue
  6. // @grant GM_setValue
  7. // @version 0.0.1.20210127050821
  8. // @namespace https://greasyfork.org/users/286737
  9. // ==/UserScript==
  10.  
  11. class Player {
  12. constructor() {
  13. this.isEmbed = location.pathname.startsWith('/embed/')
  14. this.volumeSk = 'volume'
  15. }
  16.  
  17. async init() {
  18. const isEmbed = this.isEmbed
  19. const api = this.api = await (isEmbed ? this.getApiEmbed() : this.getApi())
  20.  
  21. api.setVolume(GM_getValue(this.volumeSk))
  22.  
  23. this.volume = api.getVolume()
  24.  
  25. const {$video, $eventCatcher, $volumeBar, $player} = this.getEls()
  26.  
  27. this.listenEvents($video)
  28.  
  29. new WheelVolume(this, api, $volumeBar, $player).init($eventCatcher)
  30.  
  31. if (!isEmbed) new RealAutoPlay(api).init()
  32. }
  33.  
  34. async getApi() {
  35. let $el, api
  36.  
  37. while (!($el = unsafeWindow['ytd-player'])) await wait(1000)
  38. while (!(api = $el.player_)) await wait(200)
  39. while (!api.isReady()) await wait(200)
  40.  
  41. return api
  42. }
  43.  
  44. async getApiEmbed() {
  45. let api
  46.  
  47. while (!(api = unsafeWindow.movie_player)) await wait(1000)
  48.  
  49. // api addEventListener don't support {once}
  50. await new Promise(r => {
  51. const onStateChange = () => {
  52. api.removeEventListener('onStateChange', onStateChange)
  53. r()
  54. }
  55. api.addEventListener('onStateChange', onStateChange)
  56. })
  57.  
  58. return api
  59. }
  60.  
  61. getEls() {
  62. const $player = unsafeWindow.movie_player
  63. const $video = $('video', $player)
  64. const $eventCatcher = $player.parentElement
  65. const $volumeBar = $('.ytp-volume-slider', $player)
  66.  
  67. return {$video, $eventCatcher, $volumeBar, $player}
  68. }
  69.  
  70. listenEvents($video) {
  71. const onVolumeChange = this.onVolumeChange.bind(this)
  72.  
  73. $video.addEventListener('volumechange', onVolumeChange)
  74.  
  75. addEventListener('unload', () => GM_setValue(this.volumeSk, this.volume))
  76. }
  77.  
  78. onVolumeChange() {
  79. this.volume = this.api.getVolume()
  80. }
  81. }
  82.  
  83. class WheelVolume {
  84. constructor(player, api, $volumeBar, $player) {
  85. this.player = player
  86. this.api = api
  87. this.$volumeBar = $volumeBar
  88. this.$player = $player
  89.  
  90. this.events = {
  91. mouseover: new Event('mouseover', {bubbles: true}),
  92. mouseout: new Event('mouseout', {bubbles: true}),
  93. mousemove: new Event('mousemove')
  94. }
  95. }
  96.  
  97. init($eventCatcher) {
  98. const onWheel = this.onWheel.bind(this)
  99. const onClick = this.onClick.bind(this)
  100.  
  101. $eventCatcher.addEventListener('wheel', onWheel)
  102. $eventCatcher.addEventListener('mousedown', onClick)
  103. }
  104.  
  105. onWheel(e) {
  106. e.preventDefault()
  107. e.stopImmediatePropagation()
  108.  
  109. this.show()
  110.  
  111. const now = Date.now(), since = now - this.prevScrollDate
  112. const step = (e.deltaY < 0 ? 1 : -1) * (since < 50 ? 4 : 1)
  113.  
  114. this.api.setVolume(this.player.volume + step)
  115.  
  116. this.prevScrollDate = now
  117. }
  118.  
  119. onClick(e) {
  120. if (e.which != 2) return
  121.  
  122. e.preventDefault()
  123.  
  124. this.show()
  125.  
  126. const api = this.api
  127.  
  128. if (api.isMuted()) {
  129. api.unMute()
  130. api.setVolume(this.player.volume)
  131. }
  132. else api.mute()
  133. }
  134.  
  135. show() {
  136. const $volumeBar = this.$volumeBar, events = this.events
  137.  
  138. this.$player.dispatchEvent(events.mousemove)
  139.  
  140. clearTimeout(this.showTimeout)
  141.  
  142. $volumeBar.dispatchEvent(events.mouseover)
  143.  
  144. this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
  145. }
  146. }
  147.  
  148. class RealAutoPlay {
  149. constructor(api) {
  150. this.api = api
  151.  
  152. this.states = {unstarted: -1, ended: 0, paused: 2}
  153.  
  154. this.popupName = 'yt-confirm-dialog-renderer'
  155. this.$popupContainer = $('ytd-popup-container')
  156.  
  157. this.$toggleAutoNavBtn = $('.ytp-autonav-toggle-button')
  158. this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
  159. }
  160.  
  161. init() {
  162. const onStateChange = this.onStateChange.bind(this)
  163. const onToggleAutoNav = this.onToggleAutoNav.bind(this)
  164.  
  165. this.api.addEventListener('onStateChange', onStateChange)
  166. this.$toggleAutoNavBtn.addEventListener('click', onToggleAutoNav)
  167. }
  168.  
  169. onStateChange(state) {
  170. const states = this.states
  171.  
  172. switch (state) {
  173. case states.ended:
  174. if (this.autoNavEnabled && !document.hasFocus()) this.api.nextVideo()
  175. break
  176. case states.unstarted:
  177. case states.paused:
  178. this.bypassPopup()
  179. }
  180. }
  181.  
  182. onToggleAutoNav() {
  183. this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
  184. }
  185.  
  186. bypassPopup() {
  187. const popup = this.$popupContainer.popups_[this.popupName]
  188.  
  189. if (!popup) return
  190.  
  191. this.api.playVideo()
  192.  
  193. popup.popup.remove()
  194. delete this.$popupContainer.popups_[this.popupName]
  195. }
  196. }
  197.  
  198.  
  199. const $ = (sel, el = document) => el.querySelector(sel)
  200.  
  201. const wait = async (ms) => await new Promise(r => setTimeout(r, ms))
  202.  
  203.  
  204. new Player().init()