// ==UserScript== // @name Plyr播放器添加网页全屏按钮(通用版) // @namespace http://tampermonkey.net/ // @version 2026.7.24 // @description 为所有Plyr播放器添加网页全屏按钮,支持多播放器、兼容浏览器全屏 // @match *://*/* // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // ============================================================ // 1. 注入全局样式(使用 !important 确保覆盖网站原有样式) // ============================================================ GM_addStyle(` /* 网页全屏激活时的播放器容器:固定定位,占满视口 */ .web-fullscreen-active.plyr { position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; z-index: 9999 !important; background: #000 !important; margin: 0 !important; border-radius: 0 !important; padding: 0 !important; border: none !important; display: block !important; overflow: hidden !important; } /* 视频容器撑满播放器 */ .web-fullscreen-active.plyr .plyr__video-wrapper { height: 100% !important; padding-bottom: 0 !important; width: 100% !important; } /* 视频保持比例,并包含在容器内(类似 object-fit: contain) */ .web-fullscreen-active.plyr video { object-fit: contain !important; } /* 强制显示控制栏(某些网站可能在特定状态下隐藏控制栏) */ .web-fullscreen-active.plyr .plyr__controls { display: flex !important; opacity: 1 !important; visibility: visible !important; z-index: 99999 !important; pointer-events: auto !important; } /* 防止页面滚动(双重保险,覆盖 html 和 body) */ .web-fullscreen-active html, .web-fullscreen-active body { overflow: hidden !important; } /* 自定义按钮的图标尺寸适配 */ .plyr__control[data-plyr="web-fullscreen"] svg { width: 18px !important; height: 18px !important; } `); // ============================================================ // 2. 图标 SVG(使用类似 Plyr 风格的 1024x1024 视图) // ============================================================ // 进入网页全屏(四个角向外展开) const enterIcon = ` `; // 退出网页全屏(四个角向内收缩) const exitIcon = ` `; // ============================================================ // 3. 全局事件管理(避免每个播放器重复绑定) // ============================================================ let globalEventsAttached = false; // 标记全局事件是否已绑定 // 退出所有网页全屏(同一时间只有一个,但安全起见遍历) function exitAllWebFullscreen() { const activePlayers = document.querySelectorAll('.web-fullscreen-active.plyr'); activePlayers.forEach(plyrEl => { plyrEl.dataset.webFullscreenActive = 'false'; // 新增 plyrEl.classList.remove('web-fullscreen-active'); const btn = plyrEl.querySelector('[data-plyr="web-fullscreen"]'); if (btn) { btn.innerHTML = enterIcon; // 恢复为“进入”图标 btn.setAttribute('aria-label', '网页全屏'); } }); // 恢复页面滚动 document.documentElement.style.removeProperty('overflow'); document.body.style.removeProperty('overflow'); window.dispatchEvent(new Event('resize')); } function attachGlobalEvents() { if (globalEventsAttached) return; globalEventsAttached = true; // ESC 键退出 document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { const active = document.querySelector('.web-fullscreen-active.plyr'); if (active) { exitAllWebFullscreen(); } } }); // 浏览器全屏变化:若进入浏览器全屏,则退出网页全屏 document.addEventListener('fullscreenchange', () => { if (document.fullscreenElement) { const active = document.querySelector('.web-fullscreen-active.plyr'); if (active) { exitAllWebFullscreen(); } } }); } // ============================================================ // 4. 核心逻辑 // ============================================================ function init() { // 查找页面上所有 Plyr 播放器(类名为 .plyr 的元素) const players = document.querySelectorAll('.plyr'); if (players.length === 0) { // 若尚未加载,延迟重试(适应动态加载的播放器) setTimeout(init, 500); return; } // ---------- 新增:只绑定一次全局事件 ---------- attachGlobalEvents(); // 遍历每个播放器,独立添加按钮和状态管理 players.forEach(plyrEl => { plyrEl.dataset.webFullscreenActive = 'false'; // 为每个播放器新增状态 const controls = plyrEl.querySelector('.plyr__controls'); if (!controls) return; // 安全性检查 // 防止重复添加(脚本可能因重试多次运行) if (controls.querySelector('[data-plyr="web-fullscreen"]')) return; // 创建自定义按钮 const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'plyr__controls__item plyr__control'; btn.setAttribute('data-plyr', 'web-fullscreen'); btn.setAttribute('aria-label', '网页全屏'); btn.innerHTML = enterIcon; // 初始显示“进入”图标 // 将按钮插入到原生“浏览器全屏”按钮之前(若存在),否则追加到末尾 const fsBtn = controls.querySelector('[data-plyr="fullscreen"]'); if (fsBtn) { controls.insertBefore(btn, fsBtn); } else { controls.appendChild(btn); } // 当前播放器的状态 let processing = false; // 异步操作锁(仅用于点击事件中退出浏览器全屏的过程) // ---------- 状态切换函数(纯 UI 操作,无锁) ---------- // 该函数只负责更新 DOM 和样式,不处理异步逻辑,因此可被任意事件安全调用 function setState(state) { // 读取当前状态 const current = plyrEl.dataset.webFullscreenActive === 'true'; if (state === current) return; // 更新数据属性 plyrEl.dataset.webFullscreenActive = state ? 'true' : 'false'; plyrEl.classList.toggle('web-fullscreen-active', state); btn.innerHTML = state ? exitIcon : enterIcon; btn.setAttribute('aria-label', state ? '退出网页全屏' : '网页全屏'); // 控制页面滚动 if (state) { // 使用 setProperty 加 !important 提高优先级 document.documentElement.style.setProperty('overflow', 'hidden', 'important'); document.body.style.setProperty('overflow', 'hidden', 'important'); } else { // 退出时检查是否还有其他全屏(实际上不会有,但保留安全) if (!document.querySelector('.web-fullscreen-active.plyr')) { // 移除之前设置的 overflow,恢复默认 document.documentElement.style.removeProperty('overflow'); document.body.style.removeProperty('overflow'); } } // 触发 resize 事件,让播放器重新计算自身尺寸和布局 window.dispatchEvent(new Event('resize')); } // ---------- 按钮点击事件 ---------- btn.addEventListener('click', (e) => { e.stopPropagation(); // 阻止事件冒泡,避免干扰其他控件 if (processing) return; // 若有异步操作进行中,忽略本次点击 // 情况1:当前未激活,但浏览器正处于全屏状态 // 必须先将浏览器全屏退出,再进入网页全屏(避免两者冲突) const isActive = plyrEl.dataset.webFullscreenActive === 'true'; // 从 data 读取 if (!isActive && document.fullscreenElement) { processing = true; // 上锁 document.exitFullscreen() .then(() => { setState(true); // 退出成功后进入网页全屏 }) .catch(() => { // 即使退出失败(例如某些浏览器限制),也强制进入网页全屏 // 因为用户明确点击了“网页全屏”按钮 setState(true); }) .finally(() => { processing = false; // 无论成败释放锁 }); } else { // 情况2:直接切换(普通切换 或 从网页全屏切回普通) setState(!isActive); } }); // ---------- 通过 Plyr 实例监听全屏事件 ---------- // 如果播放器对象已挂载到元素上,则绑定其全屏事件(和上面监听互补) const player = plyrEl.plyr; if (player) { player.on('enterfullscreen', () => { // 当 Plyr 进入浏览器全屏时,若正处在网页全屏则退出网页全屏 const isActive = plyrEl.dataset.webFullscreenActive === 'true'; if (isActive && !processing) { setState(false); } }); } }); } // ============================================================ // 5. 启动脚本(等待 DOM 就绪) // ============================================================ if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();