Speed & Loop (userscript)

Control HTML5 video speed and AB loop actions.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

Advertisement:

// ==UserScript==
// @name         Speed & Loop (userscript)
// @namespace    https://github.com/grad13/Speed-and-Loop
// @version      2.1.3
// @description  Control HTML5 video speed and AB loop actions.
// @author       speed-and-loop
// @license      MIT
// @homepageURL  https://greasyfork.org/en/scripts/583175-speed-loop-userscript
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @run-at       document-idle
// ==/UserScript==

(function () {
  function injectStyle(id, css) {
    if (!document || !document.head || document.getElementById(id)) { return; }
    var style = document.createElement("style");
    style.id = id;
    style.textContent = css;
    document.head.appendChild(style);
  }
  injectStyle("psl-userscript-content-css", ".mpc-nosource {\n  display: none !important;\n}\n.mpc-manual {\n  visibility: visible !important;\n  opacity: 1 !important;\n}\n\n.mpc-controller {\n  /* In case of pages using `white-space: pre-line` (eg Discord), don't render vsc's whitespace */\n  white-space: normal;\n}\n\n/* Origin specific overrides */\n/* YouTube player */\n.ytp-hide-info-bar .mpc-controller {\n  position: relative;\n  top: 10px;\n}\n\n.ytp-autohide .mpc-controller {\n  visibility: visible;\n  opacity: 1;\n}\n\n.ytp-autohide .mpc-show {\n  visibility: visible;\n  opacity: 1;\n}\n\n/* YouTube embedded player */\n/* e.g. https://www.igvita.com/2012/09/12/web-fonts-performance-making-pretty-fast/ */\n.html5-video-player:not(.ytp-hide-info-bar) .mpc-controller {\n  position: relative;\n  top: 60px;\n}\n\n/* Facebook player */\n#facebook .mpc-controller {\n  position: relative;\n  top: 40px;\n}\n\n/* Google Photos player */\n/* Inline preview doesn't have any additional hooks, relying on Aria label */\na[aria-label^=\"Video\"] .mpc-controller {\n  position: relative;\n  top: 35px;\n}\n/* Google Photos full-screen view */\n#player .house-brand .mpc-controller {\n  position: relative;\n  top: 50px;\n}\n\n/* Netflix player */\n#netflix-player:not(.player-cinema-mode) .mpc-controller {\n  position: relative;\n  top: 85px;\n}\n\n/* shift controller on vine.co */\n/* e.g. https://vine.co/v/OrJj39YlL57 */\n.video-container .vine-video-container .mpc-controller {\n  margin-left: 40px;\n}\n\n/* shift YT 3D controller down */\n/* e.g. https://www.youtube.com/watch?v=erftYPflJzQ */\n.ytp-webgl-spherical-control {\n  top: 60px !important;\n}\n\n.ytp-fullscreen .ytp-webgl-spherical-control {\n  top: 100px !important;\n}\n\n/* disable Vimeo video overlay */\ndiv.video-wrapper + div.target {\n  height: 0;\n}\n\n/* Fix black overlay on Kickstarter */\ndiv.video-player.has_played.vertically_center:before,\ndiv.legacy-video-player.has_played.vertically_center:before {\n  content: none !important;\n}\n\n/* Fix black overlay on openai.com */\n.Shared-Video-player > .mpc-controller {\n  height: 0;\n}\n");

// ---- code/extension/content/namespace.js ----
(function (global) {
  var PSL = global.PlaySpeedLoop || {};
  var pendingInjectionSessionId = global.__PSL_PENDING_INJECTION_SESSION_ID;

  PSL.regStrip = /^[\r\t\f\v ]+|[\r\t\f\v ]+$/gm;
  PSL.instanceId =
    "psl-" + Date.now() + "-" + Math.random().toString(36).slice(2);
  PSL.injectionSessionId =
    typeof pendingInjectionSessionId === "string"
      ? pendingInjectionSessionId
      : PSL.instanceId;
  PSL.__injectionSessionId = PSL.injectionSessionId;
  PSL.state = {
    mediaElements: [],
    startTimes: {},
    endTimes: {},
    pendingLoopPoints: {},
    loopsEnabled: {},
    controllerTimer: null,
    nextMediaId: 1,
    instanceId: PSL.instanceId
  };

  PSL.requestIdle = function (callback, options) {
    if (typeof global.requestIdleCallback === "function") {
      return global.requestIdleCallback.call(global, callback, options);
    }
    return global.setTimeout(function () {
      callback({ didTimeout: false, timeRemaining: function () { return 0; } });
    }, 0);
  };

  global.PlaySpeedLoop = PSL;
  try {
    delete global.__PSL_PENDING_INJECTION_SESSION_ID;
  } catch (e) {
    global.__PSL_PENDING_INJECTION_SESSION_ID = undefined;
  }

  function getGlobalStorage() {
    if (typeof browser !== "undefined" && browser.storage && browser.storage.sync) {
      return browser.storage.sync;
    }
    if (typeof chrome !== "undefined" && chrome.storage && chrome.storage.sync) {
      return chrome.storage.sync;
    }
    return null;
  }

  PSL.getAssetUrl = function (path) {
    try {
      var rt = global.chrome && global.chrome.runtime;
      if (rt && typeof rt.getURL === "function") {
        return rt.getURL(path);
      }
    } catch (e) {}

    try {
      var browserRuntime = global.browser && global.browser.runtime;
      if (browserRuntime && typeof browserRuntime.getURL === "function") {
        return browserRuntime.getURL(path);
      }
    } catch (e) {}

    return null;
  };

  PSL.getStorageBridge = function () {
    return getGlobalStorage();
  };

  PSL.traceSpeed = function (label, detail) {
    try {
      if (typeof detail === "undefined") {
        console.log("[Speed & Loop trace] " + label);
      } else {
        console.log("[Speed & Loop trace] " + label, detail);
      }
    } catch (e) {}
  };
})(globalThis);

// ---- code/extension/content/defaults.js ----
(function (PSL) {
  PSL.defaultKeyBindings = [
    { action: "slower", key: 83, value: 0.5, force: false, predefined: true },
    { action: "faster", key: 68, value: 0.5, force: false, predefined: true },
    { action: "reset", key: 82, value: 1, force: false, predefined: true },
    { action: "mark-loop-range", key: 65, value: 0, force: false, predefined: true },
    { action: "clear-loop", key: 69, value: 0, force: false, predefined: true }
  ];

  PSL.defaultSettings = {
    lastSpeed: 1.0,
    enabled: true,
    speeds: {},
    domainSpeeds: {},
    displayKeyCode: 86,
    audioBoolean: false,
    speedControlsEnabled: true,
    loopControlsEnabled: true,
    speedStep: 0.5,
    preferredSpeed: 1.8,
    reverseFrameRate: 1,
    keepSpeedSites: "",
    controllerOpacity: 0.96,
    keyBindings: PSL.defaultKeyBindings,
    blacklist: "www.instagram.com\ntwitter.com\nvine.co\nimgur.com\nteams.microsoft.com",
    defaultLogLevel: 4,
    logLevel: 1
  };

  PSL.cloneDefaultSettings = function () {
    var settings = Object.assign({}, PSL.defaultSettings);
    settings.speeds = {};
    settings.domainSpeeds = {};
    settings.keyBindings = PSL.defaultKeyBindings.map(function (binding) {
      return Object.assign({}, binding);
    });
    return settings;
  };

  PSL.settings = PSL.cloneDefaultSettings();
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/logger.js ----
(function (PSL) {
  PSL.log = function (message, level) {
    var verbosity = PSL.settings.logLevel;
    if (typeof level === "undefined") {
      level = PSL.settings.defaultLogLevel;
    }
    if (verbosity >= level) {
      if (level === 2) {
        console.log("ERROR:" + message);
      } else if (level === 3) {
        console.log("WARNING:" + message);
      } else if (level === 4) {
        console.log("INFO:" + message);
      } else if (level === 5) {
        console.log("DEBUG:" + message);
      } else if (level === 6) {
        console.log("DEBUG (VERBOSE):" + message);
        console.trace();
      }
    }
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/unavailable-content-settings-storage.js ----
(function (PSL) {
  function logUnavailable(operation) {
    if (typeof PSL.log === "function") {
      PSL.log("Content settings storage unavailable during " + operation, 3);
    }
  }

  PSL.createUnavailableContentSettingsStorage = function () {
    return {
      get: function (defaults, callback) {
        logUnavailable("get");
        callback(defaults || {});
      },
      set: function (_values, callback) {
        logUnavailable("set");
        if (callback) {
          callback();
        }
      },
      remove: function (_keys, callback) {
        logUnavailable("remove");
        if (callback) {
          callback();
        }
      }
    };
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/content-settings-storage-backend.js ----
(function (PSL) {
  function hasBrowserStorage() {
    return (
      typeof browser !== "undefined" &&
      browser.storage &&
      browser.storage.sync
    );
  }

  function hasChromeStorage() {
    return (
      typeof chrome !== "undefined" &&
      chrome.storage &&
      chrome.storage.sync
    );
  }

  function hasGMStorage() {
    return typeof GM_getValue === "function" && typeof GM_setValue === "function";
  }

  function parseGMValue(rawValue, fallback) {
    if (typeof rawValue === "undefined" || rawValue === null) {
      return fallback;
    }

    if (typeof rawValue === "string") {
      try {
        return JSON.parse(rawValue);
      } catch (error) {
        return rawValue;
      }
    }

    return rawValue;
  }

  var platform;
  if (hasBrowserStorage()) {
    platform = {
      get: function (defaults, callback) {
        defaults = defaults || {};
        browser.storage.sync.get(defaults).then(
          callback,
          function () {
            callback(defaults);
          }
        );
      },
      set: function (values, callback) {
        browser.storage.sync.set(values).then(function () {
          if (callback) {
            callback();
          }
        });
      },
      remove: function (keys, callback) {
        browser.storage.sync.remove(keys).then(function () {
          if (callback) {
            callback();
          }
        });
      }
    };
  } else if (hasChromeStorage()) {
    platform = {
      get: function (defaults, callback) {
        chrome.storage.sync.get(defaults || {}, callback);
      },
      set: function (values, callback) {
        chrome.storage.sync.set(values, callback);
      },
      remove: function (keys, callback) {
        chrome.storage.sync.remove(keys, callback);
      }
    };
  } else if (hasGMStorage()) {
    platform = {
      get: function (defaults, callback) {
        var result = Object.assign({}, defaults || {});
        var error;
        var keys = Object.keys(result);

        try {
          keys.forEach(function (key) {
            var stored = GM_getValue(key, null);
            result[key] = parseGMValue(stored, result[key]);
          });
          callback(result);
          return;
        } catch (errorInner) {
          error = errorInner;
        }

        if (error) {
          callback(defaults || {});
        }
      },
      set: function (values, callback) {
        var error;
        try {
          Object.keys(values).forEach(function (key) {
            if (values[key] === undefined) {
              if (typeof GM_deleteValue === "function") {
                GM_deleteValue(key);
              }
            } else {
              GM_setValue(key, JSON.stringify(values[key]));
            }
          });
          if (callback) {
            callback();
          }
          return;
        } catch (errorInner) {
          error = errorInner;
        }

        if (callback && error) {
          callback();
        }
      },
      remove: function (keys, callback) {
        if (!Array.isArray(keys)) {
          keys = [keys];
        }
        if (typeof GM_deleteValue === "function") {
          keys.forEach(function (key) {
            GM_deleteValue(key);
          });
        }
        if (callback) {
          callback();
        }
      }
    };
  } else {
    platform = PSL.createUnavailableContentSettingsStorage();
  }

  PSL.storagePlatform = platform;

  PSL.getStorage = function (defaults, callback) {
    if (typeof callback !== "function") {
      return platform;
    }
    platform.get(defaults, callback);
  };

  PSL.setStorage = function (values, callback) {
    platform.set(values, callback);
  };

  PSL.removeStorage = function (keys, callback) {
    platform.remove(keys, callback);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/settings-migration.js ----
(function (PSL) {
  PSL.hasStoredSetting = function (storage, key) {
    return Object.prototype.hasOwnProperty.call(storage || {}, key);
  };

  PSL.hasLegacyKeyBindingSettings = function (storage) {
    return [
      "speedStep",
      "resetKeyCode",
      "slowerKeyCode",
      "fasterKeyCode",
      "rewindKeyCode",
      "advanceKeyCode",
      "fastKeyCode"
    ].some(function (key) {
      return PSL.hasStoredSetting(storage, key);
    });
  };

  PSL.buildLegacyKeyBindings = function (storage) {
    return [
      {
        action: "slower",
        key: Number(storage.slowerKeyCode) || 83,
        value: Number(storage.speedStep) || 0.5,
        force: false,
        predefined: true
      },
      {
        action: "faster",
        key: Number(storage.fasterKeyCode) || 68,
        value: Number(storage.speedStep) || 0.5,
        force: false,
        predefined: true
      },
      {
        action: "rewind",
        key: Number(storage.rewindKeyCode) || 90,
        value: Number(storage.rewindTime) || 10,
        force: false,
        predefined: true
      },
      {
        action: "advance",
        key: Number(storage.advanceKeyCode) || 88,
        value: Number(storage.advanceTime) || 10,
        force: false,
        predefined: true
      },
      {
        action: "reset",
        key: Number(storage.resetKeyCode) || 82,
        value: 1.0,
        force: false,
        predefined: true
      },
      {
        action: "fast",
        key: Number(storage.fastKeyCode) || 71,
        value: Number(storage.fastSpeed) || 1.8,
        force: false,
        predefined: true
      }
    ];
  };

  PSL.ensureDefaultBinding = function (settings, action) {
    if (settings.keyBindings.some(function (binding) { return binding.action === action; })) {
      return false;
    }

    var defaultBinding = PSL.defaultKeyBindings.find(function (binding) {
      return binding.action === action;
    });
    if (!defaultBinding) {
      return false;
    }

    settings.keyBindings.push(Object.assign({}, defaultBinding));
    return true;
  };

  PSL.ensureDefaultBindingValue = function (settings, action) {
    var binding = settings.keyBindings.find(function (item) {
      return item.action === action;
    });
    var defaultBinding = PSL.defaultKeyBindings.find(function (item) {
      return item.action === action;
    });
    if (!binding || !defaultBinding) {
      return false;
    }

    var migrated = false;
    if (typeof binding.key === "undefined") {
      binding.key = defaultBinding.key;
      migrated = true;
    }

    if (
      (action === "slower" || action === "faster") &&
      (isNaN(Number(binding.value)) || Number(binding.value) < 0.01)
    ) {
      binding.value = defaultBinding.value;
      migrated = true;
    }

    if (
      (action === "reset" || action === "fast") &&
      (isNaN(Number(binding.value)) || Number(binding.value) <= 0)
    ) {
      binding.value = defaultBinding.value;
      migrated = true;
    }

    return migrated;
  };

  PSL.migrateDefaultSpeedStep = function (settings) {
    var migrated = false;
    settings.keyBindings.forEach(function (binding) {
      if (
        (binding.action === "slower" || binding.action === "faster") &&
        binding.predefined !== false &&
        Number(binding.value) === 0.1
      ) {
        binding.value = 0.5;
        migrated = true;
      }
    });
    return migrated;
  };

  PSL.migrateSpeedStep = function (storage, settings) {
    var speedStep = Number(storage.speedStep);
    if (
      PSL.hasStoredSetting(storage, "speedStep") &&
      !isNaN(speedStep) &&
      speedStep >= 0.01 &&
      speedStep <= 15.93
    ) {
      return { value: speedStep, migrated: false };
    }

    var binding = settings.keyBindings.find(function (item) {
      return item.action === "slower" || item.action === "faster";
    });
    speedStep = binding ? Number(binding.value) : NaN;
    if (!isNaN(speedStep) && speedStep >= 0.01 && speedStep <= 15.93) {
      return { value: speedStep, migrated: true };
    }

    return { value: 0.5, migrated: true };
  };

  PSL.migratePreferredSpeed = function (storage, settings) {
    var preferredSpeed = Number(storage.preferredSpeed);
    var storedBindings = Array.isArray(storage.keyBindings)
      ? storage.keyBindings
      : [];
    var binding;
    if (
      PSL.hasStoredSetting(storage, "preferredSpeed") &&
      !isNaN(preferredSpeed) &&
      preferredSpeed >= 0.07 &&
      preferredSpeed <= 16
    ) {
      return { value: preferredSpeed, migrated: false };
    }

    binding = storedBindings.find(function (item) {
      return item.action === "fast";
    }) || storedBindings.find(function (item) {
      return item.action === "reset";
    }) || settings.keyBindings.find(function (item) {
      return item.action === "fast";
    });
    preferredSpeed = binding ? Number(binding.value) : NaN;
    if (!isNaN(preferredSpeed) && preferredSpeed >= 0.07 && preferredSpeed <= 16) {
      return { value: preferredSpeed, migrated: true };
    }

    return { value: 1.8, migrated: true };
  };

  PSL.migrateReverseFrameRate = function (storage) {
    var reverseFrameRate = Number(storage.reverseFrameRate);
    if (
      PSL.hasStoredSetting(storage, "reverseFrameRate") &&
      !isNaN(reverseFrameRate) &&
      reverseFrameRate >= 0.1 &&
      reverseFrameRate <= 15
    ) {
      return { value: reverseFrameRate, migrated: false };
    }

    return { value: 1, migrated: PSL.hasStoredSetting(storage, "reverseFrameRate") };
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/storage.js ----
(function (PSL) {
  PSL.getKeyBinding = function (action, what) {
    if (!what) {
      what = "value";
    }
    try {
      return PSL.settings.keyBindings.find(function (item) {
        return item.action === action;
      })[what];
    } catch (e) {
      return false;
    }
  };

  PSL.setKeyBinding = function (action, value) {
    var binding = PSL.settings.keyBindings.find(function (item) {
      return item.action === action;
    });
    if (binding) {
      binding.value = value;
    }
  };

  PSL.loadSettings = function (callback) {
    PSL.getStorage(null, function (rawStorage) {
      rawStorage = rawStorage || {};
      var storage = Object.assign(PSL.cloneDefaultSettings(), rawStorage);
      var settings = PSL.cloneDefaultSettings();
      var migrated = false;
      var speedStep;
      var preferredSpeed;
      var reverseFrameRate;

      if (Array.isArray(rawStorage.keyBindings)) {
        settings.keyBindings = rawStorage.keyBindings.slice();
      } else if (PSL.hasLegacyKeyBindingSettings(rawStorage)) {
        settings.keyBindings = PSL.buildLegacyKeyBindings(rawStorage);
        settings.version = "0.5.3";
        migrated = true;
      }

      ["slower", "faster", "reset", "mark-loop-range", "clear-loop"].forEach(function (action) {
        migrated = PSL.ensureDefaultBinding(settings, action) || migrated;
        migrated = PSL.ensureDefaultBindingValue(settings, action) || migrated;
      });
      migrated = PSL.migrateDefaultSpeedStep(settings) || migrated;

      speedStep = PSL.migrateSpeedStep(rawStorage, settings);
      settings.speedStep = speedStep.value;
      migrated = speedStep.migrated || migrated;

      preferredSpeed = PSL.migratePreferredSpeed(rawStorage, settings);
      settings.preferredSpeed = preferredSpeed.value;
      migrated = preferredSpeed.migrated || migrated;

      reverseFrameRate = PSL.migrateReverseFrameRate(rawStorage, settings);
      settings.reverseFrameRate = reverseFrameRate.value;
      migrated = reverseFrameRate.migrated || migrated;

      settings.keyBindings = settings.keyBindings.filter(function (binding) {
        return PSL.defaultKeyBindings.some(function (defaultBinding) {
          return defaultBinding.action === binding.action;
        });
      });

      settings.lastSpeed = Number(storage.lastSpeed) || 1.0;
      settings.displayKeyCode = Number(storage.displayKeyCode) || 86;
      settings.audioBoolean = Boolean(storage.audioBoolean);
      settings.enabled =
        storage.enabled === undefined
          ? PSL.defaultSettings.enabled
          : Boolean(storage.enabled);
      settings.speedControlsEnabled = storage.speedControlsEnabled !== false;
      settings.loopControlsEnabled = storage.loopControlsEnabled !== false;
      settings.keepSpeedSites = String(storage.keepSpeedSites || "");
      settings.controllerOpacity = Number(storage.controllerOpacity);
      if (isNaN(settings.controllerOpacity)) {
        settings.controllerOpacity = PSL.defaultSettings.controllerOpacity;
      }
      settings.blacklist = String(storage.blacklist);
      settings.logLevel = Number(storage.logLevel) || PSL.defaultSettings.logLevel;
      settings.defaultLogLevel =
        Number(storage.defaultLogLevel) || PSL.defaultSettings.defaultLogLevel;
      settings.speeds = storage.speeds || {};
      settings.domainSpeeds = storage.domainSpeeds || {};

      PSL.settings = settings;
      if (PSL.updateKeepSpeedSiteMatchers) {
        PSL.updateKeepSpeedSiteMatchers();
      }

      if (migrated) {
        PSL.setStorage({
          keyBindings: PSL.settings.keyBindings,
          version: PSL.settings.version,
          displayKeyCode: PSL.settings.displayKeyCode,
          audioBoolean: PSL.settings.audioBoolean,
          speedControlsEnabled: PSL.settings.speedControlsEnabled,
          loopControlsEnabled: PSL.settings.loopControlsEnabled,
          speedStep: PSL.settings.speedStep,
          preferredSpeed: PSL.settings.preferredSpeed,
          reverseFrameRate: PSL.settings.reverseFrameRate,
          keepSpeedSites: PSL.settings.keepSpeedSites,
          enabled: PSL.settings.enabled,
          controllerOpacity: PSL.settings.controllerOpacity,
          blacklist: PSL.settings.blacklist.replace(PSL.regStrip, "")
        });
      }

      callback();
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/feature-gates.js ----
(function (PSL) {
  var SPEED_ACTIONS = ["slower", "faster", "reset"];
  var LOOP_ACTIONS = ["mark-loop-range", "clear-loop"];

  function numberInRange(value, fallback, min, max) {
    var number = Number(value);
    if (isNaN(number) || number < min || number > max) {
      return fallback;
    }
    return number;
  }

  PSL.isSpeedAction = function (action) {
    return SPEED_ACTIONS.indexOf(action) !== -1;
  };

  PSL.isLoopAction = function (action) {
    return LOOP_ACTIONS.indexOf(action) !== -1;
  };

  PSL.isActionEnabled = function (action) {
    if (PSL.isSpeedAction(action)) {
      return PSL.settings.speedControlsEnabled !== false;
    }
    if (PSL.isLoopAction(action)) {
      return PSL.settings.loopControlsEnabled !== false;
    }
    return false;
  };

  PSL.getSpeedStep = function () {
    return numberInRange(PSL.settings.speedStep, 0.5, 0.01, 15.93);
  };

  PSL.getPreferredSpeed = function () {
    return numberInRange(PSL.settings.preferredSpeed, 1.8, 0.07, 16);
  };

  PSL.getReverseFrameRate = function () {
    return numberInRange(PSL.settings.reverseFrameRate, 1, 0.1, 15);
  };

  PSL.shouldAttachController = function () {
    return (
      PSL.settings.speedControlsEnabled !== false ||
      PSL.settings.loopControlsEnabled !== false
    );
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/blacklist.js ----
(function (PSL) {
  PSL.escapeStringRegExp = function (str) {
    var matchOperatorsRe = /[|\\{}()[\]^$+*?.]/g;
    return str.replace(matchOperatorsRe, "\\$&");
  };

  PSL.isBlacklisted = function (url) {
    var targetUrl = typeof url === "string" ? url : location.href;
    var blacklisted = false;
    PSL.settings.blacklist.split("\n").forEach(function (match) {
      var regexp;
      match = match.replace(PSL.regStrip, "");
      if (match.length === 0) {
        return;
      }

      if (match.startsWith("/")) {
        try {
          regexp = new RegExp(match.replace(/^\/|\/$/g, ""));
        } catch (err) {
          return;
        }
      } else {
        regexp = new RegExp(PSL.escapeStringRegExp(match));
      }

      if (regexp.test(targetUrl)) {
        blacklisted = true;
      }
    });
    return blacklisted;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/media-registry.js ----
(function (PSL) {
  PSL.getMediaKey = function (media) {
    if (media.currentSrc || media.src) {
      return media.currentSrc || media.src;
    }
    if (!media._pslMediaId) {
      media._pslMediaId = "media-" + PSL.state.nextMediaId++;
    }
    return media._pslMediaId;
  };

  PSL.registerMedia = function (media) {
    if (PSL.state.mediaElements.indexOf(media) === -1) {
      PSL.state.mediaElements.push(media);
    }
  };

  PSL.unregisterMedia = function (media) {
    var idx = PSL.state.mediaElements.indexOf(media);
    if (idx !== -1) {
      PSL.state.mediaElements.splice(idx, 1);
    }
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/media-element-query.js ----
(function (PSL) {
  PSL.findMediaElements = function (root) {
    var media = [];

    function collect(node) {
      if (!node || !node.querySelectorAll) {
        return;
      }

      node.querySelectorAll("video,audio").forEach(function (element) {
        if (
          element.nodeName === "VIDEO" ||
          (element.nodeName === "AUDIO" && PSL.settings.audioBoolean)
        ) {
          media.push(element);
        }
      });

      node.querySelectorAll("*").forEach(function (element) {
        if (element.shadowRoot) {
          collect(element.shadowRoot);
        }
      });
    }

    collect(root);
    return media;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-hierarchy.js ----
(function (PSL) {
  function isValidSpeed(value) {
    return typeof value === "number" && Number.isFinite(value);
  }

  function clampSpeed(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function resolveCandidate(input, scope, key) {
    if (!input || !isValidSpeed(input[key])) {
      return null;
    }
    return { speed: input[key], scope: scope };
  }

  PSL.getEffectiveSpeedFromHierarchy = function (input) {
    var candidate =
      resolveCandidate(input, "temporary", "temporarySpeed") ||
      resolveCandidate(input, "tab", "tabSpeed") ||
      resolveCandidate(input, "site", "siteSpeed") ||
      resolveCandidate(input, "global", "globalSpeed") ||
      { speed: 1, scope: "global" };
    var min = isValidSpeed(input && input.minSpeed) ? input.minSpeed : -16;
    var max = isValidSpeed(input && input.maxSpeed) ? input.maxSpeed : 16;
    return {
      speed: clampSpeed(candidate.speed, min, max),
      scope: candidate.scope
    };
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-state.js ----
(function (PSL) {
  function getRecords() {
    if (!PSL.state) {
      PSL.state = {};
    }
    if (!PSL.state.mediaSpeedRecords) {
      PSL.state.mediaSpeedRecords = new Map();
    }
    return PSL.state.mediaSpeedRecords;
  }

  function getMediaRecords() {
    if (!PSL.state) {
      PSL.state = {};
    }
    if (!PSL.state.mediaSpeedElementRecords) {
      PSL.state.mediaSpeedElementRecords = new WeakMap();
    }
    return PSL.state.mediaSpeedElementRecords;
  }

  function isFiniteNumber(value) {
    return typeof value === "number" && Number.isFinite(value);
  }

  function normalizeSpeed(speed) {
    if (PSL.clampSpeed) {
      return Number(PSL.clampSpeed(speed, -16, 16).toFixed(2));
    }
    return Number(Math.min(Math.max(speed, -16), 16).toFixed(2));
  }

  function getMediaId(video) {
    if (video && PSL.getMediaKey) {
      return PSL.getMediaKey(video);
    }
    return video && (video.currentSrc || video.src || video._pslMediaId || "media");
  }

  function getInitialSpeed(video) {
    return isFiniteNumber(video && video.playbackRate) ? video.playbackRate : 1;
  }

  function ensureRecordById(mediaId, initialSpeed, scope) {
    var key = String(mediaId || "");
    var records = getRecords();
    var record = records.get(key);

    if (!record) {
      record = {
        speed: normalizeSpeed(isFiniteNumber(initialSpeed) ? initialSpeed : 1),
        scope: scope || "global"
      };
      records.set(key, record);
    }
    return record;
  }

  function linkRecordId(mediaId, record) {
    var key = String(mediaId || "");
    if (key) {
      getRecords().set(key, record);
    }
  }

  function ensureRecordForMedia(video, initialSpeed, scope) {
    var mediaRecords = getMediaRecords();
    var mediaId = getMediaId(video);
    var record = video ? mediaRecords.get(video) : null;

    if (!record && mediaId) {
      record = getRecords().get(String(mediaId));
    }
    if (!record) {
      record = {
        speed: normalizeSpeed(isFiniteNumber(initialSpeed) ? initialSpeed : 1),
        scope: scope || "global"
      };
    }
    if (video) {
      mediaRecords.set(video, record);
    }
    linkRecordId(mediaId, record);
    return record;
  }

  PSL.setMediaSpeedById = function (mediaId, speed, scope) {
    var record = ensureRecordById(mediaId, speed, scope);
    record.speed = normalizeSpeed(speed);
    record.scope = scope || record.scope || "temporary";
    return record.speed;
  };

  PSL.getMediaSpeedById = function (mediaId) {
    return ensureRecordById(mediaId, 1, "global").speed;
  };

  PSL.getMediaSpeedRecordById = function (mediaId) {
    return ensureRecordById(mediaId, 1, "global");
  };

  PSL.hasMediaSpeed = function (video) {
    return Boolean(
      (video && getMediaRecords().has(video)) ||
      getRecords().has(String(getMediaId(video) || ""))
    );
  };

  PSL.setMediaSpeed = function (video, speed, scope) {
    var record = ensureRecordForMedia(video, speed, scope);
    record.speed = normalizeSpeed(speed);
    record.scope = scope || record.scope || "temporary";
    linkRecordId(getMediaId(video), record);
    return record.speed;
  };

  PSL.getMediaSpeed = function (video) {
    var record = video && getMediaRecords().get(video);
    if (!record) {
      record = getRecords().get(String(getMediaId(video) || ""));
      if (record && video) {
        getMediaRecords().set(video, record);
      }
    }
    if (record) {
      linkRecordId(getMediaId(video), record);
    }
    return record ? record.speed : normalizeSpeed(getInitialSpeed(video));
  };

  PSL.getMediaSpeedScope = function (video) {
    var record = video && getMediaRecords().get(video);
    if (!record) {
      record = getRecords().get(String(getMediaId(video) || ""));
    }
    return record ? record.scope : "global";
  };

  PSL.getMediaSpeedMode = function (video) {
    var speed = PSL.getMediaSpeed(video);
    if (speed < 0) {
      return "reverse";
    }
    if (speed === 0) {
      return "zero";
    }
    return "native";
  };

  PSL.clearMediaSpeed = function (video) {
    if (video) {
      getMediaRecords().delete(video);
    }
    getRecords().delete(String(getMediaId(video) || ""));
  };

  PSL.observeNativePlaybackRate = function (video, rate) {
    var speed = Number(rate);
    if (!isFiniteNumber(speed)) {
      return { action: "ignore" };
    }
    if (!PSL.hasMediaSpeed(video)) {
      PSL.setMediaSpeed(video, speed, "observed");
      return { action: "adopt", speed: speed };
    }
    if (Math.abs(PSL.getMediaSpeed(video) - speed) < 0.001) {
      return { action: "confirm", speed: speed };
    }
    return { action: "restore", speed: PSL.getMediaSpeed(video), observedSpeed: speed };
  };

  PSL.getEffectivePlaybackRate = function (video) {
    return PSL.getMediaSpeed(video);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/media-source-lifecycle.js ----
(function (PSL) {
  function getSourceKey(media) {
    if (!media) {
      return "";
    }
    return media.currentSrc || media.src || "";
  }

  function resetSourceScopedState(state) {
    if (!state) {
      return null;
    }
    state.loopStart = 0;
    state.loopEnd = 0;
    state.loopEnabled = false;
    state.syntheticMode = "none";
    return state;
  }

  function resetLoopStateForSourceChange(media, previousSourceKey) {
    var currentSourceKey = getSourceKey(media);

    if (previousSourceKey) {
      PSL.state.startTimes[previousSourceKey] = 0;
      PSL.state.endTimes[previousSourceKey] = 0;
      if (PSL.state.pendingLoopPoints) {
        delete PSL.state.pendingLoopPoints[previousSourceKey];
      }
      PSL.state.loopsEnabled[previousSourceKey] = false;
    }
    if (currentSourceKey) {
      PSL.state.startTimes[currentSourceKey] = 0;
      PSL.state.endTimes[currentSourceKey] = 0;
      if (PSL.state.pendingLoopPoints) {
        delete PSL.state.pendingLoopPoints[currentSourceKey];
      }
      PSL.state.loopsEnabled[currentSourceKey] = false;
    }
    if (media && PSL.disableLoop) {
      PSL.disableLoop(media);
    }
    if (media && media._pslControllerHandle) {
      if (media._pslControllerHandle.clearLoopRangeDisplay) {
        media._pslControllerHandle.clearLoopRangeDisplay();
      }
      if (PSL.updateLoopRangeDisplay) {
        PSL.updateLoopRangeDisplay(media);
      }
    }
  }

  function detectSourceChange(state, nextSourceKey) {
    if (!state || !nextSourceKey) {
      return false;
    }
    if (!state.currentSrc) {
      state.currentSrc = nextSourceKey;
      return false;
    }
    if (state.currentSrc === nextSourceKey) {
      return false;
    }
    state.previousSrc = state.currentSrc;
    state.currentSrc = nextSourceKey;
    resetSourceScopedState(state);
    return true;
  }

  PSL.getMediaSourceKey = getSourceKey;
  PSL.detectMediaSourceChange = detectSourceChange;
  PSL.resetLoopStateForSourceChange = resetLoopStateForSourceChange;
  PSL.resetMediaSourceScopedState = resetSourceScopedState;
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/reverse-playback-engine.js ----
(function (PSL, global) {
  function isValidLoop(loopStart, loopEnd) {
    return (
      typeof loopStart === "number" &&
      typeof loopEnd === "number" &&
      Number.isFinite(loopStart) &&
      Number.isFinite(loopEnd) &&
      loopEnd > loopStart
    );
  }

  function createReversePlaybackEngine(options) {
    var running = new WeakMap();
    var setTimer = options && options.setInterval ? options.setInterval : global.setInterval.bind(global);
    var clearTimer = options && options.clearInterval ? options.clearInterval : global.clearInterval.bind(global);
    var now = options && options.now ? options.now : Date.now;
    var defaultIntervalMs = options && options.intervalMs ? options.intervalMs : 100;

    function stop(video) {
      var state = video && running.get(video);
      if (!state) {
        return false;
      }
      clearTimer(state.timerId);
      running.delete(video);
      return true;
    }

    function tick(video) {
      var state = running.get(video);
      if (!state) {
        return false;
      }
      if (video.isConnected === false || video._pslReversing === false) {
        stop(video);
        if (state.onStop) {
          state.onStop(video);
        }
        return false;
      }
      if (!video.paused && typeof video.pause === "function") {
        video.pause();
      }
      var currentNow = now();
      var elapsedSeconds = Math.max(0, (currentNow - state.lastTick) / 1000);
      state.lastTick = currentNow;
      var stepSeconds =
        typeof state.stepSeconds === "number" && Number.isFinite(state.stepSeconds)
          ? state.stepSeconds
          : Math.abs(state.speed) * elapsedSeconds;
      var nextTime = video.currentTime - stepSeconds;

      if (isValidLoop(state.loopStart, state.loopEnd) && nextTime <= state.loopStart) {
        video.currentTime = state.loopEnd;
        return true;
      }

      if (nextTime <= 0) {
        video.currentTime = 0;
        stop(video);
        if (state.onEnd) {
          state.onEnd(video);
        }
        return true;
      }

      video.currentTime = nextTime;
      return true;
    }

    function start(video, speed, startOptions) {
      if (!video || !(speed < 0)) {
        if (video) {
          stop(video);
        }
        return false;
      }

      stop(video);
      if (typeof video.pause === "function") {
        video.pause();
      }

      var state = {
        speed: speed,
        loopStart: startOptions && startOptions.loopStart,
        loopEnd: startOptions && startOptions.loopEnd,
        onEnd: startOptions && startOptions.onEnd,
        onStop: startOptions && startOptions.onStop,
        stepSeconds: startOptions && startOptions.stepSeconds,
        intervalMs: startOptions && startOptions.intervalMs ? startOptions.intervalMs : defaultIntervalMs,
        lastTick: now(),
        timerId: null
      };
      state.timerId = setTimer(function () {
        tick(video);
      }, state.intervalMs);
      running.set(video, state);
      return true;
    }

    function isRunning(video) {
      return Boolean(video && running.has(video));
    }

    return {
      isRunning: isRunning,
      start: start,
      stop: stop,
      tick: tick
    };
  }

  PSL.createReversePlaybackEngine = createReversePlaybackEngine;
  PSL.reversePlaybackEngine = PSL.reversePlaybackEngine || createReversePlaybackEngine();
})(globalThis.PlaySpeedLoop, globalThis);

// ---- code/extension/content/media-authority.js ----
(function (PSL) {
  function attachSpeedAccessors(state) {
    var fallbackSpeed = 1;
    var fallbackScope = "global";

    Object.defineProperty(state, "targetSpeed", {
      configurable: true,
      enumerable: true,
      get: function () {
        return PSL.getMediaSpeedById
          ? PSL.getMediaSpeedById(state.mediaId)
          : fallbackSpeed;
      },
      set: function (speed) {
        fallbackSpeed = speed;
        if (PSL.setMediaSpeedById) {
          PSL.setMediaSpeedById(state.mediaId, speed, state.speedScope);
        }
      }
    });
    Object.defineProperty(state, "speedScope", {
      configurable: true,
      enumerable: true,
      get: function () {
        return PSL.getMediaSpeedRecordById
          ? PSL.getMediaSpeedRecordById(state.mediaId).scope
          : fallbackScope;
      },
      set: function (scope) {
        fallbackScope = scope || "global";
        if (PSL.getMediaSpeedRecordById) {
          PSL.getMediaSpeedRecordById(state.mediaId).scope = fallbackScope;
        }
      }
    });
    return state;
  }

  function normalizeState(mediaId, currentSrc) {
    PSL.traceSpeed("media-authority.normalizeState", {
      mediaId: mediaId,
      currentSrc: currentSrc
    });
    if (PSL.setMediaSpeedById) {
      PSL.setMediaSpeedById(mediaId, 1, "global");
    }
    return attachSpeedAccessors({
      mediaId: mediaId,
      currentSrc: currentSrc || "",
      previousSrc: "",
      loopStart: 0,
      loopEnd: 0,
      loopEnabled: false,
      syntheticMode: "none"
    });
  }

  function createMediaAuthority(options) {
    PSL.traceSpeed("media-authority.createMediaAuthority", {
      hasSendCommand: Boolean(options && typeof options.sendCommand === "function")
    });
    var states = new Map();
    var sendCommand = options && typeof options.sendCommand === "function"
      ? options.sendCommand
      : function () {};

    function getScopeRank(scope) {
      if (scope === "temporary") {
        return 3;
      }
      if (scope === "tab") {
        return 2;
      }
      if (scope === "site") {
        return 1;
      }
      return 0;
    }

    function getState(mediaId) {
      var state = states.get(mediaId) || null;
      PSL.traceSpeed("media-authority.getState", {
        mediaId: mediaId,
        found: Boolean(state),
        state: state
      });
      return state;
    }

    function registerMedia(mediaId, currentSrc) {
      var state = states.get(mediaId);
      PSL.traceSpeed("media-authority.registerMedia.enter", {
        mediaId: mediaId,
        currentSrc: currentSrc,
        exists: Boolean(state)
      });
      if (!state) {
        state = normalizeState(mediaId, currentSrc);
        states.set(mediaId, state);
        PSL.traceSpeed("media-authority.registerMedia.created", state);
        return state;
      }
      if (currentSrc) {
        handleSourceChange(mediaId, currentSrc);
      }
      PSL.traceSpeed("media-authority.registerMedia.exit", state);
      return state;
    }

    function applyTargetSpeed(mediaId, speed, scope, reason) {
      var state = registerMedia(mediaId, "");
      var nextScope = scope || "temporary";
      PSL.traceSpeed("media-authority.applyTargetSpeed.enter", {
        mediaId: mediaId,
        speed: speed,
        scope: nextScope,
        reason: reason,
        state: state
      });
      if (getScopeRank(nextScope) < getScopeRank(state.speedScope)) {
        PSL.traceSpeed("media-authority.applyTargetSpeed.skipLowerScope", {
          mediaId: mediaId,
          speed: speed,
          scope: nextScope,
          currentScope: state.speedScope,
          currentTargetSpeed: state.targetSpeed
        });
        return false;
      }
      state.speedScope = nextScope;
      if (PSL.setMediaSpeedById) {
        PSL.setMediaSpeedById(mediaId, speed, nextScope);
      } else {
        state.targetSpeed = speed;
      }

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

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

      state.syntheticMode = "reverse";
      PSL.traceSpeed("media-authority.applyTargetSpeed.negative", {
        mediaId: mediaId,
        speed: speed
      });
      return true;
    }

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

    function handleSourceChange(mediaId, nextSrc) {
      var state = getState(mediaId);
      PSL.traceSpeed("media-authority.handleSourceChange.enter", {
        mediaId: mediaId,
        nextSrc: nextSrc,
        state: state
      });
      if (!state) {
        registerMedia(mediaId, nextSrc);
        return false;
      }
      var changed = PSL.detectMediaSourceChange(state, nextSrc);
      PSL.traceSpeed("media-authority.handleSourceChange.exit", {
        mediaId: mediaId,
        changed: changed,
        state: state
      });
      return changed;
    }

    function detachMedia(mediaId) {
      var existed = states.delete(mediaId);
      PSL.traceSpeed("media-authority.detachMedia", {
        mediaId: mediaId,
        existed: existed
      });
      if (existed) {
        sendCommand({ type: "detach-media", mediaId: mediaId });
      }
      return existed;
    }

    return {
      applyTargetSpeed: applyTargetSpeed,
      detachMedia: detachMedia,
      getState: getState,
      handleObservedPlaybackRate: handleObservedPlaybackRate,
      handleSourceChange: handleSourceChange,
      registerMedia: registerMedia
    };
  }

  PSL.createMediaAuthority = createMediaAuthority;
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/page-world-bridge.js ----
(function (PSL, global) {
  var PROBE_FLAG = "PSL_V2_PAGE_WORLD_PROBE";
  var DISABLED_FLAG = "PSL_V2_PAGE_WORLD_DISABLED";
  var hookPath = "extension/page-world/media-hook.js";
  var mediaById = new Map();

  function isProbeEnabled() {
    var result;
    try {
      result = !global.localStorage || global.localStorage.getItem(DISABLED_FLAG) !== "1";
    } catch (e) {
      result = true;
    }
    PSL.traceSpeed("page-world-bridge.isProbeEnabled", { result: result });
    return result;
  }

  function isDebugEnabled() {
    try {
      return global.localStorage && global.localStorage.getItem(PROBE_FLAG) === "1";
    } catch (e) {
      return false;
    }
  }

  function log(message, detail) {
    if (!isDebugEnabled()) {
      return;
    }
    if (detail) {
      console.log("[Speed & Loop v2 probe] " + message, detail);
      return;
    }
    console.log("[Speed & Loop v2 probe] " + message);
  }

  function getEventTarget() {
    var result = typeof global.addEventListener === "function" ? global : global.window;
    PSL.traceSpeed("page-world-bridge.getEventTarget", {
      hasEventTarget: Boolean(result)
    });
    return result;
  }

  function createCustomEvent(type, detail) {
    var EventCtor = global.CustomEvent || (global.window && global.window.CustomEvent);
    var encodedDetail = encodeBridgePayload(detail);
    PSL.traceSpeed("page-world-bridge.createCustomEvent", {
      type: type,
      detail: encodedDetail,
      hasCustomEvent: typeof EventCtor === "function"
    });
    if (typeof EventCtor === "function") {
      return new EventCtor(type, { detail: encodedDetail });
    }
    return { type: type, detail: encodedDetail };
  }

  function encodeBridgePayload(detail) {
    return JSON.stringify(detail || {});
  }

  function decodeBridgePayload(detail) {
    if (typeof detail === "string") {
      try {
        return JSON.parse(detail);
      } catch (e) {
        PSL.traceSpeed("page-world-bridge.decodeBridgePayload.error", {
          message: e && e.message
        });
        return null;
      }
    }
    return detail || null;
  }

  function getMediaId(media) {
    var mediaId = PSL.getMediaKey ? PSL.getMediaKey(media) : media.currentSrc || media.src;
    PSL.traceSpeed("page-world-bridge.getMediaId", {
      mediaId: mediaId,
      currentSrc: media && media.currentSrc,
      src: media && media.src
    });
    return mediaId;
  }

  function appendScript(doc, channelId, src) {
    PSL.traceSpeed("page-world-bridge.appendScript.enter", {
      channelId: channelId,
      src: src
    });
    var script = doc.createElement("script");
    script.src = src;
    script.async = false;
    script.dataset.pslV2ProbeChannel = channelId;
    script.dataset.pslV2ProbeDebug = isDebugEnabled() ? "1" : "0";
    script.onload = function () {
      PSL.traceSpeed("page-world-bridge.appendScript.onload", { channelId: channelId });
      log("page-world hook script loaded");
      if (script.parentNode) {
        script.parentNode.removeChild(script);
      }
    };
    script.onerror = function () {
      PSL.traceSpeed("page-world-bridge.appendScript.onerror", { channelId: channelId, src: src });
      log("page-world hook script failed to load", { src: src });
    };
    (doc.documentElement || doc.head || doc.body).appendChild(script);
  }

  function installProbe(doc) {
    PSL.traceSpeed("page-world-bridge.installProbe.enter", {
      hasDoc: Boolean(doc),
      alreadyInstalled: Boolean(doc && doc.__pslV2PageWorldProbeInstalled)
    });
    if (!doc || doc.__pslV2PageWorldProbeInstalled) {
      return false;
    }

    var src = PSL.getAssetUrl(hookPath);
    if (!src) {
      PSL.traceSpeed("page-world-bridge.installProbe.noAssetUrl", { hookPath: hookPath });
      log("runtime asset URL is unavailable");
      return false;
    }

    var channelId = "psl-v2-probe-" + PSL.instanceId;
    var fromPage = channelId + ":from-page";
    var toPage = channelId + ":to-page";
    var eventTarget = getEventTarget();

    function sendCommand(command) {
      PSL.traceSpeed("page-world-bridge.sendCommand.enter", command);
      if (eventTarget && typeof eventTarget.dispatchEvent === "function") {
        eventTarget.dispatchEvent(createCustomEvent(toPage, command));
        PSL.traceSpeed("page-world-bridge.sendCommand.dispatched", command);
      }
    }

    PSL.v2MediaAuthority = PSL.v2MediaAuthority || PSL.createMediaAuthority({
      sendCommand: sendCommand
    });

    doc.__pslV2PageWorldProbeInstalled = true;
    if (eventTarget && typeof eventTarget.addEventListener === "function") {
      eventTarget.addEventListener(fromPage, function (event) {
        var detail = decodeBridgePayload(event.detail);
        PSL.traceSpeed("page-world-bridge.fromPageEvent", detail);
        log("from page", detail);
        PSL.pageWorldBridge.handlePageEvent(detail);
      });
    }

    appendScript(doc, channelId, src);
    log("bridge installed", { channelId: channelId, src: src });
    PSL.traceSpeed("page-world-bridge.installProbe.exit", {
      channelId: channelId,
      src: src
    });
    return true;
  }

  function handlePageEvent(detail) {
    var authority = PSL.v2MediaAuthority;
    PSL.traceSpeed("page-world-bridge.handlePageEvent.enter", detail);
    if (!authority || !detail || !detail.mediaId) {
      PSL.traceSpeed("page-world-bridge.handlePageEvent.skip", {
        hasAuthority: Boolean(authority),
        detail: detail
      });
      return;
    }
    if (
      detail.type === "media-seen" ||
      detail.type === "loadedmetadata" ||
      detail.type === "play" ||
      detail.type === "pause" ||
      detail.type === "load"
    ) {
      authority.registerMedia(detail.mediaId, detail.currentSrc || "");
      return;
    }
    if (detail.type === "sourcechange") {
      authority.handleSourceChange(detail.mediaId, detail.currentSrc || "");
      resetLoopForPageSourceChange(detail);
      return;
    }
    if (
      detail.type === "ratechange" ||
      detail.type === "playbackRate-set" ||
      detail.type === "defaultPlaybackRate-set"
    ) {
      authority.registerMedia(detail.mediaId, detail.currentSrc || "");
      if (detail.commandedPlaybackRate === true) {
        PSL.traceSpeed("page-world-bridge.handlePageEvent.skipCommandedRate", detail);
        return;
      }
      authority.handleObservedPlaybackRate(detail.mediaId, Number(detail.playbackRate));
    }
  }

  function applySpeed(video, speed, scope, reason) {
    var mediaId;
    PSL.traceSpeed("page-world-bridge.applySpeed.enter", {
      speed: speed,
      scope: scope,
      reason: reason,
      hasAuthority: Boolean(PSL.v2MediaAuthority)
    });
    if (!PSL.v2MediaAuthority) {
      return false;
    }
    mediaId = getMediaId(video);
    mediaById.set(mediaId, video);
    PSL.v2MediaAuthority.registerMedia(mediaId, PSL.getMediaSourceKey(video));
    PSL.v2MediaAuthority.applyTargetSpeed(mediaId, speed, scope, reason);
    PSL.traceSpeed("page-world-bridge.applySpeed.exit", {
      mediaId: mediaId,
      speed: speed
    });
    return true;
  }

  function resetLoopForPageSourceChange(detail) {
    var media = mediaById.get(detail.mediaId) || mediaById.get(detail.previousSrc);
    PSL.traceSpeed("page-world-bridge.resetLoopForPageSourceChange", {
      detail: detail,
      hasMedia: Boolean(media)
    });
    if (media && PSL.resetLoopStateForSourceChange) {
      PSL.resetLoopStateForSourceChange(media, detail.previousSrc);
    }
    if (media && detail.currentSrc) {
      mediaById.set(detail.currentSrc, media);
    }
  }

  PSL.pageWorldBridge = {
    applySpeed: applySpeed,
    handlePageEvent: handlePageEvent,
    installProbe: installProbe,
    isProbeEnabled: isProbeEnabled
  };

  if (isProbeEnabled()) {
    installProbe(document);
  }
})(globalThis.PlaySpeedLoop, globalThis);

// ---- code/extension/content/site-placement.js ----
(function (PSL) {
  PSL.isAmazonVideoDocument = function (doc, host) {
    host = host || (doc.location ? doc.location.hostname : location.hostname);
    return (
      /(^|\.)amazon\./.test(host) ||
      /(^|\.)primevideo\.com$/.test(host) ||
      Boolean(doc.getElementById("dv-web-player"))
    );
  };

  function findAmazonVideoContainer(doc) {
    var selectors = [
      "#dv-web-player",
      ".dv-player-fullscreen",
      ".atvwebplayersdk-player-container",
      ".atvwebplayersdk-overlay-container",
      ".atvwebplayersdk-overlays-container",
      ".atvwebplayersdk-persistent-component-container",
      ".webPlayerSDKContainer"
    ];

    for (var i = 0; i < selectors.length; i++) {
      var container = doc.querySelector(selectors[i]);
      if (container) {
        return container;
      }
    }

    return doc.body;
  }

  PSL.placeController = function (videoController, fragment) {
    var doc = videoController.video.ownerDocument;
    var host = doc.location ? doc.location.hostname : location.hostname;
    var parent = videoController.parent;
    var wrapper = videoController.wrapper;

    switch (true) {
      case PSL.isAmazonVideoDocument(doc, host):
        wrapper.style.position = "fixed";
        wrapper.style.zIndex = "2147483647";
        wrapper.style.top = "16px";
        wrapper.style.left = "16px";
        wrapper.style.width = "280px";
        wrapper.style.height = "90px";
        wrapper.style.pointerEvents = "auto";
        findAmazonVideoContainer(doc).appendChild(fragment);
        break;
      default:
        parent.insertBefore(fragment, parent.firstChild);
    }
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/overlay-critical-css.js ----
(function (PSL) {
  PSL.getOverlayCriticalCss = function () {
    return (
      "#controller.panel-collapsed:not(.expanded) #speed-row," +
      "#controller.panel-collapsed:not(.expanded) #loop-row," +
      "#controller.panel-collapsed:not(.expanded) .collapse-control{" +
      "display:none !important;" +
      "}" +
      "#controller.expanded #collapsed-summary," +
      "#controller.expanded .expand-control{" +
      "display:none !important;" +
      "}"
    );
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/overlay-template.js ----
(function (PSL) {
  function appendTextElement(doc, parent, tagName, text, className, id) {
    var element = doc.createElement(tagName);
    if (id) {
      element.id = id;
    }
    if (className) {
      element.className = className;
    }
    element.textContent = text;
    parent.appendChild(element);
    return element;
  }

  function appendButton(doc, parent, action, text, className, id) {
    var button = appendTextElement(doc, parent, "button", text, className, id);
    if (action) {
      button.dataset.action = action;
    }
    return button;
  }

  PSL.buildOverlayStyleElement = function (doc) {
    var style = doc.createElement("style");
    var overlayStyleUrl = PSL.getAssetUrl("extension/styles/overlay-shadow.css");
    style.textContent =
      (overlayStyleUrl ? '@import "' + overlayStyleUrl + '";' : "") +
      PSL.getOverlayCriticalCss();
    return style;
  };

  PSL.buildOverlayControllerContent = function (doc, options) {
    var fragment = doc.createDocumentFragment();
    var controller = doc.createElement("div");
    var controllerClasses = [];

    if (options.speedControlsEnabled) {
      controllerClasses.push("speed-enabled");
    }
    if (options.loopControlsEnabled) {
      controllerClasses.push("loop-enabled");
    }

    controller.id = "controller";
    controllerClasses.push("panel-collapsed");
    controller.className = controllerClasses.join(" ");
    controller.style.top = options.top;
    controller.style.left = options.left;
    if (controller.style.setProperty) {
      controller.style.setProperty("--psl-panel-background-opacity", String(options.opacity));
    } else {
      controller.style["--psl-panel-background-opacity"] = String(options.opacity);
    }

    if (options.speedControlsEnabled) {
      var speedRow = doc.createElement("div");
      speedRow.id = "speed-row";
      speedRow.className = "control-row";
      appendTextElement(doc, speedRow, "span", "Speed", "row-label");
      appendButton(doc, speedRow, "slower", "-");
      var speedExpandedSlot = doc.createElement("span");
      speedExpandedSlot.dataset.pslSlot = "speed-expanded";
      speedRow.appendChild(speedExpandedSlot);
      appendButton(doc, speedRow, "faster", "+");
      appendButton(doc, speedRow, "reset", "Reset", "reset-button");
      controller.appendChild(speedRow);
    }

    if (options.loopControlsEnabled || options.speedControlsEnabled) {
      var collapsedSummary = doc.createElement("div");
      collapsedSummary.id = "collapsed-summary";
      collapsedSummary.className = "body";
      var speedCompactSlot = doc.createElement("span");
      speedCompactSlot.dataset.pslSlot = "speed-compact";
      collapsedSummary.appendChild(speedCompactSlot);
      if (options.loopControlsEnabled) {
        var loopSummarySlot = doc.createElement("span");
        loopSummarySlot.dataset.pslSlot = "loop-summary";
        loopSummarySlot.className = "loop-summary";
        collapsedSummary.appendChild(loopSummarySlot);
      }
      controller.appendChild(collapsedSummary);
    }

    if (options.loopControlsEnabled) {
      var loopRow = doc.createElement("div");
      loopRow.id = "loop-row";
      loopRow.className = "control-row";
      appendTextElement(doc, loopRow, "span", "Loop", "row-label");
      appendTextElement(doc, loopRow, "span", "--:--", "time-value empty", "start-indicator");
      appendTextElement(doc, loopRow, "span", "--:--", "time-value empty", "end-indicator");
      appendButton(doc, loopRow, "clear-loop", "Reset", "reset-button");
      controller.appendChild(loopRow);
    }

    appendButton(
      doc,
      controller,
      null,
      "",
      "panel-toggle chevron collapse-control",
      "collapse-button"
    ).setAttribute("aria-label", "Collapse controller");
    appendButton(
      doc,
      controller,
      null,
      "",
      "panel-toggle toggle expand-control",
      "expand-button"
    ).setAttribute("aria-label", "Expand controller");

    fragment.appendChild(PSL.buildOverlayStyleElement(doc));
    fragment.appendChild(controller);
    return fragment;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-indicator-view.js ----
(function (PSL) {
  PSL.formatSpeedDisplayValue = function (speed) {
    return speed.toFixed(2);
  };

  PSL.createSpeedView = function (options) {
    var video = options.video;
    var doc = options.doc;
    var compactSlot = options.compactSlot || options.mountSlot;
    var expandedSlot = options.expandedSlot || compactSlot;
    var root = doc.createElement("span");

    root.id = "speed-indicator";
    root.className = "speed-value";
    root.dataset.pslOwner = "speed-view";

    function moveTo(slot) {
      if (slot && root.parentNode !== slot) {
        slot.appendChild(root);
      }
    }

    function render() {
      var speed = PSL.getMediaSpeed ? PSL.getMediaSpeed(video) : video.playbackRate;
      root.textContent = PSL.formatSpeedDisplayValue(speed);
    }

    return {
      mount: function () {
        moveTo(compactSlot);
      },
      render: render,
      showCompact: function () {
        moveTo(compactSlot);
      },
      showExpanded: function () {
        moveTo(expandedSlot);
      },
      destroy: function () {
        root.remove();
      }
    };
  };

  PSL.renderSpeedView = function (video) {
    if (!video._pslControllerHandle || !video._pslControllerHandle.renderSpeedView) {
      return;
    }
    video._pslControllerHandle.renderSpeedView();
  };

  PSL.updateSpeedIndicator = function (video) {
    PSL.renderSpeedView(video);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/loop-actions.js ----
(function (PSL) {
  function getLoopKey(video) {
    return PSL.getMediaKey(video);
  }

  function hasNumber(value) {
    return typeof value === "number" && !isNaN(value);
  }

  function getEffectiveRate(video) {
    if (PSL.getEffectivePlaybackRate) {
      return PSL.getEffectivePlaybackRate(video);
    }
    return video.playbackRate;
  }

  function normalizeEndpoint(video, time) {
    var point = Number(time);
    if (isNaN(point) || point < 0) {
      return NaN;
    }
    if (video.duration && point >= video.duration) {
      return video.duration - 0.05;
    }
    return point;
  }

  function formatSummary(left, right, pending, enabled) {
    if (hasNumber(pending)) {
      return {
        text: PSL.convertSecToMin(pending) + " / __:__",
        left: PSL.convertSecToMin(pending),
        right: "__:__",
        rightBlank: true
      };
    }

    if (enabled && hasNumber(left) && hasNumber(right) && right > left) {
      return {
        text: PSL.convertSecToMin(left) + " / " + PSL.convertSecToMin(right),
        left: PSL.convertSecToMin(left),
        right: PSL.convertSecToMin(right),
        rightBlank: false
      };
    }

    return {
      text: "",
      left: "",
      right: "",
      rightBlank: false
    };
  }

  PSL.convertSecToMin = function (timeInSecs) {
    var tempDate = new Date(null);
    tempDate.setSeconds(Math.round(timeInSecs));
    return timeInSecs >= 3600
      ? tempDate.toISOString().substring(11, 19)
      : tempDate.toISOString().substring(14, 19);
  };

  PSL.getLoopSummary = function (video) {
    var src = getLoopKey(video);
    var left = PSL.state.startTimes[src];
    var right = PSL.state.endTimes[src];
    var pending = PSL.state.pendingLoopPoints[src];
    return formatSummary(left, right, pending, PSL.state.loopsEnabled[src]);
  };

  PSL.renderLoopSummary = function (video) {
    var summary = PSL.getLoopSummary(video);
    return summary.text;
  };

  PSL.updateCollapsedLoopSummary = function (video) {
    return PSL.renderLoopSummary(video);
  };

  PSL.expandLoopController = function (video) {
    var handle = video._pslControllerHandle;
    if (!handle || !handle.setPanelExpanded) {
      return;
    }
    handle.setPanelExpanded(true);
  };

  PSL.updateLoopRangeDisplay = function (video) {
    var handle = video._pslControllerHandle;
    if (!handle || !handle.renderLoopRange) {
      return;
    }
    handle.renderLoopRange();
  };

  PSL.markLoopRange = function (video, time) {
    var src = getLoopKey(video);
    var point = normalizeEndpoint(video, time);
    var pending = PSL.state.pendingLoopPoints[src];
    var left;
    var right;

    PSL.traceSpeed("loop-actions.markLoopRange.enter", {
      mediaKey: src,
      time: time,
      point: point,
      pending: pending
    });

    if (isNaN(point)) {
      PSL.traceSpeed("loop-actions.markLoopRange.invalid", { mediaKey: src, time: time });
      return;
    }

    PSL.expandLoopController(video);

    if (!hasNumber(pending) || PSL.state.loopsEnabled[src]) {
      PSL.disableLoop(video);
      PSL.state.pendingLoopPoints[src] = point;
      PSL.state.startTimes[src] = point;
      PSL.state.endTimes[src] = 0;
      PSL.updateLoopRangeDisplay(video);
      PSL.traceSpeed("loop-actions.markLoopRange.pending", {
        mediaKey: src,
        point: point
      });
      return;
    }

    left = Math.min(pending, point);
    right = Math.max(pending, point);
    if (right <= left) {
      PSL.updateLoopRangeDisplay(video);
      PSL.traceSpeed("loop-actions.markLoopRange.tooShort", {
        mediaKey: src,
        left: left,
        right: right
      });
      return;
    }

    PSL.state.startTimes[src] = left;
    PSL.state.endTimes[src] = right;
    delete PSL.state.pendingLoopPoints[src];
    PSL.enableLoop(video);
    PSL.updateLoopRangeDisplay(video);
    PSL.traceSpeed("loop-actions.markLoopRange.range", {
      mediaKey: src,
      left: left,
      right: right
    });
  };

  PSL.setLoopRange = function (video, left, right) {
    var src = getLoopKey(video);
    var normalizedLeft = normalizeEndpoint(video, Math.min(left, right));
    var normalizedRight = normalizeEndpoint(video, Math.max(left, right));

    if (isNaN(normalizedLeft) || isNaN(normalizedRight) || normalizedRight <= normalizedLeft) {
      PSL.clearLoop(video);
      return;
    }

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

  PSL.enableLoop = function (video) {
    var src = getLoopKey(video);
    var left = PSL.state.startTimes[src];
    var right = PSL.state.endTimes[src];

    if (!hasNumber(left) || !hasNumber(right) || right <= left) {
      PSL.state.loopsEnabled[src] = false;
      PSL.updateLoopRangeDisplay(video);
      return;
    }

    PSL.state.loopsEnabled[src] = true;
    var handle = video._pslControllerHandle;

    if (!handle || !handle.setLoopHandler) {
      PSL.updateLoopRangeDisplay(video);
      return;
    }

    handle.setLoopHandler(function () {
      var currentSrc = getLoopKey(video);
      var currentLeft = PSL.state.startTimes[currentSrc];
      var currentRight = PSL.state.endTimes[currentSrc];
      var rate = getEffectiveRate(video);

      if (!PSL.state.loopsEnabled[currentSrc]) {
        if (video._pslControllerHandle && video._pslControllerHandle.clearLoopHandler) {
          video._pslControllerHandle.clearLoopHandler();
        }
        return;
      }

      if (rate < 0) {
        if (video.currentTime <= currentLeft) {
          video.currentTime = currentRight;
        }
        return;
      }

      if (video.currentTime >= currentRight || video.currentTime < currentLeft) {
        video.currentTime = currentLeft;
      }
    });
    PSL.updateLoopRangeDisplay(video);
  };

  PSL.disableLoop = function (video) {
    var src = getLoopKey(video);
    var handle = video._pslControllerHandle;
    PSL.state.loopsEnabled[src] = false;
    if (handle && handle.clearLoopHandler) {
      handle.clearLoopHandler();
    }
    PSL.updateLoopRangeDisplay(video);
  };

  PSL.clearLoop = function (video) {
    var src = getLoopKey(video);
    PSL.disableLoop(video);
    PSL.state.startTimes[src] = 0;
    PSL.state.endTimes[src] = 0;
    delete PSL.state.pendingLoopPoints[src];
    PSL.updateLoopRangeDisplay(video);
    PSL.traceSpeed("loop-actions.clearLoop", { mediaKey: src });
  };

  PSL.updateLoopToggleDisplay = function (video) {
    PSL.updateLoopRangeDisplay(video);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/loop-view.js ----
(function (PSL) {
  function hasNumber(value) {
    return typeof value === "number" && !isNaN(value);
  }

  function setIndicator(indicator, text, empty) {
    if (!indicator) {
      return;
    }
    indicator.textContent = text;
    if (indicator.classList) {
      indicator.classList.toggle("empty", Boolean(empty));
    }
  }

  function appendSummaryPart(doc, parent, text, className) {
    var span = doc.createElement("span");
    span.className = className;
    span.textContent = text;
    parent.appendChild(span);
  }

  PSL.createLoopView = function (options) {
    var video = options.video;
    var startSlot = options.startSlot;
    var endSlot = options.endSlot;
    var summarySlot = options.summarySlot;

    function renderSummary() {
      var summary = PSL.getLoopSummary ? PSL.getLoopSummary(video) : { text: "" };
      var doc;

      if (!summarySlot) {
        return summary.text;
      }

      if (!summarySlot.ownerDocument || !summarySlot.replaceChildren) {
        summarySlot.textContent = summary.text;
        return summary.text;
      }

      doc = summarySlot.ownerDocument;
      summarySlot.replaceChildren();
      if (summary.left || summary.right) {
        appendSummaryPart(doc, summarySlot, summary.left, "summary-time");
        appendSummaryPart(doc, summarySlot, " / ", "summary-separator");
        appendSummaryPart(
          doc,
          summarySlot,
          summary.right,
          summary.rightBlank ? "summary-time blank" : "summary-time"
        );
      }
      return summary.text;
    }

    function clearRange() {
      setIndicator(startSlot, "--:--", true);
      setIndicator(endSlot, "--:--", true);
      if (summarySlot) {
        if (summarySlot.replaceChildren) {
          summarySlot.replaceChildren();
        } else {
          summarySlot.textContent = "";
        }
      }
    }

    return {
      renderRange: function () {
        var src = PSL.getMediaKey(video);
        var left = PSL.state.startTimes[src];
        var right = PSL.state.endTimes[src];
        var pending = PSL.state.pendingLoopPoints[src];

        if (hasNumber(pending)) {
          setIndicator(startSlot, PSL.convertSecToMin(pending), false);
          setIndicator(endSlot, "--:--", true);
        } else if (PSL.state.loopsEnabled[src] && hasNumber(left) && hasNumber(right) && right > left) {
          setIndicator(startSlot, PSL.convertSecToMin(left), false);
          setIndicator(endSlot, PSL.convertSecToMin(right), false);
        } else {
          setIndicator(startSlot, "--:--", true);
          setIndicator(endSlot, "--:--", true);
        }

        renderSummary();
      },
      clearRange: clearRange,
      renderSummary: renderSummary,
      destroy: clearRange
    };
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/media-overlay-controller.js ----
(function (PSL) {
  function hasMediaSource(media) {
    return Boolean(media.currentSrc || media.src || media.srcObject);
  }

  function createControllerHandle(controller) {
    return {
      isCurrentInstance: function (instanceId) {
        return controller._pslInstanceId === instanceId;
      },
      getParent: function () {
        return controller.parent;
      },
      matchesControllerElement: function (element) {
        return Boolean(element && controller.div === element);
      },
      remove: function () {
        controller.remove();
      },
      setPanelExpanded: function (expanded) {
        controller.setPanelExpanded(expanded);
      },
      renderSpeedView: function () {
        controller.renderSpeedView();
      },
      renderLoopRange: function () {
        controller.renderLoopRange();
      },
      clearLoopRangeDisplay: function () {
        controller.clearLoopRangeDisplay();
      },
      setLoopHandler: function (handler) {
        controller.setLoopHandler(handler);
      },
      clearLoopHandler: function () {
        controller.clearLoopHandler();
      },
      showController: function () {
        controller.showController();
      },
      blinkController: function () {
        controller.blinkController();
      },
      updateSourceVisibility: function () {
        controller.updateSourceVisibility();
      },
      applyStoredDomainSpeed: function () {
        controller.applyStoredDomainSpeed();
      }
    };
  }

  PSL.VideoController = function (target, parent) {
    var controller = this;

    if (target._pslControllerHandle) {
      return target._pslControllerHandle;
    }

    PSL.registerMedia(target);

    this._pslInstanceId = PSL.instanceId;
    this.video = target;
    this.parent = target.parentElement || parent;
    this._loopHandler = null;
    this.updateSourceVisibility = function () {
      if (!controller.div) {
        return;
      }
      controller.div.classList.toggle("mpc-nosource", !hasMediaSource(target));
    };
    this.applyStoredDomainSpeed = function () {
      if (PSL.applyStoredDomainSpeed) {
        PSL.applyStoredDomainSpeed(target);
      }
    };

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

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

    this.handleSourceVisibilityUpdate = function () {
      controller.updateSourceVisibility();
      controller.applyStoredDomainSpeed();
    };
    target.addEventListener("loadstart", this.handleSourceVisibilityUpdate);
    target.addEventListener("loadeddata", this.handleSourceVisibilityUpdate);
    target.addEventListener("canplay", this.handleSourceVisibilityUpdate);
    target.addEventListener("emptied", this.handleSourceVisibilityUpdate);

    this.sourceObserver = new MutationObserver(
      function (mutations) {
        mutations.forEach(function (mutation) {
          if (
            mutation.type === "attributes" &&
            (mutation.attributeName === "src" || mutation.attributeName === "currentSrc")
          ) {
            PSL.log("mutation of A/V element", 5);
            if (target._pslControllerHandle) {
              controller.updateSourceVisibility();
              controller.applyStoredDomainSpeed();
            }
          }
        });
      }
    );
    this.sourceObserver.observe(target, {
      attributeFilter: ["src", "currentSrc"]
    });
  };

  PSL.VideoController.prototype.remove = function () {
    if (this.destroySpeedView) {
      this.destroySpeedView();
    }
    this.div.remove();
    this.video.removeEventListener("loadedmetadata", this.handleLoadedMetadata);
    this.video.removeEventListener("loadstart", this.handleSourceVisibilityUpdate);
    this.video.removeEventListener("loadeddata", this.handleSourceVisibilityUpdate);
    this.video.removeEventListener("canplay", this.handleSourceVisibilityUpdate);
    this.video.removeEventListener("emptied", this.handleSourceVisibilityUpdate);
    if (this._loopHandler) {
      this.video.removeEventListener("timeupdate", this._loopHandler);
    }
    if (this.sourceObserver) {
      this.sourceObserver.disconnect();
    }
    if (PSL.stopSyntheticSpeed) {
      PSL.stopSyntheticSpeed(this.video);
    }
    delete this.video._pslControllerHandle;
    PSL.unregisterMedia(this.video);
  };

  PSL.VideoController.prototype.initializeControls = function () {
    PSL.log("initializeControls Begin", 5);
    var doc = this.video.ownerDocument;
    var top = Math.max(this.video.offsetTop, 0) + "px";
    var left = Math.max(this.video.offsetLeft, 0) + "px";
    var wrapper = doc.createElement("div");

    wrapper.classList.add("mpc-controller");
    wrapper._pslInstanceId = PSL.instanceId;
    wrapper._pslMediaElement = this.video;

    if (!hasMediaSource(this.video)) {
      wrapper.classList.add("mpc-nosource");
    }
    var shadow = wrapper.attachShadow({ mode: "closed" });
    shadow.appendChild(PSL.buildOverlayControllerContent(doc, {
      top: top,
      left: left,
      opacity: PSL.settings.controllerOpacity,
      speedControlsEnabled: PSL.settings.speedControlsEnabled !== false,
      loopControlsEnabled: PSL.settings.loopControlsEnabled !== false
    }));

    wrapper._shadow = shadow;
    shadow.querySelectorAll("button").forEach(function (button) {
      button.addEventListener(
        "click",
        function (e) {
          var eventTarget = e.target || button;
          var action = eventTarget.dataset.action;
          if (!action) {
            return;
          }
          PSL.runAction(action, PSL.getKeyBinding(action), e);
          e.stopPropagation();
        },
        true
      );
    });

    var controllerElement = shadow.querySelector("#controller");
    var collapseButton = shadow.querySelector("#collapse-button");
    var expandButton = shadow.querySelector("#expand-button");
    var speedExpandedSlot = shadow.querySelector('[data-psl-slot="speed-expanded"]');
    var speedCompactSlot = shadow.querySelector('[data-psl-slot="speed-compact"]');
    var loopSummarySlot = shadow.querySelector('[data-psl-slot="loop-summary"]');
    var startSlot = shadow.querySelector("#start-indicator");
    var endSlot = shadow.querySelector("#end-indicator");
    var speedView = null;
    var loopView = null;
    var setPanelExpanded = function (expanded) {
      controllerElement.classList.toggle("expanded", Boolean(expanded));
      controllerElement.classList.toggle("panel-collapsed", !expanded);
      if (!speedView) {
        return;
      }
      if (expanded) {
        speedView.showExpanded();
      } else {
        speedView.showCompact();
      }
    };

    if (PSL.createSpeedView && speedCompactSlot) {
      speedView = PSL.createSpeedView({
        video: this.video,
        doc: doc,
        compactSlot: speedCompactSlot,
        expandedSlot: speedExpandedSlot
      });
      speedView.mount();
      speedView.render();
    }

    if (PSL.createLoopView) {
      loopView = PSL.createLoopView({
        video: this.video,
        startSlot: startSlot,
        endSlot: endSlot,
        summarySlot: loopSummarySlot
      });
    }

    this.renderSpeedView = function () {
      if (speedView) {
        speedView.render();
      }
    };
    this.destroySpeedView = function () {
      if (speedView) {
        speedView.destroy();
        speedView = null;
      }
    };
    this.renderLoopRange = function () {
      if (loopView) {
        loopView.renderRange();
      }
    };
    this.clearLoopRangeDisplay = function () {
      if (loopView) {
        loopView.clearRange();
      }
    };
    this.setLoopHandler = function (handler) {
      this.clearLoopHandler();
      if (handler) {
        this._loopHandler = handler;
        this.video.addEventListener("timeupdate", handler);
      }
    };
    this.clearLoopHandler = function () {
      if (this._loopHandler) {
        this.video.removeEventListener("timeupdate", this._loopHandler);
        this._loopHandler = null;
      }
    };
    this.showController = function () {
      wrapper.classList.add("mpc-show");
      wrapper.classList.remove("mpc-hidden");
    };
    this.blinkController = function () {
      wrapper.classList.add("mpc-show");
      wrapper.classList.remove("mpc-hidden");
    };

    if (collapseButton) {
      collapseButton.addEventListener("click", function (e) {
        setPanelExpanded(false);
        e.stopPropagation();
      }, true);
    }
    if (expandButton) {
      expandButton.addEventListener("click", function (e) {
        setPanelExpanded(true);
        e.stopPropagation();
      }, true);
    }

    controllerElement.addEventListener("mouseenter", function (e) { return e.stopPropagation(); }, false);
    controllerElement.addEventListener("mouseleave", function (e) { return e.stopPropagation(); }, false);
    controllerElement.addEventListener("click", function (e) { return e.stopPropagation(); }, false);
    controllerElement.addEventListener("mousedown", function (e) { return e.stopPropagation(); }, false);

    this.setPanelExpanded = setPanelExpanded;

    var fragment = doc.createDocumentFragment();
    fragment.appendChild(wrapper);
    this.div = wrapper;
    PSL.placeController({
      video: this.video,
      parent: this.parent,
      wrapper: wrapper
    }, fragment);
    return wrapper;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/controller-attachment-lifecycle.js ----
(function (PSL) {
  function isCurrentController(media) {
    try {
      return (
        media._pslControllerHandle &&
        media._pslControllerHandle.isCurrentInstance &&
        media._pslControllerHandle.isCurrentInstance(PSL.instanceId)
      );
    } catch (e) {
      return false;
    }
  }

  function isControllerAttachedToCurrentParent(media) {
    try {
      return (
        media._pslControllerHandle &&
        media._pslControllerHandle.getParent &&
        media._pslControllerHandle.getParent() === media.parentElement
      );
    } catch (e) {
      return false;
    }
  }

  function removeOrphanControllerElements(media) {
    var doc = media && media.ownerDocument;
    if (!doc || !doc.querySelectorAll) {
      return;
    }

    doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
      if (
        controller._pslInstanceId === PSL.instanceId &&
        controller._pslMediaElement === media &&
        (!media._pslControllerHandle ||
          !media._pslControllerHandle.matchesControllerElement ||
          !media._pslControllerHandle.matchesControllerElement(controller))
      ) {
        controller.remove();
      }
    });
  }

  PSL.removeControllerFromMedia = function (media) {
    if (!media._pslControllerHandle) {
      removeOrphanControllerElements(media);
      return;
    }

    try {
      if (typeof media._pslControllerHandle.remove === "function") {
        media._pslControllerHandle.remove();
        return;
      }
    } catch (e) {
      // Fall through and remove the visible controller element directly.
    }

    delete media._pslControllerHandle;
    PSL.unregisterMedia(media);
    removeOrphanControllerElements(media);
  };

  PSL.attachControllerToMedia = function (media) {
    removeOrphanControllerElements(media);

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

    if (isCurrentController(media)) {
      if (!isControllerAttachedToCurrentParent(media)) {
        PSL.removeControllerFromMedia(media);
        new PSL.VideoController(media);
        return;
      }
      if (media._pslControllerHandle && media._pslControllerHandle.applyStoredDomainSpeed) {
        media._pslControllerHandle.applyStoredDomainSpeed();
      }
      return;
    }

    PSL.removeControllerFromMedia(media);

    if (!media._pslControllerHandle) {
      new PSL.VideoController(media);
    }
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-domain-store.js ----
(function (PSL) {
  function getSpeedDomain(video) {
    var doc = video.ownerDocument || document;
    var host = "";

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

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

  PSL.isPositiveStoredSpeed = function (speed) {
    return typeof speed === "number" && !isNaN(speed) && speed > 0 && speed <= 16;
  };

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

    if (!domain || !PSL.isPositiveStoredSpeed(speed)) {
      return;
    }

    domainSpeeds = Object.assign({}, PSL.settings.domainSpeeds || {});
    domainSpeeds[domain] = speed;
    PSL.settings.domainSpeeds = domainSpeeds;
    PSL.settings.lastSpeed = speed;
    PSL.setStorage(
      {
        lastSpeed: speed,
        domainSpeeds: domainSpeeds
      },
      function () {
        PSL.log("Domain speed setting saved for " + domain + ": " + speed, 5);
      }
    );
  };

  PSL.getSavedDomainSpeed = function (video) {
    var domain = getSpeedDomain(video);
    return domain && PSL.settings.domainSpeeds
      ? Number(PSL.settings.domainSpeeds[domain])
      : NaN;
  };

  PSL.shouldRestoreSavedDomainSpeed = function (video) {
    return (
      (!PSL.hasMediaSpeed || !PSL.hasMediaSpeed(video)) &&
      PSL.isPositiveStoredSpeed(PSL.getSavedDomainSpeed(video))
    );
  };

  PSL.applyStoredDomainSpeed = function (video) {
    var speed = PSL.getSavedDomainSpeed(video);
    var resolvedSpeed = PSL.getEffectiveSpeedFromHierarchy
      ? PSL.getEffectiveSpeedFromHierarchy({
          temporarySpeed: null,
          siteSpeed: speed,
          globalSpeed: 1,
          minSpeed: -16,
          maxSpeed: 16
        })
      : { speed: speed, scope: "site" };

    if (
      PSL.settings.speedControlsEnabled === false ||
      !PSL.isPositiveStoredSpeed(speed) ||
      (PSL.hasMediaSpeed && PSL.hasMediaSpeed(video)) ||
      resolvedSpeed.scope !== "site" ||
      video._pslApplyingStoredDomainSpeed
    ) {
      return false;
    }

    video._pslApplyingStoredDomainSpeed = true;
    PSL.setSpeed(video, speed);
    video._pslApplyingStoredDomainSpeed = false;
    return true;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/keep-speed-site-matcher.js ----
(function (PSL) {
  function normalizeKeepSpeedPattern(pattern) {
    pattern = String(pattern || "").replace(PSL.regStrip, "");
    if (!pattern) {
      return "";
    }
    try {
      if (/^https?:\/\//i.test(pattern)) {
        return new URL(pattern).hostname;
      }
    } catch (e) {}
    return pattern.replace(/^www\./, "");
  }

  PSL.buildKeepSpeedMatcher = function (pattern) {
    var normalized = normalizeKeepSpeedPattern(pattern);
    var regex;

    if (!normalized) {
      return null;
    }

    if (normalized.startsWith("/")) {
      try {
        regex = new RegExp(normalized.slice(1, -1));
      } catch (e) {
        return null;
      }
      return function (host) {
        return regex.test(host);
      };
    }

    return function (host) {
      return host === normalized || host.endsWith("." + normalized);
    };
  };

  PSL.buildKeepSpeedMatchers = function (patterns) {
    return String(patterns || "")
      .split("\n")
      .map(PSL.buildKeepSpeedMatcher)
      .filter(Boolean);
  };

  PSL.updateKeepSpeedSiteMatchers = function () {
    PSL.keepSpeedSiteMatchers = PSL.buildKeepSpeedMatchers(
      PSL.settings.keepSpeedSites
    );
  };

  PSL.isKeepSpeedSite = function (video) {
    var doc = video.ownerDocument || document;
    var host = "";

    try {
      host = (doc.location && doc.location.hostname || "").replace(/^www\./, "");
    } catch (e) {}

    return (PSL.keepSpeedSiteMatchers || []).some(function (matches) {
      return matches(host);
    });
  };

  PSL.updateKeepSpeedSiteMatchers();
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/keep-speed-site-enforcement.js ----
(function (PSL) {
  var KEEP_SPEED_ENFORCEMENT_INTERVAL_MS = 50;

  function isPositivePlaybackRate(speed) {
    return typeof speed === "number" && !isNaN(speed) && speed > 0 && speed <= 16;
  }

  PSL.stopPersistentSpeedEnforcement = function (video) {
    if (video._pslSpeedEnforcer) {
      clearInterval(video._pslSpeedEnforcer);
      video._pslSpeedEnforcer = null;
    }
  };

  PSL.startPersistentSpeedEnforcement = function (video) {
    PSL.stopPersistentSpeedEnforcement(video);
    if (
      !PSL.isKeepSpeedSite(video) &&
      (!PSL.getMediaSpeed || !isPositivePlaybackRate(PSL.getMediaSpeed(video)))
    ) {
      return;
    }
    video._pslSpeedEnforcer = setInterval(function () {
      var target = PSL.getMediaSpeed(video);
      if (
        !video.isConnected ||
        !isPositivePlaybackRate(target) ||
        video._pslPausedForZeroSpeed ||
        video._pslReversing
      ) {
        PSL.stopPersistentSpeedEnforcement(video);
        return;
      }
      if (Math.abs(video.playbackRate - target) > 0.001) {
        video.playbackRate = target;
      }
      PSL.updateSpeedIndicator(video);
    }, KEEP_SPEED_ENFORCEMENT_INTERVAL_MS);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/temporary-playback-rate-enforcement.js ----
(function (PSL) {
  PSL.shouldTemporarilyEnforceSpeed = function (video) {
    return (
      PSL.hasMediaSpeed &&
      PSL.hasMediaSpeed(video) &&
      video._pslEnforceSpeedUntil &&
      Date.now() < video._pslEnforceSpeedUntil
    );
  };

  PSL.enforceSpeedSoon = function (video) {
    if (!PSL.shouldTemporarilyEnforceSpeed(video)) {
      return;
    }

    setTimeout(function () {
      if (!PSL.shouldTemporarilyEnforceSpeed(video) || !video.isConnected) {
        return;
      }
      var speed = PSL.getMediaSpeed(video);
      if (speed > 0 && Math.abs(video.playbackRate - speed) > 0.001) {
        video.playbackRate = speed;
      }
    }, 0);
  };

  PSL.expectPlaybackRateChange = function (video, speed) {
    video._pslExpectedPlaybackRate = speed;
  };

  PSL.consumeExpectedPlaybackRateChange = function (video) {
    var expected = video._pslExpectedPlaybackRate;
    if (
      typeof expected === "number" &&
      Math.abs(video.playbackRate - expected) < 0.001
    ) {
      video._pslExpectedPlaybackRate = undefined;
      return true;
    }
    return false;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/synthetic-speed-state.js ----
(function (PSL) {
  function rememberPauseStateBeforeSyntheticSpeed(video) {
    if (
      !PSL.isSyntheticPausedSpeed(video) &&
      typeof video._pslWasPausedBeforeSyntheticSpeed === "undefined"
    ) {
      video._pslWasPausedBeforeSyntheticSpeed = video.paused;
    }
  }

  function getReverseLoopBoundary(video) {
    var mediaKey = PSL.getMediaKey(video);
    var start = PSL.state.startTimes[mediaKey];
    var end = PSL.state.endTimes[mediaKey];

    if (
      PSL.state.loopsEnabled[mediaKey] &&
      typeof start === "number" &&
      typeof end === "number" &&
      end > start
    ) {
      return { start: start, end: end };
    }

    return null;
  }

  PSL.isSyntheticPausedSpeed = function (video) {
    return video._pslPausedForZeroSpeed || video._pslReversing;
  };

  PSL.stopReversePlayback = function (video) {
    if (PSL.reversePlaybackEngine) {
      PSL.reversePlaybackEngine.stop(video);
    }
    video._pslReversing = false;
  };

  PSL.clearSyntheticSpeedState = function (video) {
    PSL.stopReversePlayback(video);
    PSL.stopPersistentSpeedEnforcement(video);
    video._pslPausedForZeroSpeed = false;
    video._pslWasPausedBeforeSyntheticSpeed = undefined;
  };

  PSL.resumeAfterSyntheticSpeedIfNeeded = function (video, wasSyntheticSpeed) {
    var shouldResume =
      wasSyntheticSpeed && video._pslWasPausedBeforeSyntheticSpeed === false;

    PSL.clearSyntheticSpeedState(video);

    if (shouldResume) {
      var playPromise = video.play();
      if (playPromise && playPromise.catch) {
        playPromise.catch(function () {});
      }
    }
  };

  PSL.pauseAtZeroSpeed = function (video) {
    PSL.stopReversePlayback(video);
    PSL.stopPersistentSpeedEnforcement(video);
    rememberPauseStateBeforeSyntheticSpeed(video);
    video._pslPausedForZeroSpeed = true;
    video._pslEnforceSpeedUntil = 0;
    PSL.updateSpeedIndicator(video);
    video.pause();
  };

  PSL.startReversePlayback = function (video, speed) {
    var frameRate = PSL.getReverseFrameRate();
    var intervalMs = 1000 / frameRate;
    var stepSeconds = Math.abs(speed) / frameRate;
    var boundary = getReverseLoopBoundary(video);

    PSL.stopReversePlayback(video);
    rememberPauseStateBeforeSyntheticSpeed(video);

    if (!PSL.reversePlaybackEngine) {
      video._pslPausedForZeroSpeed = false;
      video._pslReversing = false;
      video._pslEnforceSpeedUntil = 0;
      PSL.traceSpeed("synthetic-speed.startReversePlayback.missingEngine", {
        speed: speed
      });
      return;
    }

    video._pslPausedForZeroSpeed = false;
    video._pslReversing = true;
    video._pslEnforceSpeedUntil = 0;
    PSL.updateSpeedIndicator(video);
    video.pause();
    PSL.reversePlaybackEngine.start(video, speed, {
      intervalMs: intervalMs,
      stepSeconds: stepSeconds,
      loopStart: boundary && boundary.start,
      loopEnd: boundary && boundary.end,
      onEnd: function () {
        PSL.setSpeed(video, 0);
      },
      onStop: function () {
        video._pslReversing = false;
      }
    });
  };

  PSL.getEffectivePlaybackRate = function (video) {
    return PSL.getMediaSpeed ? PSL.getMediaSpeed(video) : video.playbackRate;
  };

  PSL.stopSyntheticSpeed = function (video) {
    PSL.clearSyntheticSpeedState(video);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-actions.js ----
(function (PSL) {
  PSL.clampSpeed = function (speed, min, max) {
    var result = Math.min(Math.max(speed, min), max);
    PSL.traceSpeed("speed-actions.clampSpeed", {
      speed: speed,
      min: min,
      max: max,
      result: result
    });
    return result;
  };

  PSL.getSpeedAdjustmentBase = function (video) {
    var result;
    if (PSL.getEffectivePlaybackRate) {
      result = PSL.getEffectivePlaybackRate(video);
    } else {
      result = video.playbackRate;
    }
    PSL.traceSpeed("speed-actions.getSpeedAdjustmentBase", {
      nativePlaybackRate: video && video.playbackRate,
      mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video),
      result: result
    });
    return result;
  };

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

    PSL.traceSpeed("speed-actions.adjustSpeed.enter", {
      value: value,
      options: options,
      currentPlaybackRate: video && video.playbackRate,
      mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
    });

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

    if (typeof value !== "number" || isNaN(value)) {
      PSL.traceSpeed("speed-actions.adjustSpeed.invalidValue", { value: value });
      return;
    }

    if (relative) {
      currentSpeed = PSL.getSpeedAdjustmentBase(video);
      targetSpeed = currentSpeed + value;

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

    targetSpeed = Number(PSL.clampSpeed(targetSpeed, -16, 16).toFixed(2));
    PSL.traceSpeed("speed-actions.adjustSpeed.target", {
      relative: relative,
      currentSpeed: currentSpeed,
      targetSpeed: targetSpeed
    });
    PSL.setSpeed(video, targetSpeed);
  };

  PSL.setSpeed = function (video, speed) {
    PSL.traceSpeed("speed-actions.setSpeed.enter", {
      speed: speed,
      speedControlsEnabled: PSL.settings.speedControlsEnabled,
      currentPlaybackRate: video && video.playbackRate,
      mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
    });
    if (PSL.settings.speedControlsEnabled === false) {
      PSL.traceSpeed("speed-actions.setSpeed.disabled");
      return;
    }

    PSL.log("setSpeed started: " + speed, 5);
    var speedValue = speed.toFixed(2);
    var speedNumber = Number(speedValue);
    var wasSyntheticSpeed = PSL.isSyntheticPausedSpeed(video);
    var speedScope = video._pslApplyingStoredDomainSpeed ? "site" : "temporary";
    PSL.setMediaSpeed(video, speedNumber, speedScope);

    if (speedNumber <= 0) {
      video._pslEnforceSpeedUntil = 0;
      video._pslExpectedPlaybackRate = undefined;
      PSL.stopPersistentSpeedEnforcement(video);
    }

    if (
      PSL.pageWorldBridge &&
      PSL.pageWorldBridge.isProbeEnabled &&
      PSL.pageWorldBridge.isProbeEnabled() &&
      PSL.pageWorldBridge.applySpeed
    ) {
      PSL.traceSpeed("speed-actions.setSpeed.pageWorld.before", {
        speed: speedNumber,
        scope: speedScope
      });
      PSL.pageWorldBridge.applySpeed(video, speedNumber, speedScope, "set-speed");
      PSL.traceSpeed("speed-actions.setSpeed.pageWorld.after", {
        speed: speedNumber,
        scope: speedScope
      });
      PSL.log("setSpeed sent to v2 media authority: " + speedNumber, 5);
    }

    if (speedNumber === 0) {
      PSL.traceSpeed("speed-actions.setSpeed.zero.before");
      PSL.pauseAtZeroSpeed(video);
      PSL.traceSpeed("speed-actions.setSpeed.zero.after", {
        paused: video && video.paused
      });
      PSL.log("setSpeed paused media at zero speed", 5);
      return;
    }

    if (speedNumber < 0) {
      PSL.traceSpeed("speed-actions.setSpeed.negative.before", {
        speed: speedNumber
      });
      PSL.startReversePlayback(video, speedNumber);
      PSL.settings.lastSpeed = speedNumber;
      PSL.traceSpeed("speed-actions.setSpeed.negative.after", {
        reversing: video && video._pslReversing,
        mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
      });
      PSL.log("setSpeed started reverse media at " + speedNumber, 5);
      return;
    }

    video._pslEnforceSpeedUntil = Date.now() + 3000;
    try {
      PSL.traceSpeed("speed-actions.setSpeed.native.before", {
        beforePlaybackRate: video.playbackRate,
        speed: speedNumber
      });
      PSL.expectPlaybackRateChange(video, speedNumber);
      video.playbackRate = speedNumber;
      PSL.traceSpeed("speed-actions.setSpeed.native.after", {
        afterPlaybackRate: video.playbackRate,
        speed: speedNumber
      });
    } catch (e) {
      video._pslExpectedPlaybackRate = undefined;
      PSL.traceSpeed("speed-actions.setSpeed.native.error", {
        message: e && e.message
      });
      throw e;
    }
    PSL.updateSpeedIndicator(video);
    PSL.resumeAfterSyntheticSpeedIfNeeded(video, wasSyntheticSpeed);
    if (!video._pslApplyingStoredDomainSpeed) {
      PSL.saveDomainSpeed(video, speedNumber);
    }
    PSL.enforceSpeedSoon(video);
    PSL.startPersistentSpeedEnforcement(video);
    PSL.traceSpeed("speed-actions.setSpeed.exit", {
      playbackRate: video && video.playbackRate,
      mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
    });
    PSL.log("setSpeed finished: " + speed, 5);
  };

  PSL.resetSpeed = function (video, target) {
    PSL.traceSpeed("speed-actions.resetSpeed.enter", {
      target: target,
      effectivePlaybackRate: PSL.getEffectivePlaybackRate(video)
    });
    if (Math.abs(PSL.getEffectivePlaybackRate(video) - target) < 0.001) {
      PSL.log("Resetting playback speed to 1.0", 4);
      PSL.setSpeed(video, 1.0);
    } else {
      PSL.log('Toggling playback speed to "preferred" speed', 4);
      PSL.setSpeed(video, target);
    }
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/speed-domain-storage-listener.js ----
(function (PSL) {
  if (PSL.__domainSpeedStorageListenerInstalled) {
    return;
  }
  PSL.__domainSpeedStorageListenerInstalled = true;

  try {
    var storage =
      typeof browser !== "undefined" && browser.storage
        ? browser.storage
        : typeof chrome !== "undefined" && chrome.storage
          ? chrome.storage
          : null;

    if (!storage || !storage.onChanged) {
      return;
    }

    storage.onChanged.addListener(function (changes, areaName) {
      if (areaName && areaName !== "sync") {
        return;
      }
      if (!changes || !changes.domainSpeeds) {
        return;
      }

      PSL.settings.domainSpeeds = changes.domainSpeeds.newValue || {};
    });
  } catch (e) {}
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/playback-rate-change-listener.js ----
(function (PSL) {
  function updateSpeedFromEvent(video) {
    PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.enter", {
      playbackRate: video && video.playbackRate,
      mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video),
      reversing: video && video._pslReversing,
      pausedForZero: video && video._pslPausedForZeroSpeed,
      hasController: Boolean(video && video._pslControllerHandle)
    });
    if (!video._pslControllerHandle || PSL.settings.speedControlsEnabled === false) {
      PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.skip", {
        hasController: Boolean(video && video._pslControllerHandle),
        speedControlsEnabled: PSL.settings.speedControlsEnabled
      });
      return;
    }

    if (video._pslReversing) {
      PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.reversing");
      PSL.updateSpeedIndicator(video);
      return;
    }

    if (video._pslPausedForZeroSpeed) {
      PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.zero");
      PSL.updateSpeedIndicator(video);
      return;
    }

    if (
      PSL.getMediaSpeedScope &&
      PSL.getMediaSpeedScope(video) === "temporary"
    ) {
      PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.temporaryTarget", {
        mediaSpeed: PSL.getMediaSpeed(video)
      });
      PSL.updateSpeedIndicator(video);
      return;
    }

    var speed = Number(video.playbackRate.toFixed(2));
    var observation = PSL.observeNativePlaybackRate
      ? PSL.observeNativePlaybackRate(video, speed)
      : { action: "adopt", speed: speed };

    PSL.log("Playback rate changed to " + speed, 4);
    PSL.traceSpeed("playback-rate-listener.updateSpeedFromEvent.update", {
      speed: speed,
      observation: observation
    });
    PSL.updateSpeedIndicator(video);
    if (video._pslControllerHandle && video._pslControllerHandle.blinkController) {
      video._pslControllerHandle.blinkController();
    }
  }

  PSL.setupRateChangeListener = function (doc) {
    PSL.traceSpeed("playback-rate-listener.setupRateChangeListener.enter", {
      alreadyAttached: doc._pslRateChangeListenerAttached,
      instanceId: PSL.instanceId
    });
    if (doc._pslRateChangeListenerAttached === PSL.instanceId) {
      PSL.traceSpeed("playback-rate-listener.setupRateChangeListener.skipAlreadyAttached");
      return;
    }
    if (doc._pslRateChangeHandler) {
      doc.removeEventListener("ratechange", doc._pslRateChangeHandler, true);
    }

    doc._pslRateChangeHandler = function (event) {
      var video = event.target;
      PSL.traceSpeed("playback-rate-listener.handler.enter", {
        playbackRate: video && video.playbackRate,
        mediaSpeed: video && PSL.getMediaSpeed && PSL.getMediaSpeed(video)
      });

      if (PSL.consumeExpectedPlaybackRateChange(video)) {
        PSL.traceSpeed("playback-rate-listener.handler.expectedConsumed");
        PSL.log("Ignoring playback-rate change caused by setSpeed", 4);
        event.stopImmediatePropagation();
        return;
      }

      if (
        PSL.shouldTemporarilyEnforceSpeed(video) &&
        Math.abs(video.playbackRate - PSL.getMediaSpeed(video)) > 0.001
      ) {
        PSL.traceSpeed("playback-rate-listener.handler.temporaryRestore", {
          playbackRate: video.playbackRate,
          mediaSpeed: PSL.getMediaSpeed(video)
        });
        PSL.log("Reapplying target playback speed", 4);
        event.stopImmediatePropagation();
        PSL.enforceSpeedSoon(video);
        return;
      }

      updateSpeedFromEvent(video);
    };
    doc._pslRateChangeListenerAttached = PSL.instanceId;
    doc.addEventListener("ratechange", doc._pslRateChangeHandler, true);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/action-dispatcher.js ----
(function (PSL) {
  var PUBLIC_ACTIONS = [
    "slower",
    "faster",
    "reset",
    "mark-loop-range",
    "clear-loop"
  ];

  PSL.runAction = function (action, value, e) {
    PSL.traceSpeed("action-dispatcher.runAction.enter", {
      action: action,
      value: value,
      mediaCount: PSL.state.mediaElements.length,
      hasEvent: Boolean(e)
    });
    PSL.log("runAction Begin", 5);

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

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

    if (PSL.actions && typeof PSL.actions[action] === "function") {
      PSL.traceSpeed("action-dispatcher.runAction.customAction", { action: action });
      PSL.actions[action](value, e);
      return;
    }

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

    PSL.state.mediaElements.forEach(function (video) {
      PSL.traceSpeed("action-dispatcher.runAction.media.iterate", {
        action: action,
        mediaKey: PSL.getMediaKey ? PSL.getMediaKey(video) : undefined,
        hasController: Boolean(video._pslControllerHandle),
        cancelled: video.classList && video.classList.contains("mpc-cancelled"),
        playbackRate: video.playbackRate,
        mediaSpeed: PSL.getMediaSpeed && PSL.getMediaSpeed(video)
      });
      var handle = video._pslControllerHandle;
      if (!handle) {
        PSL.traceSpeed("action-dispatcher.runAction.media.skipNoController");
        return;
      }

      if (e && (!handle.matchesControllerElement || !handle.matchesControllerElement(targetController))) {
        PSL.traceSpeed("action-dispatcher.runAction.media.skipControllerMismatch");
        return;
      }

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

      if (video.classList.contains("mpc-cancelled")) {
        PSL.traceSpeed("action-dispatcher.runAction.media.skipCancelled");
        return;
      }

      if (action === "faster") {
        PSL.traceSpeed("action-dispatcher.runAction.media.faster");
        PSL.adjustSpeed(video, PSL.getSpeedStep(), { relative: true });
      } else if (action === "slower") {
        PSL.traceSpeed("action-dispatcher.runAction.media.slower");
        PSL.adjustSpeed(video, -PSL.getSpeedStep(), { relative: true });
      } else if (action === "reset") {
        PSL.traceSpeed("action-dispatcher.runAction.media.reset");
        PSL.resetSpeed(video, 1.0);
      } else if (action === "mark-loop-range") {
        PSL.markLoopRange(video, video.currentTime);
      } else if (action === "clear-loop") {
        PSL.clearLoop(video);
      }
    });
    PSL.traceSpeed("action-dispatcher.runAction.exit", { action: action });
    PSL.log("runAction End", 5);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/keyboard-shortcuts.js ----
(function (PSL) {
  function getKeyboardEventKeyCode(event) {
    var result;
    if (event.keyCode) {
      result = event.keyCode;
    } else if (event.key && event.key.length === 1) {
      result = event.key.toUpperCase().charCodeAt(0);
    } else {
      result = 0;
    }
    PSL.traceSpeed("keyboard-shortcuts.getKeyboardEventKeyCode", {
      key: event.key,
      keyCode: event.keyCode,
      result: result
    });
    return result;
  }

  PSL.attachKeyboardShortcuts = function (docs) {
    PSL.traceSpeed("keyboard-shortcuts.attachKeyboardShortcuts.enter", {
      docCount: docs.length
    });
    docs.forEach(function (doc) {
      if (doc._pslKeyboardShortcutsAttached === PSL.instanceId) {
        PSL.traceSpeed("keyboard-shortcuts.attachKeyboardShortcuts.skipAlreadyAttached");
        return;
      }
      if (doc._pslKeyboardShortcutsHandler) {
        doc.removeEventListener(
          "keydown",
          doc._pslKeyboardShortcutsHandler,
          true
        );
      }

      doc._pslKeyboardShortcutsHandler = function (event) {
        var keyCode = getKeyboardEventKeyCode(event);
        var item;
        PSL.traceSpeed("keyboard-shortcuts.handler.enter", {
          keyCode: keyCode,
          targetNodeName: event.target && event.target.nodeName,
          mediaCount: PSL.state.mediaElements.length
        });
        PSL.log("Processing keydown event: " + keyCode, 6);

        if (
          !event.getModifierState ||
          event.getModifierState("Alt") ||
          event.getModifierState("Control") ||
          event.getModifierState("Fn") ||
          event.getModifierState("Meta") ||
          event.getModifierState("Hyper") ||
          event.getModifierState("OS")
        ) {
          PSL.traceSpeed("keyboard-shortcuts.handler.skipModifier", { keyCode: keyCode });
          PSL.log("Keydown event ignored due to active modifier: " + keyCode, 5);
          return;
        }

        if (
          event.target.nodeName === "INPUT" ||
          event.target.nodeName === "TEXTAREA" ||
          event.target.isContentEditable
        ) {
          PSL.traceSpeed("keyboard-shortcuts.handler.skipEditable", { keyCode: keyCode });
          return false;
        }

        if (!PSL.state.mediaElements.length) {
          PSL.traceSpeed("keyboard-shortcuts.handler.noMedia.beforeSync", { keyCode: keyCode });
          if (PSL.attachControlsToExistingMedia) {
            PSL.attachControlsToExistingMedia(doc);
          }
          PSL.traceSpeed("keyboard-shortcuts.handler.noMedia.afterSync", {
            keyCode: keyCode,
            mediaCount: PSL.state.mediaElements.length
          });
          if (!PSL.state.mediaElements.length) {
            PSL.traceSpeed("keyboard-shortcuts.handler.skipNoMedia", { keyCode: keyCode });
            return false;
          }
        }

        item = PSL.settings.keyBindings.find(function (item) {
          return item.key === keyCode;
        });
        if (item) {
          PSL.traceSpeed("keyboard-shortcuts.handler.bindingMatched", item);
          PSL.runAction(item.action, item.value);
          if (item.force === "true") {
            event.preventDefault();
            event.stopPropagation();
          }
        } else {
          PSL.traceSpeed("keyboard-shortcuts.handler.noBinding", { keyCode: keyCode });
        }

        PSL.traceSpeed("keyboard-shortcuts.handler.exit", { keyCode: keyCode });
        return false;
      };
      doc._pslKeyboardShortcutsAttached = PSL.instanceId;
      doc.addEventListener("keydown", doc._pslKeyboardShortcutsHandler, true);
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/remove-stale-media-controls.js ----
(function (PSL) {
  PSL.removeStaleControllerElements = function (doc) {
    doc.querySelectorAll(".mpc-controller").forEach(function (controller) {
      var instanceId;
      try {
        instanceId = controller._pslInstanceId;
      } catch (e) {}

      if (instanceId !== PSL.instanceId) {
        controller.remove();
      }
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/attach-controls-to-existing-media.js ----
(function (PSL) {
  function attachControlsToExistingMedia(doc) {
    PSL.findMediaElements(doc).forEach(PSL.attachControllerToMedia);
  }

  PSL.attachControlsToExistingMedia = attachControlsToExistingMedia;

  PSL.rescanMedia = function (doc) {
    doc = doc || document;
    if (PSL.loadSettings) {
      PSL.loadSettings(function () {
        if (PSL.settings.enabled) {
          attachControlsToExistingMedia(doc);
        }
      });
      return;
    }
    attachControlsToExistingMedia(doc);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/sync-controls-with-media-dom.js ----
(function (PSL) {
  function syncControlsForMediaNode(doc, node, added) {
    if (!added && doc.body.contains(node)) {
      return;
    }
    if (
      node.nodeName === "VIDEO" ||
      (node.nodeName === "AUDIO" && PSL.settings.audioBoolean)
    ) {
      if (added) {
        PSL.attachControllerToMedia(node);
      } else if (node._pslControllerHandle) {
        PSL.removeControllerFromMedia(node);
      }
    } else if (added && node.shadowRoot) {
      PSL.findMediaElements(node.shadowRoot).forEach(PSL.attachControllerToMedia);
    } else if (node.children !== undefined) {
      for (var i = 0; i < node.children.length; i++) {
        syncControlsForMediaNode(doc, node.children[i], added);
      }
    }
  }

  PSL.installMediaDomControlSync = function (doc) {
    var observer = new MutationObserver(function (mutations) {
      PSL.requestIdle(
        function () {
          mutations.forEach(function (mutation) {
            if (mutation.type !== "childList") {
              return;
            }
            mutation.addedNodes.forEach(function (node) {
              if (typeof node !== "function") {
                syncControlsForMediaNode(doc, node, true);
              }
            });
            mutation.removedNodes.forEach(function (node) {
              if (typeof node !== "function") {
                syncControlsForMediaNode(doc, node, false);
              }
            });
          });
        },
        { timeout: 1000 }
      );
    });

    observer.observe(doc, {
      childList: true,
      subtree: true
    });
    doc._pslMediaObserver = observer;
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/refresh-controls-after-same-page-navigation.js ----
(function (PSL) {
  var samePageRefreshDelays = [0, 100, 500, 1500];

  function refreshSamePageMediaControls(doc, reason) {
    PSL.traceSpeed("same-page-refresh.enter", {
      reason: reason,
      href: window.location && window.location.href
    });
    if (PSL.pageWorldBridge && PSL.pageWorldBridge.installProbe) {
      PSL.pageWorldBridge.installProbe(doc);
    }
    samePageRefreshDelays.forEach(function (delay) {
      setTimeout(function () {
        PSL.traceSpeed("same-page-refresh.rescan", {
          reason: reason,
          delay: delay
        });
        PSL.rescanMedia(doc);
      }, delay);
    });
  }

  PSL.installSamePageMediaControlRefresh = function (doc) {
    if (doc !== window.document || window._pslUrlChangeSpeedRefreshInstalled) {
      return;
    }
    window._pslUrlChangeSpeedRefreshInstalled = PSL.instanceId;

    [
      "pageshow",
      "popstate",
      "hashchange",
      "yt-navigate-finish",
      "yt-page-data-updated"
    ].forEach(function (eventName) {
      window.addEventListener(eventName, function () {
        refreshSamePageMediaControls(doc, eventName);
      });
    });

    ["pushState", "replaceState"].forEach(function (methodName) {
      var original = window.history && window.history[methodName];
      if (typeof original !== "function" || original._pslWrapped) {
        return;
      }
      window.history[methodName] = function () {
        var result = original.apply(this, arguments);
        refreshSamePageMediaControls(doc, methodName);
        return result;
      };
      window.history[methodName]._pslWrapped = true;
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/cover-media-inside-frames.js ----
(function (PSL) {
  function inIframe() {
    try {
      return window.self !== window.top;
    } catch (e) {
      return true;
    }
  }

  function injectFrameStylesheet(doc) {
    var assetUrl;
    var link;

    if (doc === window.document) {
      return;
    }

    assetUrl = PSL.getAssetUrl("extension/styles/content.css");
    if (!assetUrl) {
      return;
    }

    link = doc.createElement("link");
    link.href = assetUrl;
    link.type = "text/css";
    link.rel = "stylesheet";
    doc.head.appendChild(link);
  }

  PSL.getMediaControlKeyboardDocuments = function (doc) {
    var docs = [doc];
    try {
      if (inIframe()) {
        docs.push(window.top.document);
      }
    } catch (e) {}
    return docs;
  };

  PSL.coverMediaInsideFrames = function (doc) {
    injectFrameStylesheet(doc);

    Array.prototype.forEach.call(doc.getElementsByTagName("iframe"), function (frame) {
      try {
        PSL.initializeWhenReady(frame.contentDocument);
      } catch (e) {}
    });
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/start-media-control-session.js ----
(function (PSL) {
  PSL.initializeWhenReady = function (doc) {
    PSL.log("Begin initializeWhenReady", 5);
    if (PSL.isBlacklisted()) {
      return;
    }

    function initializeWhenBodyExists(attempt) {
      if (!doc) {
        return;
      }
      if (doc.body) {
        PSL.initializeNow(doc);
        return;
      }

      if (attempt < 20) {
        setTimeout(function () {
          initializeWhenBodyExists(attempt + 1);
        }, 50);
      }
    }

    if (doc === window.document) {
      window.addEventListener(
        "load",
        function () {
          PSL.initializeNow(window.document);
          PSL.rescanMedia(window.document);
        },
        { once: true }
      );
    }

    initializeWhenBodyExists(0);
    if (doc && doc.addEventListener) {
      doc.addEventListener(
        "DOMContentLoaded",
        function () {
          PSL.initializeNow(doc);
          PSL.rescanMedia(doc);
        },
        { once: true }
      );
    }
    if (doc === window.document && doc.addEventListener) {
      doc.addEventListener("visibilitychange", function () {
        if (!doc.visibilityState || doc.visibilityState === "visible") {
          PSL.rescanMedia(doc);
        }
      });
    }
    PSL.log("End initializeWhenReady", 5);
  };

  PSL.initializeNow = function (doc) {
    PSL.log("Begin initializeNow", 5);
    if (!PSL.settings.enabled) {
      return;
    }
    if (!doc.body || doc._pslInitializedInstanceId === PSL.instanceId) {
      return;
    }
    try {
      if (doc._pslMediaObserver) {
        doc._pslMediaObserver.disconnect();
      }
    } catch (e) {
      // Stale observers from a previous temporary add-on load can be ignored.
    }
    try {
      PSL.setupRateChangeListener(doc);
    } catch (e) {
      // no operation
    }
    doc._pslInitializedInstanceId = PSL.instanceId;
    doc.body.classList.add("mpc-initialized");
    PSL.log("initializeNow: mpc-initialized added to document body", 5);

    PSL.attachKeyboardShortcuts(PSL.getMediaControlKeyboardDocuments(doc));
    PSL.installMediaDomControlSync(doc);
    PSL.removeStaleControllerElements(doc);
    PSL.rescanMedia(doc);

    if (doc === window.document) {
      PSL.installSamePageMediaControlRefresh(doc);
    }

    PSL.coverMediaInsideFrames(doc);
    PSL.log("End initializeNow", 5);
  };
})(globalThis.PlaySpeedLoop);

// ---- code/extension/content/init.js ----
(function (PSL) {
  if (PSL.__injectionInitialized === PSL.injectionSessionId) {
    return;
  }
  PSL.__injectionInitialized = PSL.injectionSessionId;

  try {
    PSL.loadSettings(function () {
      try {
        PSL.initializeWhenReady(document);
      } catch (error) {
        console.error("Speed & Loop failed to initialize", error);
      }
    });
  } catch (error) {
    console.error("Speed & Loop failed to load settings", error);
    PSL.initializeWhenReady(document);
  }
})(globalThis.PlaySpeedLoop);

})();