Greasy Fork is available in English.

Twitch Scroll Wheel Volume

Scroll wheel volume control

スクリプトをインストール?
作者が勧める他のスクリプト

Youtube Better Playerも気に入るかもしれません。

スクリプトをインストール
このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         Twitch Scroll Wheel Volume
// @description  Scroll wheel volume control
// @include      https://www.twitch.tv/*
// @include      /^https:\/\/(?!supervisor).*\.ext-twitch\.tv\/.*anchor=video_overlay.*$/
// @run-at       document-idle
// @allFrames    true
// @version 0.0.1.20240222233855
// @namespace https://greasyfork.org/users/286737
// ==/UserScript==

class Player {
    constructor() {
        this.playerTypeObserver = new MutationObserver(this.onPlayerTypeChange.bind(this))
        this.wheelVolume = new WheelVolume()
    }

    async init() {
        let $root

        while (!($root = $('.root-scrollable__wrapper'))) await wait(2000)

        this.$root = $root

        const onRootMutation = this.onRootMutation.bind(this)

        new MutationObserver(onRootMutation).observe($root, {childList: true})

        onRootMutation()
    }

    onRootMutation() {
        const $player = $('.persistent-player', this.$root)

        if ($player == this.$player) return

        this.$player = $player

        if ($player) this.onNewPlayer()
    }

    async onNewPlayer() {
        const api = this.api = await this.getApi()

        this.wheelVolume.init(api, this.get$eventCatcher(), this.get$volumeBar())

        this.$layout = $('.video-player', this.$player)

        this.playerTypeObserver.observe(this.$layout, {attributeFilter: ['data-a-player-type']})
    }

    async getApi() {
        const playerSel = 'div[data-a-target="player-overlay-click-handler"], .video-player'
        let $el, api

        while (!($el = $(playerSel, unsafeWindow.document))) await wait(2000)
        while (!(api = this.getReactPlayerApi($el))) await wait(500)

        return api
    }

    getReactPlayerApi($el) {
        let instance

        for (const key in $el) {
            if (key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$')) {
                instance = $el[key]
            }
        }

        let parent = instance.return

        for (let i = 0; i < 50; i++) {
            const player = parent.memoizedProps.mediaPlayerInstance

            if (player) return player.core

            parent = parent.return
        }
    }

    get$eventCatcher() {
        return $('.video-player__container', this.$player)
    }

    get$volumeBar() {
        return $('.video-ref .volume-slider__slider-container', this.$player)
    }

    onPlayerTypeChange() {
        if (this.$layout.dataset.aPlayerType == 'site') this.wheelVolume.$volumeBar = this.get$volumeBar()
    }
}

class WheelVolume {
    constructor() {
        this.onWheelHandler = this.onWheel.bind(this)
        this.onMousedownHandler = this.onMousedown.bind(this)

        this.events = {
            mouseover: new Event('mouseover', {bubbles: true}),
            mouseout: new Event('mouseout', {bubbles: true}),
            mouseenter: new Event('mouseenter')
        }

        const onExtMessage = this.onExtMessage.bind(this)

        addEventListener('message', onExtMessage)
    }

    init(api, $eventCatcher, $volumeBar) {
        this.api = api
        this.$eventCatcher = $eventCatcher
        this.$volumeBar = $volumeBar

        $eventCatcher.addEventListener('wheel', this.onWheelHandler)
        $eventCatcher.addEventListener('mousedown', this.onMousedownHandler)
    }

    onWheel(e) {
        e.preventDefault()
        e.stopImmediatePropagation()

        this.updateVolume(e.deltaY < 0)
    }

    onMousedown(e) {
        if (e.which != 2) return

        e.preventDefault()

        this.toggleMute()
    }

    onExtMessage(e) {
        const event = e.data.wheelEvent

        if (!event) return

        switch (event) {
            case 'up':
                this.updateVolume(true)
                break
            case 'down':
                this.updateVolume(false)
                break
            case 'click':
                this.toggleMute()
        }
    }

    updateVolume(shouldIncrease) {
        this.show()

        const api = this.api, volume = api.getVolume()

        if ((volume == 0 && !shouldIncrease) || (volume == 1 && shouldIncrease)) return

        const now = Date.now(), since = now - this.prevScrollDate
        const step = (shouldIncrease ? 1 : -1) * (since < 50 ? 4 : 1) * .01

        if (api.isMuted()) api.setMuted(false)

        api.setVolume(volume + step)

        this.prevScrollDate = now
    }

    toggleMute() {
        this.show()

        const api = this.api

        api.setMuted(!api.isMuted())
    }

    show() {
        const $volumeBar = this.$volumeBar, events = this.events

        this.$eventCatcher.dispatchEvent(events.mouseenter)

        clearTimeout(this.showTimeout)

        $volumeBar.dispatchEvent(events.mouseover)

        this.showTimeout = setTimeout(() => $volumeBar.dispatchEvent(events.mouseout), 1000)
    }
}

class ExtFrame {
    init() {
        const onWheel = this.onWheel.bind(this)
        const onMousedown = this.onMousedown.bind(this)

        addEventListener('wheel', onWheel, {passive: false})
        addEventListener('mousedown', onMousedown, {passive: false})
    }

    onWheel(e) {
        e.preventDefault()
        e.stopPropagation()

        this.sendEvent(e.deltaY < 0 ? 'up' : 'down')
    }

    onMousedown(e) {
        if (e.which != 2) return

        e.preventDefault()

        this.sendEvent('click')
    }

    sendEvent(name) {
        parent.postMessage({wheelEvent: name}, 'https://supervisor.ext-twitch.tv/')
    }
}

const init = async () => {
    if (location.host == 'www.twitch.tv') return new Player().init()

    new ExtFrame().init()
}


const $ = (sel, el = document) => el.querySelector(sel)

const wait = async (ms) => await new Promise(r => setTimeout(r, ms))


init()