Speed & Loop (userscript)

Control HTML5 video speed and AB loop actions.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

Advertisement:

// ==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);

})();