Skip to content

Instantly share code, notes, and snippets.

@mikecao
Created December 21, 2025 22:18
Show Gist options
  • Select an option

  • Save mikecao/f1526a733ef9ac8a4ea399589d787fc8 to your computer and use it in GitHub Desktop.

Select an option

Save mikecao/f1526a733ef9ac8a4ea399589d787fc8 to your computer and use it in GitHub Desktop.
Removes disablePictureInPicture attribute from all videos (including in iframes) so PiP works
// ==UserScript==
// @name Enable Picture-in-Picture Everywhere
// @namespace http://tampermonkey.net/
// @version 2.3
// @description Removes disablePictureInPicture attribute from all videos (including in iframes) so PiP works
// @match *://*/*
// @include *
// @run-at document-start
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Log context to help debugging
const isInIframe = window.self !== window.top;
console.log('[PiP] Script initialized in:', isInIframe ? 'IFRAME' : 'TOP WINDOW', location.href);
const ATTR = 'disablepictureinpicture'; // normalized name
// Keep originals
const origSetAttribute = Element.prototype.setAttribute;
const origSetAttributeNS = Element.prototype.setAttributeNS;
const origRemoveAttribute = Element.prototype.removeAttribute;
const origToggleAttribute = Element.prototype.toggleAttribute;
// Helper: case-insensitive compare
const isPiPAttr = name => String(name).toLowerCase() === ATTR;
// Patch setAttribute / setAttributeNS to block attempts to add the attribute
Element.prototype.setAttribute = function(name, value) {
try {
if (isPiPAttr(name)) {
console.log('[PiP] βœ… Blocked setAttribute attempt on video');
return; // don't add it
}
} catch (_) {}
return origSetAttribute.call(this, name, value);
};
Element.prototype.setAttributeNS = function(ns, name, value) {
try {
if (isPiPAttr(name)) {
console.log('[PiP] βœ… Blocked setAttributeNS attempt on video');
return;
}
} catch (_) {}
return origSetAttributeNS.call(this, ns, name, value);
};
// Optional: patch toggleAttribute (rarely used but safe to guard)
if (typeof origToggleAttribute === 'function') {
Element.prototype.toggleAttribute = function(name, force) {
try {
if (isPiPAttr(name)) {
console.log('[PiP] βœ… Blocked toggleAttribute attempt on video');
return !!force; // emulate a no-op (safe)
}
} catch (_) {}
return origToggleAttribute.call(this, name, force);
};
}
// Prevent property assignment like video.disablePictureInPicture = true
try {
Object.defineProperty(HTMLVideoElement.prototype, 'disablePictureInPicture', {
configurable: true,
enumerable: false,
get() { return false; },
set(_) {
// swallow attempts
try { console.log('[PiP] βœ… Blocked property assignment attempt on video'); } catch (_) {}
}
});
} catch (e) {
console.warn('[PiP] ❌ Could not override video property:', e);
}
// Remove attribute(s) from a single element if present
function removePiPAttrFrom(el) {
try {
if (el && el.removeAttribute) {
let removed = false;
if (el.hasAttribute && el.hasAttribute(ATTR)) {
origRemoveAttribute.call(el, ATTR);
removed = true;
}
// cover alternative casing (just in case)
if (el.hasAttribute && el.hasAttribute('disablePictureInPicture')) {
origRemoveAttribute.call(el, 'disablePictureInPicture');
removed = true;
}
if (removed) {
console.log('[PiP] βœ… Removed disablePictureInPicture from video');
}
}
} catch (e) {
console.warn('[PiP] Error removing attribute:', e);
}
}
// Process iframe - inject script if same-origin, or mark as processed
function processIframe(iframe) {
try {
// Check if already processed
if (iframe._pipProcessed) return;
iframe._pipProcessed = true;
// Listen for iframe load to process its content
iframe.addEventListener('load', () => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc) {
scanRoot(iframeDoc);
observeRoot(iframeDoc);
}
} catch (e) {
// Cross-origin iframe - silently skip
}
});
// Also try to access immediately in case it's already loaded
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;
if (iframeDoc && iframeDoc.readyState === 'complete') {
scanRoot(iframeDoc);
observeRoot(iframeDoc);
}
} catch (e) {
// Cross-origin iframe - silently skip
}
} catch (e) {
console.warn('[PiP] Error processing iframe:', e);
}
}
// Scan root and its shadow roots for videos AND iframes
function scanRoot(root) {
try {
if (!root) return;
if (root.querySelectorAll) {
// Process videos
const videos = root.querySelectorAll('video');
videos.forEach(removePiPAttrFrom);
// Process iframes (only in main document, not recursively)
if (root === document || root.nodeType === 9) {
const iframes = root.querySelectorAll('iframe');
iframes.forEach(processIframe);
}
}
// walk to find shadow roots
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let n;
while ((n = walker.nextNode())) {
if (n.shadowRoot) {
scanRoot(n.shadowRoot);
}
}
} catch (e) {
console.warn('[PiP] Error scanning root:', e);
}
}
// Set up mutation observer for a given root
function observeRoot(root) {
const mo = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.type === 'childList' && m.addedNodes.length) {
for (const node of m.addedNodes) {
if (node.nodeType !== 1) continue;
if (node.tagName === 'VIDEO') {
console.log('[PiP] πŸ“Ί New video element added');
removePiPAttrFrom(node);
}
else if (node.tagName === 'IFRAME') {
console.log('[PiP] πŸ–ΌοΈ New iframe element added');
processIframe(node);
}
else if (node.querySelectorAll) {
scanRoot(node);
}
}
}
if (m.type === 'attributes') {
// attributeFilter below restricts these to the PiP attribute name
if (m.attributeName && isPiPAttr(m.attributeName)) {
console.log('[PiP] ⚠️ disablePictureInPicture attribute was added, removing it');
removePiPAttrFrom(m.target);
}
}
}
});
try {
mo.observe(root.documentElement || root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [ATTR] // monitor only the exact attribute name
});
console.log('[PiP] πŸ‘οΈ Observer started on', root === document ? 'main document' : 'iframe');
} catch (e) {
// fallback without attributeFilter if it throws in some environments
try {
mo.observe(root.documentElement || root, { childList: true, subtree: true });
console.log('[PiP] πŸ‘οΈ Observer started (fallback mode)');
} catch (e2) {
console.warn('[PiP] Could not start observer:', e2);
}
}
}
function start() {
console.log('[PiP] πŸš€ Starting initialization...');
// initial pass
scanRoot(document);
// watch whole document (attributes filter reduces noise)
observeRoot(document);
// safety-net: periodical scan for very aggressive pages that bypass everything
setInterval(() => {
console.log('[PiP] πŸ”„ Periodic scan...');
scanRoot(document);
}, 5000);
console.log('[PiP] βœ… Initialization complete');
}
// Run immediately for document-start, or wait for DOMContentLoaded
if (document.readyState === 'loading') {
console.log('[PiP] ⏳ Waiting for DOMContentLoaded...');
window.addEventListener('DOMContentLoaded', start, { once: true });
} else {
console.log('[PiP] ⚑ Document already loaded, starting immediately');
start();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment