// ==UserScript== // @name 视频网页全屏(改) // @name:en Maximize Video(Modify) // @name:zh-CN 视频网页全屏(改) // @name:zh-TW 視頻網頁全屏(改) // @name:ja ビデオページ全画面(変更) // @name:ko 비디오 웹페이지 전체화면(수정) // @namespace https://greasyfork.org/zh-CN/users/178351-yesilin // @description 让所有视频网页全屏,开启画中画功能,支持自定义按钮位置。 // @description:en Maximize all video players.Support Piture-in-picture and custom button position. // @description:zh-CN 让所有视频网页全屏,开启画中画功能,支持自定义按钮位置。 // @description:zh-TW 讓所有視頻網頁全屏,開啟子母畫面,支援自定義按鈕位置。 // @description:ja すべての動画ページを全画面表示し、ピクチャ・イン・ピクチャ機能を有効にします。ボタン位置のカスタマイズにも対応しています。 // @description:ko 모든 비디오 웹페이지를 전체화면으로 전환하고, PIP(화면 속 화면) 기능과 사용자 지정 버튼 위치를 지원합니다. // @author 冻猫, RyomaHan, YeSilin // @include * // @exclude *www.w3school.com.cn* // @version 12.5.81 // @run-at document-end // @license MIT // @grant GM_setValue // @grant GM_getValue // @grant GM.setValue // @grant GM.getValue // @icon data:image/svg+xml;base64,PHN2ZyBpZD0i5Zu+5bGCXzEiIGRhdGEtbmFtZT0i5Zu+5bGCIDEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDIwMCAxNTIuNiI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiNmZmY7fS5jbHMtMntmaWxsOiMyMzE4MTU7fS5jbHMtM3tmaWxsOiMwNDAwMDA7fS5jbHMtNHtmaWxsOiNmNWM5MDA7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTE3MCwxNzEuNDZIMjguMzhhOC42Miw4LjYyLDAsMCwxLTguNi04LjU5Vjc1LjA3QTIxLjU1LDIxLjU1LDAsMCwxLDQxLjI3LDUzLjU4SDE1Ny4xMmEyMS41NSwyMS41NSwwLDAsMSwyMS40OSwyMS40OXY4Ny44YTguNjIsOC42MiwwLDAsMS04LjYsOC41OVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTIzLjcpIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTczLjEzLDE3Ni4zSDI3YTEyLjI0LDEyLjI0LDAsMCwxLTEyLjI1LTEyLjI1Vjc2LjM2QTI2Ljg3LDI2Ljg3LDAsMCwxLDQxLjU5LDQ5LjQ5SDE1OC40MWEyNi44NywyNi44NywwLDAsMSwyNi44NiwyNi44N3Y4Ny42OWExMi4wOCwxMi4wOCwwLDAsMS0xMi4xNCwxMi4yNVpNNDEuNTksNTYuMjZBMjAuMTQsMjAuMTQsMCwwLDAsMjEuNSw3Ni4zNnY4Ny42OUE1LjUsNS41LDAsMCwwLDI3LDE2OS41M0gxNzMuMTNhNS41LDUuNSwwLDAsMCw1LjQ4LTUuNDhWNzYuMzZhMjAuMTQsMjAuMTQsMCwwLDAtMjAuMS0yMC4xWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMjMuNykiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik02OC44OSwxMjEuMjhIMy4xMkEzLjEyLDMuMTIsMCwwLDEsMCwxMTguMTYsMywzLDAsMCwxLDMuMTIsMTE1SDY4Ljg5YTMuMTIsMy4xMiwwLDEsMSwwLDYuMjRabTEyOCwwaC02Ni4yYTMuMTIsMy4xMiwwLDEsMSwwLTYuMjRoNjYuMmEzLjEyLDMuMTIsMCwwLDEsMy4xMSwzLjEyQTMsMywwLDAsMSwxOTYuODgsMTIxLjI4Wm0tMTI4LDE1LjM2aC0zNmEzLjEyLDMuMTIsMCwxLDEsMC02LjIzaDM2YTMuMTIsMy4xMiwwLDEsMSwwLDYuMjNabTk4LDBIMTMwLjY4YTMuMTIsMy4xMiwwLDEsMSwwLTYuMjNIMTY2LjlhMy4xMiwzLjEyLDAsMCwxLDMuMTEsMy4xMkEzLDMsMCwwLDEsMTY2LjksMTM2LjY0Wk05MS4xMywxMjQuMDdhMTAsMTAsMCwwLDEtOC43LTUsMS4zMywxLjMzLDAsMSwxLDIuMjYtMS40LDcuNDgsNy40OCwwLDAsMCwxNC0zLjc2LDEuMjksMS4yOSwwLDEsMSwyLjU4LDAsMTAuMTgsMTAuMTgsMCwwLDEtMTAuMTEsMTAuMjFaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0yMy43KSIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEwOC42NSwxMjQuMDdBMTAuMDgsMTAuMDgsMCwwLDEsOTguNTUsMTE0YTEuMjksMS4yOSwwLDEsMSwyLjU4LDAsNy41NCw3LjU0LDAsMCwwLDcuNTIsNy41Miw3LjI3LDcuMjcsMCwwLDAsNi40NS0zLjc2LDEuMzMsMS4zMywwLDEsMSwyLjI2LDEuNCwxMC4xNSwxMC4xNSwwLDAsMS04LjcxLDQuOTRaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0yMy43KSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTYyLjMzLDk4LjkzYTguNzEsOC43MSwwLDAsMCwxNy40MSwwaDBhOC43MSw4LjcxLDAsMCwwLTE3LjQxLDBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0yMy43KSIvPjxwYXRoIGNsYXNzPSJjbHMtMyIgZD0iTTEyMC4zNiw5OC45M2E4LjcxLDguNzEsMCwxLDAsOC43MS04LjcxQTguNzEsOC43MSwwLDAsMCwxMjAuMzYsOTguOTNaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0yMy43KSIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEwMC4wNSw3Mi45MmEzLjYyLDMuNjIsMCwwLDEtMy42NS0zLjY1VjYxLjg1YTMuNjYsMy42NiwwLDAsMSw3LjMxLDB2Ny40MkEzLjYyLDMuNjIsMCwwLDEsMTAwLjA1LDcyLjkyWm0tMTQsMGEzLjYyLDMuNjIsMCwwLDEtMy42NS0zLjY1VjYxLjg1YTMuNjYsMy42NiwwLDAsMSw3LjMxLDB2Ny40MkEzLjYyLDMuNjIsMCwwLDEsODYuMDgsNzIuOTJabTI3Ljk0LDBhMy42MiwzLjYyLDAsMCwxLTMuNjUtMy42NVY2MS44NWEzLjY2LDMuNjYsMCwwLDEsNy4zMSwwdjcuNDJBMy42MiwzLjYyLDAsMCwxLDExNCw3Mi45MloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTIzLjcpIi8+PHBhdGggY2xhc3M9ImNscy00IiBkPSJNMzkuMjMsNTIuODJzNC4wOC0yNS44OSwxOC0yNS44OVM3NS4xMiw1Mi44Miw3NS4xMiw1Mi44MloiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTIzLjcpIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNNzUuMTIsNTUuOTRIMzkuMjNhMy4wNywzLjA3LDAsMCwxLTIuMzYtMS4wNywzLjE0LDMuMTQsMCwwLDEtLjc2LTIuNThjLjIyLTEuMTksNC43My0yOC41OSwyMS4wNy0yOC41OVM3OCw1MS4xLDc4LjI0LDUyLjI5YTMuMzUsMy4zNSwwLDAsMS0uNzUsMi41OEEzLjEsMy4xLDAsMCwxLDc1LjEyLDU1Ljk0Wm0tMzItNi4yM0g3MS4yNUM2OS40Myw0Mi4wOCw2NC45MSwzMCw1Ny4xOCwzMCw0OSwzMCw0NC43MSw0My4xNSw0My4xLDQ5LjcxWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMjMuNykiLz48cGF0aCBjbGFzcz0iY2xzLTQiIGQ9Ik0xMjQuNjYsNTIuODJzNC4wOS0yNS44OSwxOC0yNS44OSwxNy45NSwyNS44OSwxNy45NSwyNS44OVoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAgLTIzLjcpIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTYwLjQ1LDU1Ljk0SDEyNC41NmEzLjE2LDMuMTYsMCwwLDEtMy4xMi0zLjY1Yy4yMS0xLjE5LDQuNzMtMjguNTksMjEuMDYtMjguNTlzMjAuODUsMjcuNCwyMS4wNiwyOC41OWEzLjMxLDMuMzEsMCwwLDEtLjc1LDIuNThBMy4wNywzLjA3LDAsMCwxLDE2MC40NSw1NS45NFptLTMxLjkyLTYuMjNoMjguMTZDMTU0Ljg2LDQyLjA4LDE1MC4zNSwzMCwxNDIuNjEsMzAsMTM0LjMzLDMwLDEzMC4xNCw0My4xNSwxMjguNTMsNDkuNzFaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIC0yMy43KSIvPjwvc3ZnPg== // ==/UserScript== (function () { "use strict"; // 封装函数:自动兼容 GM4 (异步) 和其他管理器 (同步) async function GM_getValueAsync(key, defaultValue) { if (typeof GM_getValue === "function") { // Tampermonkey / Violentmonkey (同步 API) return GM_getValue(key, defaultValue); } else if (typeof GM !== "undefined" && GM.getValue) { // Greasemonkey 4.x (异步 API) return await GM.getValue(key, defaultValue); } else { console.warn("GM_getValue / GM.getValue 不可用"); return defaultValue; } } async function GM_setValueAsync(key, value) { if (typeof GM_setValue === "function") { // Tampermonkey / Violentmonkey GM_setValue(key, value); } else if (typeof GM !== "undefined" && GM.setValue) { // Greasemonkey 4.x await GM.setValue(key, value); } else { console.warn("GM_setValue / GM.setValue 不可用"); } } // 全局变量存储对象 (global variables) const gv = { // 状态标记类 isFull: false, // 是否处于全屏状态 isIframe: false, // 当前页面是否在 iframe 中 useCssFullscreen: false, // 是否使用了 CSS 注入的网页全屏方式 ytbStageChange: false, // YouTube 舞台模式切换标记 autoCheckCount: 0, // 自动检测计数器 // 播放器相关 player: null, // 当前激活的视频播放器元素 playerChilds: [], // 播放器子元素列表 playerParents: [], // 播放器父元素列表 backControls: null, // 全屏前视频控件状态 restoreClick: null, // 恢复视频默认单击行为(旧版暂时保留) interceptor: null, // 视频事件拦截器 videoOverlayContainer: null, // 移动 video 后的存放容器 videoOverlayOriginalParent: null, // 移动 video 前的父元素 // 页面结构备份 backHtmlId: "", // 全屏前 html 元素的 ID backBodyId: "", // 全屏前 body 元素的 ID // 滚动与交互状态 scrollTop: 0, // 保存进入全屏前的垂直滚动位置 scrollLeft: 0, // 保存进入全屏前的水平滚动位置 scrollFixTimer: null, // 滚动修正定时器 mouseoverEl: null, // 鼠标悬停元素 scrollLocker: null, // 滚动锁定 // 按钮配置与元素 btnText: {}, // 按钮文本(多语言支持) btnPosition: "top-right", // 按钮位置,默认右上角 btnFullscreenToggle: null, // 网页全屏按钮 btnPipToggle: null, // 画中画按钮 leftBtn: null, // 左侧边缘退出网页全屏按钮 rightBtn: null, // 右侧边缘退出网页全屏按钮 // 界面元素 contextMenu: null, // 右键菜单元素 }; // 异步初始化 (async () => { gv.btnPosition = await GM_getValueAsync("buttonPosition", "top-right"); })(); // Html5播放器规则[播放器最外层],适用于无法自动识别的自适应大小HTML5播放器 // 键为域名,值为该域名下播放器元素的选择器对象 const html5Rules = { "www.bilibili.com": { player: ["#bilibiliPlayer"], fullscreen: [".bpx-player-ctrl-web"], pip: [".bpx-player-ctrl-pip"], }, "v.qq.com": { player: ["#player-container"], fullscreen: [".txp_btn_fake"], pip: [".txp_btn_pip"], }, "www.youtube.com": { player: ["#ytd-player"], }, "x.com": { player: ["video"], }, "www.twitch.tv": { player: [".player"], }, "www.huya.com": { player: ["#videoContainer"], }, "www.douyu.com": { player: ["#js-player-video-case"], }, "www.douyin.com": { player: [".xg-video-container video"], pip: [".xgplayer-pip"], }, "www.acfun.cn": { player: ["#ACPlayer"], fullscreen: [".fullscreen-web"], }, "www.miguvideo.com": { player: ["#mod-player"], }, "www.yy.com": { player: ["#player"], }, "v.huya.com": { player: ["#video_embed_flash>div"], }, "*weibo.com": { player: ['[aria-label="Video Player"]', ".html5-video-live .html5-video", ".FeedPlayer_feedVideo_39PLs video"], fullscreen: ["#videoFull"], }, }; // 通用html5播放器选择器,用于匹配常见的视频播放器类名 const generalPlayerRules = [ ".dplayer", ".video-js", ".jwplayer", "[data-player]", ".art-video-player", // Artplayer.js ]; // 判断当前页面是否在iframe中 if (window.top !== window.self) { gv.isIframe = true; } // 根据浏览器语言设置按钮文本 if (navigator.language.toLocaleLowerCase() == "zh-cn") { gv.btnText = { max: "网页全屏", maxTooltip: "切换网页全屏(ESC),右键选择按钮位置", // 悬浮提示 pip: "画中画", pipTooltip: "切换画中画(F2),右键选择按钮位置", // 悬浮提示 tip: "Iframe内视频,请用鼠标点击视频后重试", menuTitle: "选择按钮位置", topLeft: "左上角", topRight: "右上角", }; } else { gv.btnText = { max: "Maximize", maxTooltip: "Toggle fullscreen (ESC). Right-click to choose button position", // 悬浮提示 pip: "PicInPic", pipTooltip: "Toggle Picture-in-Picture (F2). Right-click to choose button position", // 悬浮提示 tip: "Iframe video. Please click on the video and try again", menuTitle: "Choose Button Position", topLeft: "Top Left", topRight: "Top Right", }; } // 工具函数集合 const tool = { /** * 带时间戳的日志打印 * @param {string} log - 日志内容 */ print(log) { const now = new Date(); const format = (n) => String(n).padStart(2, "0"); const timenow = `[${now.getFullYear()}-${format(now.getMonth() + 1)}-${format(now.getDate())} ${format( now.getHours() )}:${format(now.getMinutes())}:${format(now.getSeconds())}]`; console.log(`${timenow}[Maximize Video(Modify)] >`, log); }, /** * 获取元素的位置信息 * @param {HTMLElement} element - 目标元素 * @returns {Object} 包含页面坐标和屏幕坐标的对象 */ getRect(element) { const rect = element.getBoundingClientRect(); const scroll = tool.getScroll(); return { pageX: rect.left + scroll.left, // 元素左上角在页面中的X坐标 pageY: rect.top + scroll.top, // 元素左上角在页面中的Y坐标 screenX: rect.left, // 元素左上角在视口中的X坐标 screenY: rect.top, // 元素左上角在视口中的Y坐标 width: rect.width, height: rect.height, }; }, /** * 判断元素是否接近全屏显示(宽或高接近视口,且位置符合全屏特征) * @param {HTMLElement} element - 目标元素 * @param {Object} tolerance - 容差配置对象(可选) * @returns {boolean} 是否接近全屏显示 */ isNearFullscreen(element, tolerance = {}) { const { size = 20, // 尺寸接近容差(宽高) top = 10, // 顶部对齐容差 left = 20, // 左侧对齐容差 bottom = 1, // 底部可见容差 right = 1, // 右侧可见容差 center = 20, // 居中容差 } = tolerance; const client = tool.getClient(); const rect = element.getBoundingClientRect(); const isWidthClose = Math.abs(client.width - rect.width) <= size; const isHeightClose = Math.abs(client.height - rect.height) <= size; const isTopAligned = rect.top <= top; const isLeftAligned = rect.left <= left; const isBottomVisible = rect.bottom <= client.height + bottom; const isRightVisible = rect.right <= client.width + right; const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const isCentered = Math.abs(centerX - client.width / 2) <= center && Math.abs(centerY - client.height / 2) <= center; return ( (isWidthClose || isHeightClose) && isTopAligned && isLeftAligned && isBottomVisible && isRightVisible && isCentered ); }, /** * 获取页面滚动距离 * @returns {Object} 包含左右和上下滚动距离的对象 */ getScroll() { return { left: document.documentElement.scrollLeft || document.body.scrollLeft, top: document.documentElement.scrollTop || document.body.scrollTop, }; }, /** * 获取视口尺寸 * @returns {Object} 包含视口宽高的对象 */ getClient() { return { width: document.compatMode == "CSS1Compat" ? document.documentElement.clientWidth : document.body.clientWidth, height: document.compatMode == "CSS1Compat" ? document.documentElement.clientHeight : document.body.clientHeight, }; }, /** * 向页面添加CSS样式 * @param {string} css - CSS样式字符串 * @returns {HTMLElement} 创建的 style 元素 */ addStyle(css) { const style = document.createElement("style"); style.className = "maximize-video-style"; style.appendChild(document.createTextNode(css)); document.head.appendChild(style); return style; }, /** * 匹配字符串与规则(支持通配符*) * @param {string} str - 要匹配的字符串 * @param {string} rule - 包含*的规则字符串 * @returns {boolean} 是否匹配 */ matchRule(str, rule) { return new RegExp("^" + rule.split("*").join(".*") + "$").test(str); }, /** * 创建按钮元素 * @param {string} id - 按钮id * @param {string} title - 按钮提示文本 * @param {Function} [clickHandler] - 可选的按钮点击事件处理函数 * @returns {HTMLElement} 创建的按钮元素 */ createButton(id, title, clickHandler) { const btn = document.createElement("tbdiv"); btn.id = id; btn.title = title; // 设置提示文本 // 如果提供了点击处理函数,则绑定 if (typeof clickHandler === "function") { btn.onclick = clickHandler; } document.body.appendChild(btn); return btn; }, /** * 显示提示信息 * @param {string} str - 提示文本 * @returns {Promise} 提示显示完成的Promise */ async addTip(str) { if (!document.getElementById("catTip")) { const tip = document.createElement("tbdiv"); tip.id = "catTip"; tip.innerHTML = str; tip.style.cssText = 'transition: all 0.8s ease-out;background: none repeat scroll 0 0 #27a9d8;color: #FFFFFF;font: 1.1em "微软雅黑";margin-left: -250px;overflow: hidden;padding: 10px;position: fixed;text-align: center;bottom: 100px;z-index: 300;'; document.body.appendChild(tip); tip.style.right = -tip.offsetWidth - 5 + "px"; // 显示提示动画 await new Promise((resolve) => { tip.style.display = "block"; setTimeout(() => { tip.style.right = "25px"; resolve("OK"); }, 300); }); // 停留一段时间 await new Promise((resolve) => { setTimeout(() => { tip.style.right = -tip.offsetWidth - 5 + "px"; resolve("OK"); }, 3500); }); // 移除提示元素 await new Promise((resolve) => { setTimeout(() => { document.body.removeChild(tip); resolve("OK"); }, 1000); }); } }, /** * 查找并触发网站原生按钮(网页全屏/画中画) * @param {string} type - 按钮类型,支持 'fullscreen' 或 'pip' * @returns {boolean} 是否找到并触发了原生按钮 */ triggerNativeButton(type, prompt) { const hostname = document.location.hostname; // 遍历规则匹配当前域名 for (let domain in html5Rules) { const ruleSet = html5Rules[domain]; if ( tool.matchRule(hostname, domain) && ruleSet.hasOwnProperty(type) && Array.isArray(ruleSet[type]) && ruleSet[type].length > 0 ) { for (let selector of ruleSet[type]) { const nativeBtn = document.querySelector(selector); if (nativeBtn) { nativeBtn.click(); tool.print(`优先使用 ${domain} 的原生${prompt}按钮`); return true; } } break; } } return false; // 未找到原生按钮 }, /** * 节流函数:控制函数在指定时间内最多执行一次 * @param {Function} fn - 需要节流的函数 * @param {number} delay - 延迟时间(毫秒),默认100ms * @returns {Function} 节流后的函数 */ throttle(fn, delay = 100) { let lastTime = 0; return function (...args) { const now = Date.now(); if (now - lastTime > delay) { fn.apply(this, args); // 保持原函数的this和参数 lastTime = now; } }; }, /** * 防抖函数:在事件停止触发一段时间后执行一次 * @param {Function} fn - 要执行的函数 * @param {number} delay - 延迟时间(毫秒),默认 300ms * @returns {Function} 防抖后的函数 */ debounce(fn, delay = 300) { let timer = null; return function (...args) { clearTimeout(timer); // 清除前一次等待 timer = setTimeout(() => { fn.apply(this, args); // 执行最后一次触发 }, delay); }; }, // 是否火狐浏览器 isFirefox() { return navigator.userAgent.toLowerCase().includes("firefox"); }, /** * 创建视频事件拦截器对象 * 用法: * const interceptor = createVideoEventInterceptor(); * interceptor.intercept(video); // 开始拦截 * interceptor.restore(); // 恢复默认行为 * @returns {Object} 包含 intercept 和 restore 方法的对象 */ createVideoEventInterceptor() { // 存储当前被拦截的视频元素 let video = null; // 跟踪视频当前的播放状态(true: 播放中,false: 暂停,null: 未初始化) let isPlaying = null; // 存储已注册的事件处理器,用于后续移除(恢复默认行为时使用) const handlers = []; /** * 视频进入播放状态时的回调 * 作用:更新isPlaying状态为true,同步视频实际播放状态 */ const onPlaying = () => { isPlaying = true; }; /** * 视频进入暂停状态时的回调 * 作用:更新isPlaying状态为false,同步视频实际播放状态 */ const onPause = () => { isPlaying = false; }; /** * 拦截视频点击事件的处理器 * 功能: * 1. 阻止事件冒泡和默认行为(如默认的播放/暂停控制) * 2. 初始化时通过视频当前时间和就绪状态判断初始播放状态 * 3. 根据当前播放状态切换视频的播放/暂停(点击播放→暂停,点击暂停→播放) * @param {Event} e - 点击事件对象 */ const blockClick = (e) => { e.stopImmediatePropagation(); // 阻止事件进一步传播(包括其他同阶段的监听器) e.preventDefault(); // 阻止浏览器默认点击行为 // 初始化判断:若未记录播放状态,通过视频当前时间(>0表示有播放过)和就绪状态(>2表示可播放)推断 if (isPlaying === null) { isPlaying = video.currentTime > 0 && video.readyState > 2; } // 切换播放状态 isPlaying ? video.pause() : video.play(); }; /** * 拦截视频双击事件的处理器 * 功能: * 1. 阻止事件冒泡和默认行为 * 2. 手动实现切换全屏 * @param {MouseEvent} e - 双击事件对象 */ const onDoubleClick = (e) => { e.stopImmediatePropagation(); e.preventDefault(); // 手动实现切换全屏 if (document.fullscreenElement) { document.exitFullscreen(); } else { video.requestFullscreen(); } }; let toggleFlag = false; let cooling = false; // 是否处于冷却期 const onMouseMove = (e) => { e.stopImmediatePropagation(); e.preventDefault(); // 应对火狐浏览器的偏方 if (tool.isFirefox()) { // 冷却期内,忽略事件 if (cooling) return; // console.log(video.volume); // 立即触发一次 if (toggleFlag) { if (video.volume === 0) { video.volume += 0.0000001; } else { video.volume -= 0.0000001; } } else { if (video.volume === 1) { video.volume -= 0.0000001; } else { video.volume += 0.0000001; } } toggleFlag = !toggleFlag; // 进入冷却期 cooling = true; setTimeout(() => { cooling = false; // 冷却结束,允许下一次触发 }, 500); // 冷却时间 } }; /** * 拦截视频滚轮事件的处理器(用于调节音量) * 功能: * 1. 阻止事件冒泡和默认行为(如页面滚动) * 2. 根据滚轮方向(上滚/下滚)调整视频音量,步长为0.1 * 3. 限制音量范围在0(静音)到1(最大音量)之间 * @param {WheelEvent} e - 滚轮事件对象 */ const adjustVolume = (e) => { e.stopImmediatePropagation(); e.preventDefault(); const delta = Math.sign(e.deltaY); // 获取滚轮方向:上滚为-1,下滚为1 const step = 0.1; // 音量调节步长 // 计算新音量并限制范围 video.volume = Math.max(0, Math.min(1, video.volume - delta * step)); }; // 需要在捕获阶段拦截的事件列表 const captureBlockedEvents = [ // 鼠标 "mouseenter", // 抖音 "mouseleave", // 抖音 // "mouseover", // "mouseout", // "mousemove", // "mousedown", // "mouseup", // "dblclick", // "contextmenu", // // 焦点与可视性 // "focus", // "blur", "visibilitychange", // 抖音 // // 触控 // "touchstart", // "touchend", // "touchmove", // "touchcancel", // // 指针 // "pointerenter", // "pointerleave", // "pointerover", // "pointerout", // "pointermove", "pointerdown", // 推特 // "pointerup", // // 拖拽 // "dragenter", // "dragleave", // "dragover", // "drop", ]; // 需要在冒泡阶段拦截的事件列表 const bubbleBlockedEvents = []; /** * 批量注册事件拦截器 * 为指定事件列表添加处理器,阻止事件传播和默认行为,并记录处理器以便后续移除 * @param {string[]} events - 要拦截的事件名称列表 * @param {boolean} useCapture - 是否在捕获阶段监听事件(true: 捕获阶段,false: 冒泡阶段) */ const registerBlockers = (events, useCapture) => { events.forEach((event) => { // 定义事件处理器:阻止传播和默认行为 const handler = (e) => { e.stopImmediatePropagation(); e.preventDefault(); }; // 在document上注册事件监听(使用指定的阶段) document.addEventListener(event, handler, useCapture); // 记录处理器信息,用于restore时移除 handlers.push({ event, handler, useCapture }); }); }; return { /** * 开始拦截指定视频元素的事件,启用自定义交互行为 * @param {HTMLVideoElement} target - 要拦截的目标视频元素(必须是