// ==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();
}
})();