Player controls for Coursera

Adds player controls

  1. // ==UserScript==
  2. // @name Player controls for Coursera
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.0
  5. // @description Adds player controls
  6. // @author Avi (https://avi12.com)
  7. // @copyright 2025 Avi (https://avi12.com)
  8. // @license MIT
  9. // @match https://www.coursera.org/learn/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=coursera.org
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. const OBSERVER_OPTIONS = {childList: true, subtree: true};
  18.  
  19. let elVideoContainer;
  20. let elVideo;
  21.  
  22. const observerVideoControls = new MutationObserver(async (_, observer) => {
  23. elVideo = document.querySelector(".item-page-content video");
  24. if (!elVideo?.id) {
  25. return;
  26. }
  27. elVideoContainer = document.querySelector("#persistent_fullscreen");
  28. addIdleListener();
  29. observer.disconnect();
  30. });
  31. observerVideoControls.observe(document, OBSERVER_OPTIONS);
  32.  
  33. new MutationObserver(() => {
  34. observerVideoControls.observe(document, OBSERVER_OPTIONS);
  35. }).observe(document.querySelector("title"), OBSERVER_OPTIONS);
  36.  
  37. document.addEventListener("click", () => {
  38. const isElementVideoDiv = document.activeElement === document.querySelector(".video-js");
  39. if (isElementVideoDiv) {
  40. elVideo?.focus();
  41. }
  42. });
  43.  
  44. addEventListener("focus", () => {
  45. if (!elVideo) {
  46. return;
  47. }
  48.  
  49. if (document.webkitIsFullScreen) {
  50. elVideoContainer.focus();
  51. }
  52. });
  53.  
  54. function clickPlay() {
  55. const elPlayToggle = document.querySelector(".rc-PlayToggle");
  56. elPlayToggle.click();
  57. }
  58.  
  59. document.addEventListener("keydown", e => {
  60. if (e.target.matches("input, textarea")) {
  61. return;
  62. }
  63.  
  64. switch (e.code) {
  65. case "KeyP":
  66. case "KeyN": {
  67. if (!e.shiftKey) {
  68. return;
  69. }
  70.  
  71. const [elButtonPrevious, elButtonNext] = [...document.querySelectorAll(".rc-PreviousAndNextItem a")];
  72. if (e.code === "KeyP") {
  73. elButtonPrevious.click();
  74. return;
  75. }
  76. elButtonNext.click();
  77. return;
  78. }
  79. }
  80.  
  81. if (!elVideo) {
  82. return;
  83. }
  84.  
  85. switch (e.code) {
  86. case "KeyK":
  87. case "Space":
  88. e.preventDefault();
  89. clickPlay();
  90. break;
  91.  
  92. case "KeyJ":
  93. case "KeyL":
  94. case "ArrowLeft":
  95. case "ArrowRight": {
  96. const seekMapping = {
  97. KeyJ: 10,
  98. KeyL: 10,
  99. ArrowLeft: 5,
  100. ArrowRight: 5
  101. };
  102.  
  103. const secondsToSeek = seekMapping[e.code];
  104.  
  105. const isBackward = Boolean(e.code.match(/KeyJ|ArrowLeft/));
  106.  
  107. if (isBackward) {
  108. elVideo.currentTime = Math.max(0, elVideo.currentTime - secondsToSeek);
  109. } else {
  110. elVideo.currentTime = Math.min(elVideo.duration, elVideo.currentTime + secondsToSeek);
  111. }
  112. }
  113. break;
  114.  
  115. case "ArrowUp":
  116. case "ArrowDown": {
  117. e.preventDefault();
  118.  
  119. const volumeChangeRate = 0.05;
  120. if (e.key === "ArrowUp") {
  121. elVideo.volume = Math.min(1, elVideo.volume + volumeChangeRate);
  122. return;
  123. }
  124. elVideo.volume = Math.max(0, elVideo.volume - volumeChangeRate);
  125. return;
  126. }
  127.  
  128. case "KeyM": {
  129. const elMute = document.querySelector(".rc-VolumeMenu button");
  130. elMute.click();
  131. return;
  132. }
  133.  
  134. case "KeyC": {
  135. const elSubtitles = [...document.querySelectorAll(".subtitle-button")];
  136. const elCheckbox = elSubtitles[0].querySelector(".c-subtitles-menu-item-selected-icon");
  137. const isSubtitlesOn =
  138. elCheckbox && getComputedStyle(elCheckbox, "::before").getPropertyValue("content") === "none";
  139. if (isSubtitlesOn) {
  140. elSubtitles[0].click();
  141. return;
  142. }
  143. const elEnglishSubtitle = elSubtitles.find(elSubtitle => elSubtitle.textContent.includes("English"));
  144. elEnglishSubtitle.click();
  145. return;
  146. }
  147.  
  148. case "Comma":
  149. case "Period": {
  150. if (!e.shiftKey) {
  151. return;
  152. }
  153.  
  154. const keyToButtons = {
  155. Comma: "minus",
  156. Period: "plus"
  157. };
  158.  
  159. const elPlaybackButton = document.querySelector(`.playback-rate-change-controls button:has(.cif-${keyToButtons[e.code]})`);
  160. elPlaybackButton.click();
  161. return;
  162. }
  163.  
  164. case "Home":
  165. elVideo.currentTime = 0;
  166. return;
  167.  
  168. case "End":
  169. elVideo.currentTime = elVideo.duration;
  170. return;
  171.  
  172. default:
  173. if (!isNaN(e.key) && !e.ctrlKey && !document.activeElement.matches("input, textarea")) {
  174. elVideo.currentTime = (e.key / 10) * elVideo.duration;
  175. }
  176. }
  177. });
  178.  
  179. function getIsPaused() {
  180. return Boolean(document.querySelector(".rc-PlayToggle .cif-play"));
  181. }
  182.  
  183. function addIdleListener() {
  184. let timeoutMouseMove;
  185.  
  186. const secondsBeforeHidingControls = 1;
  187.  
  188. const onMouseMoveOrSeeked = () => {
  189. clearTimeout(timeoutMouseMove);
  190. timeoutMouseMove = setTimeout(() => {
  191. if (document.webkitIsFullScreen && !getIsPaused()) {
  192. hidePlayerControls();
  193. }
  194. }, secondsBeforeHidingControls * 1000);
  195. };
  196. elVideoContainer.addEventListener("mousemove", onMouseMoveOrSeeked);
  197.  
  198. elVideo.addEventListener("pause", () => {
  199. clearTimeout(timeoutMouseMove);
  200. });
  201.  
  202. elVideo.addEventListener("seeked", onMouseMoveOrSeeked);
  203. }
  204.  
  205. async function hidePlayerControls() {
  206. const elPlayToggle = document.querySelector(".rc-PlayToggle");
  207. elPlayToggle.click();
  208. await elVideo.play();
  209. }
  210. })();