X / Twitter — Auto Pause Fix

Impide que X pause los videos por scroll, pérdida de foco, re-render o política de "un solo reproductor". Solo pausa si el usuario lo pide.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         X / Twitter — Auto Pause Fix
// @namespace    facundo
// @version      1.0
// @description  Impide que X pause los videos por scroll, pérdida de foco, re-render o política de "un solo reproductor". Solo pausa si el usuario lo pide.
// @match        https://x.com/*
// @match        https://twitter.com/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const CFG = {
    graceMs: 600,          // ventana tras un gesto real donde pause() se respeta
    protectAll: false,     // true = proteger también los muteados del timeline
    maxResumes: 6,         // tope de reintentos de resume por video
    resumeWindowMs: 3000,  // ventana del tope anterior
    debug: false,
  };

  const log = (...a) => CFG.debug && console.debug('[x-nopause]', ...a);

  const owned = new WeakSet();      // videos que el usuario tocó = suyos
  const userPaused = new WeakSet(); // pausados a propósito
  const resumes = new WeakMap();    // rate limit de resume

  let allowUntil = 0;
  let enabled = true;

  const fresh = () => performance.now() < allowUntil;
  const allow = () => { allowUntil = performance.now() + CFG.graceMs; };
  const isVideo = (el) => el instanceof HTMLVideoElement;

  const protectedVideo = (v) =>
    CFG.protectAll ||
    owned.has(v) ||
    !v.muted ||
    document.pictureInPictureElement === v ||
    (document.fullscreenElement && document.fullscreenElement.contains(v));

  // --- 1. detección de gesto real -------------------------------------------
  // Sin depender de data-testid: subo hasta 8 niveles buscando el <video>
  // y verifico que el click haya caído dentro de su rect (los controles de X
  // están superpuestos al video, así que entran).
  const videoNear = (node) => {
    let el = node instanceof Element ? node : node && node.parentElement;
    for (let i = 0; el && i < 8; i++, el = el.parentElement) {
      if (el.tagName === 'VIDEO') return el;
      const v = el.querySelector && el.querySelector('video');
      if (v) return v;
    }
    return null;
  };

  addEventListener('pointerdown', (e) => {
    if (!e.isTrusted) return;
    const v = videoNear(e.target);
    if (!v) return;
    const r = v.getBoundingClientRect();
    const m = 8;
    if (e.clientX < r.left - m || e.clientX > r.right + m ||
        e.clientY < r.top - m || e.clientY > r.bottom + m) return;
    owned.add(v);
    allow();
  }, true);

  const KEYS = new Set([' ', 'Spacebar', 'k', 'K', 'MediaPlayPause']);
  addEventListener('keydown', (e) => {
    if (e.isTrusted && KEYS.has(e.key)) allow();
  }, true);

  // Teclas multimedia / controles del SO: llegan como pause() programático,
  // así que marco la ventana desde el handler de Media Session.
  const ms = navigator.mediaSession;
  if (ms && ms.setActionHandler) {
    const orig = ms.setActionHandler.bind(ms);
    ms.setActionHandler = function (action, handler) {
      if (handler && (action === 'pause' || action === 'stop')) {
        const h = handler;
        handler = function (...args) { allow(); return h.apply(this, args); };
      }
      return orig(action, handler);
    };
  }

  // --- 2. el choke point: HTMLMediaElement.prototype.pause -------------------
  const nativePause = HTMLMediaElement.prototype.pause;

  Object.defineProperty(HTMLMediaElement.prototype, 'pause', {
    configurable: true,
    writable: true,
    value: function pause() {
      const v = this;
      if (enabled && isVideo(v) && v.isConnected && !v.ended &&
          protectedVideo(v) && !fresh()) {
        log('pause() bloqueado', v.currentSrc);
        return;
      }
      if (isVideo(v) && fresh()) userPaused.add(v);
      return nativePause.call(v);
    },
  });

  // --- 3. red de seguridad --------------------------------------------------
  // Cubre pausas que no pasan por pause(): load(), cambio de src, stalls.
  const canResume = (v) => {
    const now = performance.now();
    let r = resumes.get(v);
    if (!r || now - r.t0 > CFG.resumeWindowMs) r = { n: 0, t0: now };
    r.n++;
    resumes.set(v, r);
    return r.n <= CFG.maxResumes;
  };

  addEventListener('pause', (e) => {
    const v = e.target;
    if (!enabled || !isVideo(v)) return;
    if (!protectedVideo(v) || userPaused.has(v)) return;
    if (fresh() || v.ended || !v.isConnected) return;
    if (document.pictureInPictureElement === v) return; // controles nativos de PiP
    if (!canResume(v)) return;
    log('resume', v.currentSrc);
    v.play().catch(() => {});
  }, true);

  addEventListener('play', (e) => {
    if (isVideo(e.target)) userPaused.delete(e.target);
  }, true);

  addEventListener('enterpictureinpicture', (e) => {
    if (isVideo(e.target)) owned.add(e.target);
  }, true);

  // --- 4. control manual ----------------------------------------------------
  window.__xNoPause = {
    cfg: CFG,
    on() { enabled = true; },
    off() { enabled = false; },
    get enabled() { return enabled; },
  };
})();