Youtube Better Player

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

目前为 2022-08-06 提交的版本,查看 最新版本

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