// ==UserScript==
// @name Disable autoplay
// @namespace https://www.androidacy.com/
// @version 2.2.1
// @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-end
// ==/UserScript==
(() => {
const allowedToPlay = new WeakSet()
const mediaTags = ['video', 'audio']
const processedAttr = 'data-disable-autoplay-processed'
const debugLog = (...args) => {
console.debug('[DisableAutoplay]', ...args)
}
const warnLog = (...args) => {
console.warn('[DisableAutoplay]', ...args)
}
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 = (...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.'))
}
}
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))
media.removeEventListener('click', enablePlayback)
media.removeEventListener('touchstart', enablePlayback)
removeCoverListeners(media, enablePlayback)
}
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)
addCoverListeners(media, enablePlayback)
}
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 = style.position
const display = style.display
const visibility = style.visibility
const pointerEvents = style.pointerEvents
if (display === 'none' || visibility === 'hidden' || pointerEvents === 'none') return
if (!['absolute', 'fixed', 'relative'].includes(position)) return
const siblingRect = sibling.getBoundingClientRect()
if (isOverlapping(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 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)
})
})
})
})
})
observer.observe(document.body, { childList: true, subtree: true })
debugLog('Started observing DOM for new media elements')
}
const init = () => {
debugLog('Initializing Disable autoplay userscript')
processMediaElements()
observeMedia()
}
init()
})()