Player controls for Coursera

Adds player controls

// ==UserScript==
// @name         Player controls for Coursera
// @namespace    http://tampermonkey.net/
// @version      0.0
// @description  Adds player controls
// @author       Avi (https://avi12.com)
// @copyright    2025 Avi (https://avi12.com)
// @license      MIT
// @match        https://www.coursera.org/learn/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=coursera.org
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const OBSERVER_OPTIONS = {childList: true, subtree: true};

  let elVideoContainer;
  let elVideo;

  const observerVideoControls = new MutationObserver(async (_, observer) => {
    elVideo = document.querySelector(".item-page-content video");
    if (!elVideo?.id) {
      return;
    }
    elVideoContainer = document.querySelector("#persistent_fullscreen");
    addIdleListener();
    observer.disconnect();
  });
  observerVideoControls.observe(document, OBSERVER_OPTIONS);

  new MutationObserver(() => {
    observerVideoControls.observe(document, OBSERVER_OPTIONS);
  }).observe(document.querySelector("title"), OBSERVER_OPTIONS);

  document.addEventListener("click", () => {
    const isElementVideoDiv = document.activeElement === document.querySelector(".video-js");
    if (isElementVideoDiv) {
      elVideo?.focus();
    }
  });

  addEventListener("focus", () => {
    if (!elVideo) {
      return;
    }

    if (document.webkitIsFullScreen) {
      elVideoContainer.focus();
    }
  });

  function clickPlay() {
    const elPlayToggle = document.querySelector(".rc-PlayToggle");
    elPlayToggle.click();
  }

  document.addEventListener("keydown", e => {
    if (e.target.matches("input, textarea")) {
      return;
    }

    switch (e.code) {
      case "KeyP":
      case "KeyN": {
        if (!e.shiftKey) {
          return;
        }

        const [elButtonPrevious, elButtonNext] = [...document.querySelectorAll(".rc-PreviousAndNextItem a")];
        if (e.code === "KeyP") {
          elButtonPrevious.click();
          return;
        }
        elButtonNext.click();
        return;
      }
    }

    if (!elVideo) {
      return;
    }

    switch (e.code) {
      case "KeyK":
      case "Space":
        e.preventDefault();
        clickPlay();
        break;

      case "KeyJ":
      case "KeyL":
      case "ArrowLeft":
      case "ArrowRight": {
        const seekMapping = {
          KeyJ: 10,
          KeyL: 10,
          ArrowLeft: 5,
          ArrowRight: 5
        };

        const secondsToSeek = seekMapping[e.code];

        const isBackward = Boolean(e.code.match(/KeyJ|ArrowLeft/));

        if (isBackward) {
          elVideo.currentTime = Math.max(0, elVideo.currentTime - secondsToSeek);
        } else {
          elVideo.currentTime = Math.min(elVideo.duration, elVideo.currentTime + secondsToSeek);
        }
      }
        break;

      case "ArrowUp":
      case "ArrowDown": {
        e.preventDefault();

        const volumeChangeRate = 0.05;
        if (e.key === "ArrowUp") {
          elVideo.volume = Math.min(1, elVideo.volume + volumeChangeRate);
          return;
        }
        elVideo.volume = Math.max(0, elVideo.volume - volumeChangeRate);
        return;
      }

      case "KeyM": {
        const elMute = document.querySelector(".rc-VolumeMenu button");
        elMute.click();
        return;
      }

      case "KeyC": {
        const elSubtitles = [...document.querySelectorAll(".subtitle-button")];
        const elCheckbox = elSubtitles[0].querySelector(".c-subtitles-menu-item-selected-icon");
        const isSubtitlesOn =
          elCheckbox && getComputedStyle(elCheckbox, "::before").getPropertyValue("content") === "none";
        if (isSubtitlesOn) {
          elSubtitles[0].click();
          return;
        }
        const elEnglishSubtitle = elSubtitles.find(elSubtitle => elSubtitle.textContent.includes("English"));
        elEnglishSubtitle.click();
        return;
      }

      case "Comma":
      case "Period": {
        if (!e.shiftKey) {
          return;
        }

        const keyToButtons = {
          Comma: "minus",
          Period: "plus"
        };

        const elPlaybackButton = document.querySelector(`.playback-rate-change-controls button:has(.cif-${keyToButtons[e.code]})`);
        elPlaybackButton.click();
        return;
      }

      case "Home":
        elVideo.currentTime = 0;
        return;

      case "End":
        elVideo.currentTime = elVideo.duration;
        return;

      default:
        if (!isNaN(e.key) && !e.ctrlKey && !document.activeElement.matches("input, textarea")) {
          elVideo.currentTime = (e.key / 10) * elVideo.duration;
        }
    }
  });

  function getIsPaused() {
    return Boolean(document.querySelector(".rc-PlayToggle .cif-play"));
  }

  function addIdleListener() {
    let timeoutMouseMove;

    const secondsBeforeHidingControls = 1;

    const onMouseMoveOrSeeked = () => {
      clearTimeout(timeoutMouseMove);
      timeoutMouseMove = setTimeout(() => {
        if (document.webkitIsFullScreen && !getIsPaused()) {
          hidePlayerControls();
        }
      }, secondsBeforeHidingControls * 1000);
    };
    elVideoContainer.addEventListener("mousemove", onMouseMoveOrSeeked);

    elVideo.addEventListener("pause", () => {
      clearTimeout(timeoutMouseMove);
    });

    elVideo.addEventListener("seeked", onMouseMoveOrSeeked);
  }

  async function hidePlayerControls() {
    const elPlayToggle = document.querySelector(".rc-PlayToggle");
    elPlayToggle.click();
    await elVideo.play();
  }
})();