// ==UserScript== // @name 强制画中画播放器 // @namespace http://tampermonkey.net/ // @version 1.2.0 // @description 检测网页视频并在右上角添加强制画中画按钮,绕过网站禁用限制 // @author You // @match *://*/* // @grant none // @run-at document-end // ==/UserScript== (function() { 'use strict'; // 配置项 const CONFIG = { buttonSize: 36, // 按钮尺寸 buttonMargin: 8, // 按钮距边缘距离 buttonOpacity: 0.8, // 默认透明度 buttonHoverOpacity: 1, // 悬停透明度 zIndex: 999999, // 层级优先级 detectInterval: 2000, // 定期检测新视频间隔(ms) showOnHover: false // 是否仅鼠标悬停时显示按钮 }; // 已处理的视频集合 const processedVideos = new WeakSet(); // 强制画中画核心方法 function forcePictureInPicture(video) { // 移除所有禁用画中画的属性 video.removeAttribute('disablepictureinpicture'); video.removeAttribute('disablePictureInPicture'); video.disablePictureInPicture = false; // 移除可能拦截画中画的事件监听 video.onenterpictureinpicture = null; video.onleavepictureinpicture = null; // 清除可能的阻止默认行为 const preventDefault = Event.prototype.preventDefault; Event.prototype.preventDefault = function() {}; // 尝试进入画中画 const requestPiP = async () => { try { // 标准 API if (document.pictureInPictureEnabled && video.requestPictureInPicture) { await video.requestPictureInPicture(); return true; } // 旧版 Safari 兼容 if (video.webkitSetPresentationMode) { video.webkitSetPresentationMode('picture-in-picture'); return true; } return false; } catch (e) { console.warn('画中画启动失败,尝试备用方案:', e); return false; } }; const result = requestPiP(); // 恢复原生 preventDefault setTimeout(() => { Event.prototype.preventDefault = preventDefault; }, 100); return result; } // 退出画中画 async function exitPictureInPicture() { try { if (document.exitPictureInPicture) { await document.exitPictureInPicture(); } else if (document.webkitExitPictureInPicture) { await document.webkitExitPictureInPicture(); } } catch (e) { console.warn('退出画中画失败:', e); } } // 创建画中画按钮 function createPiPButton(video) { if (processedVideos.has(video)) return; // 找到视频的父容器 let container = video.parentElement; if (!container) return; // 确保父容器是相对定位 const containerStyle = window.getComputedStyle(container); if (containerStyle.position === 'static') { container.style.position = 'relative'; } // 创建按钮 const button = document.createElement('div'); button.className = 'pip-force-button'; button.title = '强制画中画播放'; button.innerHTML = ` `; // 按钮样式 Object.assign(button.style, { position: 'absolute', top: CONFIG.buttonMargin + 'px', right: CONFIG.buttonMargin + 'px', width: CONFIG.buttonSize + 'px', height: CONFIG.buttonSize + 'px', backgroundColor: 'rgba(0, 0, 0, 0.7)', color: '#ffffff', borderRadius: '6px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', zIndex: CONFIG.zIndex, opacity: CONFIG.showOnHover ? '0' : CONFIG.buttonOpacity, transition: 'opacity 0.2s ease, background-color 0.2s ease', pointerEvents: 'auto', userSelect: 'none' }); // SVG 图标样式 const svg = button.querySelector('svg'); svg.style.width = '20px'; svg.style.height = '20px'; // 悬停效果 button.addEventListener('mouseenter', () => { button.style.opacity = CONFIG.buttonHoverOpacity; button.style.backgroundColor = 'rgba(0, 0, 0, 0.9)'; }); button.addEventListener('mouseleave', () => { button.style.opacity = CONFIG.showOnHover ? '0' : CONFIG.buttonOpacity; button.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; }); // 容器悬停显示按钮 if (CONFIG.showOnHover) { container.addEventListener('mouseenter', () => { button.style.opacity = CONFIG.buttonOpacity; }); container.addEventListener('mouseleave', () => { button.style.opacity = '0'; }); } // 点击事件 button.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); // 如果已经在画中画状态,则退出 if (document.pictureInPictureElement === video) { exitPictureInPicture(); } else { forcePictureInPicture(video); } }); // 视频进入/退出画中画时更新按钮状态 video.addEventListener('enterpictureinpicture', () => { button.title = '退出画中画'; button.style.backgroundColor = 'rgba(66, 133, 244, 0.9)'; }); video.addEventListener('leavepictureinpicture', () => { button.title = '强制画中画播放'; button.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; }); // 将按钮插入到视频前面 container.insertBefore(button, video); processedVideos.add(video); } // 检测页面中所有视频 function detectVideos() { const videos = document.querySelectorAll('video:not([data-pip-force-processed])'); videos.forEach(video => { // 跳过太小的视频(可能是广告或装饰) if (video.offsetWidth < 100 || video.offsetHeight < 100) return; video.setAttribute('data-pip-force-processed', 'true'); createPiPButton(video); // 监听视频属性变化,防止网站重新禁用 const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.attributeName === 'disablepictureinpicture' || mutation.attributeName === 'disablePictureInPicture') { video.removeAttribute('disablepictureinpicture'); video.removeAttribute('disablePictureInPicture'); video.disablePictureInPicture = false; } }); }); observer.observe(video, { attributes: true }); }); } // 监听 DOM 变化,处理动态加载的视频 function observeDOM() { const observer = new MutationObserver((mutations) => { let shouldDetect = false; mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { shouldDetect = true; } }); if (shouldDetect) { // 防抖处理 clearTimeout(window.pipDetectTimeout); window.pipDetectTimeout = setTimeout(detectVideos, 500); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化 function init() { // 首次检测 detectVideos(); // 监听 DOM 变化 observeDOM(); // 定期检测(兜底方案) setInterval(detectVideos, CONFIG.detectInterval); // 页面完全加载后再检测一次 window.addEventListener('load', () => { setTimeout(detectVideos, 1000); }); console.log('强制画中画脚本已加载'); } // 启动 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();