Youtube Better Player

Saves volume, adds wheel volume controls, removes autoplay interruptions

目前為 2021-01-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Youtube Better Player
// @description  Saves volume, adds wheel volume controls, removes autoplay interruptions
// @match        https://www.youtube.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @version 0.0.1.20210127035309
// @namespace https://gf.qytechs.cn/users/286737
// ==/UserScript==

class Player {
    constructor() {
        this.isEmbed = location.pathname.startsWith('/embed/')
        this.volumeSk = 'volume'
    }

    async init() {
        const isEmbed = this.isEmbed
        const api = this.api = await (isEmbed ? this.getApiEmbed() : this.getApi())

        const savedVolume = GM_getValue(this.volumeSk)
        if (savedVolume != undefined) api.setVolume(savedVolume)

        this.volume = api.getVolume()

        const {$video, $eventCatcher, $volumeBar, $player} = this.getEls()

        this.listenEvents($video)

        new WheelVolume(this, api, $volumeBar, $player).init($eventCatcher)

        if (!isEmbed) new RealAutoPlay(api).init()
    }

    async getApi() {
        let $el, api

        while (!($el = unsafeWindow['ytd-player'])) await wait(1000)
        while (!(api = $el.player_)) await wait(200)
        while (!api.isReady()) await wait(200)

        return api
    }

    async getApiEmbed() {
        let api

        while (!(api = unsafeWindow.movie_player)) await wait(1000)

        // api addEventListener don't support {once}
        await new Promise(r => {
            const onStateChange = () => {
                api.removeEventListener('onStateChange', onStateChange)
                r()
            }
            api.addEventListener('onStateChange', onStateChange)
        })

        return api
    }

    getEls() {
        const $player = unsafeWindow.movie_player
        const $video = $('video', $player)
        const $eventCatcher = $player.parentElement
        const $volumeBar = $('.ytp-volume-slider', $player)

        return {$video, $eventCatcher, $volumeBar, $player}
    }

    listenEvents($video) {
        const onVolumeChange = this.onVolumeChange.bind(this)

        $video.addEventListener('volumechange', onVolumeChange)

        addEventListener('unload', () => GM_setValue(this.volumeSk, this.volume))
    }

    onVolumeChange() {
        this.volume = this.api.getVolume()
    }
}

class WheelVolume {
    constructor(player, api, $volumeBar, $player) {
        this.player = player
        this.api = api
        this.$volumeBar = $volumeBar
        this.$player = $player

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

    init($eventCatcher) {
        const onWheel = this.onWheel.bind(this)
        const onClick = this.onClick.bind(this)

        $eventCatcher.addEventListener('wheel', onWheel)
        $eventCatcher.addEventListener('mousedown', onClick)
    }

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

        this.show()

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

        this.api.setVolume(this.player.volume + step)

        this.prevScrollDate = now
    }

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

        e.preventDefault()

        this.show()

        const api = this.api

        if (api.isMuted()) {
            api.unMute()
            api.setVolume(this.player.volume)
        }
        else api.mute()
    }

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

        this.$player.dispatchEvent(events.mousemove)

        clearTimeout(this.showTimeout)

        $volumeBar.dispatchEvent(events.mouseover)

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

class RealAutoPlay {
    constructor(api) {
        this.api = api

        this.states = {unstarted: -1, ended: 0, paused: 2}

        this.popupName = 'yt-confirm-dialog-renderer'
        this.$popupContainer = $('ytd-popup-container')

        this.$toggleAutoNavBtn = $('.ytp-autonav-toggle-button')
        this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
    }

    init() {
        const onStateChange = this.onStateChange.bind(this)
        const onToggleAutoNav = this.onToggleAutoNav.bind(this)

        this.api.addEventListener('onStateChange', onStateChange)
        this.$toggleAutoNavBtn.addEventListener('click', onToggleAutoNav)
    }

    onStateChange(state) {
        const states = this.states

        switch (state) {
            case states.ended:
                if (this.autoNavEnabled && !document.hasFocus()) this.api.nextVideo()
                break
            case states.unstarted:
            case states.paused:
                this.bypassPopup()
        }
    }

    onToggleAutoNav() {
        this.autoNavEnabled = this.$toggleAutoNavBtn.ariaChecked
    }

    bypassPopup() {
        const popup = this.$popupContainer.popups_[this.popupName]

        if (!popup) return

        this.api.playVideo()

        popup.popup.remove()
        delete this.$popupContainer.popups_[this.popupName]
    }
}


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

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


new Player().init()

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址