Block autoplay before user interaction on most websites, tracking each media element separately
当前为
// ==UserScript==
// @name Disable autoplay
// @namespace https://www.androidacy.com/
// @version 2.6.0
// @description Block autoplay before user interaction on most websites, tracking each media element separately
// @author Androidacy
// @include *
// @icon https://www.androidacy.com/wp-content/uploads/cropped-cropped-cropped-cropped-New-Project-32-69C2A87-1-192x192.jpg
// @grant none
// @run-at document-end
// ==/UserScript==
(() => {
'use strict';
// WeakMap to track user interaction for each media element
const mediaInteractionMap = new WeakMap();
// Helper function to pause all media elements
const pauseAllMedia = () => {
document.querySelectorAll('video, audio').forEach(media => {
media.pause();
});
};
// Initial pause of any playing media
pauseAllMedia();
// Remove autoplay attributes from existing media elements
const removeAutoplay = (media) => {
media.removeAttribute('autoplay');
};
// Detect if a click/touch is on the media or its cover
const isTrustedInteraction = (event, media) => {
if (!event.isTrusted) return false;
const mediaRect = media.getBoundingClientRect();
const x = event.clientX;
const y = event.clientY;
// Check if the interaction is within the media bounds
if (
x >= mediaRect.left &&
x <= mediaRect.right &&
y >= mediaRect.top &&
y <= mediaRect.bottom
) {
return true;
}
// Check if within 64px of the sides
const within64px =
x >= mediaRect.left - 64 &&
x <= mediaRect.right + 64 &&
y >= mediaRect.top - 64 &&
y <= mediaRect.bottom + 64;
if (within64px) {
return true;
}
// Additional check for overlapping elements (covers)
const elementsAtPoint = document.elementsFromPoint(x, y);
for (const el of elementsAtPoint) {
if (el !== media && media.contains(el)) {
return true;
}
}
return false;
};
// Override play method to block autoplay
const overridePlay = (media) => {
const originalPlay = media.play.bind(media);
media.play = async () => {
if (mediaInteractionMap.get(media)) {
try {
await originalPlay();
} catch (e) {
console.error('Playback failed:', e);
}
} else {
media.pause();
console.log('Autoplay blocked for:', media);
return Promise.reject(new Error('Autoplay is blocked.'));
}
};
};
// Handle user interactions to allow playback for specific media elements
const handleUserInteraction = (event) => {
const mediaElements = document.querySelectorAll('video, audio');
mediaElements.forEach(media => {
if (isTrustedInteraction(event, media)) {
mediaInteractionMap.set(media, true);
media.play().catch(() => {});
}
});
};
// Add event listeners for user interactions
window.addEventListener('click', handleUserInteraction, true);
window.addEventListener('touchstart', handleUserInteraction, true);
// Process a media element: remove autoplay and override play
const processMediaElement = (media) => {
removeAutoplay(media);
overridePlay(media);
// Initialize interaction state as false
if (!mediaInteractionMap.has(media)) {
mediaInteractionMap.set(media, false);
}
// Listen for play events and pause if playback is not allowed
media.addEventListener('play', () => {
if (!mediaInteractionMap.get(media)) {
media.pause();
console.log('Playback paused for:', media, 'due to no user interaction.');
}
});
};
// Initial processing of existing media elements
document.querySelectorAll('video, audio').forEach(processMediaElement);
// Observe for dynamically added media elements
const observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('video, audio')) {
processMediaElement(node);
}
// Also check within the subtree
node.querySelectorAll && node.querySelectorAll('video, audio').forEach(processMediaElement);
}
});
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();