8 Commits

Author SHA1 Message Date
Josh Patra
73827b5ee0 Merge branch 'firefox-port' of github.com:SoPat712/videospeed into firefox-port 2025-07-02 14:36:43 -04:00
Josh Patra
b07e7cb394 youtube embeds fixed and shortcuts 2025-07-02 14:36:39 -04:00
Josh Patra
c3166cf347 Update README.md 2025-06-22 01:30:24 -04:00
Josh Patra
77a25c4f1e Merge branch 'firefox-port' of github.com:SoPat712/videospeed into firefox-port 2025-05-22 16:22:19 -04:00
Josh Patra
3fee61d2b6 fixed the reset to 1.0 on pause 2025-05-22 16:22:15 -04:00
Josh Patra
b7684aad09 Update README.md 2025-05-20 03:48:10 -04:00
Josh Patra
43dc8b773b fix appear after hiding 2025-05-19 13:15:20 -04:00
Josh Patra
2d8a4fc25f add nudge to settings 2025-05-19 12:54:35 -04:00
5 changed files with 652 additions and 840 deletions

View File

@@ -1,3 +1,7 @@
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/)
# The science of accelerated playback
**TL;DR: faster playback translates to better engagement and retention.**
@@ -74,10 +78,10 @@ You can try manually disabling Flash from the browser.
[`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) repository
is a port of [`igrigorik`](https://github.com/igrigorik)'s videospeed Chrome
add-on for Firefox. This fork modifies the Chrome add-on code so that it works
in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-us/firefox/addon/videospeed/)
in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/)
whereas the [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed)
repository contains the code behind the [Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk).
### License
(MIT License) - Copyright (c) 2014 Ilya Grigorik
(MIT License) - Copyright (c) 2025 Josh Patra

1136
inject.js

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,13 @@
{
"name": "Video Speed Controller",
"short_name": "videospeed",
"version": "0.6.3.3",
"version": "1.1.3",
"manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts",
"homepage_url": "https://github.com/codebicycle/videospeed",
"homepage_url": "https://github.com/SoPat712/videospeed",
"browser_specific_settings": {
"gecko": {
"id": "{7be2ba16-0f1e-4d93-9ebc-5164397477a9}"
"id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}"
}
},
"icons": {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<title>Video Speed Controller: Options</title>
@@ -148,8 +148,13 @@
<input id="rememberSpeed" type="checkbox" />
</div>
<div class="row">
<label for="forceLastSavedSpeed">Force last saved speed<br />
<em>Useful for video players that override the speeds set by VideoSpeed</em></label>
<label for="forceLastSavedSpeed"
>Force last saved speed<br />
<em
>Useful for video players that override the speeds set by
VideoSpeed</em
></label
>
<input id="forceLastSavedSpeed" type="checkbox" />
</div>
<div class="row">
@@ -175,6 +180,47 @@
</div>
</section>
<section id="nudgeSettings">
<h3>Subtitle Nudge Settings (Experimental - YouTube Only)</h3>
<div class="row">
<label for="enableSubtitleNudge"
>Enable Subtitle Nudge <br /><em
>Periodically 'nudges' video speed by a tiny amount to help keep
subtitles in sync on some sites (e.g. YouTube).</em
>
</label>
<input id="enableSubtitleNudge" type="checkbox" />
</div>
<div class="row">
<label for="subtitleNudgeInterval"
>Nudge Interval (milliseconds) <br /><em
>How often to nudge (e.g., 25-1000). Smaller values are more
frequent. Default: 25.</em
>
</label>
<input
id="subtitleNudgeInterval"
type="text"
value=""
placeholder="25"
/>
</div>
<div class="row">
<label for="subtitleNudgeAmount"
>Nudge Amount (decimal) <br /><em
>How much to change speed by (e.g., 0.001). Very small values
recommended. Default: 0.001.</em
>
</label>
<input
id="subtitleNudgeAmount"
type="text"
value=""
placeholder="0.001"
/>
</div>
</section>
<button id="save">Save</button>
<button id="restore">Restore Defaults</button>
<button id="experimental">Show Experimental Features</button>
@@ -186,12 +232,12 @@
<h4>Extension controls not appearing?</h4>
<p>
This extension is only compatible with HTML5 audio and video. If you don't
see the controls showing up, chances are you are viewing a Flash content.
If you want to confirm, try right-clicking on the content and inspect the
menu: if it mentions flash, then that's the issue. That said, <b>most sites
will fallback to HTML5</b> if they detect that Flash is not available. You
can try manually disabling Flash from the browser.
This extension is only compatible with HTML5 audio and video. If you
don't see the controls showing up, chances are you are viewing a Flash
content. If you want to confirm, try right-clicking on the content and
inspect the menu: if it mentions flash, then that's the issue. That
said, <b>most sites will fallback to HTML5</b> if they detect that Flash
is not available. You can try manually disabling Flash from the browser.
</p>
</div>
</body>

View File

@@ -22,13 +22,17 @@ var tcDefaults = {
twitter.com
imgur.com
teams.microsoft.com
`.replace(regStrip, "")
`.replace(regStrip, ""),
// ADDED: Nudge defaults
enableSubtitleNudge: true,
subtitleNudgeInterval: 25,
subtitleNudgeAmount: 0.001
};
var keyBindings = [];
var keyBindings = []; // This is populated during save/restore
var keyCodeAliases = {
0: "null",
/* ... same as your original ... */ 0: "null",
null: "null",
undefined: "null",
32: "Space",
@@ -74,85 +78,55 @@ var keyCodeAliases = {
220: "\\",
221: "]",
222: "'",
59: ";",
61: "+",
173: "-",
59: ";",
61: "+",
173: "-"
};
function recordKeyPress(e) {
/* ... same as your original ... */
if (
(e.keyCode >= 48 && e.keyCode <= 57) || // Numbers 0-9
(e.keyCode >= 65 && e.keyCode <= 90) || // Letters A-Z
keyCodeAliases[e.keyCode] // Other character keys
(e.keyCode >= 48 && e.keyCode <= 57) ||
(e.keyCode >= 65 && e.keyCode <= 90) ||
keyCodeAliases[e.keyCode]
) {
e.target.value =
keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode);
e.target.keyCode = e.keyCode;
e.preventDefault();
e.stopPropagation();
} else if (e.keyCode === 8) {
// Clear input when backspace pressed
e.target.value = "";
} else if (e.keyCode === 27) {
// When esc clicked, clear input
e.target.value = "null";
e.target.keyCode = null;
}
}
function inputFilterNumbersOnly(e) {
/* ... same as your original ... */
var char = String.fromCharCode(e.keyCode);
if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) {
e.preventDefault();
e.stopPropagation();
}
}
function inputFocus(e) {
e.target.value = "";
/* ... same as your original ... */ e.target.value = "";
}
function inputBlur(e) {
e.target.value =
/* ... same as your original ... */ e.target.value =
keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode);
}
function updateShortcutInputText(inputId, keyCode) {
document.getElementById(inputId).value =
keyCodeAliases[keyCode] || String.fromCharCode(keyCode);
document.getElementById(inputId).keyCode = keyCode;
}
// function updateShortcutInputText(inputId, keyCode) { /* ... same as your original ... */ } // Not directly used in provided options.js logic flow
function updateCustomShortcutInputText(inputItem, keyCode) {
inputItem.value = keyCodeAliases[keyCode] || String.fromCharCode(keyCode);
/* ... same as your original ... */ inputItem.value =
keyCodeAliases[keyCode] || String.fromCharCode(keyCode);
inputItem.keyCode = keyCode;
}
// List of custom actions for which customValue should be disabled
var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"];
var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"]; // Original
function add_shortcut() {
var html = `<select class="customDo">
<option value="slower">Decrease speed</option>
<option value="faster">Increase speed</option>
<option value="rewind">Rewind</option>
<option value="advance">Advance</option>
<option value="reset">Reset speed</option>
<option value="fast">Preferred speed</option>
<option value="muted">Mute</option>
<option value="pause">Pause</option>
<option value="mark">Set marker</option>
<option value="jump">Jump to marker</option>
<option value="display">Show/hide controller</option>
</select>
<input class="customKey" type="text" placeholder="press a key"/>
<input class="customValue" type="text" placeholder="value (0.10)"/>
<select class="customForce">
<option value="false">Do not disable website key bindings</option>
<option value="true">Disable website key bindings</option>
</select>
<button class="removeParent">X</button>`;
/* ... same as your original ... */
var html = `<select class="customDo"><option value="slower">Decrease speed</option><option value="faster">Increase speed</option><option value="rewind">Rewind</option><option value="advance">Advance</option><option value="reset">Reset speed</option><option value="fast">Preferred speed</option><option value="muted">Mute</option><option value="pause">Pause</option><option value="mark">Set marker</option><option value="jump">Jump to marker</option><option value="display">Show/hide controller</option></select><input class="customKey" type="text" placeholder="press a key"/><input class="customValue" type="text" placeholder="value (0.10)"/><select class="customForce"><option value="false">Do not disable website key bindings</option><option value="true">Disable website key bindings</option></select><button class="removeParent">X</button>`;
var div = document.createElement("div");
div.setAttribute("class", "row customs");
div.innerHTML = html;
@@ -162,14 +136,13 @@ function add_shortcut() {
customs_element.children[customs_element.childElementCount - 1]
);
}
function createKeyBindings(item) {
/* ... same as your original ... */
const action = item.querySelector(".customDo").value;
const key = item.querySelector(".customKey").keyCode;
const value = Number(item.querySelector(".customValue").value);
const force = item.querySelector(".customForce").value;
const predefined = !!item.id; //item.id ? true : false;
const predefined = !!item.id;
keyBindings.push({
action: action,
key: key,
@@ -178,9 +151,8 @@ function createKeyBindings(item) {
predefined: predefined
});
}
// Validates settings before saving
function validate() {
/* ... same as your original ... */
var valid = true;
var status = document.getElementById("status");
document
@@ -190,7 +162,7 @@ function validate() {
match = match.replace(regStrip, "");
if (match.startsWith("/")) {
try {
var regexp = new RegExp(match);
new RegExp(match);
} catch (err) {
status.textContent =
"Error: Invalid blacklist regex: " + match + ". Unable to save";
@@ -202,24 +174,45 @@ function validate() {
return valid;
}
// Saves options to chrome.storage
// MODIFIED: save_options to include nudge settings
function save_options() {
if (validate() === false) {
return;
}
keyBindings = [];
if (validate() === false) return;
keyBindings = []; // Reset global keyBindings before populating from DOM
Array.from(document.querySelectorAll(".customs")).forEach((item) =>
createKeyBindings(item)
); // Remove added shortcuts
);
var rememberSpeed = document.getElementById("rememberSpeed").checked;
var forceLastSavedSpeed = document.getElementById("forceLastSavedSpeed").checked;
var audioBoolean = document.getElementById("audioBoolean").checked;
var enabled = document.getElementById("enabled").checked;
var startHidden = document.getElementById("startHidden").checked;
var controllerOpacity = document.getElementById("controllerOpacity").value;
var blacklist = document.getElementById("blacklist").value;
var s = {}; // Object to hold all settings to be saved
s.rememberSpeed = document.getElementById("rememberSpeed").checked;
s.forceLastSavedSpeed = document.getElementById(
"forceLastSavedSpeed"
).checked;
s.audioBoolean = document.getElementById("audioBoolean").checked;
s.enabled = document.getElementById("enabled").checked;
s.startHidden = document.getElementById("startHidden").checked;
s.controllerOpacity = document.getElementById("controllerOpacity").value;
s.blacklist = document
.getElementById("blacklist")
.value.replace(regStrip, "");
s.keyBindings = keyBindings; // Use the populated global keyBindings
// ADDED: Save nudge settings
s.enableSubtitleNudge = document.getElementById(
"enableSubtitleNudge"
).checked;
s.subtitleNudgeInterval =
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
tcDefaults.subtitleNudgeInterval;
s.subtitleNudgeAmount =
parseFloat(document.getElementById("subtitleNudgeAmount").value) ||
tcDefaults.subtitleNudgeAmount;
// Basic validation for nudge interval and amount
if (s.subtitleNudgeInterval < 10) s.subtitleNudgeInterval = 10; // Min 10ms
if (s.subtitleNudgeAmount <= 0 || s.subtitleNudgeAmount > 0.1)
s.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount;
// Remove old flat settings (original logic)
chrome.storage.sync.remove([
"resetSpeed",
"speedStep",
@@ -233,33 +226,22 @@ function save_options() {
"advanceKeyCode",
"fastKeyCode"
]);
chrome.storage.sync.set(
{
rememberSpeed: rememberSpeed,
forceLastSavedSpeed: forceLastSavedSpeed,
audioBoolean: audioBoolean,
enabled: enabled,
startHidden: startHidden,
controllerOpacity: controllerOpacity,
keyBindings: keyBindings,
blacklist: blacklist.replace(regStrip, "")
},
function () {
// Update status to let user know options were saved.
var status = document.getElementById("status");
status.textContent = "Options saved";
setTimeout(function () {
status.textContent = "";
}, 1000);
}
);
chrome.storage.sync.set(s, function () {
var status = document.getElementById("status");
status.textContent = "Options saved";
setTimeout(function () {
status.textContent = "";
}, 1000);
});
}
// Restores options from chrome.storage
// MODIFIED: restore_options to include nudge settings
function restore_options() {
chrome.storage.sync.get(tcDefaults, function (storage) {
document.getElementById("rememberSpeed").checked = storage.rememberSpeed;
document.getElementById("forceLastSavedSpeed").checked = storage.forceLastSavedSpeed;
document.getElementById("forceLastSavedSpeed").checked =
storage.forceLastSavedSpeed;
document.getElementById("audioBoolean").checked = storage.audioBoolean;
document.getElementById("enabled").checked = storage.enabled;
document.getElementById("startHidden").checked = storage.startHidden;
@@ -267,65 +249,86 @@ function restore_options() {
storage.controllerOpacity;
document.getElementById("blacklist").value = storage.blacklist;
// ensure that there is a "display" binding for upgrades from versions that had it as a separate binding
// ADDED: Restore nudge settings
document.getElementById("enableSubtitleNudge").checked =
storage.enableSubtitleNudge;
document.getElementById("subtitleNudgeInterval").value =
storage.subtitleNudgeInterval;
document.getElementById("subtitleNudgeAmount").value =
storage.subtitleNudgeAmount;
// Original key binding restoration logic
if (
!Array.isArray(storage.keyBindings) ||
storage.keyBindings.length === 0
) {
// If keyBindings missing or not an array, use defaults from tcDefaults
storage.keyBindings = tcDefaults.keyBindings;
}
if (storage.keyBindings.filter((x) => x.action == "display").length == 0) {
storage.keyBindings.push({
action: "display",
value: 0,
force: false,
predefined: true
predefined: true,
key: storage.displayKeyCode || tcDefaults.displayKeyCode
});
}
// Clear existing dynamic shortcuts before restoring (if any were added by mistake)
const dynamicShortcuts = document.querySelectorAll(".customs:not([id])");
dynamicShortcuts.forEach((sc) => sc.remove());
for (let i in storage.keyBindings) {
var item = storage.keyBindings[i];
if (item.predefined) {
//do predefined ones because their value needed for overlay
// document.querySelector("#" + item["action"] + " .customDo").value = item["action"];
if (item["action"] == "display" && typeof item["key"] === "undefined") {
item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; // V
item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode;
}
if (customActionsNoValues.includes(item["action"]))
document.querySelector(
if (customActionsNoValues.includes(item["action"])) {
const el = document.querySelector(
"#" + item["action"] + " .customValue"
).disabled = true;
updateCustomShortcutInputText(
document.querySelector("#" + item["action"] + " .customKey"),
item["key"]
);
if (el) el.disabled = true;
}
const keyEl = document.querySelector(
"#" + item["action"] + " .customKey"
);
document.querySelector("#" + item["action"] + " .customValue").value =
item["value"];
document.querySelector("#" + item["action"] + " .customForce").value =
item["force"];
const valEl = document.querySelector(
"#" + item["action"] + " .customValue"
);
const forceEl = document.querySelector(
"#" + item["action"] + " .customForce"
);
if (keyEl) updateCustomShortcutInputText(keyEl, item["key"]);
if (valEl) valEl.value = item["value"];
if (forceEl) forceEl.value = String(item["force"]); // Ensure string for select value
} else {
// new ones
// Non-predefined, dynamically added shortcuts
add_shortcut();
const dom = document.querySelector(".customs:last-of-type");
const dom = document.querySelector(".customs:last-of-type"); // Gets the newly added one
dom.querySelector(".customDo").value = item["action"];
if (customActionsNoValues.includes(item["action"]))
if (customActionsNoValues.includes(item["action"])) {
dom.querySelector(".customValue").disabled = true;
}
updateCustomShortcutInputText(
dom.querySelector(".customKey"),
item["key"]
);
dom.querySelector(".customValue").value = item["value"];
dom.querySelector(".customForce").value = item["force"];
dom.querySelector(".customForce").value = String(item["force"]);
}
}
});
}
function restore_defaults() {
/* ... same as your original, tcDefaults now includes nudge defaults ... */
// Remove all dynamically added shortcuts first
document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove());
// Then set defaults and restore options, which will re-add predefined ones correctly
chrome.storage.sync.set(tcDefaults, function () {
restore_options();
document
.querySelectorAll(".removeParent")
.forEach((button) => button.click()); // Remove added shortcuts
// Update status to let user know options were saved.
restore_options(); // This will populate based on tcDefaults
var status = document.getElementById("status");
status.textContent = "Default options restored";
setTimeout(function () {
@@ -335,14 +338,15 @@ function restore_defaults() {
}
function show_experimental() {
/* ... same as your original ... */
document
.querySelectorAll(".customForce")
.forEach((item) => (item.style.display = "inline-block"));
}
document.addEventListener("DOMContentLoaded", function () {
/* ... same as your original event listeners setup ... */
restore_options();
document.getElementById("save").addEventListener("click", save_options);
document.getElementById("add").addEventListener("click", add_shortcut);
document
@@ -353,34 +357,32 @@ document.addEventListener("DOMContentLoaded", function () {
.addEventListener("click", show_experimental);
function eventCaller(event, className, funcName) {
if (!event.target.classList || !event.target.classList.contains(className)) {
if (!event.target.classList || !event.target.classList.contains(className))
return;
}
funcName(event);
}
document.addEventListener("keypress", (event) => {
eventCaller(event, "customValue", inputFilterNumbersOnly);
});
document.addEventListener("focus", (event) => {
eventCaller(event, "customKey", inputFocus);
});
document.addEventListener("blur", (event) => {
eventCaller(event, "customKey", inputBlur);
});
document.addEventListener("keydown", (event) => {
eventCaller(event, "customKey", recordKeyPress);
});
document.addEventListener("click", (event) => {
document.addEventListener("keypress", (event) =>
eventCaller(event, "customValue", inputFilterNumbersOnly)
);
document.addEventListener("focus", (event) =>
eventCaller(event, "customKey", inputFocus)
);
document.addEventListener("blur", (event) =>
eventCaller(event, "customKey", inputBlur)
);
document.addEventListener("keydown", (event) =>
eventCaller(event, "customKey", recordKeyPress)
);
document.addEventListener("click", (event) =>
eventCaller(event, "removeParent", function () {
event.target.parentNode.remove();
});
});
})
);
document.addEventListener("change", (event) => {
eventCaller(event, "customDo", function () {
if (customActionsNoValues.includes(event.target.value)) {
event.target.nextElementSibling.nextElementSibling.disabled = true;
event.target.nextElementSibling.nextElementSibling.value = 0;
event.target.nextElementSibling.nextElementSibling.value = 0; // Or "" if placeholder is preferred
} else {
event.target.nextElementSibling.nextElementSibling.disabled = false;
}