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.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

})();