├── .gitignore ├── PRIVACY_POLICY.md ├── README.md ├── background.js ├── icon128.png ├── manifest.json └── rules.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | node_modules 3 | package* 4 | .DS_Store 5 | _metadata 6 | node_modules -------------------------------------------------------------------------------- /PRIVACY_POLICY.md: -------------------------------------------------------------------------------- 1 | # Privacy Policy 2 | 1. Product does not collect any personal information. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # PiP Unblocker 3 | 4 | Clears 5 | - Permissions-Policy headers 6 | - Feature-Policy headers 7 | - Attributes that block PiP. 8 | 9 | Compatible with YoutubeTV, DisneyPlus, Crunchyroll, and more. 10 | 11 | ## To use, install the [Chrome](https://chrome.google.com/webstore/detail/pip-unblocker/djjjomidddlggllpialpgkpnkdaeggfa) or [Edge](https://microsoftedge.microsoft.com/addons/detail/pgfngmhkdmmciaegfklbialafkpgmkhh) extension. 12 | 13 | ### Changelog 14 | 1. Updated to Manifest V3 15 | 2. Now you can click the extension icon to clear disablePictureInPicture attributes (useful for Disney Plus, Crunchyroll) 16 | 17 | ### Loading from repository 18 | 1. Chrome: open extensions page, enable dev mode, load unpacked. 19 | 1. Edge: open extensions page, load unpacked. 20 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | 2 | chrome.action.onClicked.addListener(async tab => { 3 | const results = await chrome.scripting.executeScript({ 4 | func: getPageInfo, 5 | target: { 6 | tabId: tab.id, 7 | allFrames: true 8 | } 9 | }) 10 | 11 | // Check if any enabled 12 | const activeResult = results.find(r => r.result?.active) 13 | if (activeResult) { 14 | 15 | await chrome.scripting.executeScript({ 16 | func: exitPiP, 17 | target: { 18 | tabId: tab.id, 19 | frameIds: [activeResult.frameId] 20 | } 21 | }) 22 | return 23 | } 24 | 25 | const video = getVideos(results).sort((a, b) => b.score - a.score)[0] 26 | if (video) { 27 | await chrome.scripting.executeScript({ 28 | func: enterPiP, 29 | target: { 30 | tabId: tab.id, 31 | frameIds: [video.frameId] 32 | }, 33 | args: [video.pipId] 34 | }) 35 | } 36 | }) 37 | 38 | /** 39 | * @returns {VideoInfo[]} 40 | */ 41 | function getVideos(results) { 42 | return results.filter(r => r.result?.videos?.length).map(r => { 43 | const videos = r.result.videos.map(v => { 44 | v.frameId = r.frameId 45 | v.score = calculateScore(v) 46 | return v 47 | }) 48 | return videos 49 | }).flat(1) 50 | } 51 | 52 | /** 53 | * @returns {Result} 54 | */ 55 | function getPageInfo() { 56 | return { 57 | active: !!document.pictureInPictureElement, 58 | videos: [...document.getElementsByTagName("video")] 59 | .filter(v => v.duration) 60 | .map(v => { 61 | v.removeAttribute("disablePictureInPicture") 62 | v.pipTek = Math.random().toString() 63 | 64 | const b = v.getBoundingClientRect() 65 | const partInterx = b.x + b.w >= 0 && b.y < window.innerHeight 66 | const fullInterx = b.x >= 0 && b.y + b.height < window.innerHeight 67 | 68 | return { 69 | score: 0, 70 | duration: v.duration, 71 | partInterx, 72 | fullInterx, 73 | frameId: 0, 74 | pipId: v.pipTek 75 | } 76 | }) 77 | } 78 | } 79 | 80 | /** 81 | * @param {VideoInfo} video 82 | */ 83 | function calculateScore(video) { 84 | const topFrame = video.frameId === 0 85 | let score = 0 86 | if (video.fullInterx && topFrame) score += 150 87 | if (video.partInterx && topFrame) score += 70 88 | if (video.duration > 60 * 10) score += 100 89 | if (topFrame) score += 20 90 | if (video.duration > 60 * 1) score += 10 91 | 92 | return score 93 | } 94 | 95 | function exitPiP() { 96 | document.exitPictureInPicture() 97 | } 98 | 99 | 100 | function enterPiP(pipId) { 101 | [...document.getElementsByTagName("video")].find(v => v.pipTek === pipId)?.requestPictureInPicture() 102 | } -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polywock/pip-unblocker/5230ad9001eab708210295721523792c89db8e56/icon128.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PiP Unblocker", 3 | "version": "0.5", 4 | "description": "Clear Permissions-Policy headers to unblock features like Picture-in-Picture.", 5 | "manifest_version": 3, 6 | "host_permissions": ["https://*/*", "http://*/*"], 7 | "permissions": ["declarativeNetRequestWithHostAccess", "scripting"], 8 | "icons": { 9 | "128": "icon128.png" 10 | }, 11 | "background": { 12 | "service_worker": "background.js", 13 | "type": "module" 14 | }, 15 | "action": {}, 16 | "declarative_net_request" : { 17 | "rule_resources" : [{ 18 | "id": "rules", 19 | "enabled": true, 20 | "path": "rules.json" 21 | } 22 | ]} 23 | } -------------------------------------------------------------------------------- /rules.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id" : 1, 4 | "priority": 1, 5 | "action" : { 6 | "type" : "modifyHeaders", 7 | "responseHeaders": [ 8 | { 9 | "header": "Permissions-Policy", 10 | "operation": "remove" 11 | }, 12 | { 13 | "header": "Feature-Policy", 14 | "operation": "remove" 15 | } 16 | ] 17 | }, 18 | "condition" : { 19 | "urlFilter" : "*", 20 | "resourceTypes" : ["main_frame", "sub_frame"] 21 | } 22 | } 23 | ] --------------------------------------------------------------------------------