🏠 Home 

Greasy Fork is available in English.

俺的手机视频脚本

全屏横屏、快进快退、长按倍速,对各种视频网站的兼容性很强。仅适用于狐猴、kiwi等chromium内核的浏览器。使用前请先关闭同类横屏或手势脚本,以避免冲突。


Installer dette script?
  1. // ==UserScript==
  2. // @name 俺的手机视频脚本
  3. // @name:zh-TW 俺的手機視頻腳本
  4. // @name:en My Phone Video Script
  5. // @name:ja My Phone Video Script
  6. // @name:ko My Phone Video Script
  7. // @name:ru My Phone Video Script
  8. // @description 全屏横屏、快进快退、长按倍速,对各种视频网站的兼容性很强。仅适用于狐猴、kiwi等chromium内核的浏览器。使用前请先关闭同类横屏或手势脚本,以避免冲突。
  9. // @description:zh-TW 全屏橫屏、快進快退、長按倍速,對各種視頻網站的相容性很強。僅適用於狐猴、Kiwi等Chromium核心的瀏覽器。使用前請先關閉同類橫屏或手勢腳本,以避免衝突。
  10. // @description:en Full-screen landscape, fast-forward and rewind, long-press for speed adjustment. Designed for Kiwi and Lemur browsers.
  11. // @description:ja Full-screen landscape, fast-forward and rewind, long-press for speed adjustment. Designed for Kiwi and Lemur browsers.
  12. // @description:ko Full-screen landscape, fast-forward and rewind, long-press for speed adjustment. Designed for Kiwi and Lemur browsers.
  13. // @description:ru Full-screen landscape, fast-forward and rewind, long-press for speed adjustment. Designed for Kiwi and Lemur browsers.
  14. // @version 1.8.11
  15. // @author shopkeeperV
  16. // @namespace https://greasyfork.org/zh-CN/users/150069
  17. // @match *://*/*
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_registerMenuCommand
  21. // @grant GM_addStyle
  22. // ==/UserScript==
  23. /*jshint esversion: 8*/
  24. (function () {
  25. 'use strict';
  26. //有元素全屏时不能选中文字,个别网页会因此影响长按功能
  27. GM_addStyle(":not(:root):fullscreen{user-select:none !important;}");
  28. let mutationTimer;
  29. //getElementsByTagName、getElementsByClassName获取的集合是实时更新的,不需要重新获取,保留引用以提升性能
  30. let videos = document.getElementsByTagName("video");
  31. let iframes = document.getElementsByTagName("iframe");
  32. let makeVideoAndIframeReady = function () {
  33. //去除未使用框架的视频的原生全屏按钮
  34. for (let video of videos) {
  35. if (video.controls) {
  36. video.controlsList = ["nofullscreen"];
  37. console.log("俺的手机视频脚本:获取到未使用框架的视频,已去除全屏按钮。");
  38. }
  39. }
  40. //放开iframe全屏
  41. for (let iframe of iframes) {
  42. iframe.allowFullscreen = true;
  43. }
  44. }
  45. let mutationHandler = function (mutationsList) {
  46. for (let mutation of mutationsList) {
  47. if (mutation.type === 'childList') {
  48. for (let node of mutation.addedNodes) {
  49. if (node.nodeType === Node.ELEMENT_NODE) {
  50. if (node.tagName.toLowerCase() === 'video' || node.tagName.toLowerCase() === 'iframe') {
  51. let _window = `${top === window ? "top" : "iframe"}>${location.host}`;
  52. if (mutationTimer) {
  53. clearTimeout(mutationTimer);
  54. console.log(`俺的手机视频脚本:${_window}清除定时任务。`);
  55. }
  56. mutationTimer = setTimeout(() => {
  57. mutationTimer = 0;
  58. makeVideoAndIframeReady();
  59. console.log(`俺的手机视频脚本:${_window}处理完成。`);
  60. }, 1000);
  61. console.log(`俺的手机视频脚本:${_window}页面新增了一个${node.tagName.toLowerCase()},1秒后处理。`);
  62. return;
  63. }
  64. }
  65. }
  66. }
  67. }
  68. }
  69. makeVideoAndIframeReady();
  70. //观察页面变化,为新增的视频和iframe做好相应准备
  71. new MutationObserver(mutationHandler).observe(document.body, {childList: true, subtree: true});
  72. //注意,对少数iframe内视频,广告插件或使此脚本不起作用
  73. let listenTarget = document;
  74. listen();
  75. //狐猴浏览器长时间没有访问的标签页会出现与篡改猴断联的bug,尽量规避使用篡改猴存储相关的方法
  76. let settings = {
  77. voiced: true,
  78. speed: true,
  79. rate: 4,
  80. sensitivity1: 0.5,
  81. threshold: 300,
  82. sensitivity2: 0.2
  83. }
  84. for (let settingsKey in settings) {
  85. let value = GM_getValue(settingsKey);
  86. //未初始化时篡改猴GM_getValue返回undefined,null==undefined正确,null===undefined错误
  87. //为避免差异,此处不使用严格相等
  88. if (value == undefined) {
  89. GM_setValue(settingsKey, settings[settingsKey]);
  90. } else settings[settingsKey] = value;
  91. }
  92. if (window === top) {
  93. function registerBoolean(btnName, key) {
  94. GM_registerMenuCommand(btnName, () => {
  95. try {
  96. GM_setValue(key, !settings[key]);
  97. settings[key] = !settings[key];
  98. alert(`成功${settings[key] ? "开启" : "关闭"}。`);
  99. } catch (e) {
  100. alert("浏览器bug捕获,刷新页面后重试。\n" + e.message);
  101. }
  102. });
  103. }
  104. function registerInput(btnName, description, key, integer, minimum, maximum) {
  105. GM_registerMenuCommand(btnName, () => {
  106. let input = window.prompt(description, settings[key]);
  107. //prompt点取消会返回空,不输入点确认返回空串
  108. if (input === null) return;
  109. //确认是数字
  110. input = Number(input);
  111. //非法输入:NaN 0
  112. if (input && input > minimum && input <= maximum) {
  113. if (integer && !Number.isInteger(input)) {
  114. alert("要求整数!");
  115. return;
  116. }
  117. try {
  118. GM_setValue(key, input);
  119. settings[key] = input;
  120. } catch (e) {
  121. alert("浏览器bug捕获,刷新页面后重试。\n" + e.message);
  122. }
  123. } else {
  124. alert("输入错误!");
  125. }
  126. });
  127. }
  128. registerBoolean("开关【触摸视频时取消静音】", "voiced");
  129. registerBoolean("开关【显示播放速度调整按钮】", "speed");
  130. registerInput("修改长按倍速数值", "请指定需要的倍率,输入0-6的数字即可,可以是小数。", "rate", false, 0, 6);
  131. registerInput("修改长视频滑动灵敏度", "默认为0.5,可依需求增减,要求0-3之间。", "sensitivity1", false, 0, 3);
  132. registerInput("修改短视频阈值", "默认300秒,小于此时长的使用短视频滑动灵敏度。", "threshold", true, 0, 36000);
  133. registerInput("修改短视频滑动灵敏度", "默认为0.2,可依需求增减,要求0-3之间。", "sensitivity2", false, 0, 3);
  134. }
  135. function listen() {
  136. //对视频的查找与控制都是在每次touchstart后重新执行的
  137. //虽然这样更消耗性能,但是对不同的网站兼容性更强
  138. //在捕获阶段监听事件,个别网站的视频操作层事件停止冒泡
  139. listenTarget.addEventListener("touchstart", (e) => {
  140. //为了代码逻辑在普通视频与iframe内视频的通用性,分别使用了clientX和screenY
  141. let startX;
  142. let startY;
  143. let endX;
  144. let endY;
  145. //多根手指不做响应
  146. if (e.touches.length === 1) {
  147. //在全屏时,不对边缘5%的区域做响应
  148. let screenX = e.touches[0].screenX;
  149. let screenY = e.touches[0].screenY;
  150. if (document.fullscreenElement) {
  151. if (screenX < screen.width * 0.05 || screenX > screen.width * 0.95 ||
  152. screenY < screen.height * 0.05 || screenY > screen.height * 0.95)
  153. return;
  154. }
  155. //单指触摸,记录位置
  156. startX = Math.ceil(e.touches[0].clientX);
  157. startY = Math.ceil(screenY);
  158. endX = startX;
  159. endY = startY;
  160. } else return;
  161. let videoElement;
  162. //触摸的目标如果是视频或视频操控层,那他也是我们绑定手势的目标
  163. let target = e.target;
  164. //用于有操控层的网站,保存的是视频与操控层适当尺寸下的最大共同祖先节点,确认后需要在后代内搜索视频元素
  165. let biggestContainer;
  166. let targetWidth = target.clientWidth;
  167. let targetHeight = target.clientHeight;
  168. //所有大小合适的祖先节点最后一个为biggestContainer
  169. let suitParents = [];
  170. //用于判断是否含有包裹视频的a标签,需要禁止其被长按时呼出浏览器菜单
  171. let allParents = [];
  172. let temp = target;
  173. //抖音类短视频网站,特点是视频操控层占据几乎整个屏幕
  174. let maybeTiktok = false;
  175. //用于短视频判断
  176. let scrollHeightOut = false;
  177. //寻找biggestContainer
  178. while (true) {
  179. temp = temp.parentElement;
  180. if (!temp/*或直接点击到html元素,他将没有父元素*/) {
  181. return;
  182. }
  183. //allParents全部保存,用于判断是否存在a标签
  184. allParents.push(temp);
  185. //这些条件是用来找操作层对应视频的,和指示器、按钮的定位无关
  186. if (temp.clientWidth > 0 &&
  187. temp.clientWidth < targetWidth * 1.2 &&
  188. temp.clientHeight > 0 &&
  189. temp.clientHeight < targetHeight * 1.2) {
  190. suitParents.push(temp);
  191. //用非全屏状态下scrollHeight来判断可以找到抖音类网站的合适视频容器
  192. //全屏时视觉尺寸合适都可以用,youtube全屏就有滚动高度超出限制的元素
  193. if (!scrollHeightOut && temp.scrollHeight > targetHeight * 1.2) {
  194. scrollHeightOut = true;
  195. }
  196. }
  197. //循环结束条件
  198. if (temp.tagName === "BODY" ||
  199. temp.tagName === "HTML" ||
  200. !temp.parentElement) {
  201. //已找到所有符合条件的祖先节点,取最后一个
  202. if (suitParents.length > 0) {
  203. biggestContainer = suitParents[suitParents.length - 1];
  204. }
  205. //没有任何大小合适的祖先元素,且自身不是视频元素,那也肯定不是视频操控层
  206. else if (target.tagName !== "VIDEO") {
  207. return;
  208. }
  209. //gc
  210. suitParents = null;
  211. break;
  212. }
  213. }
  214. //寻找视频元素
  215. //当触摸的不是视频元素,可能是非视频相关组件,或视频的操控层
  216. if (target.tagName !== "VIDEO") {
  217. //尝试获取视频元素
  218. let videoArray = biggestContainer.getElementsByTagName("video");
  219. if (videoArray.length > 0) {
  220. if (videoArray.length > 1) {
  221. //一些短视频会在筛选的祖先元素内有多个视频
  222. for (let video of videoArray) {
  223. if (video.paused === false) {
  224. videoElement = video;
  225. }
  226. }
  227. if (!videoElement) {
  228. console.log("触摸位置找到不止一个视频,而且没有正在播放的,无法判断哪个是需要操作的视频元素。");
  229. return;
  230. }
  231. } else videoElement = videoArray[0];
  232. //找到视频元素后,可以判断是否可能是短视频
  233. //非全屏状态下,非iframe内视频,若视频操作层或视频占据大半的屏幕,判断为短视频
  234. //tiktok没有视频控件,判断这个防止有页面的预览视频铺满了屏幕,这一项只能判断到没有框架的视频
  235. //整个网页内视频数量大于1也可以辅助判断
  236. if (!document.fullscreenElement &&
  237. top === window &&
  238. !videoElement.controls &&
  239. target.clientHeight > window.innerHeight * 0.8 &&
  240. (videos.length > 1 || scrollHeightOut)) {
  241. maybeTiktok = true;
  242. }
  243. //如果是视频外很大的容器绝非我们想要的
  244. //操作层除了短视频没见过高度高视频这么多的,大概率不是视频操控层
  245. if (!maybeTiktok && targetHeight > videoElement.clientHeight * 1.5) {
  246. //不是合适的操作层
  247. return;
  248. }
  249. } else {
  250. //非视频相关组件
  251. return;
  252. }
  253. }
  254. //触摸的是视频元素,则一切清晰明了
  255. else {
  256. videoElement = target;
  257. }
  258. //用于比较单击后,视频的播放状态,如果单击暂停,则恢复播放
  259. let playing = !videoElement.paused;
  260. //下面两个连通tiktok变量3个参数用于判断是否要执行touchmove事件处理器
  261. //小于20s当做预览视频,在网页上的视频列表可能存在,不要让他们影响网页滚动
  262. let sampleVideo = false;
  263. let videoReady = false;
  264. let videoReadyHandler = function () {
  265. videoReady = true;
  266. if (videoElement.duration < 20) {
  267. sampleVideo = true;
  268. }
  269. };
  270. if (videoElement.readyState > 0) {
  271. videoReadyHandler();
  272. } else {
  273. videoElement.addEventListener("loadedmetadata", videoReadyHandler, {once: true});
  274. }
  275. //一个合适尺寸的最近祖先元素用于显示手势信息与全屏按钮
  276. let componentContainer = findComponentContainer();
  277. //指示器元素
  278. let notice;
  279. //视频快进快退量
  280. let timeChange = 0;
  281. //1表示右滑快进,2表示左滑快退,方向一旦确认就无法更改
  282. let direction;
  283. //优化a标签导致的长按手势中断问题(许多网站的视频列表的预览视频都是由a标签包裹)
  284. makeTagAQuiet();
  285. //禁止长按视频呼出浏览器菜单,为长按倍速做准备(没有视频框架的视频需要)
  286. if (!videoElement.getAttribute("disable_contextmenu")/*只添加一次监听器*/) {
  287. videoElement.addEventListener("contextmenu", (e) => {
  288. e.preventDefault();
  289. });
  290. videoElement.setAttribute("disable_contextmenu", true);
  291. }
  292. //禁止图片长按呼出浏览器菜单和拖动(部分框架视频未播放时,触摸到的是预览图,抖音类播放时摸到的都是图片)
  293. if (target.tagName === "IMG") {
  294. target.draggable = false;
  295. if (!target.getAttribute("disable_contextmenu")) {
  296. target.addEventListener("contextmenu", (e) => {
  297. e.preventDefault();
  298. });
  299. target.setAttribute("disable_contextmenu", true);
  300. }
  301. }
  302. let sharedCSS = "border-radius:4px;z-index:99999;opacity:0.5;background-color:black;color:white;" +
  303. "display:flex;justify-content:center;align-items:center;text-align:center;user-select:none;";
  304. let haveControls = videoElement.controls;
  305. let longPress = false;
  306. let rateTimerBack;
  307. //长按倍速定时器
  308. let rateTimer = setTimeout(() => {
  309. if (playing && videoElement.paused) {
  310. videoElement.play();
  311. }
  312. //添加检测机制,有些视频组件触屏的某些时机会恢复正常播放速度,使长按加速失效
  313. rateTimerBack = setTimeout(() => {
  314. if (videoElement.playbackRate === 1) {
  315. videoElement.playbackRate = settings.rate;
  316. }
  317. }, 500);
  318. videoElement.playbackRate = settings.rate;
  319. videoElement.controls = false;
  320. //禁止再快进快退
  321. target.removeEventListener("touchmove", touchmoveHandler);
  322. //显示notice
  323. notice.innerText = "x" + settings.rate;
  324. notice.style.display = "flex";
  325. longPress = true;
  326. rateTimer = null;
  327. //显示调速按钮
  328. //仅在全屏时触发
  329. if (!document.fullscreenElement || videoElement.readyState === 0 || !settings.speed) {
  330. return;
  331. }
  332. let speedBtn = componentContainer.querySelector(`:scope>.me-speed-btn`/*需要直接子元素*/);
  333. if (speedBtn) {
  334. speedBtn.style.display = "flex";
  335. } else {
  336. speedBtn = document.createElement("div");
  337. speedBtn.className = "me-speed-btn";
  338. speedBtn.style.cssText = sharedCSS + "position:absolute;width:30px;height:30px;font-size:18px;";
  339. speedBtn.style.top = "50px";
  340. speedBtn.style.right = "20px";
  341. speedBtn.textContent = "速";
  342. componentContainer.appendChild(speedBtn);
  343. speedBtn.addEventListener("touchstart", showSpeedMenu);
  344. }
  345. setTimeout(() => {
  346. speedBtn.style.display = "none";
  347. }, 4000);
  348. window.addEventListener("resize", () => {
  349. speedBtn.style.display = "none";
  350. }, {once: true});
  351. function showSpeedMenu(event) {
  352. //不阻止会打开按钮组又关闭按钮组
  353. event.stopPropagation();
  354. speedBtn.style.display = "none";
  355. let container = componentContainer.querySelector(`:scope>.me-speed-container`/*需要直接子元素*/);
  356. if (container) {
  357. container.style.display = "flex";
  358. } else {
  359. container = document.createElement("div");
  360. //在一次全屏状态中不会重复创建容器
  361. container.className = "me-speed-container";
  362. componentContainer.appendChild(container);
  363. let css;
  364. //横屏竖屏按钮显示位置不一样
  365. if (videoElement.videoHeight > videoElement.videoWidth) {
  366. css = `flex-direction:column;top:0;bottom:0;left:${(window.innerWidth * 2) / 3 + 40}px`;
  367. } else {
  368. css = `flex-direction:row;left:0;right:0;top:${(window.innerHeight / 3) - 30}px`;
  369. }
  370. container.style.cssText = "display:flex;position:absolute;flex-wrap:nowrap;z-index:99999;justify-content:center;" + css;
  371. const values = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4, 5, 6];
  372. //创建按钮
  373. values.forEach(value => {
  374. const button = document.createElement('div');
  375. container.appendChild(button);
  376. button.className = 'button';
  377. button.textContent = value + "";
  378. button.style.cssText = sharedCSS + "width:40px;height:30px;margin:2px;font-size:18px;";
  379. button.addEventListener('touchstart', (event) => {
  380. event.stopPropagation();
  381. container.style.display = "none";
  382. videoElement.playbackRate = value;
  383. //添加检测机制,有些视频组件触屏的某些时机会恢复正常播放速度,使变速失效
  384. setTimeout(() => {
  385. if (videoElement.playbackRate === 1) {
  386. videoElement.playbackRate = value;
  387. }
  388. }, 500);
  389. });
  390. });
  391. }
  392. //触摸空白处关闭container,事件必须和按钮事件一致,一个用touchstart,一个用click,无法确认执行顺序,而且click点击视频时未必触发
  393. componentContainer.addEventListener("touchstart", () => {
  394. container.style.display = "none";
  395. }, {capture: true, once: true});
  396. window.addEventListener("resize", () => {
  397. container.style.display = "none";
  398. }, {once: true});
  399. }
  400. }, 800);
  401. //有些网站预览视频位置实际在屏幕之外,需要加上平移的数值
  402. let screenWidth = screen.width;
  403. let componentMoveLeft = componentContainer.offsetLeft;
  404. let moveNum = Math.floor(componentMoveLeft * 1.1 / screenWidth);
  405. //添加指示器元素
  406. //有些视频非全屏时没有操作层,全屏时有操作层,并且视频与操作层为兄弟元素
  407. //那么在非全屏时只用getElementsByClassName判断是否已经创建me-notice可能会不准确
  408. notice = componentContainer.querySelector(`:scope>.me-notice`/*需要直接子元素*/);
  409. if (!notice) {
  410. notice = document.createElement("div");
  411. notice.className = "me-notice";
  412. let noticeWidth = 110;//未带单位,后面需要加单位
  413. let noticeTop = Math.round(componentContainer.clientHeight / 6);
  414. let noticeLeft = Math.round(moveNum * screenWidth + componentContainer.clientWidth / 2 - noticeWidth / 2);
  415. notice.style.cssText = sharedCSS + "font-size:16px;position:absolute;display:none;letter-spacing:normal;";
  416. notice.style.width = noticeWidth + "px";
  417. notice.style.height = "30px";
  418. notice.style.left = noticeLeft + "px";
  419. notice.style.top = noticeTop + "px";
  420. componentContainer.appendChild(notice);
  421. //每次全屏与退出全屏需要重新计算notice的位置
  422. window.addEventListener("resize", () => {
  423. notice.remove();
  424. }, {once: true});
  425. }
  426. //滑动流畅的关键1,passive为false代表处理器内调用preventDefault()不会被浏览器拒绝
  427. //mdn:文档级节点 Window、Document 和 Document.body默认是true,其他节点默认是false,以防万一还是写上
  428. target.addEventListener("touchmove", touchmoveHandler, {passive: false});
  429. target.addEventListener("touchend", touchendHandler, {once: true});
  430. function makeTagAQuiet() {
  431. for (let element of allParents) {
  432. if (element.tagName === "A" &&
  433. !element.getAttribute("disable_menu_and_drag")) {
  434. //禁止长按菜单
  435. element.addEventListener("contextmenu", (e) => {
  436. e.preventDefault();
  437. });
  438. //禁止长按拖动
  439. element.draggable = false;
  440. element.setAttribute("disable_menu_and_drag", true);
  441. //没有长按菜单,用target="_blank"属性来平替
  442. element.target = "_blank";
  443. //不可能a标签嵌套a标签吧
  444. break;
  445. }
  446. }
  447. allParents = null;
  448. }
  449. function findComponentContainer() {
  450. //部分网站特殊,如操控层会在触摸过程中变化,所以将在视频和操控层的最近共同祖先元素中添加组件
  451. if (getBaseDomain(location.host) === "spankbang.com") {
  452. return findCommonAncestorWithSize(document.querySelector(".vjs-controls-container"), videoElement);
  453. } else {
  454. //其余网站使用通用的方案,触摸到的是视频就用视频的父元素,触摸到的是操控层就用操控层本身
  455. if (target.tagName === "VIDEO") {
  456. let temp = videoElement;
  457. while (true) {
  458. //寻找最近的有长宽数值的祖先节点
  459. if (temp.parentElement.clientWidth > 0 &&
  460. temp.parentElement.clientHeight > 0) {
  461. return temp.parentElement;
  462. } else {
  463. temp = temp.parentElement;
  464. }
  465. }
  466. } else {
  467. if (window.getComputedStyle(target).opacity === "0") {
  468. //操作层使用opacity来控制透明度时,子元素怎么都不会显示,我们需要修改它
  469. target.style.visibility = "hidden";
  470. target.style.opacity = "1";
  471. }
  472. //直接使用触控层,之所以不使用他的祖先元素,是因为有些网站的操作层的所有祖先元素都不合适
  473. return target;
  474. }
  475. }
  476. function getBaseDomain(host) {
  477. host = host.toLowerCase();
  478. const parts = host.split('.');
  479. return parts.slice(-2).join('.');
  480. }
  481. //寻找视频和操控层的最近共同祖先
  482. function findCommonAncestorWithSize(element1, element2) {
  483. // 获取两个元素的所有祖先节点(包括自身)
  484. function getAncestors(el) {
  485. const ancestors = [];
  486. let currentEl = el;
  487. while (currentEl) {
  488. ancestors.push(currentEl);
  489. currentEl = currentEl.parentElement;
  490. }
  491. return ancestors;
  492. }
  493. const ancestors1 = getAncestors(element1);
  494. const ancestors2 = getAncestors(element2);
  495. for (let i = 0; i < ancestors1.length; i++) {
  496. for (let j = 0; j < ancestors2.length; j++) {
  497. if (ancestors1[i] === ancestors2[j]) {
  498. if (ancestors1[i].clientWidth > 0 && ancestors1[i].clientHeight > 0) {
  499. return ancestors1[i];
  500. }
  501. }
  502. }
  503. }
  504. return null;
  505. }
  506. }
  507. function getClearTimeChange(timeChange) {
  508. timeChange = Math.abs(timeChange);
  509. let minute = Math.floor(timeChange / 60);
  510. let second = timeChange % 60;
  511. return (minute === 0 ? "" : (minute + "min")) + second + "s";
  512. }
  513. function touchmoveHandler(moveEvent) {
  514. // console.log("手指移动");
  515. //触摸屏幕后,0.8s内如果有移动,清除长按定时事件
  516. if (rateTimer) {
  517. clearTimeout(rateTimer);
  518. rateTimer = null;
  519. }
  520. if (maybeTiktok || sampleVideo || !videoReady) {
  521. return;
  522. }
  523. //滑动流畅的关键2
  524. moveEvent.preventDefault();
  525. if (moveEvent.touches.length === 1) {
  526. //仅支持单指触摸,记录位置
  527. let temp = Math.ceil(moveEvent.touches[0].clientX);
  528. //x轴没变化,y轴方向移动也会触发,要避免不必要的运算
  529. if (temp === endX) {
  530. return;
  531. } else {
  532. endX = temp;
  533. }
  534. endY = Math.ceil(moveEvent.touches[0].screenY);
  535. //console.log("移动到" + endX + "," + endY);
  536. }
  537. //由第一次移动确认手势方向,就不再变更
  538. //10个像素起
  539. if (endX > startX + 10) {
  540. //快进
  541. if (!direction) {
  542. //首次移动,记录方向
  543. direction = 1;
  544. }
  545. if (direction === 1) {
  546. //方向未变化
  547. if (videoElement.duration <= settings.threshold) {
  548. timeChange = Math.round((endX - startX - 10) * settings.sensitivity2);
  549. } else timeChange = Math.round((endX - startX - 10) * settings.sensitivity1);
  550. } else {
  551. timeChange = 0;
  552. }
  553. } else if (endX < startX - 10) {
  554. //快退
  555. if (!direction) {
  556. //首次移动,记录方向
  557. direction = 2;
  558. }
  559. if (direction === 2) {
  560. //方向未变化
  561. if (videoElement.duration <= settings.threshold) {
  562. timeChange = Math.round((endX - startX + 10) * settings.sensitivity2);
  563. } else timeChange = Math.round((endX - startX + 10) * settings.sensitivity1);
  564. } else {
  565. timeChange = 0;
  566. }
  567. } else if (timeChange !== 0) {
  568. timeChange = 0;
  569. } else {
  570. return;
  571. }
  572. if (notice.style.display === "none" /*已经显示了就不管怎么滑动了*/ &&
  573. Math.abs(endY - startY) > Math.abs(endX - startX)) {
  574. //垂直滑动不显示
  575. timeChange = 0;
  576. return;
  577. }
  578. //未到阈值不显示
  579. if (direction) {
  580. notice.style.display = "flex";
  581. notice.innerText = (direction === 1 ? ">>>" : "<<<") + getClearTimeChange(timeChange);
  582. }
  583. }
  584. function touchendHandler() {
  585. if (notice) notice.style.display = "none";
  586. // console.log("手指抬起");
  587. if (settings.voiced) {
  588. videoElement.muted = false;
  589. }
  590. //所有非短视频自带的全视频区域的单击暂停,给他重新播放,手机不适合单击暂停,需要暂停的使用暂停按钮即可
  591. //带延迟是为了让网页自带的js先执行,videoElement.paused的状态才会判断准确
  592. setTimeout(() => {
  593. if (playing && videoElement.paused && !maybeTiktok) {
  594. videoElement.play();
  595. }
  596. //短视频长按后暂停的话也会恢复播放,不通过原组件自动播放可能导致播放控件不消失等问题
  597. if (longPress && maybeTiktok) {
  598. videoElement.play();
  599. }
  600. }, 500);
  601. //一般有chrome自带视频控件的就是没用框架的视频
  602. //需要替换全屏按钮,不然无法显示快进指示器
  603. //非长按后手指抬起时才添加全屏按钮
  604. if (!longPress && videoElement.controls && !document.fullscreenElement) {
  605. let btns = componentContainer.getElementsByClassName("me-fullscreen-btn");
  606. let btn;
  607. if (btns.length === 0) {
  608. btn = document.createElement("div");
  609. btn.style.cssText = sharedCSS + "position:absolute;width:40px;padding:2px;font-size:14px;font-weight:bold;" +
  610. "box-sizing:border-box;border:1px solid white;white-space:normal;line-height:normal;";
  611. btn.innerText = "点我\n全屏";
  612. //设置id是为了防止多次点击重复添加
  613. btn.className = "me-fullscreen-btn";
  614. let divHeight = 40;
  615. btn.style.height = divHeight + "px";
  616. btn.style.top = Math.round(componentContainer.clientHeight / 2 - divHeight / 2 - 10) + "px";
  617. btn.style.left = Math.round(moveNum * screenWidth + componentContainer.clientWidth * 5 / 7) + "px";
  618. componentContainer.append(btn);
  619. btn.addEventListener("touchstart", async function () {
  620. btn.style.display = "none";
  621. await componentContainer.requestFullscreen();
  622. });
  623. //屏蔽原生全屏按钮
  624. videoElement.controlsList = ["nofullscreen"];
  625. } else {
  626. btn = btns[0];
  627. btn.style.display = "flex";
  628. }
  629. setTimeout(() => {
  630. btn.style.display = "none";
  631. }, 2000);
  632. }
  633. //滑动长按判断
  634. if (endX === startX) {
  635. //长按
  636. //console.log("长按");
  637. if (rateTimer) {
  638. //定时器也许已经执行,此时清除也没关系
  639. clearTimeout(rateTimer);
  640. }
  641. if (rateTimerBack) {
  642. clearTimeout(rateTimerBack);
  643. rateTimerBack = null;
  644. }
  645. if (longPress) {
  646. //长按快进结束如果原本有控制器,则恢复
  647. videoElement.controls = haveControls;
  648. videoElement.playbackRate = 1;
  649. }
  650. } else {
  651. if (timeChange !== 0) {
  652. //快进
  653. videoElement.currentTime += timeChange;
  654. }
  655. //console.log("x轴移动" + (endX - startX));
  656. //console.log("y轴移动" + (endY - startY));
  657. }
  658. target.removeEventListener("touchmove", touchmoveHandler);
  659. }
  660. }, {capture: true});
  661. }
  662. //全屏横屏模块
  663. //将浏览器锁定方向的方法改掉,防止网页自带的js执行,当此脚本执行时又把他改回来
  664. //这是因为遇到有网站锁定为any后,且后于此脚本执行,那么手机倒着拿就会直接退出全屏
  665. window.tempLock = screen.orientation.lock;
  666. let myLock = function () {
  667. console.log("网页自带js试图执行lock()")
  668. };
  669. screen.orientation.lock = myLock;
  670. //顶层窗口负责执行横屏,因为iframe可能开启了沙箱机制无法锁定方向并无法修改
  671. //top窗口监听来自iframe的横屏通知,不再使用篡改猴的变量监听,规避浏览器偶发的插件储值访问错误
  672. if (top === window) {
  673. window.addEventListener("message", async (e) => {
  674. if (typeof e.data === 'string' && e.data.includes("MeVideoJS")) {
  675. if (document.fullscreenElement) {
  676. //恢复lock()
  677. screen.orientation.lock = window.tempLock;
  678. await screen.orientation.lock("landscape");
  679. //变向结束再次修改lock()
  680. screen.orientation.lock = myLock;
  681. }
  682. }
  683. });
  684. }
  685. //全屏后触发resize次数,如果有iframe,每个document可不是共用这个值
  686. let inTimes = 0;
  687. //利用window的resize事件监听全屏动作,监听document常用的fullscreenchange事件可能因为后代停止传播而捕获不到
  688. window.addEventListener("resize", () => {
  689. //resize事件或先于全屏事件触发,此时判断是否全屏将出错,所以得设置延迟
  690. setTimeout(fullscreenHandler, 500);
  691. });
  692. function fullscreenHandler() {
  693. //获取全屏元素,查找视频,判断视频长宽比来锁定方向
  694. let _fullscreenElement = document.fullscreenElement;
  695. if (_fullscreenElement) {
  696. //如果全屏元素是iframe,说明不是视频所在的document执行到这,记录也没用
  697. if (_fullscreenElement.tagName === "IFRAME") {
  698. return;
  699. }
  700. //inTimes==1可代表全屏
  701. inTimes++;
  702. } else if (inTimes > 0) {
  703. //此代码块可代表退出全屏
  704. inTimes = 0;
  705. } else {
  706. //退出全屏时多余的触发或者是其他与全屏无关的元素触发resize
  707. return;
  708. }
  709. if (inTimes !== 1) {
  710. return;
  711. }
  712. let videoElement;
  713. if (_fullscreenElement.tagName !== "VIDEO") {
  714. //最大的全屏元素不是视频本身,需要寻找视频元素
  715. let videoArray = _fullscreenElement.getElementsByTagName("video");
  716. if (videoArray.length > 0) {
  717. videoElement = videoArray[0];
  718. if (videoArray.length > 1) {
  719. console.log("全屏元素内找到不止一个视频。");
  720. }
  721. }
  722. } else videoElement = _fullscreenElement;
  723. //也可能不是视频在全屏
  724. if (videoElement) {
  725. let changeHandler = function () {
  726. //高度小于宽度,需要转向,landscape会自动调用陀螺仪
  727. if (videoElement.videoHeight < videoElement.videoWidth) {
  728. //开启沙盒机制的iframe修改sandbox属性无效,需要顶层窗口调用方向锁定
  729. top.postMessage("MeVideoJS", "*");
  730. }
  731. };
  732. //视频未加载,在加载后再判断需不需要转向
  733. if (videoElement.readyState < 1) {
  734. videoElement.addEventListener("loadedmetadata", changeHandler, {once: true});
  735. } else {
  736. changeHandler();
  737. }
  738. }
  739. }
  740. })();