Twitch Scroll Wheel Volume

Scroll wheel volume control

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

  1. // ==UserScript==
  2. // @name Twitch Scroll Wheel Volume
  3. // @description Scroll wheel volume control
  4. // @match https://www.twitch.tv/*
  5. // @match https://*.ext-twitch.tv/*
  6. // @version 0.0.1.20210129071034
  7. // @namespace https://greasyfork.org/users/286737
  8. // ==/UserScript==
  9.  
  10. class Player {
  11. constructor() {
  12. this.onStateMutationHandler = this.onStateMutation.bind(this)
  13.  
  14. this.stateObserver = new MutationObserver(this.onStateMutationHandler)
  15. this.wheelVolume = new WheelVolume()
  16. }
  17.  
  18. async init() {
  19. let $root
  20.  
  21. while (!($root = $('.root-scrollable__wrapper'))) await wait(2000)
  22.  
  23. const onRootMutation = this.onRootMutation.bind(this)
  24.  
  25. new MutationObserver(onRootMutation).observe($root, {childList: true})
  26.  
  27. onRootMutation()
  28. }
  29.  
  30. async getApi() {
  31. let $el, api
  32.  
  33. while (!($el = $('.video-player__container', this.$player))) await wait(2000)
  34. while (!(api = this.getReactPlayerApi($el))) await wait(500)
  35.  
  36. return api
  37. }
  38.  
  39. getReactPlayerApi($el) {
  40. let instance
  41.  
  42. for (const key in $el) {
  43. if (key.startsWith('__reactInternalInstance$')) {
  44. instance = $el[key]
  45. }
  46. }
  47.  
  48. let parent = instance.return
  49.  
  50. for (let i = 0; i < 30; i++) {
  51. const player = parent.memoizedProps.mediaPlayerInstance
  52.  
  53. if (player) return player.core
  54.  
  55. parent = parent.return
  56. }
  57. }
  58.  
  59. get$eventCatcher() {
  60. return $('.video-player__overlay', this.$player)
  61. }
  62.  
  63. get$volumeBar() {
  64. return $('[id^=player-volume-slider]', this.$player)
  65. }
  66.  
  67. getIsMinimized() {
  68. return this.$player.dataset.aPlayerState == 'mini'
  69. }
  70.  
  71. onRootMutation() {
  72. const $player = $('.persistent-player')
  73.  
  74. if ($player == this.$player) return
  75.  
  76. this.$player = $player
  77.  
  78. if ($player) this.onNewPlayer()
  79. }
  80.  
  81. onStateMutation() {
  82. // $volumeBar is recreated on maximize
  83. if (!this.getIsMinimized()) this.wheelVolume.$volumeBar = this.get$volumeBar()
  84. }
  85.  
  86. async onNewPlayer() {
  87. const api = this.api = await this.getApi()
  88.  
  89. this.wheelVolume.init(api, this.get$eventCatcher(), this.get$volumeBar())
  90.  
  91. this.stateObserver.observe(this.$player, {attributeFilter: ['data-a-player-state']})
  92. }
  93. }
  94.  
  95. class WheelVolume {
  96. constructor() {
  97. this.onWheelHandler = this.onWheel.bind(this)
  98. this.onMousedownHandler = this.onMousedown.bind(this)
  99.  
  100. this.events = {
  101. mouseover: new Event('mouseover', {bubbles: true}),
  102. mouseout: new Event('mouseout', {bubbles: true}),
  103. mousemove: new Event('mousemove')
  104. }
  105.  
  106. const onExtMessage = this.onExtMessage.bind(this)
  107.  
  108. addEventListener('message', onExtMessage)
  109. }
  110.  
  111. init(api, $eventCatcher, $volumeBar) {
  112. this.api = api
  113. this.$eventCatcher = $eventCatcher
  114. this.$volumeBar = $volumeBar
  115.  
  116. $eventCatcher.addEventListener('wheel', this.onWheelHandler)
  117. $eventCatcher.addEventListener('mousedown', this.onMousedownHandler)
  118. }
  119.  
  120. onWheel(e) {
  121. e.preventDefault()
  122. e.stopImmediatePropagation()
  123.  
  124. this.updateVolume(e.deltaY < 0)
  125. }
  126.  
  127. onMousedown(e) {
  128. if (e.which != 2) return
  129.  
  130. e.preventDefault()
  131.  
  132. this.toggleMute()
  133. }
  134.  
  135. onExtMessage(e) {
  136. const event = e.data.wheelEvent
  137.  
  138. if (!event) return
  139.  
  140. switch (event) {
  141. case 'up':
  142. this.updateVolume(true)
  143. break
  144. case 'down':
  145. this.updateVolume(false)
  146. break
  147. case 'click':
  148. this.toggleMute()
  149. }
  150. }
  151.  
  152. updateVolume(shouldIncrease) {
  153. this.show()
  154.  
  155. const api = this.api, volume = api.getVolume()
  156.  
  157. if ((volume == 0 && !shouldIncrease) || (volume == 1 && shouldIncrease)) return
  158.  
  159. const now = Date.now(), since = now - this.prevScrollDate
  160. const step = (shouldIncrease ? 1 : -1) * (since < 50 ? 4 : 1) * .01
  161.  
  162. api.setVolume(volume + step)
  163.  
  164. this.prevScrollDate = now
  165. }
  166.  
  167. toggleMute() {
  168. this.show()
  169.  
  170. const api = this.api
  171.  
  172. api.setMuted(!api.isMuted())
  173. }
  174.  
  175. show() {
  176. const $volumeBar = this.$volumeBar, events = this.events
  177.  
  178. this.$eventCatcher.dispatchEvent(events.mousemove)
  179.  
  180. clearTimeout(this.showTimeout)
  181.  
  182. $volumeBar.dispatchEvent(events.mouseover)
  183.  
  184. this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
  185. }
  186. }
  187.  
  188. class ExtFrame {
  189. init() {
  190. const onWheel = this.onWheel.bind(this)
  191. const onMousedown = this.onMousedown.bind(this)
  192.  
  193. addEventListener('wheel', onWheel, {passive: false})
  194. addEventListener('mousedown', onMousedown, {passive: false})
  195. }
  196.  
  197. onWheel(e) {
  198. e.preventDefault()
  199. e.stopPropagation()
  200.  
  201. this.sendEvent(e.deltaY < 0 ? 'up' : 'down')
  202. }
  203.  
  204. onMousedown(e) {
  205. if (e.which != 2) return
  206.  
  207. e.preventDefault()
  208.  
  209. this.sendEvent('click')
  210. }
  211.  
  212. sendEvent(name) {
  213. parent.postMessage({wheelEvent: name}, 'https://supervisor.ext-twitch.tv/')
  214. }
  215. }
  216.  
  217. const init = async () => {
  218. const host = location.host
  219.  
  220. if (host == 'www.twitch.tv') new Player().init()
  221. else if (new URLSearchParams(location.search).get('anchor') == 'video_overlay') new ExtFrame().init()
  222. }
  223.  
  224.  
  225. const $ = (sel, el = document) => el.querySelector(sel)
  226.  
  227. const wait = async (ms) => await new Promise(r => setTimeout(r, ms))
  228.  
  229.  
  230. init()