SuperPiP

Enable native video controls with Picture-in-Picture functionality on any website

当前为 2025-06-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name SuperPiP
  3. // @namespace https://github.com/tonioriol/superpip
  4. // @version 0.0.1
  5. // @description Enable native video controls with Picture-in-Picture functionality on any website
  6. // @author SuperPiP
  7. // @match https://*/*
  8. // @match http://*/*
  9. // @grant none
  10. // @run-at document-start
  11. // @license AGPL-3.0-or-later
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. "use strict";
  16.  
  17. // Enable native controls for a specific video
  18. function enableVideoControls(video) {
  19. video.setAttribute("controls", "");
  20. // set z-index to ensure it appears above other elements if position not relative
  21. // video.style.position = "absolute";
  22. // video.style.zIndex = "9999999999";
  23. }
  24.  
  25. // Simple PoC: Detect elements positioned on top of video
  26. function detectVideoOverlays(video) {
  27. const videoRect = video.getBoundingClientRect();
  28. const videoStyle = window.getComputedStyle(video);
  29. const videoZIndex = parseInt(videoStyle.zIndex) || 0;
  30.  
  31. const overlays = [];
  32. const allElements = document.querySelectorAll("*");
  33.  
  34. allElements.forEach((element) => {
  35. // Skip the video itself and its containers
  36. if (element === video || element.contains(video)) return;
  37.  
  38. const style = window.getComputedStyle(element);
  39. const rect = element.getBoundingClientRect();
  40. const zIndex = parseInt(style.zIndex) || 0;
  41.  
  42. // element must be within video bounds AND positioned
  43. const isPositioned = !["relative"].includes(style.position);
  44. const isOnTop = isPositioned && zIndex >= videoZIndex;
  45. const isWithinBounds =
  46. rect.left >= videoRect.left &&
  47. rect.right <= videoRect.right &&
  48. rect.top >= videoRect.top &&
  49. rect.bottom <= videoRect.bottom;
  50. const isVisible =
  51. style.display !== "none" &&
  52. style.visibility !== "hidden" &&
  53. style.opacity !== "0";
  54.  
  55. if (isOnTop && isWithinBounds && isVisible) {
  56. overlays.push({
  57. element: element,
  58. tagName: element.tagName,
  59. classes: Array.from(element.classList),
  60. zIndex: zIndex,
  61. });
  62.  
  63. element.style.display = "none";
  64. }
  65. });
  66.  
  67. return overlays;
  68. }
  69.  
  70. // Process all videos on the page
  71. function processVideos() {
  72. document.querySelectorAll("video").forEach((video) => {
  73. enableVideoControls(video);
  74. detectVideoOverlays(video);
  75. });
  76. }
  77.  
  78. // Initialize when DOM is ready
  79. function init() {
  80. processVideos();
  81.  
  82. // Watch for new video elements being added
  83. const observer = new MutationObserver((mutations) => {
  84. let shouldProcess = false;
  85.  
  86. mutations.forEach((mutation) => {
  87. // Check if any new nodes include video elements
  88. mutation.addedNodes.forEach((node) => {
  89. if (node.nodeType === 1) {
  90. // Element node
  91. if (node.tagName === "VIDEO" || node.querySelector("video")) {
  92. shouldProcess = true;
  93. }
  94. }
  95. });
  96.  
  97. // Check for attribute changes on video elements
  98. if (
  99. mutation.type === "attributes" &&
  100. mutation.target.tagName === "VIDEO"
  101. ) {
  102. // Re-enable controls if they were removed
  103. if (
  104. mutation.attributeName === "controls" &&
  105. !mutation.target.hasAttribute("controls")
  106. ) {
  107. shouldProcess = true;
  108. }
  109. }
  110. });
  111.  
  112. if (shouldProcess) {
  113. processVideos();
  114. }
  115. });
  116.  
  117. observer.observe(document.body, {
  118. childList: true,
  119. subtree: true,
  120. attributes: true,
  121. });
  122.  
  123. // Also process videos when they start loading or playing
  124. document.addEventListener(
  125. "loadstart",
  126. (e) => {
  127. if (e.target.tagName === "VIDEO") {
  128. enableVideoControls(e.target);
  129. }
  130. },
  131. true
  132. );
  133.  
  134. document.addEventListener(
  135. "loadedmetadata",
  136. (e) => {
  137. if (e.target.tagName === "VIDEO") {
  138. enableVideoControls(e.target);
  139. }
  140. },
  141. true
  142. );
  143. }
  144.  
  145. // iOS Safari specific handling (THIS IS WHAT ENABLES PIP ON YOUTUBE SPECIALLY)
  146. document.addEventListener(
  147. "touchstart",
  148. function initOnTouch() {
  149. let v = document.querySelector("video");
  150. if (v) {
  151. v.addEventListener(
  152. "webkitpresentationmodechanged",
  153. (e) => e.stopPropagation(),
  154. true
  155. );
  156. // Remove the touchstart listener after we've initialized
  157. document.removeEventListener("touchstart", initOnTouch);
  158. }
  159. },
  160. true
  161. );
  162.  
  163. // Initialize when page loads
  164. if (document.readyState === "loading") {
  165. document.addEventListener("DOMContentLoaded", init);
  166. } else {
  167. init();
  168. }
  169.  
  170. // Also initialize after delays to catch dynamically loaded videos
  171. setTimeout(init, 1000);
  172. setTimeout(processVideos, 3000);
  173. })();