// ==UserScript==
// @name Disable autoplay
// @namespace https://www.androidacy.com/
// @version 2.0.0
// @description Block autoplay before user interaction on most websites
// @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-start
// ==/UserScript==
(function() {
'use strict';
// Generate a random 4-8 character string for tracking processed elements
const generateRandomString = () => {
const length = Math.floor(Math.random() * 5) + 4; // 4-8 characters
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return `__disableAutoplay_${result}`;
};
const processedProp = generateRandomString();
const allowedToPlay = new WeakSet();
const mediaTags = ['video', 'audio'];
function debugLog(...args) {
console.debug('[DisableAutoplay]', ...args);
}
function warnLog(...args) {
console.warn('[DisableAutoplay]', ...args);
}
function disableAutoplay(media) {
if (media[processedProp]) {
debugLog('Already processed media element:', media);
return;
}
media[processedProp] = true;
debugLog('Processing media element:', media);
// Remove autoplay attribute if present
if (media.hasAttribute('autoplay')) {
media.removeAttribute('autoplay');
debugLog('Removed autoplay attribute from media:', media);
}
// Pause the media if it's already playing
if (!media.paused) {
media.pause();
debugLog('Paused media element:', media);
}
// Store the original play method
const originalPlay = media.play;
// Override the play method to control playback
media.play = function(...args) {
if (allowedToPlay.has(media)) {
debugLog('Playing media element:', media);
return originalPlay.apply(media, args);
} else {
warnLog('Autoplay blocked for media element:', media);
return Promise.reject(new Error('Autoplay is disabled by a userscript.'));
}
};
/**
* Enables playback when a trusted user interaction is detected
* @param {Event} event - The user interaction event
*/
const enablePlayback = (event) => {
if (!event.isTrusted) {
warnLog('Ignored untrusted event:', event);
return;
}
debugLog('User interaction detected:', event.type, 'on', event.target);
allowedToPlay.add(media);
media.play().catch(err => warnLog('Error playing media after user interaction:', err, media));
// Remove event listeners after enabling playback
media.removeEventListener('click', enablePlayback);
media.removeEventListener('touchstart', enablePlayback);
removeCoverListeners(media, enablePlayback);
};
// Add event listeners to media element without passive listeners
media.addEventListener('click', enablePlayback, { once: true, passive: false });
media.addEventListener('touchstart', enablePlayback, { once: true, passive: false });
debugLog('Added click and touchstart event listeners to media element:', media);
// Add event listeners to associated cover elements
addCoverListeners(media, enablePlayback);
}
/**
* Adds event listeners to cover elements associated with the media
* @param {HTMLMediaElement} media - The media element
* @param {Function} handler - The event handler to attach
*/
function addCoverListeners(media, handler) {
const covers = findCoverElements(media);
covers.forEach(cover => {
cover.addEventListener('click', handler, { once: true, passive: false });
cover.addEventListener('touchstart', handler, { once: true, passive: false });
debugLog('Added event listeners to cover element:', cover);
});
}
/**
* Removes event listeners from cover elements after playback is enabled
* @param {HTMLMediaElement} media - The media element
* @param {Function} handler - The event handler to remove
*/
function removeCoverListeners(media, handler) {
const covers = findCoverElements(media);
covers.forEach(cover => {
cover.removeEventListener('click', handler);
cover.removeEventListener('touchstart', handler);
debugLog('Removed event listeners from cover element:', cover);
});
}
/**
* Finds cover elements associated with a media element by analyzing computed styles
* @param {HTMLMediaElement} media - The media element
* @returns {HTMLElement[]} - Array of cover elements
*/
function findCoverElements(media) {
const covers = [];
const mediaRect = media.getBoundingClientRect();
const parent = media.parentElement;
if (!parent) return covers;
// Iterate through parent's children to find overlapping elements
Array.from(parent.children).forEach(sibling => {
if (sibling === media) return;
const style = window.getComputedStyle(sibling);
const position = style.position;
const display = style.display;
const visibility = style.visibility;
const pointerEvents = style.pointerEvents;
// Skip elements that are not visible or interactable
if (display === 'none' || visibility === 'hidden' || pointerEvents === 'none') return;
// Consider elements with certain positioning
if (!['absolute', 'fixed', 'relative'].includes(position)) return;
const siblingRect = sibling.getBoundingClientRect();
// Check if sibling overlaps significantly with media
if (isOverlapping(mediaRect, siblingRect)) {
covers.push(sibling);
}
});
return covers;
}
/**
* Determines if two rectangles overlap by a certain threshold
* @param {DOMRect} rect1 - First rectangle
* @param {DOMRect} rect2 - Second rectangle
* @returns {boolean} - True if overlapping sufficiently, else false
*/
function isOverlapping(rect1, rect2) {
const threshold = 0.3; // 30% overlap
const intersection = {
left: Math.max(rect1.left, rect2.left),
right: Math.min(rect1.right, rect2.right),
top: Math.max(rect1.top, rect2.top),
bottom: Math.min(rect1.bottom, rect2.bottom)
};
const width = intersection.right - intersection.left;
const height = intersection.bottom - intersection.top;
if (width <= 0 || height <= 0) return false;
const areaIntersection = width * height;
const areaMedia = rect1.width * rect1.height;
return (areaIntersection / areaMedia) >= threshold;
}
/**
* Processes all existing media elements on the page
*/
function processMediaElements() {
mediaTags.forEach(tag => {
document.querySelectorAll(tag).forEach(media => {
disableAutoplay(media);
});
});
}
/**
* Observes the DOM for any new media elements and processes them
*/
function observeMedia() {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
mediaTags.forEach(tag => {
// Direct match
if (node.matches(tag)) {
debugLog('New media element added:', node);
disableAutoplay(node);
}
// Nested media elements
node.querySelectorAll(tag).forEach(media => {
debugLog('New nested media element added:', media);
disableAutoplay(media);
});
});
});
});
});
observer.observe(document.documentElement, { childList: true, subtree: true });
debugLog('Started observing DOM for new media elements');
}
/**
* Initializes the userscript by processing existing media elements and setting up observers
*/
function init() {
debugLog('Initializing Disable autoplay userscript');
processMediaElements();
observeMedia();
}
// Run initialization at document-start
init();
// Also process media elements at DOMContentLoaded to catch any late-loaded media
document.addEventListener('DOMContentLoaded', () => {
debugLog('DOMContentLoaded event fired');
processMediaElements();
});
})();