Skip to content

Instantly share code, notes, and snippets.

@ZigZagT
Last active April 26, 2024 06:15
Show Gist options
  • Save ZigZagT/b992bda82b5f7a2c9d214110273d3f3c to your computer and use it in GitHub Desktop.
Save ZigZagT/b992bda82b5f7a2c9d214110273d3f3c to your computer and use it in GitHub Desktop.
Plex Web Player Playback Speed Control and Keyboard Shortcuts

Plex Web Player Playback Speed Control and Keyboard Shortcuts

Github

Greasy Fork

Automated Install and Update in Plex Server (with linuxserver/plex docker deployment)

Install and update may be automated as follows:

  1. Create a script on the docker host:
# inject_Plex_Playback_Speed_controls.sh
cd /usr/lib/plexmediaserver/Resources/Plug-ins-*/WebClient.bundle/Contents/Resources
wget -O "js/PlexPlaybackSpeed.js" "https://gist.githubusercontent.com/ZigZagT/b992bda82b5f7a2c9d214110273d3f3c/raw/Plex%2520Playback%2520Speed.user.js"
sed -i 's#</head>#<script src="/web/js/PlexPlaybackSpeed.js"></script></head>#' index.html
  1. Add execution permission to inject_Plex_Playback_Speed_controls.sh.
  2. Mount inject_Plex_Playback_Speed_controls.sh script into container as start up script:
# docker-compose.yaml
services:
  plex:
    image: linuxserver/plex
    tmpfs:
      - /tmp
    volumes:
      # ... other volumes ...
      - /path/to/inject_Plex_Playback_Speed_controls.sh:/etc/cont-init.d/99-inject_Plex_Playback_Speed_controls.sh
    devices:
      - /dev/dri:/dev/dri
    restart: always

The latest script will be installed whenever the container restarts.

Manual Install in Plex Server

  1. Locate the WebClient directory in your Plex Server installation. This path varies depends on the server setup. Taking the plex server docker image provided by linuxserver.io as example, with image linuxserver/plex:1.40.0 the WebClient bundle is located at /usr/lib/plexmediaserver/Resources/Plug-ins-c29d4c0c8/WebClient.bundle/Contents/Resources.
  2. Save the Plex Playback Speed.user.js file into the js folder.
  3. Rename the downloaded file, remove .user part from the file extension. Otherwise user script extensions in users browser may mistakenly hijack the script request.
  4. Edit index.html file, add a script tag that points to the downloaded script file. The path shuold be prefixed with /web. For example, if script file is stored at js/PlexPlaybackSpeed.js, the <script> tag should be <script src="/web/js/PlexPlaybackSpeed.js"></script>

The script will not update automatically with this installation.

Install in Desktop Chrome / Firefox

  1. Install Tampermonkey or any equivalent user script extension in your browser;
  2. Open this link in your browser. The user script extension should automatically prompt for installation.
  3. Future script updates may be checked and installed automatically by user script extension.

Install in Safari (macOS Desktop or iOS/iPadOS Safari)

  1. Install the Userscripts Safari extension from App Store.
  2. Enable the extension following its instruction. Make sure you have the Save Location setting configured.
  3. Open this link in Safari, and save the file to the Save Location of your choice.
  4. Future script updates may be checked and installed automatically by the Userscripts app.

Usage

  1. Use the turtle and rabbit icons in the control strip to slowdown / speedup

Screen Shot 2022-09-19 at 9 24 10 PM

  1. Use < or > keys on the keyboard to decrease / increase speeds.

  2. Use number keys (1-9) to quickly set a preset speeds.

Troubleshoot

Web player is laggy, sometime stuck

Try disable the Direct Play option and leave Direct Stream enabled in the Plex Web - Debug settings.

image

// ==UserScript==
// @name Plex Playback Speed
// @namespace https://github.com/ZigZagT
// @version 1.3.1
// @downloadURL https://gist.githubusercontent.com/ZigZagT/b992bda82b5f7a2c9d214110273d3f3c/raw/Plex%2520Playback%2520Speed.user.js
// @updateURL https://gist.githubusercontent.com/ZigZagT/b992bda82b5f7a2c9d214110273d3f3c/raw/Plex%2520Playback%2520Speed.user.js
// @description Add playback speed controls to plex web player with keyboard shortcuts
// @author ZigZagT
// @include /^https?://[^/]*plex[^/]*/
// @include /^https?://[^/]*:32400/
// @match https://app.plex.tv/
// @match https://plex.tv/
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const console_log = (...args) => {
console.log(`PlexPlaybackSpeed:`, ...args)
}
const cycleSpeeds = [
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
];
const quickSetSpeeds = {
1: 1,
2: 1.5,
3: 2,
4: 3,
5: 4,
6: 5,
7: 7,
8: 8,
9: 10,
};
function prompt(txt) {
const existingPrompt = document.querySelector("#playback-speed-prompt");
if (existingPrompt) {
document.body.removeChild(existingPrompt);
}
const prompt = document.createElement("div");
prompt.id = "playback-speed-prompt";
prompt.innerText = txt;
document.body.appendChild(prompt);
prompt.style = `
position: fixed;
top: 0;
left: 0;
width: 8em;
height: 2em;
background-color: rgba(0, 0, 0, 0.5);
color: white;
font-size: 2em;
text-align: center;
z-index: 99999;
pointer-events: none;
`;
setTimeout(() => {
try {
document.body.removeChild(prompt);
} catch (e) {}
}, 2000);
}
function getNextCycleSpeed(direction, currentSpeed) {
let newSpeed = currentSpeed;
for (const speed of cycleSpeeds) {
if (direction === 'slowdown') {
if (speed < currentSpeed) {
newSpeed = speed;
} else {
break;
}
} else if (direction === 'speedup') {
if (speed > currentSpeed) {
newSpeed = speed;
break;
}
} else {
console.error(`invalid change speed direction ${direction}`)
break;
}
}
return newSpeed;
}
function keyboardUpdateSpeed(e) {
const videoElem = document.querySelector("video");
if (videoElem == null) {
return;
}
const currentSpeed = videoElem.playbackRate;
let newSpeed = currentSpeed;
console_log({currentSpeed, key: e.key});
if (e.key in quickSetSpeeds) {
newSpeed = quickSetSpeeds[e.key];
} else if (["<", ","].includes(e.key)) {
newSpeed = getNextCycleSpeed('slowdown', currentSpeed);
} else if ([">", "."].includes(e.key)) {
newSpeed = getNextCycleSpeed('speedup', currentSpeed);
} else {
return;
}
console_log('change speed to', newSpeed);
videoElem.playbackRate = newSpeed;
prompt(`Speed: ${newSpeed}x`);
}
function btnSpeedUpFn() {
const currentSpeed = document.querySelector("video").playbackRate;
let newSpeed = getNextCycleSpeed('speedup', currentSpeed);
console_log('change speed to', newSpeed);
document.querySelector("video").playbackRate = newSpeed;
prompt(`Speed: ${newSpeed}x`);
}
function btnSlowdownFn() {
const currentSpeed = document.querySelector("video").playbackRate;
let newSpeed = getNextCycleSpeed('slowdown', currentSpeed);
console_log('change speed to', newSpeed);
document.querySelector("video").playbackRate = newSpeed;
prompt(`Speed: ${newSpeed}x`);
}
function addPlaybackButtonControls() {
const btnStyle = `
align-items: center;
border-radius: 15px;
display: flex;
font-size: 18px;
height: 30px;
justify-content: center;
margin-left: 5px;
text-align: center;
width: 30px;
`;
const containers = document.querySelectorAll('[class*="PlayerControls-buttonGroupRight"]');
containers.forEach(container => {
if (container.querySelector('#playback-speed-btn-slowdown')) {
return;
}
const btnSlowDown = document.createElement('button');
btnSlowDown.id = 'playback-speed-btn-slowdown';
btnSlowDown.style = btnStyle;
btnSlowDown.innerHTML = '🐢';
btnSlowDown.addEventListener('click', btnSlowdownFn);
const btnSpeedUp = document.createElement('button');
btnSpeedUp.id = 'playback-speed-btn-speedup';
btnSpeedUp.style = btnStyle;
btnSpeedUp.innerHTML = '🐇';
btnSpeedUp.addEventListener('click', btnSpeedUpFn);
console_log('adding speed controls to', container);
container.prepend(btnSlowDown, btnSpeedUp);
})
}
function scheduleLoopFrame() {
setTimeout(() => {
requestAnimationFrame(() => {
addPlaybackButtonControls();
scheduleLoopFrame();
});
}, 500);
}
if (window.__plex_playback_speed_control_registered__) {
console_log('plex playback speed controls are already registered');
} else {
window.__plex_playback_speed_control_registered__ = true;
console_log('registering plex playback speed controls');
window.addEventListener("keydown", keyboardUpdateSpeed);
scheduleLoopFrame();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment