Disable autoplay

Block autoplay before user interaction on most websites

当前为 2024-09-15 提交的版本,查看 最新版本

// ==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 })
  }
})()