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