Created
December 21, 2025 22:18
-
-
Save mikecao/f1526a733ef9ac8a4ea399589d787fc8 to your computer and use it in GitHub Desktop.
Removes disablePictureInPicture attribute from all videos (including in iframes) so PiP works
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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