Skip to content

Instantly share code, notes, and snippets.

@ZigZagT
Last active July 6, 2024 01:03
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

How is this different from native Plex clients?

This script was created way before plex announced support for playback speed controls on May 15 2024. Besides, this script supports a few neat features that may make your life a little bit easier. Keep reading :)

Feature comparison with native Plex clients support

This Script Native Plex
More speed options to choose from
(cycle through with ,.<> keys: 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
hot key with number keys: 1, 1.5, 2, 3, 4, 5, 7, 8, 10)

(0.5x, 0.75x, Normal, 1.25x, 1.5x, 1.75x, and 2x)
Keyboard shortcuts
(cycle through with ,.<> keys or hot key with number keys, on top of mouse clicking buttons)

(mouse clicking only)
Does not require plex-pass
Support browser clients (incl. iOS devices)
Support non-browser clients

How to use

There're multiple ways to alter the playback speeds:

  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 > or , or . keys on the keyboard to decrease / increase speeds. This control cycles through the following speeds:

    • 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
  2. Use number keys (1-9) to quickly set a speed. This control maps the numbers with speeds as follows:

number key mapped to speed
1 1
2 1.5
3 2
4 3
5 4
6 5
7 7
8 8
9 10

How to install

Automated Install and Update in Plex Server

This is the best option if you load the plex web client off your plex server from many different devices & browser profiles.

The following steps assumes the plex server is deployed with linuxserver/plex docker image. Installation for other deployment options should be similar.

Install and update may be automated as follows:

  1. Create a bash script on the server host. For docker deployment, this script will be mounted into plex container as a startup script.
# 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.
chmod a+x inject_Plex_Playback_Speed_controls.sh
  1. 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

This method keeps the scripts up-to-date whenever the plex server restarts.

Manual Install in Plex Server

The automated script mentioned above essentially performs the following tasks:

  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, and remove .user part from the file extension. Otherwise some browser user script extensions may mistakenly hijack the script request.
  4. Edit index.html file, add a script tag that points to the 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 as userscript 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 as userscript 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.

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