Skip to content

Source: src/settings/PlaybackRate.js

import DomSmith from '../../lib/dom/DomSmith.js';

/**
 * The PlaybackRate component shows the current playback speed and also provides a UI to change it.
 * @exports module:src/settings/PlaybackRate
 * @requires lib/dom/DomSmith
 * @author   Frank Kudermann - alphanull
 * @version  1.0.0
 * @license  MIT
 */
export default class PlaybackRate {

    /**
     * Holds the instance configuration for this component.
     * @type     {Object}
     * @property {number[]} [allowedValues=[0.25, 0.5, 1, 2, 4]]  This configures which playback rate speeds appear in the menu.
     * @property {number}   [speed=1]                             The initial playback speed.
     */
    #config = {
        speed: 1,
        allowedValues: [0.25, 0.5, 0.75, 0.85, 1, 1.25, 1.5, 2, 4]
    };

    /**
     * Reference to the main player instance.
     * @type {module:src/core/Player}
     */
    #player;

    /**
     * Reference to the quality menu.
     * @type {module:src/util/Menu}
     */
    #menu;

    /**
     * Holds tokens of subscriptions to player events, for later unsubscribe.
     * @type {number[]}
     */
    #subscriptions;

    /**
     * Creates an instance of the PlaybackRate component.
     * @param {module:src/core/Player} player  Reference to the media player instance.
     * @param {module:src/ui/Popup}    parent  Reference to the parent instance (In this case the settings popup).
     */
    constructor(player, parent) {

        this.#config = player.initConfig('playbackRate', this.#config);

        if (!this.#config || !this.#config.allowedValues.includes(this.#config.speed)) return [false];

        this.#player = player;

        this.#menu = new DomSmith({
            _ref: 'menu',
            className: 'vip-menu playbackrate-menu',
            _nodes: [{
                _tag: 'label',
                _nodes: [
                    {
                        _tag: 'span',
                        className: 'form-label-text',
                        _nodes: [
                            this.#player.locale.t('misc.playbackrate'),
                            {
                                _ref: 'speedLabel',
                                _text: ''
                            }
                        ]
                    }, {
                        _tag: 'input',
                        _ref: 'slider',
                        type: 'range',
                        min: 0,
                        max: this.#config.allowedValues.length - 1,
                        step: 1,
                        value: this.#config.opacity,
                        ariaLabel: this.#player.locale.t('misc.playbackrate'),
                        className: 'has-center-line',
                        change: ({ target }) => { this.#toggleSpeed(this.#config.allowedValues[target.value]); },
                        input: ({ target }) => { this.#toggleSpeed(this.#config.allowedValues[target.value]); }
                    }
                ]
            }]
        }, parent.getElement('center'));

        this.#subscriptions = [
            ['media/ready', this.#onMediaReady],
            ['media/ratechange', this.#onRateChange],
            ['data/nomedia', this.#disable],
            ['media/error', this.#disable],
            ['media/canplay', this.#enable]
        ].map(([event, handler]) => this.#player.subscribe(event, handler));

    }

    /**
     * Sets up the component as soon as the media is playable.
     * @listens module:src/core/Media#media/ready
     */
    #onMediaReady = () => {

        const liveStream = this.#player.getState('media.liveStream');

        if (liveStream && (this.#config.speed > 1 || this.#player.getState('media.playbackRate') > 1)) {
            this.#config.speed = 1;
        }

        this.#toggleSpeed(this.#config.speed);
        this.#onRateChange();

    };

    /**
     * Changes playback speed.
     * @param {number} value  The desired speed (1 is normal speed).
     */
    #toggleSpeed(value) {

        this.#config.speed = value;
        if (this.#player.getState('media.playbackRate') !== value) this.#player.media.playbackRate(value);

    }

    /**
     * This handler is called if playback speed is changed (either by this component or otherwise).
     * Updates the menus' "is-active" state accordingly.
     * @listens module:src/core/Media#media/ratechange
     */
    #onRateChange = () => {

        const playerRate = this.#player.getState('media.playbackRate');

        this.#menu.speedLabel.nodeValue = ` (x${playerRate})`;
        this.#menu.slider.setAttribute('aria-valuetext', `${playerRate}`);
        this.#menu.slider.value = this.#config.allowedValues.findIndex(val => val === playerRate);

    };

    /**
     * Enables the menu functionality. This method listens to canplay events in order to restore a usable state again
     * when the player recovered from a media error (for example by loading another file).
     * @listens module:src/core/Media#media/canplay
     */
    #enable = () => {

        this.#menu.slider.disabled = false;

    };

    /**
     * Disables the menu functionality. This method listens to media error events which cause the button to be disabled.
     * @listens module:src/core/Media#media/error
     * @listens module:src/core/Data#data/nomedia
     */
    #disable = () => {

        this.#menu.slider.disabled = true;

    };

    /**
     * This method removes all events, subscriptions and DOM nodes created by this component.
     */
    destroy() {

        this.#menu.destroy();
        this.#player.unsubscribe(this.#subscriptions);
        this.#player = this.#menu = null;

    }

}