// ==UserScript== // @name 俺的手机视频脚本 // @description 全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。 // @version 1.3.1 // @author shopkeeperV // @namespace https://greasyfork.org/zh-CN/users/150069 // @match http*://*/* // @downloadURL none // ==/UserScript== /*jshint esversion: 8*/ (function () { 'use strict'; //部分网站阻止视频操作层触摸事件传播,需要指定监听目标,默认是document let listenTarget = document; if (window.location.host === "m.youtube.com") { //整个网页就完全不刷新,内容却变来变去 let timer; let observer; let lastLocation = ""; let refresh = function () { //监听网页内容变化,每个新地址执行一次 if (window.location.href === lastLocation) { return; } //记录本次地址 lastLocation = window.location.href; //youtube视频在脚本执行时还没加载,需要个定时器循环获取状态 timer = setInterval(() => { //通过元素页面【中断于->删除节点】发现#app是改变内容的父容器 if (!observer) { let target = document.getElementById("app"); observer = new MutationObserver(refresh); observer.observe(target, {subtree: true, childList: true}); } if (window.location.href.search("watch") > 0) { //特定的视频操控层 let listenTargetArray = document.getElementsByClassName("player-controls-background"); if (listenTargetArray.length > 0) { //视频已加载 listenTarget = listenTargetArray[0]; listen(); //已获取视频控制层 clearInterval(timer); } } else { //当前不是播放页 clearInterval(timer); } }, 500); }; refresh(); } //通用 else { //没有定制的网站将监听document的touchstart事件 listen(); } function listen() { //对视频的查找与控制都是在每次touchstart后重新执行的 //虽然这样更消耗性能,但是对不同的网站兼容性更强 listenTarget.addEventListener("touchstart", (e) => { //为了代码逻辑在普通视频与iframe内视频的通用性,分别使用了clientX和screenY let startX; let startY; let endX; let endY; let videoElement; //触摸的目标如果是视频或视频操控层,那他也是我们绑定手势的目标 let target = e.target; //用于有操控层的网站,保存的是视频与操控层适当尺寸下的最大共同祖先节点,确认后需要在后代内搜索视频元素 let biggestContainer; let _width = target.clientWidth; let _height = target.clientHeight; //所有大小合适的祖先节点最后一个为biggestContainer let suitParents = []; //用于判断是否含有包裹视频的a标签,需要禁止其被长按时呼出浏览器菜单 let allParents = []; let temp = target; while (true) { temp = temp.parentElement; //allParents全部保存,用于判断是否存在a标签 allParents.push(temp); if (temp.clientWidth >= _width && temp.clientWidth < _width * 1.2 && temp.clientHeight >= _height && temp.clientHeight < _height * 1.2) { //suitParents保存适合的尺寸的祖先节点 suitParents.push(temp); } //循环结束条件 if (temp.tagName === "BODY" || temp.tagName === "HTML") { //已找到所有符合条件的祖先节点,取最后一个 if (suitParents.length > 0) { biggestContainer = suitParents[suitParents.length - 1]; } else { //没有任何大小合适的祖先元素,肯定不是视频相关元素 return; } //gc suitParents = null; break; } } //当触摸的不是视频元素,可能是非视频相关组件,或视频的操控层 if (target.tagName !== "VIDEO") { //尝试获取视频元素 let videoArray = biggestContainer.getElementsByTagName("video"); if (videoArray.length > 0) { //优化a标签导致的长按手势中断问题(许多网站的视频列表的预览视频都是由a标签包裹) makeTagAQuiet(); videoElement = videoArray[0]; if (videoArray.length > 1) { console.log("触摸位置找到不止一个视频。"); } } else { //非视频相关组件 return; } } //触摸的是视频元素,则一切清晰明了 else { makeTagAQuiet(); videoElement = target; } //用于判断是否要执行touchmove事件的preventDefault() let shortVideo = false; let videoReady = false; let videoReadyHandler = function () { videoReady = true; //视频如果是30秒内的超短视频不要让它影响页面滑动 if (videoElement.duration < 30) { shortVideo = true; } }; if (videoElement.readyState > 0) { videoReadyHandler(); } else { videoElement.addEventListener("loadedmetadata", videoReadyHandler, {once: true}); } //一个合适尺寸的最近祖先元素用于显示手势信息 let noticeContainer = findNoticeContainer(); //指示器元素 let notice; //视频快进快退量 let timeChange = 0; //1表示右滑快进,2表示左滑快退,方向一旦确认就无法更改 let direction; //禁止长按视频呼出浏览器菜单,为长按倍速做准备(没有视频框架的视频需要) if (!videoElement.getAttribute("disableContextmenu")/*只添加一次监听器*/) { videoElement.addEventListener("contextmenu", (e) => { e.preventDefault(); }); videoElement.setAttribute("disableContextmenu", true); } //禁止图片长按拖动(部分框架视频未播放时,触摸到的是预览图) if (target.tagName === "IMG") { target.draggable = false; } //长按倍速定时器 let rateTimer = setTimeout(() => { videoElement.playbackRate = 4; //禁止再快进快退 target.removeEventListener("touchmove", touchmoveHandler); //显示notice notice.innerText = "x4"; notice.style.display = "block"; }, 800); //添加提示元素 if (noticeContainer) { notice = document.createElement("div"); let noticeWidth = 100;//未带单位,后面需要加单位 let noticeHeight = 30; let noticeLeft = noticeContainer.clientWidth / 2 - noticeWidth / 2; notice.style.cssText = "position:absolute;display:none;z-index:99999;top:10px;" + "text-align:center;opacity:0.5;background-color:black;color:white;" + "font:16px/1.8 sans-serif;letter-spacing:normal;border-radius:4px;"; notice.style.width = noticeWidth + "px"; notice.style.height = noticeHeight + "px"; notice.style.left = noticeLeft + "px"; noticeContainer.appendChild(notice); } else { //怎么可能有视频没有div包着啊 console.log("该视频没有可以用于给notice定位的祖先元素。"); } if (e.touches.length === 1) { //单指触摸,记录位置 startX = Math.ceil(e.touches[0].clientX); startY = Math.ceil(e.touches[0].screenY); endX = startX; endY = startY; //console.log("起始位置" + startX + "," + startY); } //滑动流畅的关键1,passive为false代表处理器内调用preventDefault()不会被浏览器拒绝 //mdn:文档级节点 Window、Document 和 Document.body默认是true,其他节点默认是false target.addEventListener("touchmove", touchmoveHandler/*, {passive: false}*/); target.addEventListener("touchend", touchendHandler); function makeTagAQuiet() { for (let element of allParents) { if (element.tagName === "A" && !element.getAttribute("disableMenuAndDrag")) { //禁止长按菜单 element.addEventListener("contextmenu", (e) => { e.preventDefault(); }); //禁止长按拖动 element.draggable = false; element.setAttribute("disableMenuAndDrag", true); //没有长按菜单,用target="_blank"属性来平替 element.target = "_blank"; //不可能a标签嵌套a标签吧 break; } } allParents = null; } function findNoticeContainer() { let temp = videoElement; let _width = videoElement.clientWidth; let _height = videoElement.clientHeight; while (true) { //寻找最近的长宽大于>=视频的祖先节点 if (temp.parentElement.clientWidth >= _width && temp.parentElement.clientHeight >= _height) { return temp.parentElement; } else { temp = temp.parentElement; } } } function getClearTimeChange(timeChange) { timeChange = Math.abs(timeChange); let minute = Math.floor(timeChange / 60); let second = timeChange % 60; return (minute === 0 ? "" : (minute + "min")) + second + "s"; } function touchmoveHandler(moveEvent) { //触摸屏幕后,0.8s内如果有移动,清除长按定时事件 if (rateTimer) { clearTimeout(rateTimer); rateTimer = null; } //小于30秒的都是网页的视频预览列表,不要影响页面滑动,也不需要快进快推功能 //视频未就绪也不执行 if (shortVideo || !videoReady) { return; } //滑动流畅的关键2 moveEvent.preventDefault(); if (moveEvent.touches.length === 1) { //仅支持单指触摸,记录位置 let temp = Math.ceil(moveEvent.touches[0].clientX); //x轴没变化,y轴方向移动也会触发,要避免不必要的运算 if (temp === endX) { return; } else { endX = temp; } endY = Math.ceil(moveEvent.touches[0].screenY); //console.log("移动到" + endX + "," + endY); } //由第一次移动确认手势方向,就不再变更 //10个像素起 if (endX > startX + 10) { //快进 if (!direction) { //首次移动,记录方向 direction = 1; } if (direction === 1) { //方向未变化 timeChange = endX - startX - 10; } else { timeChange = 0; } } else if (endX < startX - 10) { //快退 if (!direction) { //首次移动,记录方向 direction = 2; } if (direction === 2) { //方向未变化 timeChange = endX - startX + 10; } else { timeChange = 0; } } else if (timeChange !== 0) { timeChange = 0; } else { return; } if (notice.style.display === "none" /*已经显示了就不管怎么滑动了*/ && Math.abs(endY - startY) > Math.abs(endX - startX)) { //垂直滑动不显示 timeChange = 0; return; } //未到阈值不显示 if (direction) { notice.style.display = "block"; notice.innerText = (direction === 1 ? ">>>" : "<<<") + getClearTimeChange(timeChange); } } function touchendHandler() { if (endX === startX) { //长按 //console.log("长按"); if (rateTimer) { //定时器也许已经执行,此时清除也没关系 clearTimeout(rateTimer); videoElement.playbackRate = 1; } } else { if (timeChange !== 0) { //快进 videoElement.currentTime += timeChange; } //console.log("x轴移动" + (endX - startX)); //console.log("y轴移动" + (endY - startY)); } target.removeEventListener("touchmove", touchmoveHandler); target.removeEventListener("touchend", touchendHandler); if (notice) notice.remove(); } }); } //全屏横屏模块 //利用window的resize事件监听全屏动作,监听document常用的fullscreenchange事件可能因为后代停止传播而捕获不到 window.addEventListener("resize", () => { //不设置延迟很容易黑屏 setTimeout(fullscreenHandler, 500); }); async function fullscreenHandler() { //获取全屏元素,查找视频,判断视频长宽比来锁定方向 let _fullscreenElement = document.fullscreenElement; //一个document内视频(iframe也是一个document)的全屏动作,会触发两次resize,全屏时一次,转向时一次(lock()方法) //那么一个iframe视频横屏至少触发四次,还有别的元素调整宽高就会更多 //没有全屏元素、top内iframe大小调整、已横屏三种情况直接返回 if (!_fullscreenElement || _fullscreenElement.tagName === "IFRAME" || screen.orientation.type.search("landscape") >= 0) { return; } let videoElement; if (_fullscreenElement.tagName !== "VIDEO") { //最大的全屏元素不是视频本身,需要寻找视频元素 let videoArray = _fullscreenElement.getElementsByTagName("video"); if (videoArray.length > 0) { videoElement = videoArray[0]; } } else videoElement = _fullscreenElement; if (videoElement) { let changeHandler = async function () { if (videoElement.videoHeight < videoElement.videoWidth) { //高度小于宽度,需要转向,landscape会自动调用陀螺仪 await screen.orientation.lock("landscape"); } }; //视频未加载,在加载后再判断需不需要转向 if (videoElement.readyState < 1) { videoElement.addEventListener("loadedmetadata", changeHandler, {once: true}); } else { await changeHandler(); } } } })();