Disable autoplay

Block autoplay before user interaction on most websites

目前為 2024-09-15 提交的版本,檢視 最新版本

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