Control HTML5 video speed and AB loop actions.
// ==UserScript==
// @name Speed & Loop (userscript)
// @namespace https://github.com/grad13/Speed-and-Loop
// @version 2.1.3
// @description Control HTML5 video speed and AB loop actions.
// @author speed-and-loop
// @license MIT
// @homepageURL https://greasyfork.org/en/scripts/583175-speed-loop-userscript
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @run-at document-idle
// ==/UserScript==
(function () {
function injectStyle(id, css) {
if (!document || !document.head || document.getElementById(id)) { return; }
var style = document.createElement("style");
style.id = id;
style.textContent = css;
document.head.appendChild(style);
}
injectStyle("psl-userscript-content-css", ".mpc-nosource {\n display: none !important;\n}\n.mpc-manual {\n visibility: visible !important;\n opacity: 1 !important;\n}\n\n.mpc-controller {\n /* In case of pages using `white-space: pre-line` (eg Discord), don't render vsc's whitespace */\n white-space: normal;\n}\n\n/* Origin specific overrides */\n/* YouTube player */\n.ytp-hide-info-bar .mpc-controller {\n position: relative;\n top: 10px;\n}\n\n.ytp-autohide .mpc-controller {\n visibility: visible;\n opacity: 1;\n}\n\n.ytp-autohide .mpc-show {\n visibility: visible;\n opacity: 1;\n}\n\n/* YouTube embedded player */\n/* e.g. https://www.igvita.com/2012/09/12/web-fonts-performance-making-pretty-fast/ */\n.html5-video-player:not(.ytp-hide-info-bar) .mpc-controller {\n position: relative;\n top: 60px;\n}\n\n/* Facebook player */\n#facebook .mpc-controller {\n position: relative;\n top: 40px;\n}\n\n/* Google Photos player */\n/* Inline preview doesn't have any additional hooks, relying on Aria label */\na[aria-label^=\"Video\"] .mpc-controller {\n position: relative;\n top: 35px;\n}\n/* Google Photos full-screen view */\n#player .house-brand .mpc-controller {\n position: relative;\n top: 50px;\n}\n\n/* Netflix player */\n#netflix-player:not(.player-cinema-mode) .mpc-controller {\n position: relative;\n top: 85px;\n}\n\n/* shift controller on vine.co */\n/* e.g. https://vine.co/v/OrJj39YlL57 */\n.video-container .vine-video-container .mpc-controller {\n margin-left: 40px;\n}\n\n/* shift YT 3D controller down */\n/* e.g. https://www.youtube.com/watch?v=erftYPflJzQ */\n.ytp-webgl-spherical-control {\n top: 60px !important;\n}\n\n.ytp-fullscreen .ytp-webgl-spherical-control {\n top: 100px !important;\n}\n\n/* disable Vimeo video overlay */\ndiv.video-wrapper + div.target {\n height: 0;\n}\n\n/* Fix black overlay on Kickstarter */\ndiv.video-player.has_played.vertically_center:before,\ndiv.legacy-video-player.has_played.vertically_center:before {\n content: none !important;\n}\n\n/* Fix black overlay on openai.com */\n.Shared-Video-player > .mpc-controller {\n height: 0;\n}\n");
// ---- code/extension/content/namespace.js ----
(function (global) {
var PSL = global.PlaySpeedLoop || {};
var pendingInjectionSessionId = global.__PSL_PENDING_INJECTION_SESSION_ID;
PSL.regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
PSL.instanceId =
"psl-" + Date.now() + "-" + Math.random().toString(36).slice(2);
PSL.injectionSessionId =
typeof pendingInjectionSessionId === "string"
? pendingInjectionSessionId
: PSL.instanceId;
PSL.__injectionSessionId = PSL.injectionSessionId;
PSL.state = {
mediaElements: [],
startTimes: {},
endTimes: {},
pendingLoopPoints: {},
loopsEnabled: {},
controllerTimer: null,
nextMediaId: 1,
instanceId: PSL.instanceId
};
PSL.requestIdle = function (callback, options) {
if (typeof global.requestIdleCallback === "function") {
return global.requestIdleCallback.call(global, callback, options);
}
return global.setTimeout(function () {
callback({ didTimeout: false, timeRemaining: function () { return 0; } });
}, 0);
};
global.PlaySpeedLoop = PSL;
try {
delete global.__PSL_PENDING_INJECTION_SESSION_ID;
} catch (e) {
global.__PSL_PENDING_INJECTION_SESSION_ID = undefined;
}
function getGlobalStorage() {
if (typeof browser !== "undefined" && browser.storage && browser.storage.sync) {
return browser.storage.sync;
}
if (typeof chrome !== "undefined" && chrome.storage && chrome.storage.sync) {
return chrome.storage.sync;
}
return null;
}
PSL.getAssetUrl = function (path) {
try {
var rt = global.chrome && global.chrome.runtime;
if (rt && typeof rt.getURL === "function") {
return rt.getURL(path);
}
} catch (e) {}
try {
var browserRuntime = global.browser && global.browser.runtime;
if (browserRuntime && typeof browserRuntime.getURL === "function") {
return browserRuntime.getURL(path);
}
} catch (e) {}
return null;
};
PSL.getStorageBridge = function () {
return getGlobalStorage();
};
PSL.traceSpeed = function (label, detail) {
try {
if (typeof detail === "undefined") {
console.log("[Speed & Loop trace] " + label);
} else {
console.log("[Speed & Loop trace] " + label, detail);
}
} catch (e) {}
};
})(globalThis);
// ---- code/extension/content/defaults.js ----
(function (PSL) {
PSL.defaultKeyBindings = [
{ action: "slower", key: 83, value: 0.5, force: false, predefined: true },
{ action: "faster", key: 68, value: 0.5, force: false, predefined: true },
{ action: "reset", key: 82, value: 1, force: false, predefined: true },
{ action: "mark-loop-range", key: 65, value: 0, force: false, predefined: true },
{ action: "clear-loop", key: 69, value: 0, force: false, predefined: true }
];
PSL.defaultSettings = {
lastSpeed: 1.0,
enabled: true,
speeds: {},
domainSpeeds: {},
displayKeyCode: 86,
audioBoolean: false,
speedControlsEnabled: true,
loopControlsEnabled: true,
speedStep: 0.5,
preferredSpeed: 1.8,
reverseFrameRate: 1,
keepSpeedSites: "",
controllerOpacity: 0.96,
keyBindings: PSL.defaultKeyBindings,
blacklist: "www.instagram.com\ntwitter.com\nvine.co\nimgur.com\nteams.microsoft.com",
defaultLogLevel: 4,
logLevel: 1
};
PSL.cloneDefaultSettings = function () {
var settings = Object.assign({}, PSL.defaultSettings);
settings.speeds = {};
settings.domainSpeeds = {};
settings.keyBindings = PSL.defaultKeyBindings.map(function (binding) {
return Object.assign({}, binding);
});
return settings;
};
PSL.settings = PSL.cloneDefaultSettings();
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/logger.js ----
(function (PSL) {
PSL.log = function (message, level) {
var verbosity = PSL.settings.logLevel;
if (typeof level === "undefined") {
level = PSL.settings.defaultLogLevel;
}
if (verbosity >= level) {
if (level === 2) {
console.log("ERROR:" + message);
} else if (level === 3) {
console.log("WARNING:" + message);
} else if (level === 4) {
console.log("INFO:" + message);
} else if (level === 5) {
console.log("DEBUG:" + message);
} else if (level === 6) {
console.log("DEBUG (VERBOSE):" + message);
console.trace();
}
}
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/unavailable-content-settings-storage.js ----
(function (PSL) {
function logUnavailable(operation) {
if (typeof PSL.log === "function") {
PSL.log("Content settings storage unavailable during " + operation, 3);
}
}
PSL.createUnavailableContentSettingsStorage = function () {
return {
get: function (defaults, callback) {
logUnavailable("get");
callback(defaults || {});
},
set: function (_values, callback) {
logUnavailable("set");
if (callback) {
callback();
}
},
remove: function (_keys, callback) {
logUnavailable("remove");
if (callback) {
callback();
}
}
};
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/content-settings-storage-backend.js ----
(function (PSL) {
function hasBrowserStorage() {
return (
typeof browser !== "undefined" &&
browser.storage &&
browser.storage.sync
);
}
function hasChromeStorage() {
return (
typeof chrome !== "undefined" &&
chrome.storage &&
chrome.storage.sync
);
}
function hasGMStorage() {
return typeof GM_getValue === "function" && typeof GM_setValue === "function";
}
function parseGMValue(rawValue, fallback) {
if (typeof rawValue === "undefined" || rawValue === null) {
return fallback;
}
if (typeof rawValue === "string") {
try {
return JSON.parse(rawValue);
} catch (error) {
return rawValue;
}
}
return rawValue;
}
var platform;
if (hasBrowserStorage()) {
platform = {
get: function (defaults, callback) {
defaults = defaults || {};
browser.storage.sync.get(defaults).then(
callback,
function () {
callback(defaults);
}
);
},
set: function (values, callback) {
browser.storage.sync.set(values).then(function () {
if (callback) {
callback();
}
});
},
remove: function (keys, callback) {
browser.storage.sync.remove(keys).then(function () {
if (callback) {
callback();
}
});
}
};
} else if (hasChromeStorage()) {
platform = {
get: function (defaults, callback) {
chrome.storage.sync.get(defaults || {}, callback);
},
set: function (values, callback) {
chrome.storage.sync.set(values, callback);
},
remove: function (keys, callback) {
chrome.storage.sync.remove(keys, callback);
}
};
} else if (hasGMStorage()) {
platform = {
get: function (defaults, callback) {
var result = Object.assign({}, defaults || {});
var error;
var keys = Object.keys(result);
try {
keys.forEach(function (key) {
var stored = GM_getValue(key, null);
result[key] = parseGMValue(stored, result[key]);
});
callback(result);
return;
} catch (errorInner) {
error = errorInner;
}
if (error) {
callback(defaults || {});
}
},
set: function (values, callback) {
var error;
try {
Object.keys(values).forEach(function (key) {
if (values[key] === undefined) {
if (typeof GM_deleteValue === "function") {
GM_deleteValue(key);
}
} else {
GM_setValue(key, JSON.stringify(values[key]));
}
});
if (callback) {
callback();
}
return;
} catch (errorInner) {
error = errorInner;
}
if (callback && error) {
callback();
}
},
remove: function (keys, callback) {
if (!Array.isArray(keys)) {
keys = [keys];
}
if (typeof GM_deleteValue === "function") {
keys.forEach(function (key) {
GM_deleteValue(key);
});
}
if (callback) {
callback();
}
}
};
} else {
platform = PSL.createUnavailableContentSettingsStorage();
}
PSL.storagePlatform = platform;
PSL.getStorage = function (defaults, callback) {
if (typeof callback !== "function") {
return platform;
}
platform.get(defaults, callback);
};
PSL.setStorage = function (values, callback) {
platform.set(values, callback);
};
PSL.removeStorage = function (keys, callback) {
platform.remove(keys, callback);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/settings-migration.js ----
(function (PSL) {
PSL.hasStoredSetting = function (storage, key) {
return Object.prototype.hasOwnProperty.call(storage || {}, key);
};
PSL.hasLegacyKeyBindingSettings = function (storage) {
return [
"speedStep",
"resetKeyCode",
"slowerKeyCode",
"fasterKeyCode",
"rewindKeyCode",
"advanceKeyCode",
"fastKeyCode"
].some(function (key) {
return PSL.hasStoredSetting(storage, key);
});
};
PSL.buildLegacyKeyBindings = function (storage) {
return [
{
action: "slower",
key: Number(storage.slowerKeyCode) || 83,
value: Number(storage.speedStep) || 0.5,
force: false,
predefined: true
},
{
action: "faster",
key: Number(storage.fasterKeyCode) || 68,
value: Number(storage.speedStep) || 0.5,
force: false,
predefined: true
},
{
action: "rewind",
key: Number(storage.rewindKeyCode) || 90,
value: Number(storage.rewindTime) || 10,
force: false,
predefined: true
},
{
action: "advance",
key: Number(storage.advanceKeyCode) || 88,
value: Number(storage.advanceTime) || 10,
force: false,
predefined: true
},
{
action: "reset",
key: Number(storage.resetKeyCode) || 82,
value: 1.0,
force: false,
predefined: true
},
{
action: "fast",
key: Number(storage.fastKeyCode) || 71,
value: Number(storage.fastSpeed) || 1.8,
force: false,
predefined: true
}
];
};
PSL.ensureDefaultBinding = function (settings, action) {
if (settings.keyBindings.some(function (binding) { return binding.action === action; })) {
return false;
}
var defaultBinding = PSL.defaultKeyBindings.find(function (binding) {
return binding.action === action;
});
if (!defaultBinding) {
return false;
}
settings.keyBindings.push(Object.assign({}, defaultBinding));
return true;
};
PSL.ensureDefaultBindingValue = function (settings, action) {
var binding = settings.keyBindings.find(function (item) {
return item.action === action;
});
var defaultBinding = PSL.defaultKeyBindings.find(function (item) {
return item.action === action;
});
if (!binding || !defaultBinding) {
return false;
}
var migrated = false;
if (typeof binding.key === "undefined") {
binding.key = defaultBinding.key;
migrated = true;
}
if (
(action === "slower" || action === "faster") &&
(isNaN(Number(binding.value)) || Number(binding.value) < 0.01)
) {
binding.value = defaultBinding.value;
migrated = true;
}
if (
(action === "reset" || action === "fast") &&
(isNaN(Number(binding.value)) || Number(binding.value) <= 0)
) {
binding.value = defaultBinding.value;
migrated = true;
}
return migrated;
};
PSL.migrateDefaultSpeedStep = function (settings) {
var migrated = false;
settings.keyBindings.forEach(function (binding) {
if (
(binding.action === "slower" || binding.action === "faster") &&
binding.predefined !== false &&
Number(binding.value) === 0.1
) {
binding.value = 0.5;
migrated = true;
}
});
return migrated;
};
PSL.migrateSpeedStep = function (storage, settings) {
var speedStep = Number(storage.speedStep);
if (
PSL.hasStoredSetting(storage, "speedStep") &&
!isNaN(speedStep) &&
speedStep >= 0.01 &&
speedStep <= 15.93
) {
return { value: speedStep, migrated: false };
}
var binding = settings.keyBindings.find(function (item) {
return item.action === "slower" || item.action === "faster";
});
speedStep = binding ? Number(binding.value) : NaN;
if (!isNaN(speedStep) && speedStep >= 0.01 && speedStep <= 15.93) {
return { value: speedStep, migrated: true };
}
return { value: 0.5, migrated: true };
};
PSL.migratePreferredSpeed = function (storage, settings) {
var preferredSpeed = Number(storage.preferredSpeed);
var storedBindings = Array.isArray(storage.keyBindings)
? storage.keyBindings
: [];
var binding;
if (
PSL.hasStoredSetting(storage, "preferredSpeed") &&
!isNaN(preferredSpeed) &&
preferredSpeed >= 0.07 &&
preferredSpeed <= 16
) {
return { value: preferredSpeed, migrated: false };
}
binding = storedBindings.find(function (item) {
return item.action === "fast";
}) || storedBindings.find(function (item) {
return item.action === "reset";
}) || settings.keyBindings.find(function (item) {
return item.action === "fast";
});
preferredSpeed = binding ? Number(binding.value) : NaN;
if (!isNaN(preferredSpeed) && preferredSpeed >= 0.07 && preferredSpeed <= 16) {
return { value: preferredSpeed, migrated: true };
}
return { value: 1.8, migrated: true };
};
PSL.migrateReverseFrameRate = function (storage) {
var reverseFrameRate = Number(storage.reverseFrameRate);
if (
PSL.hasStoredSetting(storage, "reverseFrameRate") &&
!isNaN(reverseFrameRate) &&
reverseFrameRate >= 0.1 &&
reverseFrameRate <= 15
) {
return { value: reverseFrameRate, migrated: false };
}
return { value: 1, migrated: PSL.hasStoredSetting(storage, "reverseFrameRate") };
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/storage.js ----
(function (PSL) {
PSL.getKeyBinding = function (action, what) {
if (!what) {
what = "value";
}
try {
return PSL.settings.keyBindings.find(function (item) {
return item.action === action;
})[what];
} catch (e) {
return false;
}
};
PSL.setKeyBinding = function (action, value) {
var binding = PSL.settings.keyBindings.find(function (item) {
return item.action === action;
});
if (binding) {
binding.value = value;
}
};
PSL.loadSettings = function (callback) {
PSL.getStorage(null, function (rawStorage) {
rawStorage = rawStorage || {};
var storage = Object.assign(PSL.cloneDefaultSettings(), rawStorage);
var settings = PSL.cloneDefaultSettings();
var migrated = false;
var speedStep;
var preferredSpeed;
var reverseFrameRate;
if (Array.isArray(rawStorage.keyBindings)) {
settings.keyBindings = rawStorage.keyBindings.slice();
} else if (PSL.hasLegacyKeyBindingSettings(rawStorage)) {
settings.keyBindings = PSL.buildLegacyKeyBindings(rawStorage);
settings.version = "0.5.3";
migrated = true;
}
["slower", "faster", "reset", "mark-loop-range", "clear-loop"].forEach(function (action) {
migrated = PSL.ensureDefaultBinding(settings, action) || migrated;
migrated = PSL.ensureDefaultBindingValue(settings, action) || migrated;
});
migrated = PSL.migrateDefaultSpeedStep(settings) || migrated;
speedStep = PSL.migrateSpeedStep(rawStorage, settings);
settings.speedStep = speedStep.value;
migrated = speedStep.migrated || migrated;
preferredSpeed = PSL.migratePreferredSpeed(rawStorage, settings);
settings.preferredSpeed = preferredSpeed.value;
migrated = preferredSpeed.migrated || migrated;
reverseFrameRate = PSL.migrateReverseFrameRate(rawStorage, settings);
settings.reverseFrameRate = reverseFrameRate.value;
migrated = reverseFrameRate.migrated || migrated;
settings.keyBindings = settings.keyBindings.filter(function (binding) {
return PSL.defaultKeyBindings.some(function (defaultBinding) {
return defaultBinding.action === binding.action;
});
});
settings.lastSpeed = Number(storage.lastSpeed) || 1.0;
settings.displayKeyCode = Number(storage.displayKeyCode) || 86;
settings.audioBoolean = Boolean(storage.audioBoolean);
settings.enabled =
storage.enabled === undefined
? PSL.defaultSettings.enabled
: Boolean(storage.enabled);
settings.speedControlsEnabled = storage.speedControlsEnabled !== false;
settings.loopControlsEnabled = storage.loopControlsEnabled !== false;
settings.keepSpeedSites = String(storage.keepSpeedSites || "");
settings.controllerOpacity = Number(storage.controllerOpacity);
if (isNaN(settings.controllerOpacity)) {
settings.controllerOpacity = PSL.defaultSettings.controllerOpacity;
}
settings.blacklist = String(storage.blacklist);
settings.logLevel = Number(storage.logLevel) || PSL.defaultSettings.logLevel;
settings.defaultLogLevel =
Number(storage.defaultLogLevel) || PSL.defaultSettings.defaultLogLevel;
settings.speeds = storage.speeds || {};
settings.domainSpeeds = storage.domainSpeeds || {};
PSL.settings = settings;
if (PSL.updateKeepSpeedSiteMatchers) {
PSL.updateKeepSpeedSiteMatchers();
}
if (migrated) {
PSL.setStorage({
keyBindings: PSL.settings.keyBindings,
version: PSL.settings.version,
displayKeyCode: PSL.settings.displayKeyCode,
audioBoolean: PSL.settings.audioBoolean,
speedControlsEnabled: PSL.settings.speedControlsEnabled,
loopControlsEnabled: PSL.settings.loopControlsEnabled,
speedStep: PSL.settings.speedStep,
preferredSpeed: PSL.settings.preferredSpeed,
reverseFrameRate: PSL.settings.reverseFrameRate,
keepSpeedSites: PSL.settings.keepSpeedSites,
enabled: PSL.settings.enabled,
controllerOpacity: PSL.settings.controllerOpacity,
blacklist: PSL.settings.blacklist.replace(PSL.regStrip, "")
});
}
callback();
});
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/feature-gates.js ----
(function (PSL) {
var SPEED_ACTIONS = ["slower", "faster", "reset"];
var LOOP_ACTIONS = ["mark-loop-range", "clear-loop"];
function numberInRange(value, fallback, min, max) {
var number = Number(value);
if (isNaN(number) || number < min || number > max) {
return fallback;
}
return number;
}
PSL.isSpeedAction = function (action) {
return SPEED_ACTIONS.indexOf(action) !== -1;
};
PSL.isLoopAction = function (action) {
return LOOP_ACTIONS.indexOf(action) !== -1;
};
PSL.isActionEnabled = function (action) {
if (PSL.isSpeedAction(action)) {
return PSL.settings.speedControlsEnabled !== false;
}
if (PSL.isLoopAction(action)) {
return PSL.settings.loopControlsEnabled !== false;
}
return false;
};
PSL.getSpeedStep = function () {
return numberInRange(PSL.settings.speedStep, 0.5, 0.01, 15.93);
};
PSL.getPreferredSpeed = function () {
return numberInRange(PSL.settings.preferredSpeed, 1.8, 0.07, 16);
};
PSL.getReverseFrameRate = function () {
return numberInRange(PSL.settings.reverseFrameRate, 1, 0.1, 15);
};
PSL.shouldAttachController = function () {
return (
PSL.settings.speedControlsEnabled !== false ||
PSL.settings.loopControlsEnabled !== false
);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/blacklist.js ----
(function (PSL) {
PSL.escapeStringRegExp = function (str) {
var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
return str.replace(matchOperatorsRe, "\\$&");
};
PSL.isBlacklisted = function (url) {
var targetUrl = typeof url === "string" ? url : location.href;
var blacklisted = false;
PSL.settings.blacklist.split("\n").forEach(function (match) {
var regexp;
match = match.replace(PSL.regStrip, "");
if (match.length === 0) {
return;
}
if (match.startsWith("/")) {
try {
regexp = new RegExp(match.replace(/^\/|\/$/g, ""));
} catch (err) {
return;
}
} else {
regexp = new RegExp(PSL.escapeStringRegExp(match));
}
if (regexp.test(targetUrl)) {
blacklisted = true;
}
});
return blacklisted;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/media-registry.js ----
(function (PSL) {
PSL.getMediaKey = function (media) {
if (media.currentSrc || media.src) {
return media.currentSrc || media.src;
}
if (!media._pslMediaId) {
media._pslMediaId = "media-" + PSL.state.nextMediaId++;
}
return media._pslMediaId;
};
PSL.registerMedia = function (media) {
if (PSL.state.mediaElements.indexOf(media) === -1) {
PSL.state.mediaElements.push(media);
}
};
PSL.unregisterMedia = function (media) {
var idx = PSL.state.mediaElements.indexOf(media);
if (idx !== -1) {
PSL.state.mediaElements.splice(idx, 1);
}
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/media-element-query.js ----
(function (PSL) {
PSL.findMediaElements = function (root) {
var media = [];
function collect(node) {
if (!node || !node.querySelectorAll) {
return;
}
node.querySelectorAll("video,audio").forEach(function (element) {
if (
element.nodeName === "VIDEO" ||
(element.nodeName === "AUDIO" && PSL.settings.audioBoolean)
) {
media.push(element);
}
});
node.querySelectorAll("*").forEach(function (element) {
if (element.shadowRoot) {
collect(element.shadowRoot);
}
});
}
collect(root);
return media;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-hierarchy.js ----
(function (PSL) {
function isValidSpeed(value) {
return typeof value === "number" && Number.isFinite(value);
}
function clampSpeed(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function resolveCandidate(input, scope, key) {
if (!input || !isValidSpeed(input[key])) {
return null;
}
return { speed: input[key], scope: scope };
}
PSL.getEffectiveSpeedFromHierarchy = function (input) {
var candidate =
resolveCandidate(input, "temporary", "temporarySpeed") ||
resolveCandidate(input, "tab", "tabSpeed") ||
resolveCandidate(input, "site", "siteSpeed") ||
resolveCandidate(input, "global", "globalSpeed") ||
{ speed: 1, scope: "global" };
var min = isValidSpeed(input && input.minSpeed) ? input.minSpeed : -16;
var max = isValidSpeed(input && input.maxSpeed) ? input.maxSpeed : 16;
return {
speed: clampSpeed(candidate.speed, min, max),
scope: candidate.scope
};
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-state.js ----
(function (PSL) {
function getRecords() {
if (!PSL.state) {
PSL.state = {};
}
if (!PSL.state.mediaSpeedRecords) {
PSL.state.mediaSpeedRecords = new Map();
}
return PSL.state.mediaSpeedRecords;
}
function getMediaRecords() {
if (!PSL.state) {
PSL.state = {};
}
if (!PSL.state.mediaSpeedElementRecords) {
PSL.state.mediaSpeedElementRecords = new WeakMap();
}
return PSL.state.mediaSpeedElementRecords;
}
function isFiniteNumber(value) {
return typeof value === "number" && Number.isFinite(value);
}
function normalizeSpeed(speed) {
if (PSL.clampSpeed) {
return Number(PSL.clampSpeed(speed, -16, 16).toFixed(2));
}
return Number(Math.min(Math.max(speed, -16), 16).toFixed(2));
}
function getMediaId(video) {
if (video && PSL.getMediaKey) {
return PSL.getMediaKey(video);
}
return video && (video.currentSrc || video.src || video._pslMediaId || "media");
}
function getInitialSpeed(video) {
return isFiniteNumber(video && video.playbackRate) ? video.playbackRate : 1;
}
function ensureRecordById(mediaId, initialSpeed, scope) {
var key = String(mediaId || "");
var records = getRecords();
var record = records.get(key);
if (!record) {
record = {
speed: normalizeSpeed(isFiniteNumber(initialSpeed) ? initialSpeed : 1),
scope: scope || "global"
};
records.set(key, record);
}
return record;
}
function linkRecordId(mediaId, record) {
var key = String(mediaId || "");
if (key) {
getRecords().set(key, record);
}
}
function ensureRecordForMedia(video, initialSpeed, scope) {
var mediaRecords = getMediaRecords();
var mediaId = getMediaId(video);
var record = video ? mediaRecords.get(video) : null;
if (!record && mediaId) {
record = getRecords().get(String(mediaId));
}
if (!record) {
record = {
speed: normalizeSpeed(isFiniteNumber(initialSpeed) ? initialSpeed : 1),
scope: scope || "global"
};
}
if (video) {
mediaRecords.set(video, record);
}
linkRecordId(mediaId, record);
return record;
}
PSL.setMediaSpeedById = function (mediaId, speed, scope) {
var record = ensureRecordById(mediaId, speed, scope);
record.speed = normalizeSpeed(speed);
record.scope = scope || record.scope || "temporary";
return record.speed;
};
PSL.getMediaSpeedById = function (mediaId) {
return ensureRecordById(mediaId, 1, "global").speed;
};
PSL.getMediaSpeedRecordById = function (mediaId) {
return ensureRecordById(mediaId, 1, "global");
};
PSL.hasMediaSpeed = function (video) {
return Boolean(
(video && getMediaRecords().has(video)) ||
getRecords().has(String(getMediaId(video) || ""))
);
};
PSL.setMediaSpeed = function (video, speed, scope) {
var record = ensureRecordForMedia(video, speed, scope);
record.speed = normalizeSpeed(speed);
record.scope = scope || record.scope || "temporary";
linkRecordId(getMediaId(video), record);
return record.speed;
};
PSL.getMediaSpeed = function (video) {
var record = video && getMediaRecords().get(video);
if (!record) {
record = getRecords().get(String(getMediaId(video) || ""));
if (record && video) {
getMediaRecords().set(video, record);
}
}
if (record) {
linkRecordId(getMediaId(video), record);
}
return record ? record.speed : normalizeSpeed(getInitialSpeed(video));
};
PSL.getMediaSpeedScope = function (video) {
var record = video && getMediaRecords().get(video);
if (!record) {
record = getRecords().get(String(getMediaId(video) || ""));
}
return record ? record.scope : "global";
};
PSL.getMediaSpeedMode = function (video) {
var speed = PSL.getMediaSpeed(video);
if (speed < 0) {
return "reverse";
}
if (speed === 0) {
return "zero";
}
return "native";
};
PSL.clearMediaSpeed = function (video) {
if (video) {
getMediaRecords().delete(video);
}
getRecords().delete(String(getMediaId(video) || ""));
};
PSL.observeNativePlaybackRate = function (video, rate) {
var speed = Number(rate);
if (!isFiniteNumber(speed)) {
return { action: "ignore" };
}
if (!PSL.hasMediaSpeed(video)) {
PSL.setMediaSpeed(video, speed, "observed");
return { action: "adopt", speed: speed };
}
if (Math.abs(PSL.getMediaSpeed(video) - speed) < 0.001) {
return { action: "confirm", speed: speed };
}
return { action: "restore", speed: PSL.getMediaSpeed(video), observedSpeed: speed };
};
PSL.getEffectivePlaybackRate = function (video) {
return PSL.getMediaSpeed(video);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/media-source-lifecycle.js ----
(function (PSL) {
function getSourceKey(media) {
if (!media) {
return "";
}
return media.currentSrc || media.src || "";
}
function resetSourceScopedState(state) {
if (!state) {
return null;
}
state.loopStart = 0;
state.loopEnd = 0;
state.loopEnabled = false;
state.syntheticMode = "none";
return state;
}
function resetLoopStateForSourceChange(media, previousSourceKey) {
var currentSourceKey = getSourceKey(media);
if (previousSourceKey) {
PSL.state.startTimes[previousSourceKey] = 0;
PSL.state.endTimes[previousSourceKey] = 0;
if (PSL.state.pendingLoopPoints) {
delete PSL.state.pendingLoopPoints[previousSourceKey];
}
PSL.state.loopsEnabled[previousSourceKey] = false;
}
if (currentSourceKey) {
PSL.state.startTimes[currentSourceKey] = 0;
PSL.state.endTimes[currentSourceKey] = 0;
if (PSL.state.pendingLoopPoints) {
delete PSL.state.pendingLoopPoints[currentSourceKey];
}
PSL.state.loopsEnabled[currentSourceKey] = false;
}
if (media && PSL.disableLoop) {
PSL.disableLoop(media);
}
if (media && media._pslControllerHandle) {
if (media._pslControllerHandle.clearLoopRangeDisplay) {
media._pslControllerHandle.clearLoopRangeDisplay();
}
if (PSL.updateLoopRangeDisplay) {
PSL.updateLoopRangeDisplay(media);
}
}
}
function detectSourceChange(state, nextSourceKey) {
if (!state || !nextSourceKey) {
return false;
}
if (!state.currentSrc) {
state.currentSrc = nextSourceKey;
return false;
}
if (state.currentSrc === nextSourceKey) {
return false;
}
state.previousSrc = state.currentSrc;
state.currentSrc = nextSourceKey;
resetSourceScopedState(state);
return true;
}
PSL.getMediaSourceKey = getSourceKey;
PSL.detectMediaSourceChange = detectSourceChange;
PSL.resetLoopStateForSourceChange = resetLoopStateForSourceChange;
PSL.resetMediaSourceScopedState = resetSourceScopedState;
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/reverse-playback-engine.js ----
(function (PSL, global) {
function isValidLoop(loopStart, loopEnd) {
return (
typeof loopStart === "number" &&
typeof loopEnd === "number" &&
Number.isFinite(loopStart) &&
Number.isFinite(loopEnd) &&
loopEnd > loopStart
);
}
function createReversePlaybackEngine(options) {
var running = new WeakMap();
var setTimer = options && options.setInterval ? options.setInterval : global.setInterval.bind(global);
var clearTimer = options && options.clearInterval ? options.clearInterval : global.clearInterval.bind(global);
var now = options && options.now ? options.now : Date.now;
var defaultIntervalMs = options && options.intervalMs ? options.intervalMs : 100;
function stop(video) {
var state = video && running.get(video);
if (!state) {
return false;
}
clearTimer(state.timerId);
running.delete(video);
return true;
}
function tick(video) {
var state = running.get(video);
if (!state) {
return false;
}
if (video.isConnected === false || video._pslReversing === false) {
stop(video);
if (state.onStop) {
state.onStop(video);
}
return false;
}
if (!video.paused && typeof video.pause === "function") {
video.pause();
}
var currentNow = now();
var elapsedSeconds = Math.max(0, (currentNow - state.lastTick) / 1000);
state.lastTick = currentNow;
var stepSeconds =
typeof state.stepSeconds === "number" && Number.isFinite(state.stepSeconds)
? state.stepSeconds
: Math.abs(state.speed) * elapsedSeconds;
var nextTime = video.currentTime - stepSeconds;
if (isValidLoop(state.loopStart, state.loopEnd) && nextTime <= state.loopStart) {
video.currentTime = state.loopEnd;
return true;
}
if (nextTime <= 0) {
video.currentTime = 0;
stop(video);
if (state.onEnd) {
state.onEnd(video);
}
return true;
}
video.currentTime = nextTime;
return true;
}
function start(video, speed, startOptions) {
if (!video || !(speed < 0)) {
if (video) {
stop(video);
}
return false;
}
stop(video);
if (typeof video.pause === "function") {
video.pause();
}
var state = {
speed: speed,
loopStart: startOptions && startOptions.loopStart,
loopEnd: startOptions && startOptions.loopEnd,
onEnd: startOptions && startOptions.onEnd,
onStop: startOptions && startOptions.onStop,
stepSeconds: startOptions && startOptions.stepSeconds,
intervalMs: startOptions && startOptions.intervalMs ? startOptions.intervalMs : defaultIntervalMs,
lastTick: now(),
timerId: null
};
state.timerId = setTimer(function () {
tick(video);
}, state.intervalMs);
running.set(video, state);
return true;
}
function isRunning(video) {
return Boolean(video && running.has(video));
}
return {
isRunning: isRunning,
start: start,
stop: stop,
tick: tick
};
}
PSL.createReversePlaybackEngine = createReversePlaybackEngine;
PSL.reversePlaybackEngine = PSL.reversePlaybackEngine || createReversePlaybackEngine();
})(globalThis.PlaySpeedLoop, globalThis);
// ---- code/extension/content/media-authority.js ----
(function (PSL) {
function attachSpeedAccessors(state) {
var fallbackSpeed = 1;
var fallbackScope = "global";
Object.defineProperty(state, "targetSpeed", {
configurable: true,
enumerable: true,
get: function () {
return PSL.getMediaSpeedById
? PSL.getMediaSpeedById(state.mediaId)
: fallbackSpeed;
},
set: function (speed) {
fallbackSpeed = speed;
if (PSL.setMediaSpeedById) {
PSL.setMediaSpeedById(state.mediaId, speed, state.speedScope);
}
}
});
Object.defineProperty(state, "speedScope", {
configurable: true,
enumerable: true,
get: function () {
return PSL.getMediaSpeedRecordById
? PSL.getMediaSpeedRecordById(state.mediaId).scope
: fallbackScope;
},
set: function (scope) {
fallbackScope = scope || "global";
if (PSL.getMediaSpeedRecordById) {
PSL.getMediaSpeedRecordById(state.mediaId).scope = fallbackScope;
}
}
});
return state;
}
function normalizeState(mediaId, currentSrc) {
PSL.traceSpeed("media-authority.normalizeState", {
mediaId: mediaId,
currentSrc: currentSrc
});
if (PSL.setMediaSpeedById) {
PSL.setMediaSpeedById(mediaId, 1, "global");
}
return attachSpeedAccessors({
mediaId: mediaId,
currentSrc: currentSrc || "",
previousSrc: "",
loopStart: 0,
loopEnd: 0,
loopEnabled: false,
syntheticMode: "none"
});
}
function createMediaAuthority(options) {
PSL.traceSpeed("media-authority.createMediaAuthority", {
hasSendCommand: Boolean(options && typeof options.sendCommand === "function")
});
var states = new Map();
var sendCommand = options && typeof options.sendCommand === "function"
? options.sendCommand
: function () {};
function getScopeRank(scope) {
if (scope === "temporary") {
return 3;
}
if (scope === "tab") {
return 2;
}
if (scope === "site") {
return 1;
}
return 0;
}
function getState(mediaId) {
var state = states.get(mediaId) || null;
PSL.traceSpeed("media-authority.getState", {
mediaId: mediaId,
found: Boolean(state),
state: state
});
return state;
}
function registerMedia(mediaId, currentSrc) {
var state = states.get(mediaId);
PSL.traceSpeed("media-authority.registerMedia.enter", {
mediaId: mediaId,
currentSrc: currentSrc,
exists: Boolean(state)
});
if (!state) {
state = normalizeState(mediaId, currentSrc);
states.set(mediaId, state);
PSL.traceSpeed("media-authority.registerMedia.created", state);
return state;
}
if (currentSrc) {
handleSourceChange(mediaId, currentSrc);
}
PSL.traceSpeed("media-authority.registerMedia.exit", state);
return state;
}
function applyTargetSpeed(mediaId, speed, scope, reason) {
var state = registerMedia(mediaId, "");
var nextScope = scope || "temporary";
PSL.traceSpeed("media-authority.applyTargetSpeed.enter", {
mediaId: mediaId,
speed: speed,
scope: nextScope,
reason: reason,
state: state
});
if (getScopeRank(nextScope) < getScopeRank(state.speedScope)) {
PSL.traceSpeed("media-authority.applyTargetSpeed.skipLowerScope", {
mediaId: mediaId,
speed: speed,
scope: nextScope,
currentScope: state.speedScope,
currentTargetSpeed: state.targetSpeed
});
return false;
}
state.speedScope = nextScope;
if (PSL.setMediaSpeedById) {
PSL.setMediaSpeedById(mediaId, speed, nextScope);
} else {
state.targetSpeed = speed;
}
if (speed > 0) {
state.syntheticMode = "none";
PSL.traceSpeed("media-authority.applyTargetSpeed.positive", {
mediaId: mediaId,
speed: speed
});
sendCommand({
type: "set-playback-rate",
mediaId: mediaId,
rate: speed,
reason: reason || "target-speed"
});
return true;
}
if (speed === 0) {
state.syntheticMode = "zero";
PSL.traceSpeed("media-authority.applyTargetSpeed.zero", { mediaId: mediaId });
sendCommand({
type: "set-paused-at-zero",
mediaId: mediaId,
enabled: true
});
return true;
}
state.syntheticMode = "reverse";
PSL.traceSpeed("media-authority.applyTargetSpeed.negative", {
mediaId: mediaId,
speed: speed
});
return true;
}
function handleObservedPlaybackRate(mediaId, observedRate) {
var state = getState(mediaId);
PSL.traceSpeed("media-authority.handleObservedPlaybackRate.enter", {
mediaId: mediaId,
observedRate: observedRate,
state: state
});
if (!state || state.syntheticMode !== "none" || state.targetSpeed <= 0) {
PSL.traceSpeed("media-authority.handleObservedPlaybackRate.skip", {
mediaId: mediaId,
observedRate: observedRate
});
return false;
}
if (Math.abs(observedRate - state.targetSpeed) < 0.001) {
PSL.traceSpeed("media-authority.handleObservedPlaybackRate.same", {
mediaId: mediaId,
observedRate: observedRate,
targetSpeed: state.targetSpeed
});
return false;
}
PSL.traceSpeed("media-authority.handleObservedPlaybackRate.restore", {
mediaId: mediaId,
observedRate: observedRate,
targetSpeed: state.targetSpeed
});
sendCommand({
type: "set-playback-rate",
mediaId: mediaId,
rate: state.targetSpeed,
reason: "restore-observed-rate"
});
return true;
}
function handleSourceChange(mediaId, nextSrc) {
var state = getState(mediaId);
PSL.traceSpeed("media-authority.handleSourceChange.enter", {
mediaId: mediaId,
nextSrc: nextSrc,
state: state
});
if (!state) {
registerMedia(mediaId, nextSrc);
return false;
}
var changed = PSL.detectMediaSourceChange(state, nextSrc);
PSL.traceSpeed("media-authority.handleSourceChange.exit", {
mediaId: mediaId,
changed: changed,
state: state
});
return changed;
}
function detachMedia(mediaId) {
var existed = states.delete(mediaId);
PSL.traceSpeed("media-authority.detachMedia", {
mediaId: mediaId,
existed: existed
});
if (existed) {
sendCommand({ type: "detach-media", mediaId: mediaId });
}
return existed;
}
return {
applyTargetSpeed: applyTargetSpeed,
detachMedia: detachMedia,
getState: getState,
handleObservedPlaybackRate: handleObservedPlaybackRate,
handleSourceChange: handleSourceChange,
registerMedia: registerMedia
};
}
PSL.createMediaAuthority = createMediaAuthority;
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/page-world-bridge.js ----
(function (PSL, global) {
var PROBE_FLAG = "PSL_V2_PAGE_WORLD_PROBE";
var DISABLED_FLAG = "PSL_V2_PAGE_WORLD_DISABLED";
var hookPath = "extension/page-world/media-hook.js";
var mediaById = new Map();
function isProbeEnabled() {
var result;
try {
result = !global.localStorage || global.localStorage.getItem(DISABLED_FLAG) !== "1";
} catch (e) {
result = true;
}
PSL.traceSpeed("page-world-bridge.isProbeEnabled", { result: result });
return result;
}
function isDebugEnabled() {
try {
return global.localStorage && global.localStorage.getItem(PROBE_FLAG) === "1";
} catch (e) {
return false;
}
}
function log(message, detail) {
if (!isDebugEnabled()) {
return;
}
if (detail) {
console.log("[Speed & Loop v2 probe] " + message, detail);
return;
}
console.log("[Speed & Loop v2 probe] " + message);
}
function getEventTarget() {
var result = typeof global.addEventListener === "function" ? global : global.window;
PSL.traceSpeed("page-world-bridge.getEventTarget", {
hasEventTarget: Boolean(result)
});
return result;
}
function createCustomEvent(type, detail) {
var EventCtor = global.CustomEvent || (global.window && global.window.CustomEvent);
var encodedDetail = encodeBridgePayload(detail);
PSL.traceSpeed("page-world-bridge.createCustomEvent", {
type: type,
detail: encodedDetail,
hasCustomEvent: typeof EventCtor === "function"
});
if (typeof EventCtor === "function") {
return new EventCtor(type, { detail: encodedDetail });
}
return { type: type, detail: encodedDetail };
}
function encodeBridgePayload(detail) {
return JSON.stringify(detail || {});
}
function decodeBridgePayload(detail) {
if (typeof detail === "string") {
try {
return JSON.parse(detail);
} catch (e) {
PSL.traceSpeed("page-world-bridge.decodeBridgePayload.error", {
message: e && e.message
});
return null;
}
}
return detail || null;
}
function getMediaId(media) {
var mediaId = PSL.getMediaKey ? PSL.getMediaKey(media) : media.currentSrc || media.src;
PSL.traceSpeed("page-world-bridge.getMediaId", {
mediaId: mediaId,
currentSrc: media && media.currentSrc,
src: media && media.src
});
return mediaId;
}
function appendScript(doc, channelId, src) {
PSL.traceSpeed("page-world-bridge.appendScript.enter", {
channelId: channelId,
src: src
});
var script = doc.createElement("script");
script.src = src;
script.async = false;
script.dataset.pslV2ProbeChannel = channelId;
script.dataset.pslV2ProbeDebug = isDebugEnabled() ? "1" : "0";
script.onload = function () {
PSL.traceSpeed("page-world-bridge.appendScript.onload", { channelId: channelId });
log("page-world hook script loaded");
if (script.parentNode) {
script.parentNode.removeChild(script);
}
};
script.onerror = function () {
PSL.traceSpeed("page-world-bridge.appendScript.onerror", { channelId: channelId, src: src });
log("page-world hook script failed to load", { src: src });
};
(doc.documentElement || doc.head || doc.body).appendChild(script);
}
function installProbe(doc) {
PSL.traceSpeed("page-world-bridge.installProbe.enter", {
hasDoc: Boolean(doc),
alreadyInstalled: Boolean(doc && doc.__pslV2PageWorldProbeInstalled)
});
if (!doc || doc.__pslV2PageWorldProbeInstalled) {
return false;
}
var src = PSL.getAssetUrl(hookPath);
if (!src) {
PSL.traceSpeed("page-world-bridge.installProbe.noAssetUrl", { hookPath: hookPath });
log("runtime asset URL is unavailable");
return false;
}
var channelId = "psl-v2-probe-" + PSL.instanceId;
var fromPage = channelId + ":from-page";
var toPage = channelId + ":to-page";
var eventTarget = getEventTarget();
function sendCommand(command) {
PSL.traceSpeed("page-world-bridge.sendCommand.enter", command);
if (eventTarget && typeof eventTarget.dispatchEvent === "function") {
eventTarget.dispatchEvent(createCustomEvent(toPage, command));
PSL.traceSpeed("page-world-bridge.sendCommand.dispatched", command);
}
}
PSL.v2MediaAuthority = PSL.v2MediaAuthority || PSL.createMediaAuthority({
sendCommand: sendCommand
});
doc.__pslV2PageWorldProbeInstalled = true;
if (eventTarget && typeof eventTarget.addEventListener === "function") {
eventTarget.addEventListener(fromPage, function (event) {
var detail = decodeBridgePayload(event.detail);
PSL.traceSpeed("page-world-bridge.fromPageEvent", detail);
log("from page", detail);
PSL.pageWorldBridge.handlePageEvent(detail);
});
}
appendScript(doc, channelId, src);
log("bridge installed", { channelId: channelId, src: src });
PSL.traceSpeed("page-world-bridge.installProbe.exit", {
channelId: channelId,
src: src
});
return true;
}
function handlePageEvent(detail) {
var authority = PSL.v2MediaAuthority;
PSL.traceSpeed("page-world-bridge.handlePageEvent.enter", detail);
if (!authority || !detail || !detail.mediaId) {
PSL.traceSpeed("page-world-bridge.handlePageEvent.skip", {
hasAuthority: Boolean(authority),
detail: detail
});
return;
}
if (
detail.type === "media-seen" ||
detail.type === "loadedmetadata" ||
detail.type === "play" ||
detail.type === "pause" ||
detail.type === "load"
) {
authority.registerMedia(detail.mediaId, detail.currentSrc || "");
return;
}
if (detail.type === "sourcechange") {
authority.handleSourceChange(detail.mediaId, detail.currentSrc || "");
resetLoopForPageSourceChange(detail);
return;
}
if (
detail.type === "ratechange" ||
detail.type === "playbackRate-set" ||
detail.type === "defaultPlaybackRate-set"
) {
authority.registerMedia(detail.mediaId, detail.currentSrc || "");
if (detail.commandedPlaybackRate === true) {
PSL.traceSpeed("page-world-bridge.handlePageEvent.skipCommandedRate", detail);
return;
}
authority.handleObservedPlaybackRate(detail.mediaId, Number(detail.playbackRate));
}
}
function applySpeed(video, speed, scope, reason) {
var mediaId;
PSL.traceSpeed("page-world-bridge.applySpeed.enter", {
speed: speed,
scope: scope,
reason: reason,
hasAuthority: Boolean(PSL.v2MediaAuthority)
});
if (!PSL.v2MediaAuthority) {
return false;
}
mediaId = getMediaId(video);
mediaById.set(mediaId, video);
PSL.v2MediaAuthority.registerMedia(mediaId, PSL.getMediaSourceKey(video));
PSL.v2MediaAuthority.applyTargetSpeed(mediaId, speed, scope, reason);
PSL.traceSpeed("page-world-bridge.applySpeed.exit", {
mediaId: mediaId,
speed: speed
});
return true;
}
function resetLoopForPageSourceChange(detail) {
var media = mediaById.get(detail.mediaId) || mediaById.get(detail.previousSrc);
PSL.traceSpeed("page-world-bridge.resetLoopForPageSourceChange", {
detail: detail,
hasMedia: Boolean(media)
});
if (media && PSL.resetLoopStateForSourceChange) {
PSL.resetLoopStateForSourceChange(media, detail.previousSrc);
}
if (media && detail.currentSrc) {
mediaById.set(detail.currentSrc, media);
}
}
PSL.pageWorldBridge = {
applySpeed: applySpeed,
handlePageEvent: handlePageEvent,
installProbe: installProbe,
isProbeEnabled: isProbeEnabled
};
if (isProbeEnabled()) {
installProbe(document);
}
})(globalThis.PlaySpeedLoop, globalThis);
// ---- code/extension/content/site-placement.js ----
(function (PSL) {
PSL.isAmazonVideoDocument = function (doc, host) {
host = host || (doc.location ? doc.location.hostname : location.hostname);
return (
/(^|\.)amazon\./.test(host) ||
/(^|\.)primevideo\.com$/.test(host) ||
Boolean(doc.getElementById("dv-web-player"))
);
};
function findAmazonVideoContainer(doc) {
var selectors = [
"#dv-web-player",
".dv-player-fullscreen",
".atvwebplayersdk-player-container",
".atvwebplayersdk-overlay-container",
".atvwebplayersdk-overlays-container",
".atvwebplayersdk-persistent-component-container",
".webPlayerSDKContainer"
];
for (var i = 0; i < selectors.length; i++) {
var container = doc.querySelector(selectors[i]);
if (container) {
return container;
}
}
return doc.body;
}
PSL.placeController = function (videoController, fragment) {
var doc = videoController.video.ownerDocument;
var host = doc.location ? doc.location.hostname : location.hostname;
var parent = videoController.parent;
var wrapper = videoController.wrapper;
switch (true) {
case PSL.isAmazonVideoDocument(doc, host):
wrapper.style.position = "fixed";
wrapper.style.zIndex = "2147483647";
wrapper.style.top = "16px";
wrapper.style.left = "16px";
wrapper.style.width = "280px";
wrapper.style.height = "90px";
wrapper.style.pointerEvents = "auto";
findAmazonVideoContainer(doc).appendChild(fragment);
break;
default:
parent.insertBefore(fragment, parent.firstChild);
}
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/overlay-critical-css.js ----
(function (PSL) {
PSL.getOverlayCriticalCss = function () {
return (
"#controller.panel-collapsed:not(.expanded) #speed-row," +
"#controller.panel-collapsed:not(.expanded) #loop-row," +
"#controller.panel-collapsed:not(.expanded) .collapse-control{" +
"display:none !important;" +
"}" +
"#controller.expanded #collapsed-summary," +
"#controller.expanded .expand-control{" +
"display:none !important;" +
"}"
);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/overlay-template.js ----
(function (PSL) {
function appendTextElement(doc, parent, tagName, text, className, id) {
var element = doc.createElement(tagName);
if (id) {
element.id = id;
}
if (className) {
element.className = className;
}
element.textContent = text;
parent.appendChild(element);
return element;
}
function appendButton(doc, parent, action, text, className, id) {
var button = appendTextElement(doc, parent, "button", text, className, id);
if (action) {
button.dataset.action = action;
}
return button;
}
PSL.buildOverlayStyleElement = function (doc) {
var style = doc.createElement("style");
var overlayStyleUrl = PSL.getAssetUrl("extension/styles/overlay-shadow.css");
style.textContent =
(overlayStyleUrl ? '@import "' + overlayStyleUrl + '";' : "") +
PSL.getOverlayCriticalCss();
return style;
};
PSL.buildOverlayControllerContent = function (doc, options) {
var fragment = doc.createDocumentFragment();
var controller = doc.createElement("div");
var controllerClasses = [];
if (options.speedControlsEnabled) {
controllerClasses.push("speed-enabled");
}
if (options.loopControlsEnabled) {
controllerClasses.push("loop-enabled");
}
controller.id = "controller";
controllerClasses.push("panel-collapsed");
controller.className = controllerClasses.join(" ");
controller.style.top = options.top;
controller.style.left = options.left;
if (controller.style.setProperty) {
controller.style.setProperty("--psl-panel-background-opacity", String(options.opacity));
} else {
controller.style["--psl-panel-background-opacity"] = String(options.opacity);
}
if (options.speedControlsEnabled) {
var speedRow = doc.createElement("div");
speedRow.id = "speed-row";
speedRow.className = "control-row";
appendTextElement(doc, speedRow, "span", "Speed", "row-label");
appendButton(doc, speedRow, "slower", "-");
var speedExpandedSlot = doc.createElement("span");
speedExpandedSlot.dataset.pslSlot = "speed-expanded";
speedRow.appendChild(speedExpandedSlot);
appendButton(doc, speedRow, "faster", "+");
appendButton(doc, speedRow, "reset", "Reset", "reset-button");
controller.appendChild(speedRow);
}
if (options.loopControlsEnabled || options.speedControlsEnabled) {
var collapsedSummary = doc.createElement("div");
collapsedSummary.id = "collapsed-summary";
collapsedSummary.className = "body";
var speedCompactSlot = doc.createElement("span");
speedCompactSlot.dataset.pslSlot = "speed-compact";
collapsedSummary.appendChild(speedCompactSlot);
if (options.loopControlsEnabled) {
var loopSummarySlot = doc.createElement("span");
loopSummarySlot.dataset.pslSlot = "loop-summary";
loopSummarySlot.className = "loop-summary";
collapsedSummary.appendChild(loopSummarySlot);
}
controller.appendChild(collapsedSummary);
}
if (options.loopControlsEnabled) {
var loopRow = doc.createElement("div");
loopRow.id = "loop-row";
loopRow.className = "control-row";
appendTextElement(doc, loopRow, "span", "Loop", "row-label");
appendTextElement(doc, loopRow, "span", "--:--", "time-value empty", "start-indicator");
appendTextElement(doc, loopRow, "span", "--:--", "time-value empty", "end-indicator");
appendButton(doc, loopRow, "clear-loop", "Reset", "reset-button");
controller.appendChild(loopRow);
}
appendButton(
doc,
controller,
null,
"",
"panel-toggle chevron collapse-control",
"collapse-button"
).setAttribute("aria-label", "Collapse controller");
appendButton(
doc,
controller,
null,
"",
"panel-toggle toggle expand-control",
"expand-button"
).setAttribute("aria-label", "Expand controller");
fragment.appendChild(PSL.buildOverlayStyleElement(doc));
fragment.appendChild(controller);
return fragment;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-indicator-view.js ----
(function (PSL) {
PSL.formatSpeedDisplayValue = function (speed) {
return speed.toFixed(2);
};
PSL.createSpeedView = function (options) {
var video = options.video;
var doc = options.doc;
var compactSlot = options.compactSlot || options.mountSlot;
var expandedSlot = options.expandedSlot || compactSlot;
var root = doc.createElement("span");
root.id = "speed-indicator";
root.className = "speed-value";
root.dataset.pslOwner = "speed-view";
function moveTo(slot) {
if (slot && root.parentNode !== slot) {
slot.appendChild(root);
}
}
function render() {
var speed = PSL.getMediaSpeed ? PSL.getMediaSpeed(video) : video.playbackRate;
root.textContent = PSL.formatSpeedDisplayValue(speed);
}
return {
mount: function () {
moveTo(compactSlot);
},
render: render,
showCompact: function () {
moveTo(compactSlot);
},
showExpanded: function () {
moveTo(expandedSlot);
},
destroy: function () {
root.remove();
}
};
};
PSL.renderSpeedView = function (video) {
if (!video._pslControllerHandle || !video._pslControllerHandle.renderSpeedView) {
return;
}
video._pslControllerHandle.renderSpeedView();
};
PSL.updateSpeedIndicator = function (video) {
PSL.renderSpeedView(video);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/loop-actions.js ----
(function (PSL) {
function getLoopKey(video) {
return PSL.getMediaKey(video);
}
function hasNumber(value) {
return typeof value === "number" && !isNaN(value);
}
function getEffectiveRate(video) {
if (PSL.getEffectivePlaybackRate) {
return PSL.getEffectivePlaybackRate(video);
}
return video.playbackRate;
}
function normalizeEndpoint(video, time) {
var point = Number(time);
if (isNaN(point) || point < 0) {
return NaN;
}
if (video.duration && point >= video.duration) {
return video.duration - 0.05;
}
return point;
}
function formatSummary(left, right, pending, enabled) {
if (hasNumber(pending)) {
return {
text: PSL.convertSecToMin(pending) + " / __:__",
left: PSL.convertSecToMin(pending),
right: "__:__",
rightBlank: true
};
}
if (enabled && hasNumber(left) && hasNumber(right) && right > left) {
return {
text: PSL.convertSecToMin(left) + " / " + PSL.convertSecToMin(right),
left: PSL.convertSecToMin(left),
right: PSL.convertSecToMin(right),
rightBlank: false
};
}
return {
text: "",
left: "",
right: "",
rightBlank: false
};
}
PSL.convertSecToMin = function (timeInSecs) {
var tempDate = new Date(null);
tempDate.setSeconds(Math.round(timeInSecs));
return timeInSecs >= 3600
? tempDate.toISOString().substring(11, 19)
: tempDate.toISOString().substring(14, 19);
};
PSL.getLoopSummary = function (video) {
var src = getLoopKey(video);
var left = PSL.state.startTimes[src];
var right = PSL.state.endTimes[src];
var pending = PSL.state.pendingLoopPoints[src];
return formatSummary(left, right, pending, PSL.state.loopsEnabled[src]);
};
PSL.renderLoopSummary = function (video) {
var summary = PSL.getLoopSummary(video);
return summary.text;
};
PSL.updateCollapsedLoopSummary = function (video) {
return PSL.renderLoopSummary(video);
};
PSL.expandLoopController = function (video) {
var handle = video._pslControllerHandle;
if (!handle || !handle.setPanelExpanded) {
return;
}
handle.setPanelExpanded(true);
};
PSL.updateLoopRangeDisplay = function (video) {
var handle = video._pslControllerHandle;
if (!handle || !handle.renderLoopRange) {
return;
}
handle.renderLoopRange();
};
PSL.markLoopRange = function (video, time) {
var src = getLoopKey(video);
var point = normalizeEndpoint(video, time);
var pending = PSL.state.pendingLoopPoints[src];
var left;
var right;
PSL.traceSpeed("loop-actions.markLoopRange.enter", {
mediaKey: src,
time: time,
point: point,
pending: pending
});
if (isNaN(point)) {
PSL.traceSpeed("loop-actions.markLoopRange.invalid", { mediaKey: src, time: time });
return;
}
PSL.expandLoopController(video);
if (!hasNumber(pending) || PSL.state.loopsEnabled[src]) {
PSL.disableLoop(video);
PSL.state.pendingLoopPoints[src] = point;
PSL.state.startTimes[src] = point;
PSL.state.endTimes[src] = 0;
PSL.updateLoopRangeDisplay(video);
PSL.traceSpeed("loop-actions.markLoopRange.pending", {
mediaKey: src,
point: point
});
return;
}
left = Math.min(pending, point);
right = Math.max(pending, point);
if (right <= left) {
PSL.updateLoopRangeDisplay(video);
PSL.traceSpeed("loop-actions.markLoopRange.tooShort", {
mediaKey: src,
left: left,
right: right
});
return;
}
PSL.state.startTimes[src] = left;
PSL.state.endTimes[src] = right;
delete PSL.state.pendingLoopPoints[src];
PSL.enableLoop(video);
PSL.updateLoopRangeDisplay(video);
PSL.traceSpeed("loop-actions.markLoopRange.range", {
mediaKey: src,
left: left,
right: right
});
};
PSL.setLoopRange = function (video, left, right) {
var src = getLoopKey(video);
var normalizedLeft = normalizeEndpoint(video, Math.min(left, right));
var normalizedRight = normalizeEndpoint(video, Math.max(left, right));
if (isNaN(normalizedLeft) || isNaN(normalizedRight) || normalizedRight <= normalizedLeft) {
PSL.clearLoop(video);
return;
}
PSL.state.startTimes[src] = normalizedLeft;
PSL.state.endTimes[src] = normalizedRight;
delete PSL.state.pendingLoopPoints[src];
PSL.enableLoop(video);
PSL.updateLoopRangeDisplay(video);
};
PSL.enableLoop = function (video) {
var src = getLoopKey(video);
var left = PSL.state.startTimes[src];
var right = PSL.state.endTimes[src];
if (!hasNumber(left) || !hasNumber(right) || right <= left) {
PSL.state.loopsEnabled[src] = false;
PSL.updateLoopRangeDisplay(video);
return;
}
PSL.state.loopsEnabled[src] = true;
var handle = video._pslControllerHandle;
if (!handle || !handle.setLoopHandler) {
PSL.updateLoopRangeDisplay(video);
return;
}
handle.setLoopHandler(function () {
var currentSrc = getLoopKey(video);
var currentLeft = PSL.state.startTimes[currentSrc];
var currentRight = PSL.state.endTimes[currentSrc];
var rate = getEffectiveRate(video);
if (!PSL.state.loopsEnabled[currentSrc]) {
if (video._pslControllerHandle && video._pslControllerHandle.clearLoopHandler) {
video._pslControllerHandle.clearLoopHandler();
}
return;
}
if (rate < 0) {
if (video.currentTime <= currentLeft) {
video.currentTime = currentRight;
}
return;
}
if (video.currentTime >= currentRight || video.currentTime < currentLeft) {
video.currentTime = currentLeft;
}
});
PSL.updateLoopRangeDisplay(video);
};
PSL.disableLoop = function (video) {
var src = getLoopKey(video);
var handle = video._pslControllerHandle;
PSL.state.loopsEnabled[src] = false;
if (handle && handle.clearLoopHandler) {
handle.clearLoopHandler();
}
PSL.updateLoopRangeDisplay(video);
};
PSL.clearLoop = function (video) {
var src = getLoopKey(video);
PSL.disableLoop(video);
PSL.state.startTimes[src] = 0;
PSL.state.endTimes[src] = 0;
delete PSL.state.pendingLoopPoints[src];
PSL.updateLoopRangeDisplay(video);
PSL.traceSpeed("loop-actions.clearLoop", { mediaKey: src });
};
PSL.updateLoopToggleDisplay = function (video) {
PSL.updateLoopRangeDisplay(video);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/loop-view.js ----
(function (PSL) {
function hasNumber(value) {
return typeof value === "number" && !isNaN(value);
}
function setIndicator(indicator, text, empty) {
if (!indicator) {
return;
}
indicator.textContent = text;
if (indicator.classList) {
indicator.classList.toggle("empty", Boolean(empty));
}
}
function appendSummaryPart(doc, parent, text, className) {
var span = doc.createElement("span");
span.className = className;
span.textContent = text;
parent.appendChild(span);
}
PSL.createLoopView = function (options) {
var video = options.video;
var startSlot = options.startSlot;
var endSlot = options.endSlot;
var summarySlot = options.summarySlot;
function renderSummary() {
var summary = PSL.getLoopSummary ? PSL.getLoopSummary(video) : { text: "" };
var doc;
if (!summarySlot) {
return summary.text;
}
if (!summarySlot.ownerDocument || !summarySlot.replaceChildren) {
summarySlot.textContent = summary.text;
return summary.text;
}
doc = summarySlot.ownerDocument;
summarySlot.replaceChildren();
if (summary.left || summary.right) {
appendSummaryPart(doc, summarySlot, summary.left, "summary-time");
appendSummaryPart(doc, summarySlot, " / ", "summary-separator");
appendSummaryPart(
doc,
summarySlot,
summary.right,
summary.rightBlank ? "summary-time blank" : "summary-time"
);
}
return summary.text;
}
function clearRange() {
setIndicator(startSlot, "--:--", true);
setIndicator(endSlot, "--:--", true);
if (summarySlot) {
if (summarySlot.replaceChildren) {
summarySlot.replaceChildren();
} else {
summarySlot.textContent = "";
}
}
}
return {
renderRange: function () {
var src = PSL.getMediaKey(video);
var left = PSL.state.startTimes[src];
var right = PSL.state.endTimes[src];
var pending = PSL.state.pendingLoopPoints[src];
if (hasNumber(pending)) {
setIndicator(startSlot, PSL.convertSecToMin(pending), false);
setIndicator(endSlot, "--:--", true);
} else if (PSL.state.loopsEnabled[src] && hasNumber(left) && hasNumber(right) && right > left) {
setIndicator(startSlot, PSL.convertSecToMin(left), false);
setIndicator(endSlot, PSL.convertSecToMin(right), false);
} else {
setIndicator(startSlot, "--:--", true);
setIndicator(endSlot, "--:--", true);
}
renderSummary();
},
clearRange: clearRange,
renderSummary: renderSummary,
destroy: clearRange
};
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/media-overlay-controller.js ----
(function (PSL) {
function hasMediaSource(media) {
return Boolean(media.currentSrc || media.src || media.srcObject);
}
function createControllerHandle(controller) {
return {
isCurrentInstance: function (instanceId) {
return controller._pslInstanceId === instanceId;
},
getParent: function () {
return controller.parent;
},
matchesControllerElement: function (element) {
return Boolean(element && controller.div === element);
},
remove: function () {
controller.remove();
},
setPanelExpanded: function (expanded) {
controller.setPanelExpanded(expanded);
},
renderSpeedView: function () {
controller.renderSpeedView();
},
renderLoopRange: function () {
controller.renderLoopRange();
},
clearLoopRangeDisplay: function () {
controller.clearLoopRangeDisplay();
},
setLoopHandler: function (handler) {
controller.setLoopHandler(handler);
},
clearLoopHandler: function () {
controller.clearLoopHandler();
},
showController: function () {
controller.showController();
},
blinkController: function () {
controller.blinkController();
},
updateSourceVisibility: function () {
controller.updateSourceVisibility();
},
applyStoredDomainSpeed: function () {
controller.applyStoredDomainSpeed();
}
};
}
PSL.VideoController = function (target, parent) {
var controller = this;
if (target._pslControllerHandle) {
return target._pslControllerHandle;
}
PSL.registerMedia(target);
this._pslInstanceId = PSL.instanceId;
this.video = target;
this.parent = target.parentElement || parent;
this._loopHandler = null;
this.updateSourceVisibility = function () {
if (!controller.div) {
return;
}
controller.div.classList.toggle("mpc-nosource", !hasMediaSource(target));
};
this.applyStoredDomainSpeed = function () {
if (PSL.applyStoredDomainSpeed) {
PSL.applyStoredDomainSpeed(target);
}
};
PSL.state.startTimes[PSL.getMediaKey(target)] = 0;
target.addEventListener(
"loadedmetadata",
(this.handleLoadedMetadata = function () {
controller.updateSourceVisibility();
controller.applyStoredDomainSpeed();
})
);
PSL.state.loopsEnabled[PSL.getMediaKey(target)] = false;
this.div = this.initializeControls();
target._pslControllerHandle = createControllerHandle(this);
this.applyStoredDomainSpeed();
if (PSL.updateLoopRangeDisplay) {
PSL.updateLoopRangeDisplay(this.video);
}
this.handleSourceVisibilityUpdate = function () {
controller.updateSourceVisibility();
controller.applyStoredDomainSpeed();
};
target.addEventListener("loadstart", this.handleSourceVisibilityUpdate);
target.addEventListener("loadeddata", this.handleSourceVisibilityUpdate);
target.addEventListener("canplay", this.handleSourceVisibilityUpdate);
target.addEventListener("emptied", this.handleSourceVisibilityUpdate);
this.sourceObserver = new MutationObserver(
function (mutations) {
mutations.forEach(function (mutation) {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "src" || mutation.attributeName === "currentSrc")
) {
PSL.log("mutation of A/V element", 5);
if (target._pslControllerHandle) {
controller.updateSourceVisibility();
controller.applyStoredDomainSpeed();
}
}
});
}
);
this.sourceObserver.observe(target, {
attributeFilter: ["src", "currentSrc"]
});
};
PSL.VideoController.prototype.remove = function () {
if (this.destroySpeedView) {
this.destroySpeedView();
}
this.div.remove();
this.video.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
this.video.removeEventListener("loadstart", this.handleSourceVisibilityUpdate);
this.video.removeEventListener("loadeddata", this.handleSourceVisibilityUpdate);
this.video.removeEventListener("canplay", this.handleSourceVisibilityUpdate);
this.video.removeEventListener("emptied", this.handleSourceVisibilityUpdate);
if (this._loopHandler) {
this.video.removeEventListener("timeupdate", this._loopHandler);
}
if (this.sourceObserver) {
this.sourceObserver.disconnect();
}
if (PSL.stopSyntheticSpeed) {
PSL.stopSyntheticSpeed(this.video);
}
delete this.video._pslControllerHandle;
PSL.unregisterMedia(this.video);
};
PSL.VideoController.prototype.initializeControls = function () {
PSL.log("initializeControls Begin", 5);
var doc = this.video.ownerDocument;
var top = Math.max(this.video.offsetTop, 0) + "px";
var left = Math.max(this.video.offsetLeft, 0) + "px";
var wrapper = doc.createElement("div");
wrapper.classList.add("mpc-controller");
wrapper._pslInstanceId = PSL.instanceId;
wrapper._pslMediaElement = this.video;
if (!hasMediaSource(this.video)) {
wrapper.classList.add("mpc-nosource");
}
var shadow = wrapper.attachShadow({ mode: "closed" });
shadow.appendChild(PSL.buildOverlayControllerContent(doc, {
top: top,
left: left,
opacity: PSL.settings.controllerOpacity,
speedControlsEnabled: PSL.settings.speedControlsEnabled !== false,
loopControlsEnabled: PSL.settings.loopControlsEnabled !== false
}));
wrapper._shadow = shadow;
shadow.querySelectorAll("button").forEach(function (button) {
button.addEventListener(
"click",
function (e) {
var eventTarget = e.target || button;
var action = eventTarget.dataset.action;
if (!action) {
return;
}
PSL.runAction(action, PSL.getKeyBinding(action), e);
e.stopPropagation();
},
true
);
});
var controllerElement = shadow.querySelector("#controller");
var collapseButton = shadow.querySelector("#collapse-button");
var expandButton = shadow.querySelector("#expand-button");
var speedExpandedSlot = shadow.querySelector('[data-psl-slot="speed-expanded"]');
var speedCompactSlot = shadow.querySelector('[data-psl-slot="speed-compact"]');
var loopSummarySlot = shadow.querySelector('[data-psl-slot="loop-summary"]');
var startSlot = shadow.querySelector("#start-indicator");
var endSlot = shadow.querySelector("#end-indicator");
var speedView = null;
var loopView = null;
var setPanelExpanded = function (expanded) {
controllerElement.classList.toggle("expanded", Boolean(expanded));
controllerElement.classList.toggle("panel-collapsed", !expanded);
if (!speedView) {
return;
}
if (expanded) {
speedView.showExpanded();
} else {
speedView.showCompact();
}
};
if (PSL.createSpeedView && speedCompactSlot) {
speedView = PSL.createSpeedView({
video: this.video,
doc: doc,
compactSlot: speedCompactSlot,
expandedSlot: speedExpandedSlot
});
speedView.mount();
speedView.render();
}
if (PSL.createLoopView) {
loopView = PSL.createLoopView({
video: this.video,
startSlot: startSlot,
endSlot: endSlot,
summarySlot: loopSummarySlot
});
}
this.renderSpeedView = function () {
if (speedView) {
speedView.render();
}
};
this.destroySpeedView = function () {
if (speedView) {
speedView.destroy();
speedView = null;
}
};
this.renderLoopRange = function () {
if (loopView) {
loopView.renderRange();
}
};
this.clearLoopRangeDisplay = function () {
if (loopView) {
loopView.clearRange();
}
};
this.setLoopHandler = function (handler) {
this.clearLoopHandler();
if (handler) {
this._loopHandler = handler;
this.video.addEventListener("timeupdate", handler);
}
};
this.clearLoopHandler = function () {
if (this._loopHandler) {
this.video.removeEventListener("timeupdate", this._loopHandler);
this._loopHandler = null;
}
};
this.showController = function () {
wrapper.classList.add("mpc-show");
wrapper.classList.remove("mpc-hidden");
};
this.blinkController = function () {
wrapper.classList.add("mpc-show");
wrapper.classList.remove("mpc-hidden");
};
if (collapseButton) {
collapseButton.addEventListener("click", function (e) {
setPanelExpanded(false);
e.stopPropagation();
}, true);
}
if (expandButton) {
expandButton.addEventListener("click", function (e) {
setPanelExpanded(true);
e.stopPropagation();
}, true);
}
controllerElement.addEventListener("mouseenter", function (e) { return e.stopPropagation(); }, false);
controllerElement.addEventListener("mouseleave", function (e) { return e.stopPropagation(); }, false);
controllerElement.addEventListener("click", function (e) { return e.stopPropagation(); }, false);
controllerElement.addEventListener("mousedown", function (e) { return e.stopPropagation(); }, false);
this.setPanelExpanded = setPanelExpanded;
var fragment = doc.createDocumentFragment();
fragment.appendChild(wrapper);
this.div = wrapper;
PSL.placeController({
video: this.video,
parent: this.parent,
wrapper: wrapper
}, fragment);
return wrapper;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/controller-attachment-lifecycle.js ----
(function (PSL) {
function isCurrentController(media) {
try {
return (
media._pslControllerHandle &&
media._pslControllerHandle.isCurrentInstance &&
media._pslControllerHandle.isCurrentInstance(PSL.instanceId)
);
} catch (e) {
return false;
}
}
function isControllerAttachedToCurrentParent(media) {
try {
return (
media._pslControllerHandle &&
media._pslControllerHandle.getParent &&
media._pslControllerHandle.getParent() === media.parentElement
);
} catch (e) {
return false;
}
}
function removeOrphanControllerElements(media) {
var doc = media && media.ownerDocument;
if (!doc || !doc.querySelectorAll) {
return;
}
doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
if (
controller._pslInstanceId === PSL.instanceId &&
controller._pslMediaElement === media &&
(!media._pslControllerHandle ||
!media._pslControllerHandle.matchesControllerElement ||
!media._pslControllerHandle.matchesControllerElement(controller))
) {
controller.remove();
}
});
}
PSL.removeControllerFromMedia = function (media) {
if (!media._pslControllerHandle) {
removeOrphanControllerElements(media);
return;
}
try {
if (typeof media._pslControllerHandle.remove === "function") {
media._pslControllerHandle.remove();
return;
}
} catch (e) {
// Fall through and remove the visible controller element directly.
}
delete media._pslControllerHandle;
PSL.unregisterMedia(media);
removeOrphanControllerElements(media);
};
PSL.attachControllerToMedia = function (media) {
removeOrphanControllerElements(media);
if (!PSL.shouldAttachController()) {
return;
}
if (isCurrentController(media)) {
if (!isControllerAttachedToCurrentParent(media)) {
PSL.removeControllerFromMedia(media);
new PSL.VideoController(media);
return;
}
if (media._pslControllerHandle && media._pslControllerHandle.applyStoredDomainSpeed) {
media._pslControllerHandle.applyStoredDomainSpeed();
}
return;
}
PSL.removeControllerFromMedia(media);
if (!media._pslControllerHandle) {
new PSL.VideoController(media);
}
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-domain-store.js ----
(function (PSL) {
function getSpeedDomain(video) {
var doc = video.ownerDocument || document;
var host = "";
try {
host = doc.location && doc.location.hostname;
} catch (e) {}
return String(host || "").replace(/^www\./, "");
}
PSL.isPositiveStoredSpeed = function (speed) {
return typeof speed === "number" && !isNaN(speed) && speed > 0 && speed <= 16;
};
PSL.saveDomainSpeed = function (video, speed) {
var domain = getSpeedDomain(video);
var domainSpeeds;
if (!domain || !PSL.isPositiveStoredSpeed(speed)) {
return;
}
domainSpeeds = Object.assign({}, PSL.settings.domainSpeeds || {});
domainSpeeds[domain] = speed;
PSL.settings.domainSpeeds = domainSpeeds;
PSL.settings.lastSpeed = speed;
PSL.setStorage(
{
lastSpeed: speed,
domainSpeeds: domainSpeeds
},
function () {
PSL.log("Domain speed setting saved for " + domain + ": " + speed, 5);
}
);
};
PSL.getSavedDomainSpeed = function (video) {
var domain = getSpeedDomain(video);
return domain && PSL.settings.domainSpeeds
? Number(PSL.settings.domainSpeeds[domain])
: NaN;
};
PSL.shouldRestoreSavedDomainSpeed = function (video) {
return (
(!PSL.hasMediaSpeed || !PSL.hasMediaSpeed(video)) &&
PSL.isPositiveStoredSpeed(PSL.getSavedDomainSpeed(video))
);
};
PSL.applyStoredDomainSpeed = function (video) {
var speed = PSL.getSavedDomainSpeed(video);
var resolvedSpeed = PSL.getEffectiveSpeedFromHierarchy
? PSL.getEffectiveSpeedFromHierarchy({
temporarySpeed: null,
siteSpeed: speed,
globalSpeed: 1,
minSpeed: -16,
maxSpeed: 16
})
: { speed: speed, scope: "site" };
if (
PSL.settings.speedControlsEnabled === false ||
!PSL.isPositiveStoredSpeed(speed) ||
(PSL.hasMediaSpeed && PSL.hasMediaSpeed(video)) ||
resolvedSpeed.scope !== "site" ||
video._pslApplyingStoredDomainSpeed
) {
return false;
}
video._pslApplyingStoredDomainSpeed = true;
PSL.setSpeed(video, speed);
video._pslApplyingStoredDomainSpeed = false;
return true;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/keep-speed-site-matcher.js ----
(function (PSL) {
function normalizeKeepSpeedPattern(pattern) {
pattern = String(pattern || "").replace(PSL.regStrip, "");
if (!pattern) {
return "";
}
try {
if (/^https?:\/\//i.test(pattern)) {
return new URL(pattern).hostname;
}
} catch (e) {}
return pattern.replace(/^www\./, "");
}
PSL.buildKeepSpeedMatcher = function (pattern) {
var normalized = normalizeKeepSpeedPattern(pattern);
var regex;
if (!normalized) {
return null;
}
if (normalized.startsWith("/")) {
try {
regex = new RegExp(normalized.slice(1, -1));
} catch (e) {
return null;
}
return function (host) {
return regex.test(host);
};
}
return function (host) {
return host === normalized || host.endsWith("." + normalized);
};
};
PSL.buildKeepSpeedMatchers = function (patterns) {
return String(patterns || "")
.split("\n")
.map(PSL.buildKeepSpeedMatcher)
.filter(Boolean);
};
PSL.updateKeepSpeedSiteMatchers = function () {
PSL.keepSpeedSiteMatchers = PSL.buildKeepSpeedMatchers(
PSL.settings.keepSpeedSites
);
};
PSL.isKeepSpeedSite = function (video) {
var doc = video.ownerDocument || document;
var host = "";
try {
host = (doc.location && doc.location.hostname || "").replace(/^www\./, "");
} catch (e) {}
return (PSL.keepSpeedSiteMatchers || []).some(function (matches) {
return matches(host);
});
};
PSL.updateKeepSpeedSiteMatchers();
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/keep-speed-site-enforcement.js ----
(function (PSL) {
var KEEP_SPEED_ENFORCEMENT_INTERVAL_MS = 50;
function isPositivePlaybackRate(speed) {
return typeof speed === "number" && !isNaN(speed) && speed > 0 && speed <= 16;
}
PSL.stopPersistentSpeedEnforcement = function (video) {
if (video._pslSpeedEnforcer) {
clearInterval(video._pslSpeedEnforcer);
video._pslSpeedEnforcer = null;
}
};
PSL.startPersistentSpeedEnforcement = function (video) {
PSL.stopPersistentSpeedEnforcement(video);
if (
!PSL.isKeepSpeedSite(video) &&
(!PSL.getMediaSpeed || !isPositivePlaybackRate(PSL.getMediaSpeed(video)))
) {
return;
}
video._pslSpeedEnforcer = setInterval(function () {
var target = PSL.getMediaSpeed(video);
if (
!video.isConnected ||
!isPositivePlaybackRate(target) ||
video._pslPausedForZeroSpeed ||
video._pslReversing
) {
PSL.stopPersistentSpeedEnforcement(video);
return;
}
if (Math.abs(video.playbackRate - target) > 0.001) {
video.playbackRate = target;
}
PSL.updateSpeedIndicator(video);
}, KEEP_SPEED_ENFORCEMENT_INTERVAL_MS);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/temporary-playback-rate-enforcement.js ----
(function (PSL) {
PSL.shouldTemporarilyEnforceSpeed = function (video) {
return (
PSL.hasMediaSpeed &&
PSL.hasMediaSpeed(video) &&
video._pslEnforceSpeedUntil &&
Date.now() < video._pslEnforceSpeedUntil
);
};
PSL.enforceSpeedSoon = function (video) {
if (!PSL.shouldTemporarilyEnforceSpeed(video)) {
return;
}
setTimeout(function () {
if (!PSL.shouldTemporarilyEnforceSpeed(video) || !video.isConnected) {
return;
}
var speed = PSL.getMediaSpeed(video);
if (speed > 0 && Math.abs(video.playbackRate - speed) > 0.001) {
video.playbackRate = speed;
}
}, 0);
};
PSL.expectPlaybackRateChange = function (video, speed) {
video._pslExpectedPlaybackRate = speed;
};
PSL.consumeExpectedPlaybackRateChange = function (video) {
var expected = video._pslExpectedPlaybackRate;
if (
typeof expected === "number" &&
Math.abs(video.playbackRate - expected) < 0.001
) {
video._pslExpectedPlaybackRate = undefined;
return true;
}
return false;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/synthetic-speed-state.js ----
(function (PSL) {
function rememberPauseStateBeforeSyntheticSpeed(video) {
if (
!PSL.isSyntheticPausedSpeed(video) &&
typeof video._pslWasPausedBeforeSyntheticSpeed === "undefined"
) {
video._pslWasPausedBeforeSyntheticSpeed = video.paused;
}
}
function getReverseLoopBoundary(video) {
var mediaKey = PSL.getMediaKey(video);
var start = PSL.state.startTimes[mediaKey];
var end = PSL.state.endTimes[mediaKey];
if (
PSL.state.loopsEnabled[mediaKey] &&
typeof start === "number" &&
typeof end === "number" &&
end > start
) {
return { start: start, end: end };
}
return null;
}
PSL.isSyntheticPausedSpeed = function (video) {
return video._pslPausedForZeroSpeed || video._pslReversing;
};
PSL.stopReversePlayback = function (video) {
if (PSL.reversePlaybackEngine) {
PSL.reversePlaybackEngine.stop(video);
}
video._pslReversing = false;
};
PSL.clearSyntheticSpeedState = function (video) {
PSL.stopReversePlayback(video);
PSL.stopPersistentSpeedEnforcement(video);
video._pslPausedForZeroSpeed = false;
video._pslWasPausedBeforeSyntheticSpeed = undefined;
};
PSL.resumeAfterSyntheticSpeedIfNeeded = function (video, wasSyntheticSpeed) {
var shouldResume =
wasSyntheticSpeed && video._pslWasPausedBeforeSyntheticSpeed === false;
PSL.clearSyntheticSpeedState(video);
if (shouldResume) {
var playPromise = video.play();
if (playPromise && playPromise.catch) {
playPromise.catch(function () {});
}
}
};
PSL.pauseAtZeroSpeed = function (video) {
PSL.stopReversePlayback(video);
PSL.stopPersistentSpeedEnforcement(video);
rememberPauseStateBeforeSyntheticSpeed(video);
video._pslPausedForZeroSpeed = true;
video._pslEnforceSpeedUntil = 0;
PSL.updateSpeedIndicator(video);
video.pause();
};
PSL.startReversePlayback = function (video, speed) {
var frameRate = PSL.getReverseFrameRate();
var intervalMs = 1000 / frameRate;
var stepSeconds = Math.abs(speed) / frameRate;
var boundary = getReverseLoopBoundary(video);
PSL.stopReversePlayback(video);
rememberPauseStateBeforeSyntheticSpeed(video);
if (!PSL.reversePlaybackEngine) {
video._pslPausedForZeroSpeed = false;
video._pslReversing = false;
video._pslEnforceSpeedUntil = 0;
PSL.traceSpeed("synthetic-speed.startReversePlayback.missingEngine", {
speed: speed
});
return;
}
video._pslPausedForZeroSpeed = false;
video._pslReversing = true;
video._pslEnforceSpeedUntil = 0;
PSL.updateSpeedIndicator(video);
video.pause();
PSL.reversePlaybackEngine.start(video, speed, {
intervalMs: intervalMs,
stepSeconds: stepSeconds,
loopStart: boundary && boundary.start,
loopEnd: boundary && boundary.end,
onEnd: function () {
PSL.setSpeed(video, 0);
},
onStop: function () {
video._pslReversing = false;
}
});
};
PSL.getEffectivePlaybackRate = function (video) {
return PSL.getMediaSpeed ? PSL.getMediaSpeed(video) : video.playbackRate;
};
PSL.stopSyntheticSpeed = function (video) {
PSL.clearSyntheticSpeedState(video);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-actions.js ----
(function (PSL) {
PSL.clampSpeed = function (speed, min, max) {
var result = Math.min(Math.max(speed, min), max);
PSL.traceSpeed("speed-actions.clampSpeed", {
speed: speed,
min: min,
max: max,
result: result
});
return result;
};
PSL.getSpeedAdjustmentBase = function (video) {
var result;
if (PSL.getEffectivePlaybackRate) {
result = PSL.getEffectivePlaybackRate(video);
} else {
result = video.playbackRate;
}
PSL.traceSpeed("speed-actions.getSpeedAdjustmentBase", {
nativePlaybackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video),
result: result
});
return result;
};
PSL.adjustSpeed = function (video, value, options) {
var relative;
var currentSpeed;
var targetSpeed;
PSL.traceSpeed("speed-actions.adjustSpeed.enter", {
value: value,
options: options,
currentPlaybackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
options = options || {};
relative = options.relative === true;
if (typeof value !== "number" || isNaN(value)) {
PSL.traceSpeed("speed-actions.adjustSpeed.invalidValue", { value: value });
return;
}
if (relative) {
currentSpeed = PSL.getSpeedAdjustmentBase(video);
targetSpeed = currentSpeed + value;
if (
(currentSpeed > 1.0 && targetSpeed < 1.0) ||
(currentSpeed < 1.0 && targetSpeed > 1.0)
) {
targetSpeed = 1.0;
}
} else {
targetSpeed = value;
}
targetSpeed = Number(PSL.clampSpeed(targetSpeed, -16, 16).toFixed(2));
PSL.traceSpeed("speed-actions.adjustSpeed.target", {
relative: relative,
currentSpeed: currentSpeed,
targetSpeed: targetSpeed
});
PSL.setSpeed(video, targetSpeed);
};
PSL.setSpeed = function (video, speed) {
PSL.traceSpeed("speed-actions.setSpeed.enter", {
speed: speed,
speedControlsEnabled: PSL.settings.speedControlsEnabled,
currentPlaybackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
if (PSL.settings.speedControlsEnabled === false) {
PSL.traceSpeed("speed-actions.setSpeed.disabled");
return;
}
PSL.log("setSpeed started: " + speed, 5);
var speedValue = speed.toFixed(2);
var speedNumber = Number(speedValue);
var wasSyntheticSpeed = PSL.isSyntheticPausedSpeed(video);
var speedScope = video._pslApplyingStoredDomainSpeed ? "site" : "temporary";
PSL.setMediaSpeed(video, speedNumber, speedScope);
if (speedNumber <= 0) {
video._pslEnforceSpeedUntil = 0;
video._pslExpectedPlaybackRate = undefined;
PSL.stopPersistentSpeedEnforcement(video);
}
if (
PSL.pageWorldBridge &&
PSL.pageWorldBridge.isProbeEnabled &&
PSL.pageWorldBridge.isProbeEnabled() &&
PSL.pageWorldBridge.applySpeed
) {
PSL.traceSpeed("speed-actions.setSpeed.pageWorld.before", {
speed: speedNumber,
scope: speedScope
});
PSL.pageWorldBridge.applySpeed(video, speedNumber, speedScope, "set-speed");
PSL.traceSpeed("speed-actions.setSpeed.pageWorld.after", {
speed: speedNumber,
scope: speedScope
});
PSL.log("setSpeed sent to v2 media authority: " + speedNumber, 5);
}
if (speedNumber === 0) {
PSL.traceSpeed("speed-actions.setSpeed.zero.before");
PSL.pauseAtZeroSpeed(video);
PSL.traceSpeed("speed-actions.setSpeed.zero.after", {
paused: video && video.paused
});
PSL.log("setSpeed paused media at zero speed", 5);
return;
}
if (speedNumber < 0) {
PSL.traceSpeed("speed-actions.setSpeed.negative.before", {
speed: speedNumber
});
PSL.startReversePlayback(video, speedNumber);
PSL.settings.lastSpeed = speedNumber;
PSL.traceSpeed("speed-actions.setSpeed.negative.after", {
reversing: video && video._pslReversing,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
PSL.log("setSpeed started reverse media at " + speedNumber, 5);
return;
}
video._pslEnforceSpeedUntil = Date.now() + 3000;
try {
PSL.traceSpeed("speed-actions.setSpeed.native.before", {
beforePlaybackRate: video.playbackRate,
speed: speedNumber
});
PSL.expectPlaybackRateChange(video, speedNumber);
video.playbackRate = speedNumber;
PSL.traceSpeed("speed-actions.setSpeed.native.after", {
afterPlaybackRate: video.playbackRate,
speed: speedNumber
});
} catch (e) {
video._pslExpectedPlaybackRate = undefined;
PSL.traceSpeed("speed-actions.setSpeed.native.error", {
message: e && e.message
});
throw e;
}
PSL.updateSpeedIndicator(video);
PSL.resumeAfterSyntheticSpeedIfNeeded(video, wasSyntheticSpeed);
if (!video._pslApplyingStoredDomainSpeed) {
PSL.saveDomainSpeed(video, speedNumber);
}
PSL.enforceSpeedSoon(video);
PSL.startPersistentSpeedEnforcement(video);
PSL.traceSpeed("speed-actions.setSpeed.exit", {
playbackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
PSL.log("setSpeed finished: " + speed, 5);
};
PSL.resetSpeed = function (video, target) {
PSL.traceSpeed("speed-actions.resetSpeed.enter", {
target: target,
effectivePlaybackRate: PSL.getEffectivePlaybackRate(video)
});
if (Math.abs(PSL.getEffectivePlaybackRate(video) - target) < 0.001) {
PSL.log("Resetting playback speed to 1.0", 4);
PSL.setSpeed(video, 1.0);
} else {
PSL.log('Toggling playback speed to "preferred" speed', 4);
PSL.setSpeed(video, target);
}
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/speed-domain-storage-listener.js ----
(function (PSL) {
if (PSL.__domainSpeedStorageListenerInstalled) {
return;
}
PSL.__domainSpeedStorageListenerInstalled = true;
try {
var storage =
typeof browser !== "undefined" && browser.storage
? browser.storage
: typeof chrome !== "undefined" && chrome.storage
? chrome.storage
: null;
if (!storage || !storage.onChanged) {
return;
}
storage.onChanged.addListener(function (changes, areaName) {
if (areaName && areaName !== "sync") {
return;
}
if (!changes || !changes.domainSpeeds) {
return;
}
PSL.settings.domainSpeeds = changes.domainSpeeds.newValue || {};
});
} catch (e) {}
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/playback-rate-change-listener.js ----
(function (PSL) {
function updateSpeedFromEvent(video) {
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.enter", {
playbackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video),
reversing: video && video._pslReversing,
pausedForZero: video && video._pslPausedForZeroSpeed,
hasController: Boolean(video && video._pslControllerHandle)
});
if (!video._pslControllerHandle || PSL.settings.speedControlsEnabled === false) {
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.skip", {
hasController: Boolean(video && video._pslControllerHandle),
speedControlsEnabled: PSL.settings.speedControlsEnabled
});
return;
}
if (video._pslReversing) {
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.reversing");
PSL.updateSpeedIndicator(video);
return;
}
if (video._pslPausedForZeroSpeed) {
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.zero");
PSL.updateSpeedIndicator(video);
return;
}
if (
PSL.getMediaSpeedScope &&
PSL.getMediaSpeedScope(video) === "temporary"
) {
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.temporaryTarget", {
mediaSpeed: PSL.getMediaSpeed(video)
});
PSL.updateSpeedIndicator(video);
return;
}
var speed = Number(video.playbackRate.toFixed(2));
var observation = PSL.observeNativePlaybackRate
? PSL.observeNativePlaybackRate(video, speed)
: { action: "adopt", speed: speed };
PSL.log("Playback rate changed to " + speed, 4);
PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.update", {
speed: speed,
observation: observation
});
PSL.updateSpeedIndicator(video);
if (video._pslControllerHandle && video._pslControllerHandle.blinkController) {
video._pslControllerHandle.blinkController();
}
}
PSL.setupRateChangeListener = function (doc) {
PSL.traceSpeed("playback-rate-listener.setupRateChangeListener.enter", {
alreadyAttached: doc._pslRateChangeListenerAttached,
instanceId: PSL.instanceId
});
if (doc._pslRateChangeListenerAttached === PSL.instanceId) {
PSL.traceSpeed("playback-rate-listener.setupRateChangeListener.skipAlreadyAttached");
return;
}
if (doc._pslRateChangeHandler) {
doc.removeEventListener("ratechange", doc._pslRateChangeHandler, true);
}
doc._pslRateChangeHandler = function (event) {
var video = event.target;
PSL.traceSpeed("playback-rate-listener.handler.enter", {
playbackRate: video && video.playbackRate,
mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
if (PSL.consumeExpectedPlaybackRateChange(video)) {
PSL.traceSpeed("playback-rate-listener.handler.expectedConsumed");
PSL.log("Ignoring playback-rate change caused by setSpeed", 4);
event.stopImmediatePropagation();
return;
}
if (
PSL.shouldTemporarilyEnforceSpeed(video) &&
Math.abs(video.playbackRate - PSL.getMediaSpeed(video)) > 0.001
) {
PSL.traceSpeed("playback-rate-listener.handler.temporaryRestore", {
playbackRate: video.playbackRate,
mediaSpeed: PSL.getMediaSpeed(video)
});
PSL.log("Reapplying target playback speed", 4);
event.stopImmediatePropagation();
PSL.enforceSpeedSoon(video);
return;
}
updateSpeedFromEvent(video);
};
doc._pslRateChangeListenerAttached = PSL.instanceId;
doc.addEventListener("ratechange", doc._pslRateChangeHandler, true);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/action-dispatcher.js ----
(function (PSL) {
var PUBLIC_ACTIONS = [
"slower",
"faster",
"reset",
"mark-loop-range",
"clear-loop"
];
PSL.runAction = function (action, value, e) {
PSL.traceSpeed("action-dispatcher.runAction.enter", {
action: action,
value: value,
mediaCount: PSL.state.mediaElements.length,
hasEvent: Boolean(e)
});
PSL.log("runAction Begin", 5);
if (PUBLIC_ACTIONS.indexOf(action) === -1) {
PSL.traceSpeed("action-dispatcher.runAction.unknown", { action: action });
PSL.log("Unknown public action ignored: " + action, 4);
return;
}
if (!PSL.isActionEnabled(action)) {
PSL.traceSpeed("action-dispatcher.runAction.disabled", { action: action });
PSL.log("Action disabled by settings: " + action, 4);
return;
}
if (PSL.actions && typeof PSL.actions[action] === "function") {
PSL.traceSpeed("action-dispatcher.runAction.customAction", { action: action });
PSL.actions[action](value, e);
return;
}
var targetController = e ? e.target.getRootNode().host : null;
PSL.state.mediaElements.forEach(function (video) {
PSL.traceSpeed("action-dispatcher.runAction.media.iterate", {
action: action,
mediaKey: PSL.getMediaKey ? PSL.getMediaKey(video) : undefined,
hasController: Boolean(video._pslControllerHandle),
cancelled: video.classList && video.classList.contains("mpc-cancelled"),
playbackRate: video.playbackRate,
mediaSpeed: PSL.getMediaSpeed && PSL.getMediaSpeed(video)
});
var handle = video._pslControllerHandle;
if (!handle) {
PSL.traceSpeed("action-dispatcher.runAction.media.skipNoController");
return;
}
if (e && (!handle.matchesControllerElement || !handle.matchesControllerElement(targetController))) {
PSL.traceSpeed("action-dispatcher.runAction.media.skipControllerMismatch");
return;
}
if (handle.showController) {
handle.showController();
}
if (video.classList.contains("mpc-cancelled")) {
PSL.traceSpeed("action-dispatcher.runAction.media.skipCancelled");
return;
}
if (action === "faster") {
PSL.traceSpeed("action-dispatcher.runAction.media.faster");
PSL.adjustSpeed(video, PSL.getSpeedStep(), { relative: true });
} else if (action === "slower") {
PSL.traceSpeed("action-dispatcher.runAction.media.slower");
PSL.adjustSpeed(video, -PSL.getSpeedStep(), { relative: true });
} else if (action === "reset") {
PSL.traceSpeed("action-dispatcher.runAction.media.reset");
PSL.resetSpeed(video, 1.0);
} else if (action === "mark-loop-range") {
PSL.markLoopRange(video, video.currentTime);
} else if (action === "clear-loop") {
PSL.clearLoop(video);
}
});
PSL.traceSpeed("action-dispatcher.runAction.exit", { action: action });
PSL.log("runAction End", 5);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/keyboard-shortcuts.js ----
(function (PSL) {
function getKeyboardEventKeyCode(event) {
var result;
if (event.keyCode) {
result = event.keyCode;
} else if (event.key && event.key.length === 1) {
result = event.key.toUpperCase().charCodeAt(0);
} else {
result = 0;
}
PSL.traceSpeed("keyboard-shortcuts.getKeyboardEventKeyCode", {
key: event.key,
keyCode: event.keyCode,
result: result
});
return result;
}
PSL.attachKeyboardShortcuts = function (docs) {
PSL.traceSpeed("keyboard-shortcuts.attachKeyboardShortcuts.enter", {
docCount: docs.length
});
docs.forEach(function (doc) {
if (doc._pslKeyboardShortcutsAttached === PSL.instanceId) {
PSL.traceSpeed("keyboard-shortcuts.attachKeyboardShortcuts.skipAlreadyAttached");
return;
}
if (doc._pslKeyboardShortcutsHandler) {
doc.removeEventListener(
"keydown",
doc._pslKeyboardShortcutsHandler,
true
);
}
doc._pslKeyboardShortcutsHandler = function (event) {
var keyCode = getKeyboardEventKeyCode(event);
var item;
PSL.traceSpeed("keyboard-shortcuts.handler.enter", {
keyCode: keyCode,
targetNodeName: event.target && event.target.nodeName,
mediaCount: PSL.state.mediaElements.length
});
PSL.log("Processing keydown event: " + keyCode, 6);
if (
!event.getModifierState ||
event.getModifierState("Alt") ||
event.getModifierState("Control") ||
event.getModifierState("Fn") ||
event.getModifierState("Meta") ||
event.getModifierState("Hyper") ||
event.getModifierState("OS")
) {
PSL.traceSpeed("keyboard-shortcuts.handler.skipModifier", { keyCode: keyCode });
PSL.log("Keydown event ignored due to active modifier: " + keyCode, 5);
return;
}
if (
event.target.nodeName === "INPUT" ||
event.target.nodeName === "TEXTAREA" ||
event.target.isContentEditable
) {
PSL.traceSpeed("keyboard-shortcuts.handler.skipEditable", { keyCode: keyCode });
return false;
}
if (!PSL.state.mediaElements.length) {
PSL.traceSpeed("keyboard-shortcuts.handler.noMedia.beforeSync", { keyCode: keyCode });
if (PSL.attachControlsToExistingMedia) {
PSL.attachControlsToExistingMedia(doc);
}
PSL.traceSpeed("keyboard-shortcuts.handler.noMedia.afterSync", {
keyCode: keyCode,
mediaCount: PSL.state.mediaElements.length
});
if (!PSL.state.mediaElements.length) {
PSL.traceSpeed("keyboard-shortcuts.handler.skipNoMedia", { keyCode: keyCode });
return false;
}
}
item = PSL.settings.keyBindings.find(function (item) {
return item.key === keyCode;
});
if (item) {
PSL.traceSpeed("keyboard-shortcuts.handler.bindingMatched", item);
PSL.runAction(item.action, item.value);
if (item.force === "true") {
event.preventDefault();
event.stopPropagation();
}
} else {
PSL.traceSpeed("keyboard-shortcuts.handler.noBinding", { keyCode: keyCode });
}
PSL.traceSpeed("keyboard-shortcuts.handler.exit", { keyCode: keyCode });
return false;
};
doc._pslKeyboardShortcutsAttached = PSL.instanceId;
doc.addEventListener("keydown", doc._pslKeyboardShortcutsHandler, true);
});
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/remove-stale-media-controls.js ----
(function (PSL) {
PSL.removeStaleControllerElements = function (doc) {
doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
var instanceId;
try {
instanceId = controller._pslInstanceId;
} catch (e) {}
if (instanceId !== PSL.instanceId) {
controller.remove();
}
});
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/attach-controls-to-existing-media.js ----
(function (PSL) {
function attachControlsToExistingMedia(doc) {
PSL.findMediaElements(doc).forEach(PSL.attachControllerToMedia);
}
PSL.attachControlsToExistingMedia = attachControlsToExistingMedia;
PSL.rescanMedia = function (doc) {
doc = doc || document;
if (PSL.loadSettings) {
PSL.loadSettings(function () {
if (PSL.settings.enabled) {
attachControlsToExistingMedia(doc);
}
});
return;
}
attachControlsToExistingMedia(doc);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/sync-controls-with-media-dom.js ----
(function (PSL) {
function syncControlsForMediaNode(doc, node, added) {
if (!added && doc.body.contains(node)) {
return;
}
if (
node.nodeName === "VIDEO" ||
(node.nodeName === "AUDIO" && PSL.settings.audioBoolean)
) {
if (added) {
PSL.attachControllerToMedia(node);
} else if (node._pslControllerHandle) {
PSL.removeControllerFromMedia(node);
}
} else if (added && node.shadowRoot) {
PSL.findMediaElements(node.shadowRoot).forEach(PSL.attachControllerToMedia);
} else if (node.children !== undefined) {
for (var i = 0; i < node.children.length; i++) {
syncControlsForMediaNode(doc, node.children[i], added);
}
}
}
PSL.installMediaDomControlSync = function (doc) {
var observer = new MutationObserver(function (mutations) {
PSL.requestIdle(
function () {
mutations.forEach(function (mutation) {
if (mutation.type !== "childList") {
return;
}
mutation.addedNodes.forEach(function (node) {
if (typeof node !== "function") {
syncControlsForMediaNode(doc, node, true);
}
});
mutation.removedNodes.forEach(function (node) {
if (typeof node !== "function") {
syncControlsForMediaNode(doc, node, false);
}
});
});
},
{ timeout: 1000 }
);
});
observer.observe(doc, {
childList: true,
subtree: true
});
doc._pslMediaObserver = observer;
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/refresh-controls-after-same-page-navigation.js ----
(function (PSL) {
var samePageRefreshDelays = [0, 100, 500, 1500];
function refreshSamePageMediaControls(doc, reason) {
PSL.traceSpeed("same-page-refresh.enter", {
reason: reason,
href: window.location && window.location.href
});
if (PSL.pageWorldBridge && PSL.pageWorldBridge.installProbe) {
PSL.pageWorldBridge.installProbe(doc);
}
samePageRefreshDelays.forEach(function (delay) {
setTimeout(function () {
PSL.traceSpeed("same-page-refresh.rescan", {
reason: reason,
delay: delay
});
PSL.rescanMedia(doc);
}, delay);
});
}
PSL.installSamePageMediaControlRefresh = function (doc) {
if (doc !== window.document || window._pslUrlChangeSpeedRefreshInstalled) {
return;
}
window._pslUrlChangeSpeedRefreshInstalled = PSL.instanceId;
[
"pageshow",
"popstate",
"hashchange",
"yt-navigate-finish",
"yt-page-data-updated"
].forEach(function (eventName) {
window.addEventListener(eventName, function () {
refreshSamePageMediaControls(doc, eventName);
});
});
["pushState", "replaceState"].forEach(function (methodName) {
var original = window.history && window.history[methodName];
if (typeof original !== "function" || original._pslWrapped) {
return;
}
window.history[methodName] = function () {
var result = original.apply(this, arguments);
refreshSamePageMediaControls(doc, methodName);
return result;
};
window.history[methodName]._pslWrapped = true;
});
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/cover-media-inside-frames.js ----
(function (PSL) {
function inIframe() {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
}
function injectFrameStylesheet(doc) {
var assetUrl;
var link;
if (doc === window.document) {
return;
}
assetUrl = PSL.getAssetUrl("extension/styles/content.css");
if (!assetUrl) {
return;
}
link = doc.createElement("link");
link.href = assetUrl;
link.type = "text/css";
link.rel = "stylesheet";
doc.head.appendChild(link);
}
PSL.getMediaControlKeyboardDocuments = function (doc) {
var docs = [doc];
try {
if (inIframe()) {
docs.push(window.top.document);
}
} catch (e) {}
return docs;
};
PSL.coverMediaInsideFrames = function (doc) {
injectFrameStylesheet(doc);
Array.prototype.forEach.call(doc.getElementsByTagName("iframe"), function (frame) {
try {
PSL.initializeWhenReady(frame.contentDocument);
} catch (e) {}
});
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/start-media-control-session.js ----
(function (PSL) {
PSL.initializeWhenReady = function (doc) {
PSL.log("Begin initializeWhenReady", 5);
if (PSL.isBlacklisted()) {
return;
}
function initializeWhenBodyExists(attempt) {
if (!doc) {
return;
}
if (doc.body) {
PSL.initializeNow(doc);
return;
}
if (attempt < 20) {
setTimeout(function () {
initializeWhenBodyExists(attempt + 1);
}, 50);
}
}
if (doc === window.document) {
window.addEventListener(
"load",
function () {
PSL.initializeNow(window.document);
PSL.rescanMedia(window.document);
},
{ once: true }
);
}
initializeWhenBodyExists(0);
if (doc && doc.addEventListener) {
doc.addEventListener(
"DOMContentLoaded",
function () {
PSL.initializeNow(doc);
PSL.rescanMedia(doc);
},
{ once: true }
);
}
if (doc === window.document && doc.addEventListener) {
doc.addEventListener("visibilitychange", function () {
if (!doc.visibilityState || doc.visibilityState === "visible") {
PSL.rescanMedia(doc);
}
});
}
PSL.log("End initializeWhenReady", 5);
};
PSL.initializeNow = function (doc) {
PSL.log("Begin initializeNow", 5);
if (!PSL.settings.enabled) {
return;
}
if (!doc.body || doc._pslInitializedInstanceId === PSL.instanceId) {
return;
}
try {
if (doc._pslMediaObserver) {
doc._pslMediaObserver.disconnect();
}
} catch (e) {
// Stale observers from a previous temporary add-on load can be ignored.
}
try {
PSL.setupRateChangeListener(doc);
} catch (e) {
// no operation
}
doc._pslInitializedInstanceId = PSL.instanceId;
doc.body.classList.add("mpc-initialized");
PSL.log("initializeNow: mpc-initialized added to document body", 5);
PSL.attachKeyboardShortcuts(PSL.getMediaControlKeyboardDocuments(doc));
PSL.installMediaDomControlSync(doc);
PSL.removeStaleControllerElements(doc);
PSL.rescanMedia(doc);
if (doc === window.document) {
PSL.installSamePageMediaControlRefresh(doc);
}
PSL.coverMediaInsideFrames(doc);
PSL.log("End initializeNow", 5);
};
})(globalThis.PlaySpeedLoop);
// ---- code/extension/content/init.js ----
(function (PSL) {
if (PSL.__injectionInitialized === PSL.injectionSessionId) {
return;
}
PSL.__injectionInitialized = PSL.injectionSessionId;
try {
PSL.loadSettings(function () {
try {
PSL.initializeWhenReady(document);
} catch (error) {
console.error("Speed & Loop failed to initialize", error);
}
});
} catch (error) {
console.error("Speed & Loop failed to load settings", error);
PSL.initializeWhenReady(document);
}
})(globalThis.PlaySpeedLoop);
})();