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