Greasy Fork

Greasy Fork is available in English.

俺的手机视频脚本

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

当前为 2022-12-16 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         俺的手机视频脚本
// @description  全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。
// @version      1.3
// @author       shopkeeperV
// @namespace    http://greasyfork.icu/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();
            }
        }
    }
})();