您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enable native video controls with Picture-in-Picture functionality on any website
- // ==UserScript==
- // @name SuperPiP
- // @namespace https://github.com/tonioriol
- // @version 0.1.0
- // @description Enable native video controls with Picture-in-Picture functionality on any website
- // @author SuperPiP
- // @match https://*/*
- // @match http://*/*
- // @grant none
- // @run-at document-start
- // @license AGPL-3.0-or-later
- // ==/UserScript==
- (function () {
- "use strict";
- console.log("[SuperPiP] Script started at", new Date().toISOString());
- console.log("[SuperPiP] Document readyState:", document.readyState);
- console.log("[SuperPiP] User agent:", navigator.userAgent);
- // Check if video is in viewport
- function isVideoInViewport(video) {
- const rect = video.getBoundingClientRect();
- const viewHeight = window.innerHeight || document.documentElement.clientHeight;
- const viewWidth = window.innerWidth || document.documentElement.clientWidth;
- return (
- rect.top >= 0 &&
- rect.left >= 0 &&
- rect.bottom <= viewHeight &&
- rect.right <= viewWidth &&
- rect.width > 0 &&
- rect.height > 0
- );
- }
- // Enhanced video setup for better UX
- function enableVideoControls(video) {
- // Always set controls, but only log if it's actually changing
- if (!video.hasAttribute("controls")) {
- console.log("[SuperPiP] Enabling controls for video:", video);
- }
- try {
- video.setAttribute("controls", "");
- // Set up enhanced functionality only once per video
- if (!video.hasAttribute('data-superpip-setup')) {
- video.setAttribute('data-superpip-setup', 'true');
- // Auto-unmute when playing (counter Instagram's nasty muting)
- let videoShouldBeUnmuted = false;
- video.addEventListener('play', () => {
- videoShouldBeUnmuted = true;
- if (video.muted) {
- video.muted = false;
- console.log("[SuperPiP] Auto-unmuted video on play");
- }
- });
- video.addEventListener('pause', () => {
- videoShouldBeUnmuted = false;
- });
- // Override the muted property to prevent programmatic muting during playback
- const originalMutedDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'muted');
- if (originalMutedDescriptor) {
- Object.defineProperty(video, 'muted', {
- get: function() {
- return originalMutedDescriptor.get.call(this);
- },
- set: function(value) {
- // If video is playing and something tries to mute it, prevent it
- if (value === true && videoShouldBeUnmuted && !this.paused) {
- console.log("[SuperPiP] Blocked attempt to mute playing video");
- return;
- }
- return originalMutedDescriptor.set.call(this, value);
- }
- });
- }
- // Smart autoplay: only autoplay if video is in viewport
- if (isVideoInViewport(video) && video.paused && video.readyState >= 2) {
- console.log("[SuperPiP] Autoplaying video in viewport");
- video.play().catch(() => {}); // Ignore autoplay policy errors
- }
- console.log("[SuperPiP] Enhanced video setup complete");
- }
- if (video.hasAttribute("controls")) {
- console.log("[SuperPiP] Controls enabled successfully for video");
- }
- } catch (error) {
- console.error("[SuperPiP] Error enabling controls:", error);
- }
- // set z-index to ensure it appears above other elements if position not relative
- // video.style.position = "absolute";
- // video.style.zIndex = "9999999999";
- }
- // Simple PoC: Detect elements positioned on top of video
- function detectVideoOverlays(video) {
- try {
- const videoRect = video.getBoundingClientRect();
- // Skip processing if video has no dimensions (not rendered yet) but don't log
- if (videoRect.width === 0 || videoRect.height === 0) {
- return [];
- }
- console.log("[SuperPiP] Detecting overlays for video:", video);
- const videoStyle = window.getComputedStyle(video);
- const videoZIndex = parseInt(videoStyle.zIndex) || 0;
- console.log("[SuperPiP] Video rect:", videoRect);
- console.log("[SuperPiP] Video zIndex:", videoZIndex);
- const overlays = [];
- const allElements = document.querySelectorAll("*");
- console.log("[SuperPiP] Checking", allElements.length, "elements for overlays");
- allElements.forEach((element) => {
- // Skip the video itself and its containers
- if (element === video || element.contains(video)) return;
- const style = window.getComputedStyle(element);
- const rect = element.getBoundingClientRect();
- const zIndex = parseInt(style.zIndex) || 0;
- // element must be within video bounds AND positioned
- const isPositioned = ["absolute"].includes(style.position);
- const isOnTop = isPositioned && zIndex >= videoZIndex;
- const isWithinBounds =
- rect.left >= videoRect.left &&
- rect.right <= videoRect.right &&
- rect.top >= videoRect.top &&
- rect.bottom <= videoRect.bottom;
- const isVisible =
- style.display !== "none" &&
- style.visibility !== "hidden" &&
- style.opacity !== "0";
- if (isOnTop && isWithinBounds && isVisible) {
- overlays.push({
- element: element,
- tagName: element.tagName,
- classes: Array.from(element.classList),
- zIndex: zIndex,
- });
- console.log("[SuperPiP] Hiding overlay element:", element.tagName, element.className);
- element.style.display = "none";
- }
- });
- console.log("[SuperPiP] Found", overlays.length, "overlays");
- return overlays;
- } catch (error) {
- console.error("[SuperPiP] Error detecting overlays:", error);
- return [];
- }
- }
- // Process all videos on the page
- function processVideos() {
- console.log("[SuperPiP] Processing videos...");
- const videos = document.querySelectorAll("video");
- console.log("[SuperPiP] Found", videos.length, "video elements");
- videos.forEach((video, index) => {
- console.log("[SuperPiP] Processing video", index + 1, "of", videos.length);
- enableVideoControls(video);
- detectVideoOverlays(video);
- });
- }
- // Initialize and set up observers
- function init() {
- console.log("[SuperPiP] Initializing...");
- try {
- // Process any existing videos
- processVideos();
- // Set up mutation observer to watch for video elements and their changes
- console.log("[SuperPiP] Setting up mutation observer...");
- const observer = new MutationObserver((mutations) => {
- // Pre-filter: only process mutations that might involve videos
- let newVideoCount = 0;
- mutations.forEach((mutation) => {
- // Handle new nodes being added
- if (mutation.type === "childList") {
- mutation.addedNodes.forEach((node) => {
- if (node.nodeType === 1) { // Element node
- if (node.tagName === "VIDEO") {
- // Direct video element added
- enableVideoControls(node);
- detectVideoOverlays(node);
- newVideoCount++;
- } else if (node.querySelector) {
- // Check if added node contains video elements
- const videos = node.querySelectorAll("video");
- if (videos.length > 0) {
- videos.forEach((video) => {
- enableVideoControls(video);
- detectVideoOverlays(video);
- });
- newVideoCount += videos.length;
- }
- }
- }
- });
- }
- // Handle attribute changes on video elements
- if (mutation.type === "attributes" && mutation.target.tagName === "VIDEO") {
- const video = mutation.target;
- // Re-enable controls if they were removed
- if (mutation.attributeName === "controls" && !video.hasAttribute("controls")) {
- console.log("[SuperPiP] Re-enabling removed controls");
- enableVideoControls(video);
- }
- // Re-process overlays for any video attribute change that might affect layout
- if (["src", "style", "class", "width", "height"].includes(mutation.attributeName)) {
- detectVideoOverlays(video);
- }
- }
- });
- // Only log when we actually processed videos
- if (newVideoCount > 0) {
- console.log("[SuperPiP] Processed", newVideoCount, "new videos");
- }
- });
- // Start observing - use document.documentElement if body doesn't exist yet
- const target = document.body || document.documentElement;
- console.log("[SuperPiP] Observing target:", target.tagName);
- observer.observe(target, {
- childList: true,
- subtree: true,
- attributes: true
- // No attributeFilter - listen to all attributes but filter by video tagName in callback
- });
- console.log("[SuperPiP] Mutation observer set up successfully");
- // Handle video events for when videos start loading or playing
- console.log("[SuperPiP] Setting up video event listeners...");
- document.addEventListener("loadstart", (e) => {
- if (e.target.tagName === "VIDEO") {
- console.log("[SuperPiP] Video loadstart event:", e.target);
- enableVideoControls(e.target);
- detectVideoOverlays(e.target);
- }
- }, true);
- document.addEventListener("loadedmetadata", (e) => {
- if (e.target.tagName === "VIDEO") {
- console.log("[SuperPiP] Video loadedmetadata event:", e.target);
- enableVideoControls(e.target);
- detectVideoOverlays(e.target);
- }
- }, true);
- console.log("[SuperPiP] Event listeners set up successfully");
- } catch (error) {
- console.error("[SuperPiP] Error during initialization:", error);
- }
- }
- // iOS Safari specific handling (THIS IS WHAT ENABLES PIP ON YOUTUBE SPECIALLY)
- console.log("[SuperPiP] Setting up iOS Safari specific handling...");
- document.addEventListener(
- "touchstart",
- function initOnTouch() {
- console.log("[SuperPiP] Touch event detected, setting up iOS PiP handling");
- let v = document.querySelector("video");
- if (v) {
- console.log("[SuperPiP] Found video for iOS PiP setup:", v);
- v.addEventListener(
- "webkitpresentationmodechanged",
- (e) => {
- console.log("[SuperPiP] webkitpresentationmodechanged event:", e);
- e.stopPropagation();
- },
- true
- );
- // Remove the touchstart listener after we've initialized
- document.removeEventListener("touchstart", initOnTouch);
- console.log("[SuperPiP] iOS PiP handling set up successfully");
- } else {
- console.log("[SuperPiP] No video found for iOS PiP setup");
- }
- },
- true
- );
- // Start immediately since we're running at document-start
- console.log("[SuperPiP] Starting initialization...");
- init();
- console.log("[SuperPiP] Script initialization complete");
- })();