Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions youtubeBackgroundPlay/youtube-background-play.user.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
// ==UserScript==
// @name YouTube Background Play (Firefox fix port)
// @namespace https://vgpnk-holdings-llc.github.io/
// @version 1.0.0
// @description Keeps YouTube (and YouTube Music) playing when the tab is hidden or the screen is locked. Port of the "YouTube Background Playback" Firefox fix for userscript managers (Dguard).
// @author VeigaPunk
// @match *://*.youtube.com/*
// @match *://*.youtube-nocookie.com/*
// @match *://music.youtube.com/*
// @grant none
// @run-at document-start
// @inject-into page
// @license MIT
// @downloadURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// @updateURL https://github.com/AdguardTeam/Userscripts/raw/master/youtubeBackgroundPlay/youtube-background-play.user.js
// ==/UserScript==

(function () {
'use strict';

/* ---------------------------------------------------------------
* 1. Spoof the Page Visibility API
* Make the page believe it is ALWAYS visible and focused.
* --------------------------------------------------------------- */
const spoofProps = {
hidden: false,
webkitHidden: false,
mozHidden: false,
msHidden: false,
visibilityState: 'visible',
webkitVisibilityState: 'visible',
hasFocus: true,
};

for (const [prop, value] of Object.entries(spoofProps)) {
try {
if (prop === 'hasFocus') {
Document.prototype.hasFocus = () => true;
continue;
}
Object.defineProperty(Document.prototype, prop, {
get: () => value,
configurable: true,
});
} catch (e) { /* older engines: fall through */ }
}

// Extra safety on the instance itself
try {
Object.defineProperty(document, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(document, 'visibilityState', { get: () => 'visible', configurable: true });
} catch (e) { /* noop */ }

/* ---------------------------------------------------------------
* 2. Neutralize visibility / focus events
* Intercept listener registration so site handlers never fire
* on hidden-state transitions, and kill the events outright.
* --------------------------------------------------------------- */
const BLOCKED_EVENTS = new Set([
'visibilitychange',
'webkitvisibilitychange',
'mozvisibilitychange',
'blur',
'focusout',
'pagehide',
'freeze',
]);

// Kill the events at capture phase before site code sees them
for (const type of BLOCKED_EVENTS) {
window.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
document.addEventListener(type, (e) => {
e.stopImmediatePropagation();
e.stopPropagation();
}, true);
}

// Wrap addEventListener so listeners registered for blocked events
// are silently swallowed (YouTube registers its pause logic this way)
const origAdd = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, listener, options, ...rest) {
if (BLOCKED_EVENTS.has(type) && (this === document || this === window)) {
return; // swallow
}
return origAdd.call(this, type, listener, options, ...rest);
};

/* ---------------------------------------------------------------
* 3. Block synthetic "pause" requests driven by visibility state
* YouTube calls player.pauseVideo() via its internal API when it
* thinks the tab went hidden. Filter those calls when hidden.
* --------------------------------------------------------------- */
const isUserGesture = () => {
// A pause within ~1.5s of a real user interaction is legit
return (Date.now() - (window.__ytbg_lastGesture || 0)) < 1500;
};

for (const t of ['pointerdown', 'keydown', 'touchstart', 'click']) {
window.addEventListener(t, () => { window.__ytbg_lastGesture = Date.now(); }, true);
}

const wrapPause = (proto) => {
if (!proto || typeof proto.pauseVideo !== 'function') return;
const orig = proto.pauseVideo;
proto.pauseVideo = function (...args) {
if (document.hidden !== false && !isUserGesture()) {
return; // programmatic pause while "hidden" → ignore
}
// With visibility spoofed document.hidden is always false,
// so gate purely on the gesture heuristic:
if (!isUserGesture() && window.__ytbg_blockPause === true) {
return;
}
return orig.apply(this, args);
};
};

// Detect transitions to hidden via the real (unspoofed) signal:
// we can't read document.hidden anymore, so use the page lifecycle —
// we simply enable pause-blocking permanently; legit pauses come with
// a user gesture and pass through.
window.__ytbg_blockPause = true;

// Hook the movie_player prototype once it exists
const hookPlayer = () => {
const player = document.getElementById('movie_player');
if (player) {
wrapPause(Object.getPrototypeOf(player));
return true;
}
return false;
};
if (!hookPlayer()) {
const obs = new MutationObserver(() => {
if (hookPlayer()) obs.disconnect();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
}

/* ---------------------------------------------------------------
* 4. HTMLMediaElement pause() guard (belt & suspenders)
* --------------------------------------------------------------- */
const origMediaPause = HTMLMediaElement.prototype.pause;
HTMLMediaElement.prototype.pause = function (...args) {
if (!isUserGesture()) {
return; // ignore programmatic pause without user gesture
}
return origMediaPause.apply(this, args);
};

/* ---------------------------------------------------------------
* 5. Media Session: keep lock-screen controls alive & resume
* --------------------------------------------------------------- */
if ('mediaSession' in navigator) {
try {
navigator.mediaSession.setActionHandler('pause', () => {
// Treat lock-screen "pause" as legit only on real gesture;
// otherwise keep playing.
if (isUserGesture()) {
const v = document.querySelector('video');
if (v) origMediaPause.call(v);
}
});
} catch (e) { /* noop */ }
}

/* ---------------------------------------------------------------
* 6. Block sendBeacon / heartbeat telemetry that pauses idle watch
* (YouTube "Are you still watching?" style triggers)
* --------------------------------------------------------------- */
const origBeacon = Navigator.prototype.sendBeacon;
Navigator.prototype.sendBeacon = function (url, data, ...rest) {
if (typeof url === 'string' && /\/(player_)?heartbeat|watchtime|pause/i.test(url)) {
return true; // pretend it sent
}
return origBeacon.call(this, url, data, ...rest);
};

console.debug('[yt-bg] background playback fix active');
})();