Twitch Scroll Wheel Volume

Scroll wheel volume control

目前为 2022-07-17 提交的版本。查看 最新版本

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