YouTube Mute and Skip Ads

Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.

当前为 2025-10-31 提交的版本,查看 最新版本

// ==UserScript==
// @name         YouTube Mute and Skip Ads
// @namespace    https://github.com/ion1/userscripts
// @version      31
// @author       ion
// @description  Mutes, blurs and skips ads on YouTube. Speeds up ad playback. Clicks "yes" on "are you there?" on YouTube Music.
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @homepage     https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @homepageURL  https://github.com/ion1/userscripts/tree/master/packages/youtube-mute-skip-ads
// @match        *://www.youtube.com/*
// @match        *://music.youtube.com/*
// @grant        GM_addStyle
// @run-at       document-body
// ==/UserScript==

// # YouTube Mute and Skip Ads: Change Log
//
// ## 31 2025-10-31
//
// - Include the change log in the script.
// - CSS: Add `.ytp-video-interstitial-buttoned-centered-layout__content` (a text ad in place of the video).
// - Import CSS with `?no-inline`. It seems to make the string use actual newlines again instead of `\n`.
//
// ## 0.0.30 2025-10-26
//
// - Skip shorts ads backwards if the user was moving backwards in the feed.
//
// ## 0.0.29 2025-10-26
//
// - Handle shorts ads ([PR #3](https://github.com/ion1/userscripts/pull/3), thanks [@sim6](https://github.com/sim6)!)
// - Replace Watcher with code that supports observing multiple matching elements.
//
//   Watcher only supported singular elements matching the selectors. However, sometimes there are more than one of them. Implement the observer code in a much simpler way at the expense of a more verbose API. It might be improved later.
//
// ## 0.0.28 2024-10-03
//
// - Add a missing `if (debugging)` around a `console.debug` call.
//   - TODO: Add logging functions which handle the prefix and the debug mode.
// - Add a build with debug logging.
// - Target modern browser versions when building, reducing polyfills in the output.
// - Use the `getPlaybackRate` method on the `#movie_player` element.
//
//   `video.playbackRate` may return 1 if an ad is played before the main video is loaded ([issue #2](https://github.com/ion1/userscripts/issues/2)).
//
// ## 0.0.27 2024-09-25
//
// - `.ytp-suggested-action-badge` popups are showing up on top of the video with a hidden dismiss button. Hide them using CSS rather than just blurring and trying to click on the button.
//
// ## 0.0.26 2024-09-02
//
// - Restore playback rate after ads.
//
// ## 0.0.25 2024-08-29
//
// - Resume playback at end of live video.
//
// ## 0.0.24 2024-08-27
//
// - Refrain from resuming playback if at the end.
//
// ## 0.0.23 2024-08-27
//
// - Watcher: Make onAdded callbacks return a possible onRemoved callback.
// - Watcher: In text/attr callbacks, distinguish empty value from disconnecting watcher.
// - Watcher: Also pass element to text/attr callbacks.
// - Wait for aria-hidden being removed from skip button.
// - CSS: Add `.ytp-ad-action-interstitial-slot`, `.ytp-ad-action-interstitial-background-container` (an image ad in place of the video).
// - Put all clicks behind a visibility check.
// - Add a popover on unclickable skip buttons.
// - Split `adUIAdded` into mute and speedup.
// - Use the `cancelPlayback` method on `#movie_player`.
//
// ## 0.0.22 2024-04-07
//
// - CSS: Hide `.ytp-suggested-action-badge`, `.ytp-visit-advertiser-link`.
//
// ## 0.0.21 2024-04-06
//
// - Fix CSS minification happening again.
// - Add new ad class: `.ytp-ad-player-overlay-layout__player-card-container`.
// - Add new ad player overlay class: `.ytp-ad-player-overlay-layout`.
// - Add new skip button class: `.ytp-skip-ad-button`.
//
// ## 0.0.20 2023-11-10
//
// - Handle new skip button class: `.ytp-ad-skip-button-modern`.
//
// ## 0.0.19 2023-11-07
//
// - Simplify the PostCSS output by using `:is(:hover, :focus-within)`.
// - CSS: Remove unnecessary prefers-reduced-motion handling.
//   Fading [shouldn't be a problem](https://www.smashingmagazine.com/2020/09/design-reduced-motion-sensitivities/#identifying-potentially-triggering-motion) with prefers-reduced-motion.
// - Close featured product overlay.
// - CSS: Hide `yts-merch-shelf-renderer`.
//
// ## 0.0.18 2023-04-08
//
// - New ad panel element: `ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"]`.
//
// ## 0.0.17 2023-03-29
//
// - Detect skip button on non-video ads.
// - Disable ad counter-based reloading for now.
//
//   YouTube started behaving in a more annoying way on 2023-03-28 and repeatedly showing long ads after the reload.
//
// - Rather than reloading, speed up ad playback.
// - No need to unmute; the video element is replaced.
//
// ## 0.0.16 2023-03-27
//
// - Parser: Typo fix.
//
// ## 0.0.15 2023-03-27
//
// - Add a nicer object property parser.
// - Avoid end-of-video detection while live.
//
// ## 0.0.14 2023-03-24
//
// - Disable visibility checks.
//
//   YouTube Music will pause the music and wait until it gets focused to show the are-you-there dialog.
//
// ## 0.0.13 2023-03-22
//
// - Include `duration >= 1` in end-of-video check.
//
// ## 0.0.12 2023-03-20
//
// - `getMuteButton`: Also work on YouTube Music.
//
// ## 0.0.11 2023-03-20
//
// - Increase reload-canceled notification delay.
// - Reloader: Show a notification during end-of-video ads.
// - Adjust a log message and formatting.
//
// ## 0.0.10 2023-03-19
//
// - Do not reload if at the end of the video.
// - Watcher: Add attribute watchers.
// - Blur ad title and subtitle in YT Music.
// - Reloader: pause before reloading; handle canceled reloads.
// - Avoid reloading on YouTube Music; it messes up random playlists.
//
// ## 0.0.9 2023-03-16
//
// - Upload reload reason descriptions.
// - Blur `ytd-player-legacy-desktop-watch-ads-renderer`.
// - Replace ad-hoc observers with a Watcher class.
//   - Now uses IntersectionObserver to determine when the skip button becomes visible.
//   - Now takes both the remaining time indicator and the preskip countdown into account.
//
// ## 0.0.8 2023-03-11
//
// - Update the description.
//
// ## 0.0.7 2023-03-11
//
// - Prevent CSS minification.
//
// ## 0.0.6 2023-03-11
//
// - More robust ad badge parsing.
// - Display a post-reload notification as well.
// - Restore focused element (by ID, if any) after reloading.
//
// ## 0.0.5 2023-03-09
//
// - Overhaul logging.
// - Click "yes" on "are you there?" on YouTube Music.
//
// ## 0.0.4 2023-03-07
//
// - Parse ad badges in more languages.
//
// ## 0.0.3 2023-03-07
//
// - Update the description.
//
// ## 0.0.2 2023-03-06
//
// - Add a notification for when the video page is reloaded.
//
// ## 0.0.1 2023-03-05
//
// - Initial release.

(function () {
  'use strict';

  const d=new Set;const e = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):document.head.appendChild(document.createElement("style")).append(t);})(e));};

  e(` /* Keep these in sync with the watchers. */
#movie_player
  :is(.ytp-ad-skip-button, .ytp-ad-skip-button-modern, .ytp-skip-ad-button) {
  anchor-name: --youtube-mute-skip-ads-unclickable-button;
}

body:has(
    #movie_player
      :is(
        .ytp-ad-skip-button,
        .ytp-ad-skip-button-modern,
        .ytp-skip-ad-button
      ):not([style*="display: none"], [aria-hidden="true"])
  )::after {
  /* The beginning of \`content\`: "youtube-mute-skip-ads" using the sans-serif bold characters. */
  content: "\u{1D606}\u{1D5FC}\u{1D602}\u{1D601}\u{1D602}\u{1D5EF}\u{1D5F2}-\u{1D5FA}\u{1D602}\u{1D601}\u{1D5F2}-\u{1D600}\u{1D5F8}\u{1D5F6}\u{1D5FD}-\u{1D5EE}\u{1D5F1}\u{1D600}\\A\\A"
    "Unfortunately, YouTube has started to block automated clicks based on isTrusted being false.\\A\\A"
    "Please click on the skip button manually.";
  white-space: pre-line;
  pointer-events: none;
  z-index: 9999;
  position: fixed;
  position-anchor: --youtube-mute-skip-ads-unclickable-button;
  padding: 1.5em;
  border-radius: 1.5em;
  margin-bottom: 1em;
  bottom: anchor(--youtube-mute-skip-ads-unclickable-button top);
  right: anchor(--youtube-mute-skip-ads-unclickable-button right);
  max-width: 25em;
  font-size: 1.4rem;
  line-height: 2rem;
  font-weight: 400;
  color: rgb(240 240 240);
  background-color: rgb(0 0 0 / 0.7);
  backdrop-filter: blur(10px);
  animation: fade-in 3s linear;
}

@keyframes fade-in {
  0% {
    opacity: 0;
  }
  67% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

#movie_player.ad-showing video,
#shorts-player.ad-created video {
  filter: blur(100px) opacity(0.25) grayscale(0.5);
}

#movie_player.ad-showing .ytp-title,
#movie_player.ad-showing .ytp-title-channel,
.ytp-visit-advertiser-link,
.ytp-ad-visit-advertiser-button,
ytmusic-app:has(#movie_player.ad-showing)
  ytmusic-player-bar
  :is(.title, .subtitle) {
  filter: blur(4px) opacity(0.5) grayscale(0.5);
  transition: 0.05s filter linear;
}

:is(#movie_player.ad-showing .ytp-title,#movie_player.ad-showing .ytp-title-channel,.ytp-visit-advertiser-link,.ytp-ad-visit-advertiser-button,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
    filter: none;
  }

/* These popups are showing up on top of the video with a hidden dismiss button
 * since 2024-09-25.
 */
.ytp-suggested-action-badge {
  visibility: hidden !important;
}

#movie_player.ad-showing .caption-window,
.ytp-ad-player-overlay-flyout-cta,
.ytp-ad-player-overlay-layout__player-card-container, /* Seen since 2024-04-06. */
.ytp-ad-action-interstitial-slot, /* Added on 2024-08-25. */
.ytp-video-interstitial-buttoned-centered-layout__content, /* Added on 2025-10-31. */
ytd-action-companion-ad-renderer,
ytd-display-ad-renderer,
ytd-ad-slot-renderer,
ytd-promoted-sparkles-web-renderer,
ytd-player-legacy-desktop-watch-ads-renderer,
ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],
ytd-merch-shelf-renderer {
  filter: blur(10px) opacity(0.25) grayscale(0.5);
  transition: 0.05s filter linear;
}

:is(#movie_player.ad-showing .caption-window,.ytp-ad-player-overlay-flyout-cta,.ytp-ad-player-overlay-layout__player-card-container,.ytp-ad-action-interstitial-slot,.ytp-video-interstitial-buttoned-centered-layout__content,ytd-action-companion-ad-renderer,ytd-display-ad-renderer,ytd-ad-slot-renderer,ytd-promoted-sparkles-web-renderer,ytd-player-legacy-desktop-watch-ads-renderer,ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],ytd-merch-shelf-renderer):is(:hover,:focus-within) {
    filter: none;
  }

.ytp-ad-action-interstitial-background-container /* Added on 2024-08-25. */ {
  /* An image ad in place of the video. */
  filter: blur(10px) opacity(0.25) grayscale(0.5);
} `);

  const debugging = false;
  const logPrefix = "[youtube-mute-skip-ads]";
  function error(...args) {
    console.error(logPrefix, ...args);
  }
  function warn(...args) {
    console.warn(logPrefix, ...args);
  }
  function info(...args) {
    console.info(logPrefix, ...args);
  }
  function abortableSetTimeout(signal, callback, timeout) {
    if (signal?.aborted) return;
    const timeoutId = setTimeout(() => {
      signal?.removeEventListener("abort", clearThisTimeout);
      if (signal?.aborted) return;
      callback();
    }, timeout);
    function clearThisTimeout() {
      clearTimeout(timeoutId);
    }
    signal?.addEventListener("abort", clearThisTimeout, { once: true });
  }
  function abortableSetInterval(signal, callback, interval) {
    if (signal?.aborted) return;
    const intervalId = setInterval(() => {
      if (signal?.aborted) return;
      callback();
    }, interval);
    function clearThisInterval() {
      clearInterval(intervalId);
    }
    signal?.addEventListener("abort", clearThisInterval, { once: true });
  }
  function observeSelector({
    selector,
    matcher,
    root,
    name,
    signal,
    onAdded
  }) {
    root ??= document;
    matcher ??= selector;
    if (signal?.aborted) return;
    const abortControllerMap = new Map();
    function added(elem) {
      const abortController = new AbortController();
      abortControllerMap.set(elem, abortController);
      try {
        onAdded({ elem, signal: abortController.signal });
      } catch (err) {
        reportError(err);
      }
    }
    function removed(elem) {
      const abortController = abortControllerMap.get(elem);
      if (abortController) {
        abortControllerMap.delete(elem);
        abortController.abort();
      }
    }
    for (const elem of root.querySelectorAll(selector)) {
      if (elem.matches(matcher)) {
        added(elem);
      }
    }
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const addedNode of mutation.addedNodes) {
          for (const elem of matchingElementsInTree({
            root: addedNode,
            selector,
            matcher
          })) {
            added(elem);
          }
        }
        for (const removedNode of mutation.removedNodes) {
          for (const elem of matchingElementsInTree({
            root: removedNode,
            selector,
            matcher
          })) {
            removed(elem);
          }
        }
      }
    });
    observer.observe(root, { childList: true, subtree: true });
    signal?.addEventListener(
      "abort",
      () => {
        observer.disconnect();
        while (abortControllerMap.size > 0) {
          const elem = abortControllerMap.keys().next().value;
          removed(elem);
        }
      },
      { once: true }
    );
  }
  function* matchingElementsInTree({
    root,
    selector,
    matcher
  }) {
    if (!(root instanceof Element)) return;
    if (root.matches(matcher)) {
      yield root;
      return;
    }
    for (const descendant of root.querySelectorAll(selector)) {
      if (descendant.matches(matcher)) {
        yield descendant;
      }
    }
  }
  function observeVisible({
    elem,
    name,
    periodicCheckInterval,
    signal,
    onVisible
  }) {
    if (signal?.aborted) return;
    periodicCheckInterval ??= 1e4;
    let abortController = null;
    let checkVisiblePending = false;
    function checkVisible() {
      if (checkVisiblePending) return;
      checkVisiblePending = true;
      abortableSetTimeout(signal, () => {
        checkVisiblePending = false;
        checkVisibleImmediately();
      });
    }
    function checkVisibleImmediately() {
      const visible = isElementVisible(elem);
      if (visible && abortController === null) {
        abortController = new AbortController();
        try {
          onVisible({ elem, signal: abortController.signal });
        } catch (err) {
          reportError(err);
        }
      } else if (!visible && abortController !== null) {
        abortController.abort();
        abortController = null;
      }
    }
    checkVisibleImmediately();
    const observer = new MutationObserver(checkVisible);
    for (let ancestor = elem; ancestor !== null; ancestor = ancestor.parentElement) {
      observer.observe(ancestor, {
        attributes: true,
        attributeFilter: ["style", "class"],
        subtree: false
      });
    }
    abortableSetInterval(signal, checkVisible, periodicCheckInterval);
    signal?.addEventListener(
      "abort",
      () => {
        observer.disconnect();
        abortController?.abort();
        abortController = null;
      },
      { once: true }
    );
  }
  function isElementVisible(elem) {
    for (let ancestor = elem; ancestor !== null; ancestor = ancestor.parentElement) {
      const style = getComputedStyle(ancestor);
      if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse" || style.opacity === "0")
        return false;
    }
    return true;
  }
  function observeAttr({
    elem,
    name,
    attr,
    shouldGetAttr,
    signal,
    onChanged
  }) {
    shouldGetAttr ??= true;
    if (signal?.aborted) return;
    const observer = new MutationObserver((_mutations) => {
      onChanged({
        elem,
        attr,
        value: shouldGetAttr ? elem.getAttribute(attr) : null
      });
    });
    observer.observe(elem, {
      attributeFilter: [attr],
      attributes: true
    });
    signal?.addEventListener(
      "abort",
      () => {
        observer.disconnect();
      },
      { once: true }
    );
  }
  function observeHasClass({
    elem,
    name,
    className,
    signal,
    onAdded
  }) {
    if (signal?.aborted) return;
    let abortController = null;
    function change() {
      const elemHasClass = elem.classList.contains(className);
      if (abortController === null && elemHasClass) {
        abortController = new AbortController();
        try {
          onAdded({ elem, className, signal: abortController.signal });
        } catch (err) {
          reportError(err);
        }
      } else if (abortController !== null && !elemHasClass) {
        abortController.abort();
        abortController = null;
      }
    }
    observeAttr({
      elem,
      name,
      attr: "class",
      shouldGetAttr: false,
      signal,
      onChanged: change
    });
    signal?.addEventListener(
      "abort",
      () => {
        abortController?.abort();
        abortController = null;
      },
      { once: true }
    );
  }
  const videoSelector = "#movie_player video";
  const muteButtonSelector = ":is(.ytp-mute-button, ytdDesktopShortsVolumeControlsMuteIconButton, ytmusic-player-bar tp-yt-paper-icon-button.volume)";
  const shortsRendererSelector = "ytd-reel-video-renderer";
  const shortsVideoSelector = "#shorts-player video";
  const shortsUpButtonSelector = "#navigation-button-up button";
  const shortsDownButtonSelector = "#navigation-button-down button";
  function getVideoElement() {
    return getVideoElementBySelector(videoSelector);
  }
  function getShortsVideoElement() {
    return getVideoElementBySelector(shortsVideoSelector);
  }
  function getVideoElementBySelector(selector) {
    const videoElem = getHTMLElementBySelector(selector);
    if (!(videoElem instanceof HTMLVideoElement)) {
      error(
        "Expected",
        JSON.stringify(videoSelector),
        "to be a video element, got:",
        videoElem?.cloneNode(true)
      );
      return null;
    }
    return videoElem;
  }
  function getShortsUpButton() {
    return getHTMLElementBySelector(shortsUpButtonSelector);
  }
  function getShortsDownButton() {
    return getHTMLElementBySelector(shortsDownButtonSelector);
  }
  function getHTMLElementBySelector(selector) {
    for (const elem of document.querySelectorAll(selector)) {
      if (!(elem instanceof HTMLElement)) {
        error(
          "Expected",
          JSON.stringify(muteButtonSelector),
          "to be an HTML element, got:",
          elem.cloneNode(true)
        );
        continue;
      }
      return elem;
    }
    error("Failed to find", JSON.stringify(selector));
    return null;
  }
  function getShortsParentElement(elemWithinShort) {
    const shortsRenderer = elemWithinShort.closest(shortsRendererSelector);
    if (shortsRenderer == null) {
      return null;
    }
    return shortsRenderer.parentElement;
  }
  function callMoviePlayerMethod(name, onSuccess, args) {
    try {
      const movieElem = document.getElementById("movie_player");
      if (movieElem == null) {
        warn("movie_player element not found");
        return;
      }
      const method = Object.getOwnPropertyDescriptor(
        movieElem,
        name
      )?.value;
      if (method == null) {
        warn(`movie_player element has no ${JSON.stringify(name)} property`);
        return;
      }
      if (!(typeof method === "function")) {
        warn(
          `movie_player element property ${JSON.stringify(name)} is not a function`
        );
        return;
      }
      const result = method.apply(movieElem, args);
      if (onSuccess != null) {
        onSuccess(result);
      }
      return result;
    } catch (e) {
      warn(`movie_player method ${JSON.stringify(name)} failed:`, e);
      return;
    }
  }
  function disableVisibilityChecks() {
    for (const eventName of ["visibilitychange", "blur", "focus"]) {
      document.addEventListener(
        eventName,
        (ev) => {
          ev.stopImmediatePropagation();
        },
        { capture: true }
      );
    }
    document.hasFocus = () => true;
    Object.defineProperties(document, {
      visibilityState: { value: "visible" },
      hidden: { value: false }
    });
  }
  function main() {
    disableVisibilityChecks();
    const adPlayerOverlaySelectors = [
      ".ytp-ad-player-overlay",
      ".ytp-ad-player-overlay-layout"
];
    for (const adPlayerOverlaySelector of adPlayerOverlaySelectors) {
      observeSelector({
        selector: adPlayerOverlaySelector,
        name: adPlayerOverlaySelector,
        onAdded: adIsPlaying
      });
    }
    let previousShortsParent = null;
    let currentShortsParent = null;
    observeSelector({
      selector: "#shorts-player",
      name: "#shorts-player",
      onAdded({ elem: shortsRenderer, signal }) {
        const shortsParent = getShortsParentElement(shortsRenderer);
        if (shortsParent && shortsParent.isConnected) {
          if (shortsParent !== currentShortsParent?.deref()) {
            if (debugging) ;
            [previousShortsParent, currentShortsParent] = [
              currentShortsParent,
              new WeakRef(shortsParent)
            ];
          }
        } else {
          previousShortsParent = null;
          currentShortsParent = null;
        }
        let wentBackwards = false;
        if (previousShortsParent != null && currentShortsParent != null) {
          const previousShortsParentDeref = previousShortsParent.deref();
          const currentShortsParentDeref = currentShortsParent.deref();
          if (previousShortsParentDeref != null && currentShortsParentDeref != null) {
            const pos = previousShortsParentDeref.compareDocumentPosition(
              currentShortsParentDeref
            );
            if ((pos & Node.DOCUMENT_POSITION_DISCONNECTED) === 0 && (pos & Node.DOCUMENT_POSITION_PRECEDING) !== 0) {
              wentBackwards = true;
            }
            if (debugging) ;
          }
        }
        observeHasClass({
          elem: shortsRenderer,
          name: "#shorts-player",
          className: "ad-created",
          signal,
          onAdded() {
            shortsMuteAd();
          }
        });
        if (shortsRenderer.classList.contains("ad-created")) {
          shortsSkipAd({ signal, wentBackwards });
        }
      }
    });
    observeSelector({
      selector: "#movie_player",
      name: "#movie_player",
      onAdded({ elem: moviePlayer, signal }) {
        const adSkipButtonSelectors = [
          ".ytp-ad-skip-button",
          ".ytp-ad-skip-button-modern",
".ytp-skip-ad-button"
];
        for (const adSkipButtonSelector of adSkipButtonSelectors) {
          const name = `#movie_player ${adSkipButtonSelector}`;
          observeSelector({
            root: moviePlayer,
            selector: adSkipButtonSelector,
            name,
            signal,
            onAdded({ elem: button, signal: signal2 }) {
              observeVisible({
                elem: button,
                name,
                signal: signal2,
                onVisible({ signal: signal3 }) {
                  observeAttr({
                    elem: button,
                    name,
                    attr: "aria-hidden",
                    signal: signal3,
                    onChanged(ariaHidden) {
                      if (ariaHidden === null) {
                        click(button, `skip (${adSkipButtonSelector})`);
                      }
                    }
                  });
                }
              });
            }
          });
        }
      }
    });
    observeSelector({
      selector: ".ytp-ad-overlay-close-button",
      name: ".ytp-ad-overlay-close-button",
      onAdded({ elem: button, signal }) {
        observeVisible({
          elem: button,
          name: ".ytp-ad-overlay-close-button",
          signal,
          onVisible() {
            click(button, ".ytp-ad-overlay-close-button");
          }
        });
      }
    });
    observeSelector({
      selector: "ytmusic-you-there-renderer button",
      name: "are-you-there",
      onAdded({ elem: button, signal }) {
        observeVisible({
          elem: button,
          name: "are-you-there",
          signal,
          onVisible() {
            click(button, "are-you-there");
          }
        });
      }
    });
  }
  function adIsPlaying({ signal }) {
    info("An ad is playing, muting and speeding up");
    const video = getVideoElement();
    if (video == null) {
      return;
    }
    mute(video);
    speedup(video, signal);
    cancelPlayback(video, signal);
  }
  function shortsMuteAd() {
    info("A shorts ad is playing, muting");
    const video = getShortsVideoElement();
    if (video != null) {
      mute(video);
    }
  }
  function shortsSkipAd({
    signal,
    wentBackwards
  }) {
    info("A shorts ad is playing, skipping");
    const [direction, button] = wentBackwards ? ["up", getShortsUpButton()] : ["down", getShortsDownButton()];
    const video = getShortsVideoElement();
    if (video == null) return;
    if (button == null) {
      return;
    }
    oncePlaying({
      elem: video,
      signal,
      onWaiting() {
      },
      onPlaying() {
        click(button, `${direction} button`);
      }
    });
  }
  function mute(video) {
    video.muted = true;
  }
  function speedup(video, signal) {
    for (let rate = 16; rate >= 2; rate /= 2) {
      try {
        video.playbackRate = rate;
        break;
      } catch (e) {
      }
    }
    function onRemoved() {
      const originalRate = callMoviePlayerMethod("getPlaybackRate");
      if (originalRate == null || typeof originalRate !== "number" || isNaN(originalRate)) {
        warn(
          `Restoring playback rate failed:`,
          `unable to query the current playback rate, got: ${JSON.stringify(originalRate)}.`,
          `Falling back to 1.`
        );
        restorePlaybackRate(video, 1);
        return;
      }
      restorePlaybackRate(video, originalRate);
    }
    signal?.addEventListener("abort", onRemoved, { once: true });
  }
  function restorePlaybackRate(video, originalRate) {
    try {
      video.playbackRate = originalRate;
    } catch (e) {
    }
  }
  function cancelPlayback(video, signal) {
    function doCancelPlayback() {
      info("Attempting to cancel playback");
      callMoviePlayerMethod("cancelPlayback", () => {
        signal.addEventListener(
          "abort",
          () => {
            resumePlaybackIfNotAtEnd();
          },
          { once: true }
        );
      });
    }
    oncePlaying({
      elem: video,
      signal,
      onWaiting() {
      },
      onPlaying: doCancelPlayback
    });
  }
  function oncePlaying({
    elem,
    signal,
    onWaiting,
    onPlaying
  }) {
    if (elem.paused || elem.readyState < 3) {
      onWaiting?.();
      elem.addEventListener(
        "playing",
        () => {
          onPlaying();
        },
        { signal, once: true }
      );
    } else {
      onPlaying();
    }
  }
  function resumePlaybackIfNotAtEnd() {
    const currentTime = callMoviePlayerMethod("getCurrentTime");
    const duration = callMoviePlayerMethod("getDuration");
    const isAtLiveHead = callMoviePlayerMethod("isAtLiveHead");
    if (currentTime == null || duration == null || typeof currentTime !== "number" || typeof duration !== "number" || isNaN(currentTime) || isNaN(duration)) {
      warn(
        `movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}`
      );
      return;
    }
    if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") {
      warn(
        `movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}`
      );
      return;
    }
    const atEnd = duration - currentTime < 1;
    if (atEnd && !isAtLiveHead) {
      info(
        `Video is at the end (${currentTime}/${duration}), not attempting to resume playback`
      );
      return;
    }
    info("Attempting to resume playback");
    callMoviePlayerMethod("playVideo");
  }
  function click(elem, description) {
    if (!(elem instanceof HTMLElement)) return;
    if (elem.getAttribute("aria-hidden")) {
      info("Not clicking (aria-hidden):", description);
    } else {
      info("Clicking:", description);
      elem.click();
    }
  }
  main();

})();