Crunchyroll Picture-in-Picture (page + iframe)

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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(() => {});
      }
    }
  });
})();