Plex Playback Speed

Add playback speed controls to plex web player with keyboard shortcuts

  1. // ==UserScript==
  2. // @name Plex Playback Speed
  3. // @namespace https://github.com/ZigZagT
  4. // @version 1.3.1
  5. // @description Add playback speed controls to plex web player with keyboard shortcuts
  6. // @author ZigZagT
  7. // @include /^https?://[^/]*plex[^/]*/
  8. // @include /^https?://[^/]*:32400/
  9. // @match https://app.plex.tv/
  10. // @match https://plex.tv/
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16. const console_log = (...args) => {
  17. console.log(`PlexPlaybackSpeed:`, ...args)
  18. }
  19. const cycleSpeeds = [
  20. 0.5, 0.8, 1, 1.2, 1.4, 1.6, 1.8, 2, 2.5, 3, 3, 5, 4, 5, 6, 7, 8, 9, 10, 15, 20
  21. ];
  22. const quickSetSpeeds = {
  23. 1: 1,
  24. 2: 1.5,
  25. 3: 2,
  26. 4: 3,
  27. 5: 4,
  28. 6: 5,
  29. 7: 7,
  30. 8: 8,
  31. 9: 10,
  32. };
  33.  
  34. function prompt(txt) {
  35. const existingPrompt = document.querySelector("#playback-speed-prompt");
  36. if (existingPrompt) {
  37. document.body.removeChild(existingPrompt);
  38. }
  39. const prompt = document.createElement("div");
  40. prompt.id = "playback-speed-prompt";
  41. prompt.innerText = txt;
  42. document.body.appendChild(prompt);
  43. prompt.style = `
  44. position: fixed;
  45. top: 0;
  46. left: 0;
  47. width: 8em;
  48. height: 2em;
  49. background-color: rgba(0, 0, 0, 0.5);
  50. color: white;
  51. font-size: 2em;
  52. text-align: center;
  53. z-index: 99999;
  54. pointer-events: none;
  55. `;
  56. setTimeout(() => {
  57. try {
  58. document.body.removeChild(prompt);
  59. } catch (e) {}
  60. }, 2000);
  61. }
  62.  
  63. function getNextCycleSpeed(direction, currentSpeed) {
  64. let newSpeed = currentSpeed;
  65. for (const speed of cycleSpeeds) {
  66. if (direction === 'slowdown') {
  67. if (speed < currentSpeed) {
  68. newSpeed = speed;
  69. } else {
  70. break;
  71. }
  72. } else if (direction === 'speedup') {
  73. if (speed > currentSpeed) {
  74. newSpeed = speed;
  75. break;
  76. }
  77. } else {
  78. console.error(`invalid change speed direction ${direction}`)
  79. break;
  80. }
  81. }
  82. return newSpeed;
  83. }
  84.  
  85. function keyboardUpdateSpeed(e) {
  86. const videoElem = document.querySelector("video");
  87. if (videoElem == null) {
  88. return;
  89. }
  90. const currentSpeed = videoElem.playbackRate;
  91. let newSpeed = currentSpeed;
  92. console_log({currentSpeed, key: e.key});
  93. if (e.key in quickSetSpeeds) {
  94. newSpeed = quickSetSpeeds[e.key];
  95. } else if (["<", ","].includes(e.key)) {
  96. newSpeed = getNextCycleSpeed('slowdown', currentSpeed);
  97. } else if ([">", "."].includes(e.key)) {
  98. newSpeed = getNextCycleSpeed('speedup', currentSpeed);
  99. } else {
  100. return;
  101. }
  102. console_log('change speed to', newSpeed);
  103. videoElem.playbackRate = newSpeed;
  104. prompt(`Speed: ${newSpeed}x`);
  105. }
  106.  
  107. function btnSpeedUpFn() {
  108. const currentSpeed = document.querySelector("video").playbackRate;
  109. let newSpeed = getNextCycleSpeed('speedup', currentSpeed);
  110. console_log('change speed to', newSpeed);
  111. document.querySelector("video").playbackRate = newSpeed;
  112. prompt(`Speed: ${newSpeed}x`);
  113. }
  114.  
  115. function btnSlowdownFn() {
  116. const currentSpeed = document.querySelector("video").playbackRate;
  117. let newSpeed = getNextCycleSpeed('slowdown', currentSpeed);
  118. console_log('change speed to', newSpeed);
  119. document.querySelector("video").playbackRate = newSpeed;
  120. prompt(`Speed: ${newSpeed}x`);
  121. }
  122.  
  123. function addPlaybackButtonControls() {
  124. const btnStyle = `
  125. align-items: center;
  126. border-radius: 15px;
  127. display: flex;
  128. font-size: 18px;
  129. height: 30px;
  130. justify-content: center;
  131. margin-left: 5px;
  132. text-align: center;
  133. width: 30px;
  134. `;
  135.  
  136. const containers = document.querySelectorAll('[class*="PlayerControls-buttonGroupRight"]');
  137. containers.forEach(container => {
  138. if (container.querySelector('#playback-speed-btn-slowdown')) {
  139. return;
  140. }
  141.  
  142. const btnSlowDown = document.createElement('button');
  143. btnSlowDown.id = 'playback-speed-btn-slowdown';
  144. btnSlowDown.style = btnStyle;
  145. btnSlowDown.innerHTML = '🐢';
  146. btnSlowDown.addEventListener('click', btnSlowdownFn);
  147.  
  148. const btnSpeedUp = document.createElement('button');
  149. btnSpeedUp.id = 'playback-speed-btn-speedup';
  150. btnSpeedUp.style = btnStyle;
  151. btnSpeedUp.innerHTML = '🐇';
  152. btnSpeedUp.addEventListener('click', btnSpeedUpFn);
  153.  
  154. console_log('adding speed controls to', container);
  155. container.prepend(btnSlowDown, btnSpeedUp);
  156. })
  157.  
  158. }
  159.  
  160. function scheduleLoopFrame() {
  161. setTimeout(() => {
  162. requestAnimationFrame(() => {
  163. addPlaybackButtonControls();
  164. scheduleLoopFrame();
  165. });
  166. }, 500);
  167. }
  168.  
  169. if (window.__plex_playback_speed_control_registered__) {
  170. console_log('plex playback speed controls are already registered');
  171. } else {
  172. window.__plex_playback_speed_control_registered__ = true;
  173. console_log('registering plex playback speed controls');
  174. window.addEventListener("keydown", keyboardUpdateSpeed);
  175. scheduleLoopFrame();
  176. }
  177. })();