// ==UserScript==
// @name Disable autoplay
// @namespace https://www.androidacy.com/
// @version 2.5.6
// @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==
(() => {
const mediaTags = ['video', 'audio']
const processedAttr = 'data-disable-autoplay-processed'
const allowedValue = `__pb_allowed_${Math.random().toString(36).substr(2, 10)}`
const proximityThreshold = 100 // pixels
const debugLog = (...args) => {
console.debug('[DisableAutoplay]', ...args)
}
const warnLog = (...args) => {
console.warn('[DisableAutoplay]', ...args)
}
const generateRandomString = (length = 6) => {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let result = ''
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
return result
}
const disableAutoplay = (media) => {
if (media.hasAttribute(processedAttr)) {
debugLog('Already processed media element:', media)
return
}
media.setAttribute(processedAttr, 'true')
debugLog('Processing media element:', media)
if (media.hasAttribute('autoplay')) {
media.removeAttribute('autoplay')
debugLog('Removed autoplay attribute from media:', media)
}
if (!media.paused) {
media.pause()
debugLog('Paused media element:', media)
}
const originalPlay = media.play
media.play = function (...args) {
const allowed = media.getAttribute('data-pb-allowed')
if (allowed === allowedValue) {
debugLog('Playing media element:', media)
return originalPlay.apply(this, args)
}
warnLog('Autoplay blocked for media element:', media)
return Promise.reject(new Error('Autoplay is disabled by a userscript.'))
}
const handler = (event) => {
if (!event.isTrusted) {
warnLog('Ignored untrusted event:', event)
return
}
media.setAttribute('data-pb-allowed', allowedValue)
debugLog('User interaction detected:', event.type, 'on', event.target)
media.play().catch(err => warnLog('Error playing media after user interaction:', err, media))
media.removeEventListener('click', handler)
media.removeEventListener('touchstart', handler)
removeCoverListeners(media, handler)
}
const addListeners = () => {
media.addEventListener('click', handler, { once: true, passive: false })
media.addEventListener('touchstart', handler, { once: true, passive: false })
debugLog('Added click and touchstart event listeners to media element:', media)
}
addListeners()
addCoverListeners(media, handler)
}
const 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)
})
}
const 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)
})
}
const findCoverElements = (media) => {
const covers = []
const mediaRect = media.getBoundingClientRect()
const parent = media.parentElement
if (!parent) return covers
Array.from(parent.children).forEach(sibling => {
if (sibling === media) return
const style = window.getComputedStyle(sibling)
const { position, display, visibility, pointerEvents } = style
if (display === 'none' || visibility === 'hidden' || pointerEvents === 'none') return
if (!['absolute', 'fixed', 'relative'].includes(position)) return
const siblingRect = sibling.getBoundingClientRect()
if (isOverlapping(mediaRect, siblingRect) || isWithinProximity(mediaRect, siblingRect)) {
covers.push(sibling)
}
})
return covers
}
const isOverlapping = (rect1, rect2) => {
const threshold = 0.3
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
}
const isWithinProximity = (rect1, rect2) => {
const proximity = proximityThreshold
const horizontallyClose =
Math.abs(rect1.left - rect2.left) <= proximity ||
Math.abs(rect1.right - rect2.right) <= proximity ||
Math.abs(rect1.left - rect2.right) <= proximity ||
Math.abs(rect1.right - rect2.left) <= proximity
const verticallyClose =
Math.abs(rect1.top - rect2.top) <= proximity ||
Math.abs(rect1.bottom - rect2.bottom) <= proximity ||
Math.abs(rect1.top - rect2.bottom) <= proximity ||
Math.abs(rect1.bottom - rect2.top) <= proximity
return horizontallyClose || verticallyClose
}
const processMediaElements = () => {
mediaTags.forEach(tag => {
document.querySelectorAll(tag).forEach(media => {
disableAutoplay(media)
})
})
}
const observeMedia = () => {
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return
mediaTags.forEach(tag => {
if (node.matches(tag)) {
debugLog('New media element added:', node)
disableAutoplay(node)
}
node.querySelectorAll(tag).forEach(media => {
debugLog('New nested media element added:', media)
disableAutoplay(media)
})
})
})
})
})
try {
observer.observe(document.body, { childList: true, subtree: true })
debugLog('Started observing DOM for new media elements')
} catch (error) {
warnLog('Failed to observe DOM:', error)
}
}
const init = () => {
debugLog('Initializing Disable autoplay userscript')
processMediaElements()
}
init()
if (document.readyState === 'complete' || document.readyState === 'interactive') {
observeMedia()
} else {
document.addEventListener('DOMContentLoaded', observeMedia, { once: true })
}
})()