Skip to content

Instantly share code, notes, and snippets.

@affan2021shaikh
Last active April 24, 2026 01:22
Show Gist options
  • Select an option

  • Save affan2021shaikh/efa8b9f8c31b794d2d3b2a1b99928e0f to your computer and use it in GitHub Desktop.

Select an option

Save affan2021shaikh/efa8b9f8c31b794d2d3b2a1b99928e0f to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name YouTube Background Play Plus
// @namespace https://example.invalid/
// @version 2.0.0
// @description Best-effort background play for YouTube desktop and mobile web using only standard userscript features.
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @run-at document-start
// @grant none
// @noframes
// ==/UserScript==
(() => {
'use strict';
const CFG = {
manualGraceMs: 1000,
hiddenRetryMs: 250,
visibleRetryMs: 500,
maxRetryMs: 5000,
hiddenRetryWindowMs: 30000,
debug: false,
};
const log = (...args) => {
if (CFG.debug) console.log('[YT BG Play Plus]', ...args);
};
const SEL = [
'#movie_player video',
'ytd-player video',
'video.html5-main-video',
'video',
];
const CONTROL_SEL = [
'button',
'[role="button"]',
'tp-yt-paper-icon-button',
'yt-icon-button',
'.ytp-button',
'.ytp-play-button',
'.ytp-volume-panel',
'.ytp-fullscreen-button',
].join(',');
let currentVideo = null;
let desiredPlaying = false;
let lastGestureAt = 0;
let lastExplicitControlAt = 0;
let lastManualPauseAt = 0;
let hiddenSince = 0;
let observerStarted = false;
let retryTimer = 0;
let retryDelay = CFG.visibleRetryMs;
let playInFlight = false;
let booted = false;
const now = () => Date.now();
const max2 = Math.max;
function touchGesture(event) {
const t = now();
lastGestureAt = t;
const target = event && event.target;
if (target instanceof Element && target.closest(CONTROL_SEL)) {
lastExplicitControlAt = t;
}
}
function manualGraceActive() {
const t = now();
const recent = max2(lastGestureAt, lastExplicitControlAt, lastManualPauseAt);
return (t - recent) <= CFG.manualGraceMs;
}
function setMediaSessionState() {
try {
if (!('mediaSession' in navigator)) return;
navigator.mediaSession.playbackState = desiredPlaying ? 'playing' : 'paused';
} catch (_) {}
}
function installMediaSessionHandlers() {
try {
if (!('mediaSession' in navigator)) return;
if (typeof navigator.mediaSession.setActionHandler !== 'function') return;
navigator.mediaSession.setActionHandler('play', () => {
desiredPlaying = true;
setMediaSessionState();
scheduleRetry('mediasession-play', true);
});
navigator.mediaSession.setActionHandler('pause', () => {
desiredPlaying = false;
lastManualPauseAt = now();
setMediaSessionState();
try {
if (currentVideo && !currentVideo.paused) currentVideo.pause();
} catch (_) {}
});
} catch (_) {}
}
function scoreVideo(v) {
if (!(v instanceof HTMLVideoElement) || !v.isConnected) return -1e18;
const r = v.getBoundingClientRect();
const area = max2(0, r.width) * max2(0, r.height);
let score = area;
if (r.width > 0 && r.height > 0) score += 1_000_000;
if (document.visibilityState === 'visible') score += 20_000;
if (v.readyState >= 2) score += 10_000;
if (!v.paused) score += 50_000;
if (v.currentSrc) score += 2_000;
if (v.matches('.html5-main-video')) score += 200_000;
if (v.closest('#movie_player')) score += 150_000;
if (v.closest('ytd-player')) score += 100_000;
if (v.closest('.html5-video-container')) score += 50_000;
return score;
}
function pickVideo() {
const seen = new Set();
const candidates = [];
for (const sel of SEL) {
for (const v of document.querySelectorAll(sel)) {
if (!seen.has(v)) {
seen.add(v);
candidates.push(v);
}
}
}
let best = null;
let bestScore = -1e18;
for (const v of candidates) {
const s = scoreVideo(v);
if (s > bestScore) {
bestScore = s;
best = v;
}
}
return best;
}
function attachVideo(v) {
if (!(v instanceof HTMLVideoElement)) return;
if (v === currentVideo) return;
if (currentVideo) {
currentVideo.removeEventListener('play', onPlay, true);
currentVideo.removeEventListener('playing', onPlaying, true);
currentVideo.removeEventListener('pause', onPause, true);
currentVideo.removeEventListener('ended', onEnded, true);
currentVideo.removeEventListener('emptied', onChangedSource, true);
currentVideo.removeEventListener('loadstart', onChangedSource, true);
currentVideo.removeEventListener('loadedmetadata', onChangedSource, true);
currentVideo.removeEventListener('stalled', onNeedRetry, true);
currentVideo.removeEventListener('waiting', onNeedRetry, true);
currentVideo.removeEventListener('suspend', onNeedRetry, true);
currentVideo.removeEventListener('canplay', onNeedRetry, true);
}
currentVideo = v;
try {
currentVideo.setAttribute('playsinline', '');
currentVideo.setAttribute('webkit-playsinline', '');
} catch (_) {}
currentVideo.addEventListener('play', onPlay, true);
currentVideo.addEventListener('playing', onPlaying, true);
currentVideo.addEventListener('pause', onPause, true);
currentVideo.addEventListener('ended', onEnded, true);
currentVideo.addEventListener('emptied', onChangedSource, true);
currentVideo.addEventListener('loadstart', onChangedSource, true);
currentVideo.addEventListener('loadedmetadata', onChangedSource, true);
currentVideo.addEventListener('stalled', onNeedRetry, true);
currentVideo.addEventListener('waiting', onNeedRetry, true);
currentVideo.addEventListener('suspend', onNeedRetry, true);
currentVideo.addEventListener('canplay', onNeedRetry, true);
log('attached video');
}
function syncVideo() {
const best = pickVideo();
if (best) attachVideo(best);
}
function resetRetryDelay() {
retryDelay = document.hidden ? CFG.hiddenRetryMs : CFG.visibleRetryMs;
}
function bumpRetryDelay() {
retryDelay = Math.min(Math.max(retryDelay * 1.5, 250), CFG.maxRetryMs);
}
function safePlay(reason) {
if (!currentVideo || !desiredPlaying) return Promise.resolve(false);
if (playInFlight) return Promise.resolve(true);
playInFlight = true;
let p;
try {
p = currentVideo.play();
} catch (err) {
playInFlight = false;
log('play threw', reason, err && err.name ? err.name : err);
return Promise.resolve(false);
}
if (p && typeof p.then === 'function') {
return p.then(() => {
playInFlight = false;
resetRetryDelay();
setMediaSessionState();
log('play ok', reason);
return true;
}).catch((err) => {
playInFlight = false;
bumpRetryDelay();
log('play failed', reason, err && err.name ? err.name : err);
return false;
});
}
playInFlight = false;
resetRetryDelay();
setMediaSessionState();
return Promise.resolve(true);
}
function scheduleRetry(reason, immediate = false) {
if (!desiredPlaying) return;
clearTimeout(retryTimer);
const delay = immediate ? 0 : (document.hidden ? CFG.hiddenRetryMs : retryDelay);
retryTimer = setTimeout(async () => {
if (!desiredPlaying) return;
if (document.hidden && hiddenSince && (now() - hiddenSince) > CFG.hiddenRetryWindowMs) {
return;
}
syncVideo();
if (!currentVideo) {
bumpRetryDelay();
scheduleRetry(reason);
return;
}
if (!currentVideo.paused && currentVideo.readyState >= 2) {
resetRetryDelay();
return;
}
const ok = await safePlay(reason);
if (!ok && desiredPlaying) scheduleRetry(reason);
}, delay);
}
function onPlay() {
desiredPlaying = true;
resetRetryDelay();
setMediaSessionState();
}
function onPlaying() {
desiredPlaying = true;
resetRetryDelay();
setMediaSessionState();
}
function onPause() {
if (manualGraceActive()) {
desiredPlaying = false;
lastManualPauseAt = now();
setMediaSessionState();
return;
}
if (desiredPlaying) {
scheduleRetry('pause');
}
}
function onEnded() {
desiredPlaying = false;
resetRetryDelay();
setMediaSessionState();
}
function onChangedSource() {
setTimeout(() => {
syncVideo();
if (desiredPlaying) scheduleRetry('source-change', true);
}, 0);
}
function onNeedRetry() {
if (desiredPlaying) scheduleRetry('buffering');
}
function onVisibilityChange() {
if (document.hidden) {
hiddenSince = now();
if (desiredPlaying) scheduleRetry('hidden', true);
} else {
hiddenSince = 0;
if (desiredPlaying) scheduleRetry('visible', true);
}
}
function onPageHide() {
if (desiredPlaying) scheduleRetry('pagehide', true);
}
function onPageShow() {
if (desiredPlaying) scheduleRetry('pageshow', true);
}
function onFocusChange() {
if (desiredPlaying) scheduleRetry('focus-change', true);
}
function onUserIntent(event) {
touchGesture(event);
}
function patchPause() {
const proto = HTMLMediaElement && HTMLMediaElement.prototype;
if (!proto || typeof proto.pause !== 'function') return;
const nativePause = proto.pause;
proto.pause = function patchedPause() {
try {
if (
this === currentVideo &&
desiredPlaying &&
document.hidden &&
!manualGraceActive()
) {
log('blocked hidden pause');
return;
}
} catch (_) {}
return nativePause.apply(this, arguments);
};
}
function patchHistory() {
const dispatch = () => window.dispatchEvent(new Event('ytbg-locationchange'));
try {
const pushState = history.pushState;
if (typeof pushState === 'function') {
history.pushState = function () {
const ret = pushState.apply(this, arguments);
dispatch();
return ret;
};
}
} catch (_) {}
try {
const replaceState = history.replaceState;
if (typeof replaceState === 'function') {
history.replaceState = function () {
const ret = replaceState.apply(this, arguments);
dispatch();
return ret;
};
}
} catch (_) {}
window.addEventListener('popstate', dispatch, true);
window.addEventListener('yt-navigate-finish', dispatch, true);
window.addEventListener('yt-page-data-updated', dispatch, true);
window.addEventListener('ytbg-locationchange', () => {
setTimeout(() => {
syncVideo();
if (desiredPlaying) scheduleRetry('navigation', true);
}, 0);
}, true);
}
function startObserver() {
if (observerStarted) return;
const root = document.documentElement || document;
observerStarted = true;
const mo = new MutationObserver(() => {
syncVideo();
});
mo.observe(root, { childList: true, subtree: true });
syncVideo();
log('observer started');
}
function installGlobalListeners() {
document.addEventListener('pointerdown', onUserIntent, true);
document.addEventListener('touchstart', onUserIntent, true);
document.addEventListener('mousedown', onUserIntent, true);
document.addEventListener('click', onUserIntent, true);
document.addEventListener('keydown', onUserIntent, true);
document.addEventListener('visibilitychange', onVisibilityChange, true);
window.addEventListener('pagehide', onPageHide, true);
window.addEventListener('pageshow', onPageShow, true);
window.addEventListener('focus', onFocusChange, true);
window.addEventListener('blur', onFocusChange, true);
}
function boot() {
if (booted) return;
booted = true;
patchPause();
patchHistory();
installGlobalListeners();
installMediaSessionHandlers();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
startObserver();
}, { once: true });
} else {
startObserver();
}
setInterval(() => {
if (!desiredPlaying) return;
syncVideo();
if (!currentVideo) return;
if (document.hidden) {
if (currentVideo.paused) scheduleRetry('watchdog-hidden');
return;
}
if (currentVideo.paused) {
scheduleRetry('watchdog-visible');
}
}, 300);
}
boot();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment