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.

// ==UserScript==
// @name         YouTube Mute and Skip Ads
// @namespace    https://github.com/ion1/userscripts
// @version      0.0.26
// @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==

(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const n=document.createElement("style");n.textContent=e,document.head.append(n)})(` /* 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 {
  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 {
  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,
.ytp-suggested-action-badge,
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,.ytp-suggested-action-badge,ytmusic-app:has(#movie_player.ad-showing) ytmusic-player-bar :is(.title,.subtitle)):is(:hover,:focus-within) {
    filter: none;
  }

#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. */
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,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);
} `);

(function () {
  'use strict';

  var __defProp = Object.defineProperty;
  var __typeError = (msg) => {
    throw TypeError(msg);
  };
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
  var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
  var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value);
  var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
  var _onCreatedCallbacks, _onRemovedCallbacks, _nodeObserver, _nodeWatchers, _textObserver, _onTextChangedCallbacks, _onAttrChangedCallbacks, _visibilityObserver, _isVisible, _visibilityWatchers, _Watcher_instances, assertElement_fn, assertVisibilityAncestor_fn, connect_fn, disconnect_fn, registerNodeObserver_fn, registerTextObserver_fn, registerAttrObservers_fn, registerVisibilityObserver_fn, deregisterNodeObserver_fn, deregisterTextObserver_fn, deregisterAttrObservers_fn, deregisterVisibilityObserver_fn;
  const logPrefix = "youtube-mute-skip-ads:";
  const _Watcher = class _Watcher {
    constructor(name, elem) {
      __privateAdd(this, _Watcher_instances);
      __publicField(this, "name");
      __publicField(this, "element");
      __privateAdd(this, _onCreatedCallbacks);
      __privateAdd(this, _onRemovedCallbacks);
      __privateAdd(this, _nodeObserver);
      __privateAdd(this, _nodeWatchers);
      __privateAdd(this, _textObserver);
      __privateAdd(this, _onTextChangedCallbacks);
      __privateAdd(this, _onAttrChangedCallbacks);
      __publicField(this, "visibilityAncestor");
      __privateAdd(this, _visibilityObserver);
      __privateAdd(this, _isVisible);
      __privateAdd(this, _visibilityWatchers);
      this.name = name;
      this.element = null;
      __privateSet(this, _onCreatedCallbacks, []);
      __privateSet(this, _onRemovedCallbacks, []);
      __privateSet(this, _nodeObserver, null);
      __privateSet(this, _nodeWatchers, []);
      __privateSet(this, _textObserver, null);
      __privateSet(this, _onTextChangedCallbacks, []);
      __privateSet(this, _onAttrChangedCallbacks, []);
      this.visibilityAncestor = null;
      __privateSet(this, _visibilityObserver, null);
      __privateSet(this, _isVisible, null);
      __privateSet(this, _visibilityWatchers, []);
      if (elem != null) {
        __privateMethod(this, _Watcher_instances, connect_fn).call(this, elem);
      }
    }
    onCreated(onCreatedCb) {
      __privateGet(this, _onCreatedCallbacks).push(onCreatedCb);
      if (this.element != null) {
        const onRemovedCb = onCreatedCb(this.element);
        if (onRemovedCb) {
          __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
        }
      }
      return this;
    }
    descendant(selector, name) {
      var _a;
      const watcher2 = new _Watcher(`${this.name} → ${name}`);
      __privateGet(this, _nodeWatchers).push({ selector, name, watcher: watcher2 });
      if (this.element != null) {
        for (const descElem of getDescendantsBy(this.element, selector, name)) {
          __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
        }
        __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
      }
      return watcher2;
    }
    id(idName) {
      return this.descendant("id", idName);
    }
    klass(className) {
      return this.descendant("class", className);
    }
    tag(tagName) {
      return this.descendant("tag", tagName);
    }
    visible() {
      var _a;
      const watcher2 = new _Watcher(`${this.name} (visible)`);
      __privateGet(this, _visibilityWatchers).push(watcher2);
      if (this.element != null) {
        const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
        if (__privateGet(this, _isVisible)) {
          __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, this.element, visibilityAncestor);
        }
        __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
      }
      return watcher2;
    }
    /// `null` implies null textContent. `undefined` implies that the watcher is
    /// being disconnected.
    text(callback) {
      __privateGet(this, _onTextChangedCallbacks).push(callback);
      if (this.element != null) {
        callback(this.element, this.element.textContent);
        __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
      }
      return this;
    }
    /// `null` implies no such attribute. `undefined` implies that the watcher is
    /// being disconnected.
    attr(name, callback) {
      __privateGet(this, _onAttrChangedCallbacks).push({ name, callback, observer: null });
      if (this.element != null) {
        callback(this.element, this.element.getAttribute(name));
        __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
      }
      return this;
    }
  };
  _onCreatedCallbacks = new WeakMap();
  _onRemovedCallbacks = new WeakMap();
  _nodeObserver = new WeakMap();
  _nodeWatchers = new WeakMap();
  _textObserver = new WeakMap();
  _onTextChangedCallbacks = new WeakMap();
  _onAttrChangedCallbacks = new WeakMap();
  _visibilityObserver = new WeakMap();
  _isVisible = new WeakMap();
  _visibilityWatchers = new WeakMap();
  _Watcher_instances = new WeakSet();
  assertElement_fn = function() {
    if (this.element == null) {
      throw new Error(`Watcher not connected to an element`);
    }
    return this.element;
  };
  assertVisibilityAncestor_fn = function() {
    if (this.visibilityAncestor == null) {
      throw new Error(`Watcher is missing a visibilityAncestor`);
    }
    return this.visibilityAncestor;
  };
  connect_fn = function(element, visibilityAncestor) {
    var _a;
    if (this.element != null) {
      if (this.element !== element) {
        console.error(
          logPrefix,
          `Watcher already connected to`,
          this.element,
          `while trying to connect to`,
          element
        );
      }
      return;
    }
    this.element = element;
    this.visibilityAncestor = visibilityAncestor ?? null;
    for (const onCreatedCb of __privateGet(this, _onCreatedCallbacks)) {
      const onRemovedCb = onCreatedCb(this.element);
      if (onRemovedCb) {
        __privateGet(this, _onRemovedCallbacks).push(onRemovedCb);
      }
    }
    for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
      for (const descElem of getDescendantsBy(this.element, selector, name)) {
        __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, this.element);
      }
    }
    for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
      callback(this.element, this.element.textContent);
    }
    for (const { name, callback } of __privateGet(this, _onAttrChangedCallbacks)) {
      callback(this.element, this.element.getAttribute(name));
    }
    __privateMethod(this, _Watcher_instances, registerNodeObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerTextObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerAttrObservers_fn).call(this);
    __privateMethod(this, _Watcher_instances, registerVisibilityObserver_fn).call(this);
  };
  disconnect_fn = function() {
    var _a, _b;
    if (this.element == null) {
      return;
    }
    for (const child of __privateGet(this, _nodeWatchers)) {
      __privateMethod(_a = child.watcher, _Watcher_instances, disconnect_fn).call(_a);
    }
    for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
      callback(this.element, void 0);
    }
    for (const { callback } of __privateGet(this, _onAttrChangedCallbacks)) {
      callback(this.element, void 0);
    }
    for (const child of __privateGet(this, _visibilityWatchers)) {
      __privateMethod(_b = child, _Watcher_instances, disconnect_fn).call(_b);
    }
    __privateMethod(this, _Watcher_instances, deregisterNodeObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterTextObserver_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterAttrObservers_fn).call(this);
    __privateMethod(this, _Watcher_instances, deregisterVisibilityObserver_fn).call(this);
    while (__privateGet(this, _onRemovedCallbacks).length > 0) {
      const onRemovedCb = __privateGet(this, _onRemovedCallbacks).shift();
      onRemovedCb();
    }
    this.element = null;
  };
  registerNodeObserver_fn = function() {
    if (__privateGet(this, _nodeObserver) != null) {
      return;
    }
    if (__privateGet(this, _nodeWatchers).length === 0) {
      return;
    }
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    __privateSet(this, _nodeObserver, new MutationObserver((mutations) => {
      var _a, _b;
      for (const mut of mutations) {
        for (const node of mut.addedNodes) {
          for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
            for (const descElem of getSelfOrDescendantsBy(
              node,
              selector,
              name
            )) {
              __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, descElem, elem);
            }
          }
        }
        for (const node of mut.removedNodes) {
          for (const { selector, name, watcher: watcher2 } of __privateGet(this, _nodeWatchers)) {
            for (const _descElem of getSelfOrDescendantsBy(
              node,
              selector,
              name
            )) {
              __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
            }
          }
        }
      }
    }));
    __privateGet(this, _nodeObserver).observe(elem, {
      subtree: true,
      childList: true
    });
  };
  registerTextObserver_fn = function() {
    if (__privateGet(this, _textObserver) != null) {
      return;
    }
    if (__privateGet(this, _onTextChangedCallbacks).length === 0) {
      return;
    }
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    __privateSet(this, _textObserver, new MutationObserver((_mutations) => {
      for (const callback of __privateGet(this, _onTextChangedCallbacks)) {
        callback(elem, elem.textContent);
      }
    }));
    __privateGet(this, _textObserver).observe(elem, {
      subtree: true,
      // This is needed when elements are replaced to update their text.
      childList: true,
      characterData: true
    });
  };
  registerAttrObservers_fn = function() {
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    for (const handler of __privateGet(this, _onAttrChangedCallbacks)) {
      if (handler.observer != null) {
        continue;
      }
      const { name, callback } = handler;
      handler.observer = new MutationObserver((_mutations) => {
        callback(elem, elem.getAttribute(name));
      });
      handler.observer.observe(elem, {
        attributes: true,
        attributeFilter: [name]
      });
    }
  };
  registerVisibilityObserver_fn = function() {
    if (__privateGet(this, _visibilityObserver) != null) {
      return;
    }
    if (__privateGet(this, _visibilityWatchers).length === 0) {
      return;
    }
    __privateSet(this, _isVisible, false);
    const elem = __privateMethod(this, _Watcher_instances, assertElement_fn).call(this);
    const visibilityAncestor = __privateMethod(this, _Watcher_instances, assertVisibilityAncestor_fn).call(this);
    __privateSet(this, _visibilityObserver, new IntersectionObserver(
      (entries) => {
        var _a, _b;
        const oldVisible = __privateGet(this, _isVisible);
        for (const entry of entries) {
          __privateSet(this, _isVisible, entry.isIntersecting);
        }
        if (__privateGet(this, _isVisible) !== oldVisible) {
          if (__privateGet(this, _isVisible)) {
            for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
              __privateMethod(_a = watcher2, _Watcher_instances, connect_fn).call(_a, elem, visibilityAncestor);
            }
          } else {
            for (const watcher2 of __privateGet(this, _visibilityWatchers)) {
              __privateMethod(_b = watcher2, _Watcher_instances, disconnect_fn).call(_b);
            }
          }
        }
      },
      {
        root: visibilityAncestor
      }
    ));
    __privateGet(this, _visibilityObserver).observe(elem);
  };
  deregisterNodeObserver_fn = function() {
    if (__privateGet(this, _nodeObserver) == null) {
      return;
    }
    __privateGet(this, _nodeObserver).disconnect();
    __privateSet(this, _nodeObserver, null);
  };
  deregisterTextObserver_fn = function() {
    if (__privateGet(this, _textObserver) == null) {
      return;
    }
    __privateGet(this, _textObserver).disconnect();
    __privateSet(this, _textObserver, null);
  };
  deregisterAttrObservers_fn = function() {
    for (const handler of __privateGet(this, _onAttrChangedCallbacks)) {
      if (handler.observer == null) {
        continue;
      }
      handler.observer.disconnect();
      handler.observer = null;
    }
  };
  deregisterVisibilityObserver_fn = function() {
    if (__privateGet(this, _visibilityObserver) == null) {
      return;
    }
    __privateGet(this, _visibilityObserver).disconnect();
    __privateSet(this, _visibilityObserver, null);
    __privateSet(this, _isVisible, null);
  };
  let Watcher = _Watcher;
  function getSelfOrDescendantsBy(node, selector, name) {
    if (!(node instanceof HTMLElement)) {
      return [];
    }
    if (selector === "id" || selector === "class" || selector === "tag") {
      if (selector === "id" && node.id === name || selector === "class" && node.classList.contains(name) || selector === "tag" && node.tagName.toLowerCase() === name.toLowerCase()) {
        return [node];
      } else {
        return getDescendantsBy(node, selector, name);
      }
    } else {
      const impossible = selector;
      throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
    }
  }
  function getDescendantsBy(node, selector, name) {
    if (!(node instanceof HTMLElement)) {
      return [];
    }
    let cssSelector = "";
    if (selector === "id") {
      cssSelector += "#";
    } else if (selector === "class") {
      cssSelector += ".";
    } else if (selector === "tag") ;
    else {
      const impossible = selector;
      throw new Error(`Impossible selector type: ${JSON.stringify(impossible)}`);
    }
    cssSelector += CSS.escape(name);
    return Array.from(node.querySelectorAll(cssSelector));
  }
  const videoSelector = "#movie_player video";
  function getVideoElement() {
    const videoElem = document.querySelector(videoSelector);
    if (!(videoElem instanceof HTMLVideoElement)) {
      console.error(
        logPrefix,
        "Expected",
        JSON.stringify(videoSelector),
        "to be a video element, got:",
        videoElem == null ? void 0 : videoElem.cloneNode(true)
      );
      return null;
    }
    return videoElem;
  }
  function callMoviePlayerMethod(name, onSuccess, args) {
    var _a;
    try {
      const movieElem = document.getElementById("movie_player");
      if (movieElem == null) {
        console.warn(logPrefix, "movie_player element not found");
        return;
      }
      const method = (_a = Object.getOwnPropertyDescriptor(
        movieElem,
        name
      )) == null ? void 0 : _a.value;
      if (method == null) {
        console.warn(
          logPrefix,
          `movie_player element has no ${JSON.stringify(name)} property`
        );
        return;
      }
      if (!(typeof method === "function")) {
        console.warn(
          logPrefix,
          `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) {
      console.warn(
        logPrefix,
        `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 adIsPlaying(_elem) {
    console.info(logPrefix, "An ad is playing, muting and speeding up");
    const video = getVideoElement();
    if (video == null) {
      return;
    }
    const onRemovedCallbacks = [
      mute(video),
      speedup(video),
      cancelPlayback(video)
    ];
    return function onRemoved() {
      for (const callback of onRemovedCallbacks) {
        callback();
      }
    };
  }
  function mute(video) {
    console.debug(logPrefix, "Muting");
    video.muted = true;
    return unmute;
  }
  function unmute() {
    {
      return;
    }
  }
  function speedup(video) {
    const originalRate = video.playbackRate;
    for (let rate = 16; rate >= 2; rate /= 2) {
      try {
        video.playbackRate = rate;
        break;
      } catch (e) {
        console.debug(logPrefix, `Setting playback rate to`, rate, `failed:`, e);
      }
    }
    return function onRemoved() {
      console.debug(logPrefix, `Restoring playback rate:`, originalRate);
      try {
        video.playbackRate = originalRate;
      } catch (e) {
        console.debug(
          logPrefix,
          `Restoring playback rate to`,
          originalRate,
          `failed:`,
          e
        );
      }
    };
  }
  function cancelPlayback(video) {
    let shouldResume = false;
    function doCancelPlayback() {
      console.info(logPrefix, "Attempting to cancel playback");
      callMoviePlayerMethod("cancelPlayback", () => {
        shouldResume = true;
      });
    }
    if (video.paused) {
      console.debug(
        logPrefix,
        "Ad paused, waiting for it to play before canceling playback"
      );
      video.addEventListener("play", doCancelPlayback);
    } else {
      doCancelPlayback();
    }
    return function onRemoved() {
      video.removeEventListener("play", doCancelPlayback);
      if (shouldResume) {
        resumePlaybackIfNotAtEnd();
      }
    };
  }
  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)) {
      console.warn(
        logPrefix,
        `movie_player methods getCurrentTime/getDuration failed, got time: ${JSON.stringify(currentTime)}, duration: ${JSON.stringify(duration)}`
      );
      return;
    }
    if (isAtLiveHead == null || typeof isAtLiveHead !== "boolean") {
      console.warn(
        logPrefix,
        `movie_player method isAtLiveHead failed, got: ${JSON.stringify(isAtLiveHead)}`
      );
      return;
    }
    const atEnd = duration - currentTime < 1;
    if (atEnd && !isAtLiveHead) {
      console.info(
        logPrefix,
        `Video is at the end (${currentTime}/${duration}), not attempting to resume playback`
      );
      return;
    }
    console.info(logPrefix, "Attempting to resume playback");
    callMoviePlayerMethod("playVideo");
  }
  function click(description) {
    return (elem) => {
      if (elem.getAttribute("aria-hidden")) {
        console.info(logPrefix, "Not clicking (aria-hidden):", description);
      } else {
        console.info(logPrefix, "Clicking:", description);
        elem.click();
      }
    };
  }
  disableVisibilityChecks();
  const watcher = new Watcher("body", document.body);
  const adPlayerOverlayClasses = [
    "ytp-ad-player-overlay",
    "ytp-ad-player-overlay-layout"
    // Seen since 2024-04-06.
  ];
  for (const adPlayerOverlayClass of adPlayerOverlayClasses) {
    watcher.klass(adPlayerOverlayClass).onCreated(adIsPlaying);
  }
  const adSkipButtonClasses = [
    "ytp-ad-skip-button",
    "ytp-ad-skip-button-modern",
    // Seen since 2023-11-10.
    "ytp-skip-ad-button"
    // Seen since 2024-04-06.
  ];
  for (const adSkipButtonClass of adSkipButtonClasses) {
    watcher.id("movie_player").klass(adSkipButtonClass).visible().attr("aria-hidden", (elem, value) => {
      if (value === null) {
        click(`skip (${adSkipButtonClass})`)(elem);
      }
    });
  }
  watcher.klass("ytp-ad-overlay-close-button").visible().onCreated(click("overlay close"));
  watcher.klass("ytp-featured-product").klass("ytp-suggested-action-badge-dismiss-button-icon").visible().onCreated(click("suggested action close"));
  watcher.tag("ytmusic-you-there-renderer").tag("button").visible().onCreated(click("are-you-there"));

})();