Crunchyroll Picture-in-Picture (page + iframe)

Enables PiP on Crunchyroll player by fixing iframe and video element restrictions.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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(() => {});
      }
    }
  });
})();