v5.3.0.0-beta.1

This commit is contained in:
2026-05-22 13:37:49 -04:00
10 changed files with 141 additions and 14 deletions
+16 -10
View File
@@ -1,8 +1,8 @@
# Available for Firefox
# Available for Firefox
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/)
# The science of accelerated playback
## The science of accelerated playback
**TL;DR: faster playback translates to better engagement and retention.**
@@ -33,9 +33,11 @@ last point to listen to it a few more times.
![Player](https://cloud.githubusercontent.com/assets/2400185/24076745/5723e6ae-0c41-11e7-820c-1d8e814a2888.png)
#### *Install [Chrome](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk) or [Firefox](https://addons.mozilla.org/en-us/firefox/addon/speeder/) Extension*
## Using the extension
\*\* Once the extension is installed simply navigate to any page that offers
[![Add to Firefox](https://img.shields.io/badge/Add%20to-Firefox-orange?logo=firefox&logoColor=white)](https://addons.mozilla.org/firefox/addon/speeder/)
Once the extension is installed simply navigate to any page that offers
HTML5 video ([example](https://www.youtube.com/watch?v=E9FxNzv1Tr8)), and you'll
see a speed indicator in top left corner. Hover over the indicator to reveal the
controls to accelerate, slowdown, and quickly rewind or advance the video. Or,
@@ -65,21 +67,25 @@ listens both for lower and upper case values (i.e. you can use
key. This is not a perfect solution, as some sites may listen to both, but works
most of the time.
### FAQ
## FAQ
**The video controls are not showing up?** This extension is only compatible
### The video controls are not showing up?
This extension is only compatible
with HTML5 video. If you don't see the controls showing up, chances are you are
viewing a Flash video. If you want to confirm, try right-clicking on the video
and inspect the menu: if it mentions flash, then that's the issue. That said,
most sites will fallback to HTML5 if they detect that Flash is not available.
You can try manually disabling Flash from the browser.
**What is this fork all about?** This is a fork of
[CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed)
### What is this fork all about?
This is a fork of
[CodeBicycle's Video Speed Controller extension for Firefox](https://github.com/codebicycle/videospeed)
which is a fork of [Igrigorik's Video Speed Controller extension for Chromium](https://github.com/igrigorik/videospeed).
The goal of this fork is fix bugs in the upstream code as well as add new features.
### License
## License
(GPLv3) - Copyright (c) 2025 Josh Patra
+12 -2
View File
@@ -39,6 +39,7 @@ var tc = {
defaultLogLevel: 3,
logLevel: 3,
enableSubtitleNudge: false,
subtitleNudgeEnabledByDefault: true,
subtitleNudgeInterval: 50, // Default 50ms balances subtitle tracking with CPU cost
subtitleNudgeAmount: 0.001,
customButtonIcons: {}
@@ -384,6 +385,7 @@ function captureSiteRuleBase() {
controllerMarginTop: tc.settings.controllerMarginTop,
controllerMarginBottom: tc.settings.controllerMarginBottom,
enableSubtitleNudge: tc.settings.enableSubtitleNudge,
subtitleNudgeEnabledByDefault: tc.settings.subtitleNudgeEnabledByDefault,
subtitleNudgeInterval: tc.settings.subtitleNudgeInterval,
controllerButtons: Array.isArray(tc.settings.controllerButtons)
? tc.settings.controllerButtons.slice()
@@ -410,6 +412,7 @@ function resetSettingsFromSiteRuleBase() {
tc.settings.controllerMarginTop = base.controllerMarginTop;
tc.settings.controllerMarginBottom = base.controllerMarginBottom;
tc.settings.enableSubtitleNudge = base.enableSubtitleNudge;
tc.settings.subtitleNudgeEnabledByDefault = base.subtitleNudgeEnabledByDefault;
tc.settings.subtitleNudgeInterval = base.subtitleNudgeInterval;
tc.settings.controllerButtons = Array.isArray(base.controllerButtons)
? base.controllerButtons.slice()
@@ -659,13 +662,15 @@ function isSubtitleNudgeAvailableForVideo(video) {
function isSubtitleNudgeEnabledForVideo(video) {
if (!isSubtitleNudgeAvailableForVideo(video)) return false;
if (!video || !video.vsc) return true;
if (!video || !video.vsc) {
return Boolean(tc.settings.subtitleNudgeEnabledByDefault);
}
if (typeof video.vsc.subtitleNudgeEnabledOverride === "boolean") {
return video.vsc.subtitleNudgeEnabledOverride;
}
return true;
return Boolean(tc.settings.subtitleNudgeEnabledByDefault);
}
function setSubtitleNudgeEnabledForVideo(video, enabled) {
@@ -1195,6 +1200,10 @@ chrome.storage.sync.get(tc.settings, function(storage) {
typeof storage.enableSubtitleNudge !== "undefined"
? Boolean(storage.enableSubtitleNudge)
: tc.settings.enableSubtitleNudge;
tc.settings.subtitleNudgeEnabledByDefault =
typeof storage.subtitleNudgeEnabledByDefault !== "undefined"
? Boolean(storage.subtitleNudgeEnabledByDefault)
: tc.settings.subtitleNudgeEnabledByDefault;
tc.settings.subtitleNudgeInterval = Math.min(
1000,
Math.max(10, Number(storage.subtitleNudgeInterval) || 50)
@@ -2051,6 +2060,7 @@ function applySiteRuleOverrides() {
"controllerMarginTop",
"controllerMarginBottom",
"enableSubtitleNudge",
"subtitleNudgeEnabledByDefault",
"subtitleNudgeInterval"
];
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Speeder",
"short_name": "Speeder",
"version": "5.2.7.0",
"version": "5.3.0.0",
"manifest_version": 2,
"description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts (New and improved version of \"Video Speed Controller\")",
"homepage_url": "https://github.com/SoPat712/speeder",
+20 -1
View File
@@ -394,11 +394,21 @@
<div class="row row-checkbox">
<label for="enableSubtitleNudge"
>Enable subtitle nudge<br /><em
>Makes tiny playback changes to help keep subtitles aligned.</em
>Makes tiny playback changes to help keep subtitles aligned and
allows nudge toggle controls.</em
>
</label>
<input id="enableSubtitleNudge" type="checkbox" />
</div>
<div class="row row-checkbox">
<label for="subtitleNudgeEnabledByDefault"
>Start subtitle nudge enabled<br /><em
>When off, each video starts with nudging disabled until you
toggle it.</em
>
</label>
<input id="subtitleNudgeEnabledByDefault" type="checkbox" />
</div>
<div class="row">
<label for="subtitleNudgeInterval"
>Nudge interval (milliseconds)<br /><em
@@ -827,6 +837,15 @@
>
<input type="checkbox" class="site-enableSubtitleNudge" />
</div>
<div class="site-rule-option site-rule-option-checkbox">
<label
>Start subtitle nudge enabled:<br /><em
>When off, matching videos start with nudging disabled
until you toggle it.</em
></label
>
<input type="checkbox" class="site-subtitleNudgeEnabledByDefault" />
</div>
<div class="site-rule-option site-rule-option-field">
<label
>Nudge interval (10&ndash;1000ms):<br /><em
+9
View File
@@ -210,6 +210,7 @@ var tcDefaults = {
popupMatchHoverControls: true,
popupControllerButtons: ["rewind", "slower", "faster", "advance", "display"],
enableSubtitleNudge: false,
subtitleNudgeEnabledByDefault: true,
subtitleNudgeInterval: 50,
subtitleNudgeAmount: 0.001
};
@@ -823,6 +824,8 @@ function save_options() {
settings.keyBindings = keyBindings;
settings.enableSubtitleNudge =
document.getElementById("enableSubtitleNudge").checked;
settings.subtitleNudgeEnabledByDefault =
document.getElementById("subtitleNudgeEnabledByDefault").checked;
settings.subtitleNudgeInterval =
parseInt(document.getElementById("subtitleNudgeInterval").value, 10) ||
tcDefaults.subtitleNudgeInterval;
@@ -906,6 +909,8 @@ function save_options() {
if (ruleEl.querySelector(".override-subtitleNudge").checked) {
rule.enableSubtitleNudge =
ruleEl.querySelector(".site-enableSubtitleNudge").checked;
rule.subtitleNudgeEnabledByDefault =
ruleEl.querySelector(".site-subtitleNudgeEnabledByDefault").checked;
var nudgeIv = parseInt(
ruleEl.querySelector(".site-subtitleNudgeInterval").value,
10
@@ -1162,10 +1167,12 @@ function createSiteRule(rule) {
var hasSubtitleNudgeOverride = Boolean(
rule &&
(rule.enableSubtitleNudge !== undefined ||
rule.subtitleNudgeEnabledByDefault !== undefined ||
rule.subtitleNudgeInterval !== undefined)
);
ruleEl.querySelector(".override-subtitleNudge").checked = hasSubtitleNudgeOverride;
syncSiteRuleField(ruleEl, rule, "enableSubtitleNudge", true);
syncSiteRuleField(ruleEl, rule, "subtitleNudgeEnabledByDefault", true);
syncSiteRuleField(ruleEl, rule, "subtitleNudgeInterval", false);
applySiteRuleOverrideState(
ruleEl,
@@ -1650,6 +1657,8 @@ function restore_options() {
storage.showPopupControlBar !== false;
document.getElementById("enableSubtitleNudge").checked =
storage.enableSubtitleNudge;
document.getElementById("subtitleNudgeEnabledByDefault").checked =
storage.subtitleNudgeEnabledByDefault;
document.getElementById("subtitleNudgeInterval").value =
storage.subtitleNudgeInterval;
+3
View File
@@ -15,6 +15,7 @@
"audioBoolean",
"controllerOpacity",
"enableSubtitleNudge",
"subtitleNudgeEnabledByDefault",
"subtitleNudgeInterval",
"controllerButtons",
"showPopupControlBar",
@@ -43,6 +44,7 @@
"popupMatchHoverControls",
"popupControllerButtons",
"enableSubtitleNudge",
"subtitleNudgeEnabledByDefault",
"subtitleNudgeInterval",
"subtitleNudgeAmount"
];
@@ -180,6 +182,7 @@
popupMatchHoverControls: true,
popupControllerButtons: DEFAULT_BUTTONS.slice(),
enableSubtitleNudge: false,
subtitleNudgeEnabledByDefault: true,
subtitleNudgeInterval: 50,
subtitleNudgeAmount: 0.001
};
+1
View File
@@ -33,6 +33,7 @@
"speed",
"startHidden",
"subtitleNudgeAmount",
"subtitleNudgeEnabledByDefault",
"subtitleNudgeInterval"
]);
+48
View File
@@ -112,4 +112,52 @@ describe("inject runtime", () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
it("uses the configured subtitle nudge default until a video is toggled", async () => {
await bootInject({
sync: {
enableSubtitleNudge: true,
subtitleNudgeEnabledByDefault: false
}
});
const startSubtitleNudge = vi.fn();
const video = {
paused: false,
playbackRate: 1.5,
vsc: {
startSubtitleNudge,
stopSubtitleNudge: vi.fn(),
subtitleNudgeEnabledOverride: null,
subtitleNudgeIndicator: null,
nudgeFlashIndicator: document.createElement("span")
}
};
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(false);
expect(window.setSubtitleNudgeEnabledForVideo(video, true)).toBe(true);
expect(video.vsc.subtitleNudgeEnabledOverride).toBe(true);
expect(window.isSubtitleNudgeEnabledForVideo(video)).toBe(true);
expect(startSubtitleNudge).toHaveBeenCalledTimes(1);
});
it("applies subtitle nudge default state from matching site rules", async () => {
await bootInject();
window.tc.settings.enableSubtitleNudge = true;
window.tc.settings.subtitleNudgeEnabledByDefault = true;
window.tc.settings.siteRules = [
{
pattern: "example.org",
subtitleNudgeEnabledByDefault: false
}
];
window.captureSiteRuleBase();
expect(window.applySiteRuleOverrides()).toBe(false);
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(false);
window.resetSettingsFromSiteRuleBase();
expect(window.tc.settings.subtitleNudgeEnabledByDefault).toBe(true);
});
});
+21
View File
@@ -29,6 +29,7 @@ describe("options page", () => {
sync: {
rememberSpeed: true,
enabled: false,
subtitleNudgeEnabledByDefault: false,
popupMatchHoverControls: false,
popupControllerButtons: ["rewind", "settings", "advance", "advance"],
keyBindings: [
@@ -39,6 +40,7 @@ describe("options page", () => {
{
pattern: "youtube.com",
enabled: true,
subtitleNudgeEnabledByDefault: false,
showPopupControlBar: false,
popupControllerButtons: ["advance", "settings", "advance"]
}
@@ -49,12 +51,22 @@ describe("options page", () => {
expect(document.getElementById("app-version").textContent).toBe("5.1.7.0");
expect(document.getElementById("rememberSpeed").checked).toBe(true);
expect(document.getElementById("enabled").checked).toBe(false);
expect(document.getElementById("subtitleNudgeEnabledByDefault").checked).toBe(
false
);
expect(document.querySelector('.shortcut-row[data-action="pause"]')).not.toBe(
null
);
expect(document.getElementById("siteRulesContainer").children.length).toBe(
1
);
expect(document.querySelector(".site-rule .override-subtitleNudge").checked).toBe(
true
);
expect(
document.querySelector(".site-rule .site-subtitleNudgeEnabledByDefault")
.checked
).toBe(false);
expect(globalThis.getPopupControlBarOrder()).toEqual(["rewind", "advance"]);
});
@@ -159,6 +171,7 @@ describe("options page", () => {
document.getElementById("controllerMarginTop").value = "250";
document.getElementById("controllerMarginBottom").value = "-4";
document.getElementById("enableSubtitleNudge").checked = true;
document.getElementById("subtitleNudgeEnabledByDefault").checked = false;
document.getElementById("subtitleNudgeInterval").value = "5";
document.getElementById("popupMatchHoverControls").checked = false;
document.getElementById("showPopupControlBar").checked = false;
@@ -177,6 +190,10 @@ describe("options page", () => {
rule.querySelector(".site-rememberSpeed").checked = true;
rule.querySelector(".override-opacity").checked = true;
rule.querySelector(".site-controllerOpacity").value = "0";
rule.querySelector(".override-subtitleNudge").checked = true;
rule.querySelector(".site-enableSubtitleNudge").checked = true;
rule.querySelector(".site-subtitleNudgeEnabledByDefault").checked = false;
rule.querySelector(".site-subtitleNudgeInterval").value = "75";
rule.querySelector(".override-popup-controlbar").checked = true;
rule.querySelector(".site-showPopupControlBar").checked = false;
globalThis.populateControlBarZones(
@@ -201,6 +218,7 @@ describe("options page", () => {
expect(savedSettings.controllerOpacity).toBe(0);
expect(savedSettings.controllerMarginTop).toBe(200);
expect(savedSettings.controllerMarginBottom).toBe(0);
expect(savedSettings.subtitleNudgeEnabledByDefault).toBe(false);
expect(savedSettings.subtitleNudgeInterval).toBe(10);
expect(savedSettings.showPopupControlBar).toBe(false);
expect(savedSettings.popupMatchHoverControls).toBe(false);
@@ -211,6 +229,9 @@ describe("options page", () => {
pattern: "youtube.com",
rememberSpeed: true,
controllerOpacity: 0,
enableSubtitleNudge: true,
subtitleNudgeEnabledByDefault: false,
subtitleNudgeInterval: 75,
showPopupControlBar: false,
popupControllerButtons: ["advance"]
})
+10
View File
@@ -152,6 +152,16 @@ describe("shared helpers", () => {
localSettings: null
});
expect(
importExportUtils.extractImportSettings({
subtitleNudgeEnabledByDefault: false
})
).toEqual({
isWrappedBackup: false,
settings: { subtitleNudgeEnabledByDefault: false },
localSettings: null
});
expect(
importExportUtils.extractImportSettings({ enabled: true })
).toEqual({