Disable autoplay

Block autoplay before user interaction on most websites

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Disable autoplay
// @namespace    https://www.androidacy.com/
// @version      2.5.7
// @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 allowedAttr = 'data-pb-allowed'
  const allowedPrefix = '__pb_allowed_'
  const proximityThreshold = 100 // pixels

  const debugLog = (...args) => {
    console.debug('[DisableAutoplay]', ...args)
  }

  const warnLog = (...args) => {
    console.warn('[DisableAutoplay]', ...args)
  }

  const generateRandomString = (length = 10) => {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    let result = ''
    for (let i = 0; i < length; i++) {
      result += chars.charAt(Math.floor(Math.random() * chars.length))
    }
    return result
  }

  const secret = generateRandomString()

  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(allowedAttr)
      if (allowed === secret) {
        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 playHandler = () => {
      if (!media.hasAttribute(allowedAttr) || media.getAttribute(allowedAttr) !== secret) {
        media.pause()
        debugLog('Playback blocked and media paused:', media)
      }
    }

    media.addEventListener('play', playHandler)

    const handler = (event) => {
      if (!event.isTrusted) {
        warnLog('Ignored untrusted event:', event)
        return
      }

      media.setAttribute(allowedAttr, secret)
      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)
    }

    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)

    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)
            })
          })
        })
      })
    })

    observer.observe(document.body, { childList: true, subtree: true })
    debugLog('Started observing DOM for new media elements')
  }

  const initObserver = () => {
    observeMedia()
  }

  processMediaElements()

  if (document.readyState === 'complete' || document.readyState === 'interactive') {
    initObserver()
  } else {
    document.addEventListener('DOMContentLoaded', initObserver)
  }
})()