您需要先安装一个扩展,例如 篡改猴、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"); })();