Greasy Fork is available in English.
全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。
当前为
// ==UserScript==
// @name 俺的手机视频脚本
// @description 全屏横屏、快进快退、长按倍速,为不可描述的网站特别设计,兼容性很强。使用前请先关闭同类横屏或手势脚本,以避免冲突。
// @version 1.1
// @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") {
if (window.location.href.search("watch") >= 0) {
let timer;
let observer;
//视频页下面的推荐视频点击后不是跳转链接,而是用AJAX来重新渲染页面,这样,需要重新给视频操控层绑定事件
let refresh = function (mutations) {
if (mutations && mutations[0].removedNodes.length > 0) {
//删除和添加子节点个执行一次,需要中断其中任意一个
return;
}
//youtube视频在脚本执行时还没加载,需要个定时器循环获取状态
timer = setInterval(() => {
let listenTargetArray = document.getElementsByClassName("player-controls-background");
//该元素通过开发者工具元素页面【中断于->删除节点】确认
let containerArray = document.getElementsByClassName("page-container");
if (listenTargetArray.length > 0 && containerArray.length > 0) {
//视频已加载
listenTarget = listenTargetArray[0];
listen();
if (!observer) {
observer = new MutationObserver(refresh);
observer.observe(containerArray[0], {childList: true});
}
//清除定时器
clearInterval(timer);
}
}, 500);
};
refresh();
} else {
//非视频页面
return;
}
}
//通用
else {
//没有定制的网站将监听document的touchstart事件
listen();
}
function listen() {
//所有逻辑都是在手触摸屏幕后开始执行,在手指离开后对页面没有干扰
//虽然这样更消耗性能,但是对不同的网站兼容性更强
listenTarget.addEventListener("touchstart", (e) => {
let startX;
let endX;
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];
}
//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;
}
//一个合适尺寸的最近祖先元素用于显示手势信息
let noticeContainer = findNoticeContainer();
//指示器元素
let notice;
//视频快进快退量
let timeChange = 0;
//1表示右滑快进,2表示左滑快退,方向一旦确认就无法更改
let direction;
//支持视频快进的最小长度
let durationThreshold = 60;
//禁止长按视频呼出浏览器菜单,为长按倍速做准备(没有视频框架的视频需要)
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 = 3;
//禁止再快进快退
target.removeEventListener("touchmove", touchmoveHandler);
//显示notice
notice.innerText = "x3";
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.floor(e.touches[0].clientX);
endX = 0;
}
target.addEventListener("touchmove", touchmoveHandler);
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内如果有移动,清除长按定时事件
clearTimeout(rateTimer);
//视频太短无需进行快进相关操作
if (videoElement.duration <= durationThreshold) {
return;
}
if (moveEvent.touches.length === 1) {
//仅支持单指触摸,记录位置
endX = Math.floor(moveEvent.touches[0].clientX);
}
//由第一次移动确认手势方向,就不再变更
//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;
}
}
//未到阈值或垂直方向上的移动不显示
if (direction) {
notice.style.display = "block";
notice.innerText = (direction === 1 ? ">>>" : "<<<") + getClearTimeChange(timeChange);
}
}
function touchendHandler() {
if (endX === 0) {
//长按
if (rateTimer) {
//定时器也许已经执行,此时清除也没关系
clearTimeout(rateTimer);
videoElement.playbackRate = 1;
}
} else if (timeChange !== 0 && videoElement.duration > durationThreshold/*视频太短不执行快进*/) {
//快进
videoElement.currentTime += timeChange;
}
target.removeEventListener("touchmove", touchmoveHandler);
target.removeEventListener("touchend", touchendHandler);
if (notice) notice.remove();
}
});
}
//全屏横屏模块
//利用resize事件监听全屏动作,fullscreenchange部分网站监听不到
window.addEventListener("resize", () => {
//不设置延迟很容易黑屏
setTimeout(fullscreenHandler, 500);
}, true/*设为在捕获阶段触发,可以在视频就绪后触发一次*/);
async function fullscreenHandler() {
//获取全屏元素,查找视频,判断视频长宽比来锁定方向
let _fullscreenElement = document.fullscreenElement;
//退出全屏、top内iframe大小调整、已横屏三种情况直接返回
//一个文档内(iframe也是一个文档)的全屏动作,会触发两次resize,全屏时一次,转向时一次(lock()方法)
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 && videoElement.videoHeight < videoElement.videoWidth) {
//高度小于宽度,需要转向,landscape会自动调用陀螺仪
await screen.orientation.lock("landscape");
}
}
})();