Auto Picture-in-Picture on Tab Change

Auto-enables PiP for playing videos in Chromium browsers on tab switch. Requires an initial click to unlock and exits on return.

// ==UserScript==
// @name         Auto Picture-in-Picture on Tab Change
// @name:ar      التبديل التلقائي إلى وضع الصورة داخل الصورة (PiP)
// @namespace    https://github.com/EzioTheGoat/EzioUserscripts
// @version      1.3.1
// @description  Auto-enables PiP for playing videos in Chromium browsers on tab switch. Requires an initial click to unlock and exits on return.
// @description:ar  يُفعّل وضع الصورة داخل الصورة (PiP) تلقائيًا لمقاطع الفيديو في متصفحات Chromium عند التبديل بين علامات التبويب. يتطلب نقرة أولى لإلغاء القفل ويخرج عند الرجوع.
// @author       Ezio Auditore
// @license      MIT
// @icon         https://img.icons8.com/ios-filled/64/000000/picture-in-picture.png
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  /**
   * Auto PiP for Chromium browsers on tab change.
   *
   * The script waits for an initial user click to "unlock" the auto-PiP feature,
   * satisfying the user gesture requirement enforced by browsers. When the page loses focus,
   * if there is an actively playing video, the script requests Picture-in-Picture mode.
   * On regaining focus, it exits Picture-in-Picture mode.
   *
   * Note: PiP mode will not be activated if the video is paused.
   */

  // Flag to indicate that auto-PiP has been unlocked by a user gesture
  let unlocked = false;

  /**
   * Unlocks auto-PiP after the first user click.
   */
  function unlockPiP() {
    unlocked = true;
    document.removeEventListener("click", unlockPiP);
    console.log("Auto-PiP unlocked by user interaction.");
  }
  document.addEventListener("click", unlockPiP);

  /**
   * Returns the first available video element that is actively playing.
   * @returns {HTMLVideoElement|null} The active video element or null if none found.
   */
  function getActiveVideo() {
    const videos = document.querySelectorAll("video");
    for (const video of videos) {
      if (!video.paused && !video.ended) {
        return video;
      }
    }
    return videos.length ? videos[0] : null;
  }

  /**
   * Enters Picture-in-Picture mode if an active (playing) video is found.
   */
  async function enterPiP() {
    if (!unlocked) {
      console.log(
        "Auto-PiP is locked. Please click anywhere on the page to enable it."
      );
      return;
    }
    try {
      const video = getActiveVideo();
      // Only enter PiP if a video exists and it is playing
      if (
        video &&
        !video.paused &&
        document.pictureInPictureElement !== video
      ) {
        await video.requestPictureInPicture();
        console.log("Entered Picture-in-Picture mode.");
      } else {
        console.log("No active (playing) video available for PiP.");
      }
    } catch (error) {
      console.error("Error entering Picture-in-Picture mode:", error);
    }
  }

  /**
   * Exits Picture-in-Picture mode if active.
   */
  async function exitPiP() {
    try {
      if (document.pictureInPictureElement) {
        await document.exitPictureInPicture();
        console.log("Exited Picture-in-Picture mode.");
      }
    } catch (error) {
      console.error("Error exiting Picture-in-Picture mode:", error);
    }
  }

  /**
   * Helper function to delay the execution of an action.
   * @param {Function} action - The action to perform after the delay.
   * @param {number} delay - The delay in milliseconds.
   */
  function delayedAction(action, delay = 100) {
    setTimeout(action, delay);
  }

  // Event listeners for tab focus and blur events to trigger PiP mode changes.
  window.addEventListener("blur", () => {
    delayedAction(enterPiP);
  });

  window.addEventListener("focus", () => {
    delayedAction(exitPiP);
  });

  // Additional visibility change handling for more robust behavior.
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
      delayedAction(enterPiP);
    } else if (document.visibilityState === "visible") {
      delayedAction(exitPiP);
    }
  });
})();