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