Video Element Rate Controller Re-dux

Add keyboard shortcuts that will increase/decrease the playback rate for video elements.

// ==UserScript==
// @name         Video Element Rate Controller Re-dux
// @namespace    https://github.com/mirnhoj/video-element-playbackrate-setter
// @version      2.5
// @description  Add keyboard shortcuts that will increase/decrease the playback rate for video elements.
// @include      http*://*.youtube.com/*
// @include      http*://*.gfycat.com/*
// @include      http*://*.vimeo.com/*
// @include      https://www.facebook.com/video.php*
// @include      https://www.facebook.com/*/videos/*
// @include      https://www.kickstarter.com/*
// @grant        GM_registerMenuCommand

// ==/UserScript==
/* jshint esversion: 6 */
//
// if you want to extend the functionality of this script to other sites
// besides youtube, add additional @include keys to the metadata block or to
// the "User includes" or "User matches" sections in the Settings.
//
// if you want to change the default playback rate from 1x, change the line
// "var currentPlaybackRate = 1;" to equal something other than 1, like 1.3 to
// have all videos start playing at an increased speed, or 0.7 to have all
// videos start playing at a decreased speed.
//
// if you want change the granularity of the playback rate adjustment, change
// the line "var speedStep = 0.1;" to equal something other than 0.1, like 0.01
// for more granular adjustments, or 0.25 for less granular adjustments.

// These values are the default values for initialization
const defaults = {
    speedStep: 0.1,
    displayTimeMilliSec: 1500
};

// script variables
let timeoutID = null;
let showValuesOnVideo = true; // true to show new value on the video
let keyIncreaseSpeed = ']';
let keyReduceSpeed = '[';
let keyResetSpeed = '\\';
const infoboxId = 'playbackrate-indicator';

function getVal(variable) {
    let value;
    let storage = (localStorage || (sessionStorage ||
        (window.content.localStorage ? window.content.localStorage : null)));
    try {
        switch (variable) {
            case 'speedStep':
                value = storage.getItem('VERCRspeedStep');
                return Number(value);
            case 'displayTimeMilliSec':
                value = storage.getItem('VERCRdisplayTimeMS');
                return Number(value);
            case 'keyIncreaseSpeed':
                return value;
            case 'keyReduceSpeed':
                return value;
            case 'keyResetSpeed':
                return value;
            default:
                return null;
        }
    } catch (e) {
        if (e.name === 'NS_ERROR_FILE_CORRUPTED') {
            storage = sessionStorage || null; // set the new storage if fails
            storage.setItem('VERCRspeedStep', defaults.speedStep);
            storage.setItem('VERCRdisplayTimeMS', defaults.displayTimeMilliSec);
        }
    }
}

function setVal(variable, value) {
    let storage = (localStorage || (sessionStorage ||
        (window.content.localStorage ? window.content.localStorage : null)));
    try {
        switch (variable) {
        case 'speedStep':
            storage.setItem('VERCRspeedStep', Number(value));
            return value;
        case 'displayTimeMilliSec':
            storage.setItem('VERCRdisplayTimeMS', Number(value));
            return value;
        default:
            return null;
        }
    } catch (e) {
        if (e.name === 'NS_ERROR_FILE_CORRUPTED') {
            storage = sessionStorage || null; // set the new storage if fails
            storage.setItem('VERCRspeedStep', defaults.speedStep);
            storage.setItem('VERCRdisplayTimeMS', defaults.displayTimeMilliSec);
        }
    }
}

function GMsetup() {
    if (GM_registerMenuCommand) {
        GM_registerMenuCommand('Set adjustment rate', () => {
            const curEntry = getVal('speedStep');
            let speedStep = prompt('New adjustment rate:\n(e.g., 0.1 = 10% faster)', curEntry);
            if (speedStep !== null) {
                while (isNaN(speedStep)) {
                    speedStep = prompt('Please input a valid number!\n\nNew adjustment rate:\n(e.g., 0.1 = 10% faster)', curEntry);
                }
                setVal('speedStep', speedStep);
            }
        });
        // GM_registerMenuCommand('Video Rate Re-dux: Set keyboard shortcuts', () => {
        //     const curEntry = `${getVal('keyIncreaseSpeed')}, ${getVal('keyReduceSpeed')}, ${getVal('keyResetSpeed')}`;
        //     // W.I.P.
        // });
        GM_registerMenuCommand('Set display timeout', () => {
            const curEntry = getVal('displayTimeMilliSec');
            let displayTimeMilliSec = prompt('New display timeout length (in milliseconds):', curEntry);
            if (displayTimeMilliSec !== null) {
                while (isNaN(displayTimeMilliSec)) {
                    displayTimeMilliSec = prompt('Please input a valid number!\n\nNew display timeout length (in milliseconds):', curEntry);
                }
                setVal('displayTimeMilliSec', displayTimeMilliSec);
            }
        });
    }
}

function init() {
    let VERCRspeedStep = localStorage.getItem('VERCRspeedStep');
    let VERCRdisplayTimeMS = localStorage.getItem('VERCRdisplayTimeMS');
    if (!VERCRspeedStep) {
        VERCRspeedStep = defaults.speedStep;
        localStorage.setItem('VERCRspeedStep', Number(VERCRspeedStep));
    }
    if (!VERCRdisplayTimeMS) {
        VERCRdisplayTimeMS = defaults.displayTimeMilliSec;
        localStorage.setItem('VERCRdisplayTimeMS', Number(VERCRdisplayTimeMS));
    }
}

function getInfobox(videoElement) {
    if (!videoElement) return;
    let infoboxEl = document.getElementById(infoboxId);
    if (!infoboxEl) {
        // create and add infobox to dom if it doesn't already exist.
        infoboxEl = document.createElement('h1');
        infoboxEl.setAttribute('id', infoboxId);
        infoboxEl.style.position = 'absolute';
        infoboxEl.style.top = '10%';
        infoboxEl.style.right = '10%';
        infoboxEl.style.color = 'rgba(255, 0, 0, 1)';
        infoboxEl.style.zIndex = '99999'; // ensures that it shows above other elements.
        infoboxEl.style.visibility = 'hidden';
        infoboxEl.style.marginTop = '3%';
    }
    if (videoElement.parentElement !== infoboxEl.parentElement)
        videoElement.parentElement.appendChild(infoboxEl);
    return infoboxEl;
}

// update rate indicator.
function showInfobox(videoElement, rate) {
    const infobox = getInfobox(videoElement);
    infobox.innerHTML = `${rate}x`;
    // show infobox
    infobox.style.visibility = 'visible';
    // clear out any previous timers and have the infobox hide after the pre-set time period
    window.clearTimeout(timeoutID);
    timeoutID = window.setTimeout(() => {
        infobox.style.visibility = 'hidden';
    }, getVal('displayTimeMilliSec'));
}

function setPlaybackRate(videoElement, rate, shouldShowInfobox) {
    // grab the video elements and set their playback rate.
    if (!videoElement) return;
    videoElement.playbackRate = rate;
    if (shouldShowInfobox) showInfobox(videoElement, rate);
}

// mimic vlc keyboard shortcuts
function addKeyListener() {
    window.addEventListener('keydown', (event) => {
        let videoElement, modification = 0;
        switch (event.key) {
            case keyReduceSpeed:
                modification = -1;
                break;
            case keyIncreaseSpeed:
                modification = 1;
                break;
            case keyResetSpeed:
                videoElement = document.getElementsByTagName('video')[0];
                setPlaybackRate(videoElement, 1, showValuesOnVideo);
                return;
            default:
                return;
        }
        videoElement = document.getElementsByTagName('video')[0];
        if (!videoElement) return;
        const currentPlaybackRate = videoElement.playbackRate;
        const speedStep = getVal('speedStep');
        const newPlaybackRate = parseFloat((currentPlaybackRate + (speedStep * modification)).toFixed(3));
        // console.log(`Raising "playbackRate" from ${currentPlaybackRate} to ${newPlaybackRate}`);
        setPlaybackRate(videoElement, newPlaybackRate, showValuesOnVideo);
    });
}

function onReady() { addKeyListener(); }

function main() {
    init();
    GMsetup();
    if (document.readyState !== 'loading') {
        onReady(); // Or setTimeout(onReady, 0); if you want it consistently async
    } else {
        document.addEventListener('DOMContentLoaded', onReady);
    }
}

main();