Speed & Loop (userscript)

Control HTML5 video speed and AB loop actions.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         Speed & Loop (userscript)
// @namespace    https://github.com/grad13/Speed-and-Loop
// @version      2.1.48
// @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;

  // S0 (documents/plan/2026-06-26-content-runtime-singleton.md): a second
  // execution of this bundle in the SAME world (double injection) must not mint
  // a new instanceId. Otherwise a controller attached before the overwrite and
  // one attached after carry different _pslInstanceId and both survive on the
  // same media while idle (no rescan fires) — the observed duplicate panels.
  // Reuse the existing instanceId unless S0 is explicitly disabled for
  // fix-attribution testing (localStorage PSL_FIX_S0 === "0").
  //
  // injectionSessionId is intentionally NOT made idempotent: it tracks the
  // background injection session so that a genuinely new session (e.g. the
  // add-on re-enabled) still re-initializes via the init.js guard. In a
  // same-session double injection the pending session id is identical anyway, so
  // leaving this as-is does not contribute to the duplicate.
  var s0Disabled = false;
  try {
    s0Disabled = !!(
      global.localStorage && global.localStorage.getItem("PSL_FIX_S0") === "0"
    );
  } catch (e) {}
  PSL.instanceId =
    (!s0Disabled && PSL.instanceId) ||
    "psl-" + Date.now() + "-" + Math.random().toString(36).slice(2);
  PSL.injectionSessionId =
    typeof pendingInjectionSessionId === "string"
      ? pendingInjectionSessionId
      : PSL.instanceId;

  // Stamp every diagnostic line with the extension version so captured logs are
  // self-identifying: no need to cross-check manifest/version after the fact to
  // know which build produced a capture (avoids the version-mismatch confusion
  // between plan docs). Read from the manifest; null if unavailable.
  PSL.version = (function () {
    try {
      var rt =
        (global.chrome && global.chrome.runtime) ||
        (global.browser && global.browser.runtime);
      if (rt && typeof rt.getManifest === "function") {
        return rt.getManifest().version;
      }
    } catch (e) {}
    return null;
  })();

  PSL.__injectionSessionId = PSL.injectionSessionId;
  // A re-evaluation of this bundle in the SAME world (double injection within one
  // background session) rebuilds PSL.state from scratch. The tab-scope maintained
  // speed lives ONLY here (PSL.state.tabSpeed) — no storage, no frame sync — so an
  // unconditional rebuild silently drops it: video.playbackRate (a DOM property)
  // survives the re-eval but tabSpeed becomes undefined, and the next relative
  // adjustment resolves its base from global=1 instead of the maintained speed.
  // Carry tabSpeed across the rebuild, the same way instanceId is preserved in S0
  // above. tabSpeed can be negative (reverse, kept across navigation per SC-28)
  // but is never 0 (speed-actions.js guards speedNumber !== 0), so accept any
  // finite number. isFiniteNumber is local to speed-state.js and out of scope
  // here, so use a plain typeof/isFinite check.
  var prevState = PSL.state;
  PSL.state = {
    mediaElements: [],
    startTimes: {},
    endTimes: {},
    pendingLoopPoints: {},
    loopsEnabled: {},
    controllerTimer: null,
    nextMediaId: 1,
    instanceId: PSL.instanceId
  };
  if (
    prevState &&
    typeof prevState.tabSpeed === "number" &&
    isFinite(prevState.tabSpeed)
  ) {
    PSL.state.tabSpeed = prevState.tabSpeed;
  }

  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();
  };

})(globalThis);

// ---- code/extension/content/runtime-ownership.js ----
(function (PSL) {
  // S1/S2 (documents/plan/2026-06-26-content-runtime-singleton.md): the current
  // content runtime claims ownership of the document by stamping its instanceId
  // on documentElement. When two runtimes coexist (extension reload / background
  // restart leaving two generations), the last to claim wins; the others become
  // non-owners and their controllers are swept by owner-based reconcile (S2).
  //
  // Safety net only: with S0 in place a second instanceId is not minted in the
  // first place, so in the single-runtime case the owner marker equals the
  // current instanceId and ownership is a no-op relative to the pre-S2 behavior.
  //
  // Gated for fix-attribution testing via localStorage (default on):
  //   PSL_FIX_S1 === "0"  -> do not claim ownership.
  //   PSL_FIX_S2 === "0"  -> reconcile ignores the marker (pre-S2 behavior).
  function flagDisabled(name) {
    try {
      return !!(
        typeof localStorage !== "undefined" &&
        localStorage.getItem(name) === "0"
      );
    } catch (e) {
      return false;
    }
  }

  function ownerElement(doc) {
    var d = doc || (typeof document !== "undefined" ? document : null);
    return d ? d.documentElement || null : null;
  }

  PSL.claimRuntimeOwnership = function (doc) {
    if (flagDisabled("PSL_FIX_S1")) {
      return;
    }
    var el = ownerElement(doc);
    var existing = el && el.dataset ? el.dataset.pslRuntimeOwner : null;
    if (el && el.dataset && !existing) {
      // First-writer-wins: a second content-script world that shares this
      // document must NOT steal ownership, or the owner marker would flip and
      // both worlds' panels would survive. The first runtime to claim owns the
      // document; later runtimes stay non-owners and do not attach panels.
      el.dataset.pslRuntimeOwner = PSL.instanceId;
    }
  };

  // The instanceId that currently owns the document. With S2 enabled this is the
  // owner marker, falling back to the current instanceId when unset
  // (single-runtime / back-compat). With S2 disabled it is always the current
  // instanceId — the pre-S2 behavior.
  PSL.getRuntimeOwner = function (doc) {
    if (flagDisabled("PSL_FIX_S2")) {
      return PSL.instanceId;
    }
    var el = ownerElement(doc);
    var marker = el && el.dataset ? el.dataset.pslRuntimeOwner : null;
    return marker || PSL.instanceId;
  };

  PSL.isRuntimeOwner = function (doc) {
    return PSL.getRuntimeOwner(doc) === PSL.instanceId;
  };
})(globalThis.PlaySpeedLoop);

// ---- 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,
    speedScope: "tab",
    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.SPEED_SCOPES = ["tab", "domain", "video"];

  // Round any stored/legacy value to a supported scope. Unknown/invalid values
  // fall back to the default "tab" (see plan: speedScope single source of truth).
  PSL.normalizeSpeedScope = function (value) {
    return PSL.SPEED_SCOPES.indexOf(value) >= 0 ? value : "tab";
  };

  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(null, 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) {
  // Callback error contract (all platforms conform):
  //   get(defaults, callback) -> callback(error, result)
  //     success: callback(null, result); failure: callback(error, defaults)
  //   set(values, callback)   -> callback(error)
  //     success: callback();          failure: callback(error)
  //   remove(keys, callback)  -> callback(error)
  // A write/read failure is never reported as success. Callers can distinguish
  // a genuine failure from "no stored value" by inspecting the error argument.
  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) {
        // null/undefined defaults => read the entire store (get(null)). Coercing
        // to {} would ask the backend for "no keys" and come back empty, which is
        // how a domain speed set on video 1 was lost on the next read.
        var query = defaults == null ? null : defaults;
        browser.storage.sync.get(query).then(
          function (result) {
            callback(null, result);
          },
          function (error) {
            callback(error, defaults || {});
          }
        );
      },
      set: function (values, callback) {
        browser.storage.sync.set(values).then(
          function () {
            if (callback) {
              callback();
            }
          },
          function (error) {
            if (callback) {
              callback(error);
            }
          }
        );
      },
      remove: function (keys, callback) {
        browser.storage.sync.remove(keys).then(
          function () {
            if (callback) {
              callback();
            }
          },
          function (error) {
            if (callback) {
              callback(error);
            }
          }
        );
      }
    };
  } else if (hasChromeStorage()) {
    platform = {
      get: function (defaults, callback) {
        // See the browser branch: null/undefined defaults => full read (get(null)).
        var query = defaults == null ? null : defaults;
        chrome.storage.sync.get(query, function (result) {
          var error = chrome.runtime && chrome.runtime.lastError;
          if (error) {
            callback(error, defaults || {});
          } else {
            callback(null, result);
          }
        });
      },
      set: function (values, callback) {
        chrome.storage.sync.set(values, function () {
          var error = chrome.runtime && chrome.runtime.lastError;
          if (callback) {
            callback(error || undefined);
          }
        });
      },
      remove: function (keys, callback) {
        chrome.storage.sync.remove(keys, function () {
          var error = chrome.runtime && chrome.runtime.lastError;
          if (callback) {
            callback(error || undefined);
          }
        });
      }
    };
  } else if (hasGMStorage()) {
    platform = {
      get: function (defaults, callback) {
        var result = Object.assign({}, defaults || {});

        try {
          // null/undefined defaults => full read: enumerate every stored key
          // (matches storage.sync.get(null)). With explicit defaults, read only
          // those keys, falling back to the supplied default per key.
          if (defaults == null && typeof GM_listValues === "function") {
            GM_listValues().forEach(function (key) {
              result[key] = parseGMValue(GM_getValue(key, null), undefined);
            });
          } else {
            Object.keys(result).forEach(function (key) {
              result[key] = parseGMValue(GM_getValue(key, null), result[key]);
            });
          }
          callback(null, result);
        } catch (error) {
          callback(error, defaults || {});
        }
      },
      set: function (values, callback) {
        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();
          }
        } catch (error) {
          if (callback) {
            callback(error);
          }
        }
      },
      remove: function (keys, callback) {
        if (!Array.isArray(keys)) {
          keys = [keys];
        }
        try {
          if (typeof GM_deleteValue === "function") {
            keys.forEach(function (key) {
              GM_deleteValue(key);
            });
          }
          if (callback) {
            callback();
          }
        } catch (error) {
          if (callback) {
            callback(error);
          }
        }
      }
    };
  } 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 (error, rawStorage) {
      if (error) {
        PSL.log("Failed to read settings from storage; using defaults: " + error, 3);
      }
      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.speedScope = PSL.normalizeSpeedScope(storage.speedScope);
      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, "")
        }, function (error) {
          if (error) {
            PSL.log("Failed to persist migrated settings: " + error, 3);
          }
        });
      }

      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
    );
  };

  // Whether a media element should receive an on-video controller. Mirrors the
  // approach proven by other speed extensions: gate on visibility, not on a size
  // threshold. A video that is detached, CSS-invisible
  // (display:none/visibility:hidden/opacity:0), or rendered at zero size gets no
  // controller. This keeps panels off the zero-size transformed <video> layers
  // that sites (e.g. YouTube) keep alongside the real player and that otherwise
  // produce a second, duplicate panel. Audio has no rendered box and stays
  // eligible. Layout APIs are probed defensively so non-browser test
  // environments fall back to offset metrics.
  PSL.isAttachableMediaTarget = function (media) {
    var dkey = PSL.getMediaKey && media ? PSL.getMediaKey(media) : "?";
    if (!media || media.isConnected === false) {
      return false;
    }
    if (media.nodeName === "AUDIO") {
      return true;
    }

    // A <video> with no media source (no currentSrc/src/srcObject) is not a real
    // player. YouTube keeps placeholder/secondary <video> elements beside the real
    // player; they are visible and non-zero-size (so the size gate below does not
    // catch them) but control nothing. Attaching to one produces the observed
    // second, dead panel next to the working player. It becomes attachable again
    // on a later rescan once a source loads, so the real player is not lost.
    if (!(media.currentSrc || media.src || media.srcObject)) {
      return false;
    }

    try {
      if (typeof getComputedStyle === "function") {
        var style = getComputedStyle(media);
        if (
          style &&
          (style.display === "none" ||
            style.visibility === "hidden" ||
            style.opacity === "0")
        ) {
          return false;
        }
      }
    } catch (e) {}

    var width;
    var height;
    var measured = false;
    try {
      if (typeof media.getBoundingClientRect === "function") {
        var rect = media.getBoundingClientRect();
        if (rect) {
          width = rect.width;
          height = rect.height;
          measured = true;
        }
      }
    } catch (e) {}
    if (!measured) {
      width = media.offsetWidth;
      height = media.offsetHeight;
    }

    var ok = Boolean(width) && Boolean(height);
    return ok;
  };
})(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) {
    // Dead in shipping (clampSpeed always loads), but retained: the test harness
    // loads speed-state in partial bundles without speed-actions, and the
    // fallback is a correct reimplementation. See Plan B P4 (guards kept).
    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);
    }
    // getMediaKey (media-registry.js) is always loaded before this runs; this
    // branch is unreachable in shipped builds. No static shared key: an
    // unidentifiable media falls through to falsy so it is never folded onto a
    // shared record (linkRecordId/ensureRecordForMedia skip falsy ids).
    return video && (video.currentSrc || video.src || video._pslMediaId);
  }

  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;
  };

  // The page-world authority reads its target speed through this id-keyed path
  // (media-authority.js targetSpeed getter). It MUST resolve the selected scope
  // layer (the single source of truth) exactly like the video-keyed getMediaSpeed
  // — not return the per-media record raw. Returning the raw record let a stale /
  // orphaned record (e.g. 2.5 left behind by a blob-URL swap) override the active
  // tab layer, so the authority kept restoring the old value (the ping-pong bug).
  // A synthetic per-media state (<=0: zero pause / reverse) still wins, mirroring
  // getMediaSpeed.
  PSL.getMediaSpeedById = function (mediaId) {
    var record = ensureRecordById(mediaId, 1, "global");

    if (record && isFiniteNumber(record.speed) && record.speed <= 0) {
      return record.speed;
    }

    if (!PSL.getEffectiveSpeedFromHierarchy) {
      return record.speed;
    }

    var scope = getSelectedScope();
    var layerSpeed = getSelectedLayerSpeed(null, scope, record);

    return PSL.getEffectiveSpeedFromHierarchy({
      temporarySpeed: scope === "video" ? layerSpeed : null,
      tabSpeed: scope === "tab" ? layerSpeed : null,
      siteSpeed: scope === "domain" ? layerSpeed : null,
      globalSpeed: 1,
      minSpeed: -16,
      maxSpeed: 16
    }).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;
  };

  function findRecord(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 || null;
  }

  // Resolve a value to a supported scope. Prefers PSL.normalizeSpeedScope
  // (defaults.js) but stays self-contained so it never silently falls back to
  // "tab" just because defaults.js has not loaded yet (e.g. partial bundles).
  PSL.resolveSpeedScope = function (scope) {
    if (PSL.normalizeSpeedScope) {
      return PSL.normalizeSpeedScope(scope);
    }
    return scope === "tab" || scope === "domain" || scope === "video"
      ? scope
      : "tab";
  };

  function getSelectedScope() {
    return PSL.resolveSpeedScope(PSL.settings && PSL.settings.speedScope);
  }

  // The single source of truth is the value of the selected scope layer. Returns
  // null when that layer holds no value (which resolves to global=1 downstream).
  // video=per-media record, tab=PSL.state.tabSpeed, domain=saved domain speed.
  function getSelectedLayerSpeed(video, scope, record) {
    if (scope === "tab") {
      return PSL.state && isFiniteNumber(PSL.state.tabSpeed)
        ? PSL.state.tabSpeed
        : null;
    }
    if (scope === "domain") {
      var saved = PSL.getSavedDomainSpeed ? Number(PSL.getSavedDomainSpeed(video)) : NaN;
      return isFiniteNumber(saved) ? saved : null;
    }
    return record && isFiniteNumber(record.speed) ? record.speed : null;
  }

  // Effective speed = resolve the selected layer through the speed hierarchy.
  // No cross-layer fallback: only the selected layer is populated, others null,
  // final fallback global=1. Synthetic per-media states (zero pause / reverse)
  // are actuated on a specific element and bypass resolution in every scope.
  PSL.getMediaSpeed = function (video) {
    var record = findRecord(video);

    if (record && isFiniteNumber(record.speed) && record.speed <= 0) {
      return record.speed;
    }

    // Resolver absent in partial test bundles: fall back to legacy per-media.
    if (!PSL.getEffectiveSpeedFromHierarchy) {
      return record ? record.speed : normalizeSpeed(getInitialSpeed(video));
    }

    var scope = getSelectedScope();
    var layerSpeed = getSelectedLayerSpeed(video, scope, record);

    // video mode, brand-new element with no record: adopt its native rate.
    if (scope === "video" && layerSpeed === null && !record) {
      return normalizeSpeed(getInitialSpeed(video));
    }

    return PSL.getEffectiveSpeedFromHierarchy({
      temporarySpeed: scope === "video" ? layerSpeed : null,
      tabSpeed: scope === "tab" ? layerSpeed : null,
      siteSpeed: scope === "domain" ? layerSpeed : null,
      globalSpeed: 1,
      minSpeed: -16,
      maxSpeed: 16
    }).speed;
  };

  // The maintained value of the selected scope layer (the single source), or
  // null when that layer holds nothing yet. Unlike getMediaSpeed it does NOT
  // fall back to global=1, so callers can tell "maintained speed" from "nothing
  // to maintain" (used by the apply hook to avoid stomping a native rate).
  PSL.getMaintainedSpeed = function (video) {
    var record = findRecord(video);
    // A synthetic per-media state (zero pause / reverse) owns this element and
    // must not be overridden by the selected layer's value — mirror getMediaSpeed.
    if (record && isFiniteNumber(record.speed) && record.speed <= 0) {
      return record.speed;
    }
    return getSelectedLayerSpeed(video, getSelectedScope(), record);
  };

  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) {
    // A brand-new media id (e.g. the new blob source after a YouTube navigation)
    // is seeded to 1x/global here as a pure actuator default. The maintained
    // speed is the content runtime's selected scope layer, which re-applies it to
    // the new source via applyEffectiveSpeed — the authority no longer carries
    // speed across the swap itself.
    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) {
    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;
      return state;
    }

    function registerMedia(mediaId, currentSrc) {
      var state = states.get(mediaId);
      if (!state) {
        state = normalizeState(mediaId, currentSrc);
        states.set(mediaId, state);
        return state;
      }
      if (currentSrc) {
        handleSourceChange(mediaId, currentSrc);
      }
      return state;
    }

    function applyTargetSpeed(mediaId, speed, scope, reason) {
      var state = registerMedia(mediaId, "");
      var nextScope = scope || "temporary";
      if (getScopeRank(nextScope) < getScopeRank(state.speedScope)) {
        return false;
      }
      state.speedScope = nextScope;
      if (PSL.setMediaSpeedById) {
        PSL.setMediaSpeedById(mediaId, speed, nextScope);
      } else {
        state.targetSpeed = speed;
      }

      if (speed > 0) {
        state.syntheticMode = "none";
        sendCommand({
          type: "set-playback-rate",
          mediaId: mediaId,
          rate: speed,
          reason: reason || "target-speed"
        });
        return true;
      }

      if (speed === 0) {
        state.syntheticMode = "zero";
        sendCommand({
          type: "set-paused-at-zero",
          mediaId: mediaId,
          enabled: true
        });
        return true;
      }

      state.syntheticMode = "reverse";
      return true;
    }

    function handleObservedPlaybackRate(mediaId, observedRate) {
      var state = getState(mediaId);
      if (!state || state.syntheticMode !== "none" || state.targetSpeed <= 0) {
        return false;
      }
      if (Math.abs(observedRate - state.targetSpeed) < 0.001) {
        return false;
      }
      sendCommand({
        type: "set-playback-rate",
        mediaId: mediaId,
        rate: state.targetSpeed,
        reason: "restore-observed-rate"
      });
      return true;
    }

    function handleSourceChange(mediaId, nextSrc) {
      var state = getState(mediaId);
      if (!state) {
        registerMedia(mediaId, nextSrc);
        return false;
      }
      var changed = PSL.detectMediaSourceChange(state, nextSrc);
      return changed;
    }

    function detachMedia(mediaId) {
      var existed = states.delete(mediaId);
      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 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;
    }
    return result;
  }

  function getEventTarget() {
    var result = typeof global.addEventListener === "function" ? global : global.window;
    return result;
  }

  function createCustomEvent(type, detail) {
    var EventCtor = global.CustomEvent || (global.window && global.window.CustomEvent);
    var encodedDetail = encodeBridgePayload(detail);
    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) {
        return null;
      }
    }
    return detail || null;
  }

  function getMediaId(media) {
    var mediaId = PSL.getMediaKey ? PSL.getMediaKey(media) : media.currentSrc || media.src;
    return mediaId;
  }

  function appendScript(doc, channelId, src) {
    var script = doc.createElement("script");
    script.src = src;
    script.async = false;
    script.dataset.pslV2ProbeChannel = channelId;
    script.onload = function () {
      if (script.parentNode) {
        script.parentNode.removeChild(script);
      }
    };
    (doc.documentElement || doc.head || doc.body).appendChild(script);
  }

  function installProbe(doc) {
    if (!doc || doc.__pslV2PageWorldProbeInstalled) {
      return false;
    }

    var src = PSL.getAssetUrl(hookPath);
    if (!src) {
      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) {
      if (eventTarget && typeof eventTarget.dispatchEvent === "function") {
        eventTarget.dispatchEvent(createCustomEvent(toPage, 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.pageWorldBridge.handlePageEvent(detail);
      });
    }

    appendScript(doc, channelId, src);
    return true;
  }

  function handlePageEvent(detail) {
    var authority = PSL.v2MediaAuthority;
    if (!authority || !detail || !detail.mediaId) {
      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") {
      // Register the new blob source with the authority. The authority no longer
      // carries speed across the swap — the single source of truth is the
      // selected scope layer (re-applied by the content runtime, not here).
      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) {
        return;
      }
      authority.handleObservedPlaybackRate(detail.mediaId, Number(detail.playbackRate));
    }
  }

  function applySpeed(video, speed, scope, reason) {
    var mediaId;
    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);
    return true;
  }

  function resetLoopForPageSourceChange(detail) {
    var media = mediaById.get(detail.mediaId) || mediaById.get(detail.previousSrc);
    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.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;

    if (isNaN(point)) {
      return;
    }

    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);
      return;
    }

    left = Math.min(pending, point);
    right = Math.max(pending, point);
    if (right <= left) {
      PSL.updateLoopRangeDisplay(video);
      return;
    }

    PSL.state.startTimes[src] = left;
    PSL.state.endTimes[src] = right;
    delete PSL.state.pendingLoopPoints[src];
    PSL.enableLoop(video);
    PSL.updateLoopRangeDisplay(video);
  };

  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.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();
      },
      maintainSpeed: function (trigger) {
        controller.maintainSpeed(trigger || "handle");
      }
    };
  }

  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));
    };
    // On attach and every source swap, apply the maintained speed (the selected
    // scope layer: tab / domain / video) to the new source exactly once. This is
    // the single source of truth — no fallback chain. New videos resolve the
    // selected layer; reverse carries too (see speed-apply.js).
    this.maintainSpeed = function (trigger) {
      if (PSL.applyEffectiveSpeed) {
        PSL.applyEffectiveSpeed(target);
      }
    };

    PSL.state.startTimes[PSL.getMediaKey(target)] = 0;
    target.addEventListener(
      "loadedmetadata",
      (this.handleLoadedMetadata = function () {
        controller.updateSourceVisibility();
        controller.maintainSpeed("loadedmetadata");
      })
    );
    PSL.state.loopsEnabled[PSL.getMediaKey(target)] = false;

    this.div = this.initializeControls();
    target._pslControllerHandle = createControllerHandle(this);
    this.maintainSpeed("attach");
    if (PSL.updateLoopRangeDisplay) {
      PSL.updateLoopRangeDisplay(this.video);
    }

    this.handleSourceVisibilityUpdate = function (event) {
      controller.updateSourceVisibility();
      controller.maintainSpeed((event && event.type) || "sourceVisibility");
    };
    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.maintainSpeed("srcMutation:" + mutation.attributeName);
            }
          }
        });
      }
    );
    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;
    // Page-world-readable mirror of the bound media/instance (the expandos above
    // are invisible across the content/page boundary). Lets a page console probe
    // tell whether two .mpc-controller wrappers share a media element and a
    // content instance when diagnosing duplicate panels.
    try {
      wrapper.dataset.pslMedia = String(PSL.getMediaKey ? PSL.getMediaKey(this.video) : "");
      wrapper.dataset.pslInstance = String(PSL.instanceId);
      wrapper.dataset.pslFrame = window.self === window.top ? "top" : "iframe";
    } catch (e) {}

    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) {
    var mediaDoc = media && media.ownerDocument;
    removeOrphanControllerElements(media);

    if (!PSL.shouldAttachController()) {
      return;
    }

    // Do not attach to invisible/zero-size media (e.g. a site's zero-size
    // transformed <video> layer). Existing controllers are left untouched; this
    // only suppresses new attachment. Guarded so partial bundles without
    // feature-gates keep working.
    if (PSL.isAttachableMediaTarget && !PSL.isAttachableMediaTarget(media)) {
      return;
    }

    if (isCurrentController(media)) {
      if (!isControllerAttachedToCurrentParent(media)) {
        PSL.removeControllerFromMedia(media);
        new PSL.VideoController(media);
        return;
      }
      // The element already has this runtime's controller attached to its
      // current parent. YouTube reuses the SAME <video> element across
      // recommendation / SPA navigations (only currentSrc changes), and a
      // stale-controller sweep during the navigation churn can unregister the
      // element from state.mediaElements while leaving _pslControllerHandle
      // intact. Re-register (idempotent) so keyboard runAction — which iterates
      // state.mediaElements and skips handle-less entries — keeps reaching the
      // reused player after navigation. Without this, s/d silently stops working
      // on every video opened after the first.
      PSL.registerMedia(media);
      if (media._pslControllerHandle && media._pslControllerHandle.maintainSpeed) {
        media._pslControllerHandle.maintainSpeed("rescan");
      }
      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 && video.ownerDocument) || document;
    var host = "";

    try {
      host = doc.location && doc.location.hostname;
    } catch (e) {}

    return String(host || "").replace(/^www\./, "");
  }

  // A speed worth persisting as a domain (site) speed: any non-zero finite speed
  // in the supported range. Negative (reverse) speeds are stored too so reverse
  // is maintained across navigation in domain mode; zero (pause) is a momentary
  // per-media state and is never stored.
  PSL.isStorableDomainSpeed = function (speed) {
    return (
      typeof speed === "number" &&
      Number.isFinite(speed) &&
      speed !== 0 &&
      speed >= -16 &&
      speed <= 16
    );
  };

  PSL.saveDomainSpeed = function (video, speed) {
    var domain = getSpeedDomain(video);
    var domainSpeeds;

    if (!domain || !PSL.isStorableDomainSpeed(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 (error) {
        if (error) {
          PSL.log("Failed to save domain speed for " + domain + ": " + error, 3);
          return;
        }
        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.isStorableDomainSpeed(PSL.getSavedDomainSpeed(video))
    );
  };
})(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);
    // Persistent 50ms forcing runs ONLY on user-registered keep-speed sites
    // (e.g. niconico) that actively reset playbackRate. Everywhere else (YouTube
    // is never a keep-speed site by default) speed is held by set-once: the
    // direct assignment in setSpeed, the temporary 3s enforcement window, and the
    // page-world authority's event-driven restore. Starting an interval per video
    // here previously spawned one 50ms loop for every positive-rate media —
    // including each YouTube thumbnail preview — that accumulated and never
    // stopped.
    if (!PSL.isKeepSpeedSite(video)) {
      return;
    }
    video._pslSpeedEnforcer = setInterval(function () {
      var target = PSL.getMediaSpeed(video);
      if (
        !video.isConnected ||
        !isPositivePlaybackRate(target) ||
        video._pslPausedForZeroSpeed ||
        video._pslReversing
      ) {
        PSL.stopPersistentSpeedEnforcement(video);
        return;
      }
      // The 50ms keep-speed enforcer is a content-world writer that bypasses the
      // page-world setter guard. Trace every actual write (each one fires a
      // ratechange a site comment renderer can resync on); leave no-op ticks
      // untraced to avoid 20/sec flooding.
      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) {
        // 3s temporary-window content-world write (bypasses the page-world
        // setter guard). Trace it so post-change rate churn is attributable.
        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;
      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);
    return result;
  };

  PSL.getSpeedAdjustmentBase = function (video) {
    var result;
    if (PSL.getEffectivePlaybackRate) {
      result = PSL.getEffectivePlaybackRate(video);
    } else {
      result = video.playbackRate;
    }
    return result;
  };

  // Single source for the relative-step math (base ± delta, 1.0-crossing snap,
  // clamp/round). Shared by adjustSpeed's relative branch and the dispatcher's
  // multi-element aggregation, so both compute an identical target. Pass `base`
  // to reuse an already-read adjustment base and avoid a redundant read.
  PSL.computeRelativeTarget = function (video, delta, base) {
    var currentSpeed = typeof base === "number" ? base : PSL.getSpeedAdjustmentBase(video);
    var targetSpeed = currentSpeed + delta;

    if (
      (currentSpeed > 1.0 && targetSpeed < 1.0) ||
      (currentSpeed < 1.0 && targetSpeed > 1.0)
    ) {
      targetSpeed = 1.0;
    }

    return Number(PSL.clampSpeed(targetSpeed, -16, 16).toFixed(2));
  };

  PSL.adjustSpeed = function (video, value, options) {
    var relative;
    var currentSpeed;
    var targetSpeed;


    options = options || {};
    relative = options.relative === true;

    if (typeof value !== "number" || isNaN(value)) {
      return;
    }

    if (relative) {
      currentSpeed = PSL.getSpeedAdjustmentBase(video);
      targetSpeed = PSL.computeRelativeTarget(video, value, currentSpeed);
    } else {
      targetSpeed = Number(PSL.clampSpeed(value, -16, 16).toFixed(2));
    }
    PSL.setSpeed(video, targetSpeed);
  };

  PSL.setSpeed = function (video, speed) {
    if (PSL.settings.speedControlsEnabled === false) {
      return;
    }

    PSL.log("setSpeed started: " + speed, 5);
    var speedValue = speed.toFixed(2);
    var speedNumber = Number(speedValue);

    // Write the chosen non-zero speed to the selected scope layer — the single
    // source of truth that resolves the effective speed for every media. tab
    // survives SPA navigation in the same runtime; domain persists to storage
    // (below); video uses the per-media record written here. A negative
    // (reverse) speed is maintained too, so reverse carries across navigation;
    // the reverse engine clamps at currentTime 0 so it never rewinds past the
    // start. Zero (pause) is a momentary per-video state and never defines the
    // maintained layer.
    var selectedScope = PSL.resolveSpeedScope
      ? PSL.resolveSpeedScope(PSL.settings.speedScope)
      : "tab";
    if (speedNumber !== 0) {
      if (selectedScope === "tab" && PSL.state) {
        PSL.state.tabSpeed = speedNumber;
      } else if (selectedScope === "domain" && !video._pslApplyingStoredDomainSpeed) {
        // Persist here (not on the positive path below) so reverse speeds —
        // whose branch returns early — are stored and maintained too.
        PSL.saveDomainSpeed(video, speedNumber);
      }
    }
    var wasSyntheticSpeed = PSL.isSyntheticPausedSpeed(video);
    var recordScope = video._pslApplyingStoredDomainSpeed ? "site" : "temporary";
    PSL.setMediaSpeed(video, speedNumber, recordScope);

    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.pageWorldBridge.applySpeed(video, speedNumber, recordScope, "set-speed");
      PSL.log("setSpeed sent to v2 media authority: " + speedNumber, 5);
    }

    if (speedNumber === 0) {
      PSL.pauseAtZeroSpeed(video);
      PSL.log("setSpeed paused media at zero speed", 5);
      return;
    }

    if (speedNumber < 0) {
      PSL.startReversePlayback(video, speedNumber);
      PSL.settings.lastSpeed = speedNumber;
      PSL.log("setSpeed started reverse media at " + speedNumber, 5);
      return;
    }

    video._pslEnforceSpeedUntil = Date.now() + 3000;
    try {
      PSL.expectPlaybackRateChange(video, speedNumber);
      video.playbackRate = speedNumber;
    } catch (e) {
      video._pslExpectedPlaybackRate = undefined;
      throw e;
    }
    PSL.updateSpeedIndicator(video);
    PSL.resumeAfterSyntheticSpeedIfNeeded(video, wasSyntheticSpeed);
    PSL.enforceSpeedSoon(video);
    PSL.startPersistentSpeedEnforcement(video);
    PSL.log("setSpeed finished: " + speed, 5);
  };

  PSL.resetSpeed = function (video, target) {
    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-apply.js ----
(function (PSL) {
  // Single application point for the maintained speed. Called on media attach and
  // on every source-change event. Reads the selected scope layer (the single
  // source of truth) and applies it to the element ONCE, so the user's speed —
  // including reverse — carries across SPA navigation and blob source swaps.
  // Replaces the former reassert / applyStoredDomainSpeed / applyLastSessionSpeed
  // / page-world carry-over fallback chain.
  PSL.applyEffectiveSpeed = function (video) {
    if (!video) {
      return false;
    }
    if (PSL.settings && PSL.settings.speedControlsEnabled === false) {
      return false;
    }

    var speed = PSL.getMaintainedSpeed ? PSL.getMaintainedSpeed(video) : null;
    if (typeof speed !== "number" || !Number.isFinite(speed) || speed === 0) {
      // No maintained value for the selected scope (or a momentary pause): leave
      // the new source at its native rate instead of stomping it.
      return false;
    }

    // Already at the maintained speed — avoid restarting the enforcement window
    // or the reverse engine on each redundant source event (loadstart/
    // loadeddata/canplay/emptied all fire maintainSpeed).
    if (speed > 0 && Math.abs((video.playbackRate || 0) - speed) < 0.001) {
      return false;
    }
    if (speed < 0 && video._pslReversing) {
      return false;
    }

    PSL.setSpeed(video, speed);
    return true;
  };
})(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) {
    if (!video._pslControllerHandle || PSL.settings.speedControlsEnabled === false) {
      return;
    }

    if (video._pslReversing) {
      PSL.updateSpeedIndicator(video);
      return;
    }

    if (video._pslPausedForZeroSpeed) {
      PSL.updateSpeedIndicator(video);
      return;
    }

    if (
      PSL.getMediaSpeedScope &&
      PSL.getMediaSpeedScope(video) === "temporary"
    ) {
      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.updateSpeedIndicator(video);
    if (video._pslControllerHandle && video._pslControllerHandle.blinkController) {
      video._pslControllerHandle.blinkController();
    }
  }

  PSL.setupRateChangeListener = function (doc) {
    if (doc._pslRateChangeListenerAttached === PSL.instanceId) {
      return;
    }
    if (doc._pslRateChangeHandler) {
      doc.removeEventListener("ratechange", doc._pslRateChangeHandler, true);
    }

    doc._pslRateChangeHandler = function (event) {
      var video = event.target;

      if (PSL.consumeExpectedPlaybackRateChange(video)) {
        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.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.log("runAction Begin", 5);

    if (PUBLIC_ACTIONS.indexOf(action) === -1) {
      PSL.log("Unknown public action ignored: " + action, 4);
      return;
    }

    if (!PSL.isActionEnabled(action)) {
      PSL.log("Action disabled by settings: " + action, 4);
      return;
    }

    if (PSL.actions && typeof PSL.actions[action] === "function") {
      PSL.actions[action](value, e);
      return;
    }

    var targetController = e ? e.target.getRootNode().host : null;

    // tab/domain scope share one layer base, so a per-element relative add chains
    // into ±step×N when several media co-reside (the +1.0 bug). Compute the
    // relative target ONCE from the first surviving element and apply it as an
    // absolute set to every survivor, so the net change is ±step regardless of N.
    // video scope keeps per-element relative (each has its own record).
    var selectedScope = PSL.resolveSpeedScope
      ? PSL.resolveSpeedScope(PSL.settings.speedScope)
      : "tab";
    var aggregateRelative = selectedScope === "tab" || selectedScope === "domain";
    var aggregateTarget = null;
    var aggregateTargetComputed = false;

    PSL.state.mediaElements.forEach(function (video) {
      var handle = video._pslControllerHandle;
      if (!handle) {
        return;
      }

      if (e && (!handle.matchesControllerElement || !handle.matchesControllerElement(targetController))) {
        return;
      }

      if (handle.showController) {
        handle.showController();
      }

      if (video.classList.contains("mpc-cancelled")) {
        return;
      }

      if (action === "faster" || action === "slower") {
        var step = PSL.getSpeedStep();
        var delta = action === "faster" ? step : -step;
        if (aggregateRelative) {
          // Compute the shared relative target once, then absolute-set every
          // survivor so N co-resident elements can't chain into ±step×N.
          if (!aggregateTargetComputed) {
            aggregateTarget = PSL.computeRelativeTarget(video, delta);
            aggregateTargetComputed = true;
          }
          PSL.adjustSpeed(video, aggregateTarget, { relative: false });
        } else {
          PSL.adjustSpeed(video, delta, { relative: true });
        }
      } else if (action === "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.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;
    }
    return result;
  }

  PSL.attachKeyboardShortcuts = function (docs) {
    docs.forEach(function (doc) {
      if (doc._pslKeyboardShortcutsAttached === PSL.instanceId) {
        return;
      }
      if (doc._pslKeyboardShortcutsHandler) {
        doc.removeEventListener(
          "keydown",
          doc._pslKeyboardShortcutsHandler,
          true
        );
      }

      doc._pslKeyboardShortcutsHandler = function (event) {
        var keyCode = getKeyboardEventKeyCode(event);
        var item;
        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.log("Keydown event ignored due to active modifier: " + keyCode, 5);
          return;
        }

        if (
          event.target.nodeName === "INPUT" ||
          event.target.nodeName === "TEXTAREA" ||
          event.target.isContentEditable
        ) {
          return false;
        }

        if (!PSL.state.mediaElements.length) {
          if (PSL.attachControlsToExistingMedia) {
            PSL.attachControlsToExistingMedia(doc);
          }
          if (!PSL.state.mediaElements.length) {
            return false;
          }
        }

        item = PSL.settings.keyBindings.find(function (item) {
          return item.key === keyCode;
        });
        if (item) {
          PSL.runAction(item.action, item.value);
          if (item.force === "true") {
            event.preventDefault();
            event.stopPropagation();
          }
        }

        return false;
      };
      doc._pslKeyboardShortcutsAttached = PSL.instanceId;
      doc.addEventListener("keydown", doc._pslKeyboardShortcutsHandler, true);
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/remove-stale-media-controls.js ----
(function (PSL) {
  // Read a controller's owning instanceId in a cross-world-safe way. Two
  // content-script worlds can share one document; a .mpc-controller created by
  // the other world exposes its id via the shared DOM dataset (dataset.pslInstance)
  // but NOT via the _pslInstanceId JS expando (expandos are per-world). Prefer
  // the dataset, fall back to the expando for same-world / older controllers.
  function readControllerInstance(controller) {
    try {
      if (controller.dataset && controller.dataset.pslInstance) {
        return controller.dataset.pslInstance;
      }
    } catch (e) {}
    try {
      return controller._pslInstanceId;
    } catch (e) {}
    return undefined;
  }

  PSL.removeStaleControllerElements = function (doc) {
    doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
      if (readControllerInstance(controller) !== PSL.instanceId) {
        controller.remove();
      }
    });
  };

  // Remove controllers whose bound media element is no longer in the live DOM.
  // SPA navigation (e.g. YouTube in a single tab) can swap out the player
  // <video>; the old controller is otherwise only cleaned by the debounced
  // MutationObserver, so a rescan can attach a new controller while the orphan
  // is still visible (duplicate panels). Sweeping detached-media controllers at
  // rescan time removes the orphan immediately instead of after the debounce.
  PSL.removeDetachedControllerElements = function (doc) {
    doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
      var media;
      try {
        media = controller._pslMediaElement;
      } catch (e) {}

      if (media && media.isConnected === false) {
        controller.remove();
        try {
          delete media._pslControllerHandle;
        } catch (e) {}
        if (PSL.unregisterMedia) {
          PSL.unregisterMedia(media);
        }
      }
    });
  };

  // Normalize controllers so that each live media element owns at most one
  // visible .mpc-controller. This generalizes the orphan/stale/detached sweeps
  // into one pass run at rescan time:
  //   1. live-set membership: remove controllers whose bound media is no longer
  //      reachable via findMediaElements (covers detached === isConnected:false
  //      AND media that left the scanned document/shadow tree but still reports
  //      connected, which the detached sweep cannot catch).
  //   2. stale instance: remove controllers from a previous content injection
  //      (_pslInstanceId !== PSL.instanceId), even on still-live media.
  //   3. dedupe: among controllers left on the same media, keep the one the
  //      media's current handle owns (matchesControllerElement); remove the rest.
  // Removal uses the same raw element .remove() as the existing sweeps; the
  // discarded duplicate wrappers have no back-reference to a VideoController, so
  // a full handle teardown is neither reachable nor attempted here (same
  // limitation as removeStale/removeDetached). The speed-view span is a child of
  // the wrapper and is removed with it.
  PSL.reconcileControllers = function (doc) {
    doc = doc || document;

    var live = new Set(PSL.findMediaElements(doc));

    function readMedia(controller) {
      try {
        return controller._pslMediaElement;
      } catch (e) {
        return undefined;
      }
    }
    function readInstance(controller) {
      return readControllerInstance(controller);
    }
    function drop(controller, media, reason, releaseMedia) {
      controller.remove();
      if (releaseMedia && media) {
        try {
          delete media._pslControllerHandle;
        } catch (e) {}
        if (PSL.unregisterMedia) {
          PSL.unregisterMedia(media);
        }
      }
    }

    // Pass 1: drop live-non-members and stale-instance controllers; collect the
    // rest grouped by their bound media for the dedupe pass.
    var byMedia = new Map();
    doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
      var media = readMedia(controller);

      if (!media || !live.has(media)) {
        drop(controller, media, "not-live", true);
        return;
      }
      if (readInstance(controller) !== PSL.instanceId) {
        drop(controller, media, "stale-instance", false);
        return;
      }

      var bucket = byMedia.get(media);
      if (bucket) {
        bucket.push(controller);
      } else {
        byMedia.set(media, [controller]);
      }
    });

    // Pass 2: dedupe survivors per media, preferring the handle's own element.
    byMedia.forEach(function (controllers, media) {
      if (controllers.length < 2) {
        return;
      }
      var handle = media._pslControllerHandle;
      var keep = null;
      if (handle && handle.matchesControllerElement) {
        for (var i = 0; i < controllers.length; i++) {
          if (handle.matchesControllerElement(controllers[i])) {
            keep = controllers[i];
            break;
          }
        }
      }
      if (!keep) {
        keep = controllers[0];
      }
      controllers.forEach(function (controller) {
        if (controller !== keep) {
          drop(controller, media, "duplicate", false);
        }
      });
    });
  };
})(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;
    // Normalize controllers before re-attaching: one .mpc-controller per live
    // media. This synchronously removes swapped-out (detached), stale-instance,
    // and duplicate panels so they cannot coexist with the controller a rescan
    // is about to (re)attach (the debounced MutationObserver would otherwise
    // clean detached ones only ~1s later, and never the connected duplicates).
    if (PSL.reconcileControllers) {
      PSL.reconcileControllers(doc);
    } else if (PSL.removeDetachedControllerElements) {
      PSL.removeDetachedControllerElements(doc);
    }
    // Re-cover same-origin iframes that may have loaded since the last scan: a
    // lazy about:blank -> player iframe is not caught by the one-shot cover in
    // initializeNow. Idempotent (see coverMediaInsideFrames). This is the
    // top-only equivalent of each frame's own DOMContentLoaded re-injection.
    if (PSL.coverMediaInsideFrames) {
      PSL.coverMediaInsideFrames(doc);
    }
    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) {
    if (PSL.pageWorldBridge && PSL.pageWorldBridge.installProbe) {
      PSL.pageWorldBridge.installProbe(doc);
    }
    samePageRefreshDelays.forEach(function (delay) {
      setTimeout(function () {
        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);

    // Top-only injection: the background injects the content bundle into the MAIN
    // frame only, so same-origin iframes have no runtime of their own. The top
    // runtime descends into each accessible iframe's contentDocument and
    // initializes it in-place — same JS world, same PSL.instanceId — so exactly
    // one panel and one keyboard handler set exist per logical player. Reaching
    // into a cross-origin iframe's contentDocument throws; those are skipped
    // (cross-origin players are out of scope — same-origin only). Re-entry is
    // idempotent: the _pslInitializedInstanceId guard in initializeNow no-ops an
    // already-initialized document, and we skip re-arming initializeWhenReady on
    // a document this runtime has already initialized.
    var frames = doc.getElementsByTagName("iframe");
    for (var i = 0; i < frames.length; i++) {
      try {
        var frameDoc = frames[i].contentDocument;
        if (frameDoc && frameDoc._pslInitializedInstanceId !== PSL.instanceId) {
          PSL.initializeWhenReady(frameDoc);
        }
      } catch (e) {
        // Cross-origin iframe: contentDocument is not accessible. Out of scope.
      }
    }

  };
})(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);

})();