Greasy Fork

俺的手机视频脚本

全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。

目前为 2022-12-16 提交的版本。查看 最新版本

// ==UserScript==
// @name         俺的手机视频脚本
// @description  全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。
// @version      1.3
// @author       shopkeeperV
// @namespace    https://greasyfork.org/zh-CN/users/150069
// @match        http*://*/*
// ==/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这个节点没变
                //但是他是一个自定义标签,只能观察他的父标签body了
                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() {
        //对视频的查找与控制都是在每次手指触摸后,重新执行的
        //虽然这样更消耗性能,但是对不同的网站兼容性更强
        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();
            }
        }
    }
})();