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.1
// @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');
};
/**
* Extracts the clientX and clientY coordinates from the event based on its type.
* @param {Event} event - The event object.
* @returns {{x: number, y: number} | null} - The extracted coordinates or null if not available.
*/
const extractCoordinates = (event) => {
if (event.type === 'click') {
const { clientX: x, clientY: y } = event;
return isFinite(x) && isFinite(y) ? { x, y } : null;
} else if (event.type === 'touchstart') {
if (event.touches.length > 0) {
const touch = event.touches[0];
const { clientX: x, clientY: y } = touch;
return isFinite(x) && isFinite(y) ? { x, y } : null;
}
}
return null;
};
/**
* Detects if the interaction is trusted for the given media element.
* @param {Event} event - The user interaction event.
* @param {HTMLElement} media - The media element to check against.
* @param {number} x - The x-coordinate of the interaction.
* @param {number} y - The y-coordinate of the interaction.
* @returns {boolean} - True if the interaction is trusted, else false.
*/
const isTrustedInteraction = (event, media, x, y) => {
if (!event.isTrusted) return false;
const mediaRect = media.getBoundingClientRect();
// 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 coordinates = extractCoordinates(event);
if (!coordinates) return;
const { x, y } = coordinates;
// Ensure coordinates are finite numbers
if (!isFinite(x) || !isFinite(y)) return;
const mediaElements = document.querySelectorAll('video, audio');
mediaElements.forEach(media => {
if (isTrustedInteraction(event, media, x, y)) {
if (!mediaInteractionMap.get(media)) {
mediaInteractionMap.set(media, true);
media.play().catch(() => {});
console.log('User interacted with media:', media);
}
}
});
};
// 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 });
})();