Khan Academy YouTube Playback Rate Enforcer

Remembers the playback rate you set on Khan Academy's YouTube player and enforces it across lessons

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Khan Academy YouTube Playback Rate Enforcer
// @namespace    https://jeffschofield.com/
// @version      0.4
// @description  Remembers the playback rate you set on Khan Academy's YouTube player and enforces it across lessons
// @author       Jeff Schofield
// @match        https://www.khanacademy.org/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const ENFORCED = Symbol('enforced');
    const GM_KEY = 'jeffschofield.com-playback_rate';
    const YOUTUBE_ORIGIN = /^https?:\/\/[-\.\w]*\.youtube(-nocookie)?\.com.*$/;

    var PLAYBACK_RATE = GM_getValue(GM_KEY, 1);
    function updatePlaybackRate({ data }) {
        if (data === PLAYBACK_RATE) return; // No change
        GM_setValue(GM_KEY, PLAYBACK_RATE = data);

        // console.log(`Debug: Updated playback rate to '${ PLAYBACK_RATE }'`);
    }

    window.addEventListener('message', ({ data, origin }) => { // The YouTube iFrame API uses the `message` event to communicate, so we watch here for any activity in the page
        if (!origin.match(YOUTUBE_ORIGIN)) return; // Ensure this message is from YouTube

        data = JSON.parse(data); // Message data is passed as JSON. Parse it
        let { event, info, id, channel } = data; // Destructure the parsed message data

        if (event === 'onReady') { // Watch for the onReady event coming from any iframe player

            // Note: I briefly explored using the ID and Channel to communicate directly with the player instead of re-scanning every `onReady` event,
            // ultimately the scanning method was faster to implement for the time I have. Revisions are welcome!

            document.querySelectorAll('iframe[id]').forEach($iframe => { // Scan through all iframes with an ID attribute defined on the page
                let player = YT.get($iframe.id); // Try to get a reference to the YT.Player instance attached to this iframe, if any

                if (!player) return; // No player instance on this iframe
                if (player[ENFORCED]) return; // Already enforced playback on this player

                player.setPlaybackRate(PLAYBACK_RATE); // Enforce playback rate!
                player.addEventListener('onPlaybackRateChange', updatePlaybackRate); // Listen for playback rate changes on this player to update the remembered value
                player[ENFORCED] = true; // Brand this player instance with our symbol to indicate it has been enforced

                // console.log(`Debug: Enforcing playback rate on '${ $iframe.id }'`);
            });
        }
    });
})();