Enables PiP on Crunchyroll player by fixing iframe and video element restrictions.
// ==UserScript==
// @name Crunchyroll Picture-in-Picture (page + iframe)
// @version 1.3
// @description Enables PiP on Crunchyroll player by fixing iframe and video element restrictions.
// @author cyberaguiar
// @match https://www.crunchyroll.com/*
// @match https://static.crunchyroll.com/*vilos*/web/vilos/player.html*
// @run-at document-start
// @icon https://www.google.com/s2/favicons?sz=64&domain=crunchyroll.com
// @grant none
// @license MIT
// @namespace https://greasyfork.org/users/1519555
// ==/UserScript==
(function () {
'use strict';
const IS_IFRAME_CONTEXT = location.hostname.endsWith('static.crunchyroll.com');
// --------- MAIN PAGE CONTEXT (crunchyroll.com) ---------
if (!IS_IFRAME_CONTEXT) {
// Ensure iframe has PiP permission in its "allow" attribute
const ensurePiPAllowed = (frame) => {
if (!(frame instanceof HTMLIFrameElement)) return;
const cur = frame.getAttribute('allow') || '';
if (!/\bpicture-in-picture\b/.test(cur)) {
const updated = (cur.trim() ? cur.trim() + '; ' : '') + 'picture-in-picture *';
frame.setAttribute('allow', updated);
}
};
// Scan for Crunchyroll video player iframes
const scanFrames = () => {
document
.querySelectorAll(
'iframe.video-player, iframe[src*="/vilos/"], iframe[src*="/vilos-v2/"]'
)
.forEach(ensurePiPAllowed);
};
// Watch DOM for new/updated iframes
new MutationObserver(scanFrames).observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src', 'class', 'allow'],
});
scanFrames();
return; // The rest runs only inside the iframe
}
// --------- IFRAME CONTEXT (static.crunchyroll.com) ---------
// Remove disablepictureinpicture and keep it removed
const enablePiPOn = (video) => {
if (!video) return;
video.removeAttribute('disablepictureinpicture');
// Keep watching in case the site re-applies the attribute
new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'attributes' && m.attributeName === 'disablepictureinpicture') {
video.removeAttribute('disablepictureinpicture');
}
}
}).observe(video, { attributes: true, attributeFilter: ['disablepictureinpicture'] });
// Some sites set attributes late, so check again on metadata load
video.addEventListener('loadedmetadata', () => {
video.removeAttribute('disablepictureinpicture');
});
};
// Scan all possible video elements (including shadow DOM)
const scanVideos = () => {
document.querySelectorAll('video, video#player0').forEach(enablePiPOn);
document.querySelectorAll('*').forEach((el) => {
if (el.shadowRoot) el.shadowRoot.querySelectorAll('video').forEach(enablePiPOn);
});
};
// Watch DOM for video element replacements
new MutationObserver(scanVideos).observe(document, { childList: true, subtree: true });
scanVideos();
// Optional: keyboard shortcut Ctrl+Alt+P to trigger PiP
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && e.key.toLowerCase() === 'p') {
const direct = document.querySelector('video');
const shadow = (() => {
for (const el of document.querySelectorAll('*')) {
if (el.shadowRoot) {
const v = el.shadowRoot.querySelector('video');
if (v) return v;
}
}
})();
const v = direct || shadow;
if (v && document.pictureInPictureEnabled && !document.pictureInPictureElement) {
v.requestPictureInPicture?.().catch(() => {});
}
}
});
})();