HTML5 Video Playing Tools

Enable hotkeys for HTML5 playback: video screenshot; enable/disable picture-in-picture; copy cached video; send any video to full screen or browser window size; fast forward, rewind, pause/play, volume, skip to next video, skip to previous or next frame, set playback speed. Video sites supported: YouTube, TED, Youku, QQ.com, bilibili, ixigua, iQiyi, support mainstream video sites in mainland China; Live broadcasts: Twitch, Douyu.com, YY.com, Huya.com. Custom sites can be added

  1. /* globals jQuery, $, Vue */
  2. // ==UserScript==
  3. // @name HTML5视频播放工具
  4. // @name:en HTML5 Video Playing Tools
  5. // @name:it Strumenti di riproduzione video HTML5
  6. // @description 视频截图;切换画中画;缓存视频;万能网页全屏;添加快捷键:快进、快退、暂停/播放、音量、下一集、切换(网页)全屏、上下帧、播放速度。支持视频站点:油管、TED、优.土、QQ、B站、西瓜视频、爱奇艺、A站、PPTV、芒果TV、咪咕视频、新浪、微博、网易[娱乐、云课堂、新闻]、搜狐、风行、百度云视频等;直播:twitch、斗鱼、YY、虎牙、龙珠、战旗。可增加自定义站点
  7. // @description:en Enable hotkeys for HTML5 playback: video screenshot; enable/disable picture-in-picture; copy cached video; send any video to full screen or browser window size; fast forward, rewind, pause/play, volume, skip to next video, skip to previous or next frame, set playback speed. Video sites supported: YouTube, TED, Youku, QQ.com, bilibili, ixigua, iQiyi, support mainstream video sites in mainland China; Live broadcasts: Twitch, Douyu.com, YY.com, Huya.com. Custom sites can be added
  8. // @description:it Abilita tasti di scelta rapida per riproduzione HTML5: screenshot del video; abilita/disabilita picture-in-picture; copia il video nella cache; manda qualsiasi video a schermo intero o a dimensione finestra del browser; avanzamento veloce, riavvolgimento, pausa/riproduzione, imposta velocità di riproduzione. Siti video supportati: YouTube, TED, Supporto dei siti video mainstream nella Cina continentale. È possibile aggiungere siti personalizzati
  9. // @homepage https://bbs.kafan.cn/thread-2093014-1-1.html
  10. // @match https://*.qq.com/*
  11. // @exclude https://user.qzone.qq.com/*
  12. // @match https://www.weiyun.com/video_*
  13. // @match https://v.youku.com/v_play/*
  14. // @match https://v.youku.com/v_show/id_*
  15. // @match https://vku.youku.com/live/*
  16. // @match https://video.tudou.com/v/*
  17. // @match https://www.iqiyi.com/*
  18. // @match https://live.bilibili.com/*
  19. // @match https://www.bilibili.com/*
  20. // @match https://www.ixigua.com/*
  21. // @match https://www.toutiao.com/video/*
  22. // @match https://www.acfun.cn/*
  23. // @match https://live.acfun.cn/live/*
  24. // @match http://v.pptv.com/show/*
  25. // @match https://v.pptv.com/show/*
  26. // @match https://www.miguvideo.com/*
  27. // @match https://tv.sohu.com/*
  28. // @match https://film.sohu.com/album/*
  29. // @match https://www.mgtv.com/*
  30. // @version 1.9.9
  31. // @match https://pan.baidu.com/*
  32. // @match https://yun.baidu.com/*
  33. // @match https://*.163.com/*
  34. // @match https://*.icourse163.org/*
  35. // @match http://video.sina.*/*
  36. // @match https://video.sina.*/*
  37. // @match http://k.sina.*/*
  38. // @match https://k.sina.*/*
  39. // @match https://weibo.com/*
  40. // @match https://*.weibo.com/*
  41. // @match https://pan.baidu.com/*
  42. // @match https://yun.baidu.com/*
  43. // @match http://v.ifeng.com/*
  44. // @match https://v.ifeng.com/*
  45. // @match http://news.mtime.com/*
  46. // @match http://video.mtime.com/*
  47. // @GM_info
  48. // @match https://www.youtube.com/*
  49. // @match https://www.ted.com/talks/*
  50. // @match https://www.twitch.tv/*
  51.  
  52. // @match https://www.yy.com/*
  53. // @match https://www.huya.com/*
  54. // @match https://v.douyu.com/*
  55. // @match https://www.douyu.com/*
  56. // @match https://live.douyin.com/*
  57. // @match https://www.douyin.com/*
  58.  
  59. // @match https://www.longzhu.com/*
  60. // @match https://www.zhanqi.tv/*
  61. // @run-at document-start
  62. // @require https://cdn.staticfile.org/vue/2.6.11/vue.min.js
  63. // @require https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js
  64. // @grant GM_addStyle
  65. // @include */play*
  66. // @include *play/*
  67. // @grant window.onurlchange
  68. // @grant unsafeWindow
  69. // @grant GM_download
  70. // @grant GM_openInTab
  71. // @grant GM_notification
  72. // @grant GM_registerMenuCommand
  73. // @grant GM_setValue
  74. // @grant GM_getValue
  75. // @namespace https://greasyfork.org/users/7036
  76. // @license MIT
  77. // ==/UserScript==
  78.  
  79. 'use strict';
  80.  
  81. const curLang = navigator.language.slice(0, 2);
  82. //感谢 Dario Costa 提供的英语和意大利语翻译
  83. const i18n = {
  84. 'zh': {
  85. 'console': '%c脚本[%s] 反馈:%s\n%s',
  86. 'cacheStoringError': '直接媒体类型(如MP4格式)缓存无效果!',
  87. 'cacheStoringConfirm': '视频切片数据能否缓存?检测方法:刷新页面,已观看视频片段不产生网络流量则可缓存。如果能缓存视频切片,选择确认直接缓存全部视频时段;点取消则按默认缓冲区大小进行缓冲。',
  88. 'cantOpenPIP': '无法进入画中画模式!错误:\n',
  89. 'cantExitPIP': '无法退出画中画模式!错误:\n',
  90. 'rememberRateMenuOption': '记忆播放速度',
  91. 'speedRate': '播放速度 ',
  92. 'ready': '准备就绪! 待命中.',
  93. 'mainPageOnly': '只处理主页面',
  94. 'download': '下载: ',
  95. 'videoLag': '视频卡顿',
  96. 'fullScreen': '全屏',
  97. 'helpMenuOption': '脚本功能快捷键表',
  98. 'helpBody': `双击:切换(网页)全屏 鼠标中键:快进5
  99.  
  100. P:视频截图 i:切换画中画 M:(停止)缓存视频(hls.js)
  101. →方向键:快退、快进5秒; 方向键 + shift: 20
  102. ↓方向键:音量调节 ESC:退出(网页)全屏
  103. 空格键:暂停/播放 N:播放下一集
  104. 回车键:切换全屏; 回车键 + shift: 切换网页全屏
  105. C(抖音V):加速0.1 X(抖音S):减速0.1 Z(抖音A):切换加速状态
  106. D:上一帧 F:下一帧(youtube.comE键)`
  107. },
  108. 'en': {
  109. 'console': '%cScript[%s] Feedback:%s\n%s',
  110. 'cacheStoringError': 'Trying to cache direct media types (such as MP4 format) has no effect!',
  111. 'cacheStoringConfirm': 'Do you want all segments of the video to be cached? The detection method used is as follows: when the page is refreshed, the watched video clips will be cached so that no additional network traffic is generated. If you want all segments of the videos to be cached, select OK; or select Cancel to buffer a portion of the video based on the default buffer size (which is the default browser behavior).',
  112. 'cantOpenPIP': 'Unable to access picture-in-picture mode! Error:\n',
  113. 'cantExitPIP': 'Unable to exit picture-in-picture mode! Error:\n',
  114. 'rememberRateMenuOption': 'Remember video playback speed',
  115. 'speedRate': 'Speed rate ',
  116. 'ready': ' ready! Waiting for you commands.',
  117. 'mainPageOnly': 'Process the main page only',
  118. 'download': 'Download: ',
  119. 'videoLag': 'Video lag',
  120. 'fullScreen': 'Full screen',
  121. 'helpMenuOption': 'Hotkeys list:',
  122. 'helpBody': `Double-click: activate full screen.
  123. Middle mouse button: fast forward 5 seconds
  124.  
  125. P key Take a screenshot
  126. I key Enter/Exit picture-in-picture mode
  127. M key Enable/disable caching of video(hls.js)
  128.  
  129. Arrow keys and →: Fast forward or rewind by 5 seconds
  130. Shift + Arrow keys and →: Fast forward or rewind 20 seconds
  131. Arrow keys and ↓: Raise or lower the volume
  132.  
  133. ESC Exit full screen (or exit video enlarged to window size)
  134. Spacebar Stop/Play
  135. Enter Enable/disable full screen video
  136. Shift + Enter: Set/unset video enlarged to window size
  137.  
  138. N key Play the next video (if any)
  139. C key Speed up video playback by 0.1
  140. X key: Slow down video playback by 0.1
  141. Z key, Set video playback speed: 1.0 ←→ X
  142. D key: Previous frame
  143. F key: Next frame (except on YouTube)
  144. E key: Next frame (YouTube only)`
  145. },
  146. 'it': {
  147. 'console': '%cScript[%s] Feedback:%s\n%s',
  148. 'cacheStoringError': 'Cercare di memorizzazione nella cache tipi di media diretti (come ad esempio il formato MP4) non ha alcuna efficacia!',
  149. 'cacheStoringConfirm': 'Vuoi che tutti i segmenti del video siano memorizzati nella cache? Il metodo di rilevamento utilizzato è il seguente: all\'aggiornamento della pagina, i video clip guardati saranno memorizzati nella cache in modo da non generare ulteriore traffico di rete. Se vuoi che tutti i segmenti dei video siano memorizzati nella cache, seleziona OK; seleziona invece Annulla per bufferizzare una parte del video in base alla dimensione predefinita del buffer (come da comportamento predefinito del browser).',
  150. 'cantOpenPIP': 'Impossibile accedere alla modalità picture-in-picture! Errore:\n',
  151. 'cantExitPIP': 'Impossibile uscire dalla modalità picture-in-picture! Errore:\n',
  152. 'rememberRateMenuOption': 'Memorizza la velocità di riproduzione dei video',
  153. 'speedRate': 'Velocità di riproduzione ',
  154. 'ready': "Pronto! In attesa dei comandi dell'utente.",
  155. 'mainPageOnly': 'Elaborazione della sola pagina principale',
  156. 'download': 'Scarica: ',
  157. 'videoLag': 'Ritardo del video',
  158. 'fullScreen': 'Schermo intero',
  159. 'helpMenuOption': 'Elenco dei tasti di scelta rapida',
  160. 'helpBody': `Doppio clic: attiva lo schermo intero
  161. Pulsante centrale del mouse: avanzamento rapido di 5 secondi
  162.  
  163. Tasto P: Esegui uno screenshot
  164. Tasto I Attiva modalità picture-in-picture
  165. Tasto M Attiva/disattiva memorizzazione del video nella cache(hls.js)
  166.  
  167. Tasti freccia e →: Avanza o riavvolgi di 5 secondi
  168. Shift + Tasti freccia e →: Avanza o riavvolgi di 20 secondi
  169. Tasti freccia e ↓: Alza o abbassa il volume
  170. ESC Esci da schermo intero
  171. Barra spaziatrice: Ferma/Riproduci
  172. Invio Attiva/disattiva ingrandimento del video a schermo intero
  173. Shift + Invio: Attiva/disattiva ingrandimento del video a dimensione della finestra
  174.  
  175. Tasto N Riproduzione del video successivo (se presente)
  176. Tasto C: Velocizza riproduzione video di 0,1
  177. Tasto X: Rallenta riproduzione video di 0,1
  178. Tasto Z, Impostare la velocità di riproduzione video: 1,0 ←→ X
  179. Tasto D: Vai al frame precedente
  180. Tasto F: Vai al frame successivo (escluso YouTube)
  181. Tasto E: Vai al frame successivo (solo su YouTube)`
  182. }
  183. };
  184. const MSG = i18n[curLang] || i18n.en;
  185.  
  186. const w = unsafeWindow || window;
  187. const { host, pathname: path } = location;
  188. const d = document, find = [].find;
  189. let $msg, v, _fp, _fs, by; // document.body
  190. const observeOpt = {childList : true, subtree : true};
  191. const noopFn = function(){};
  192. const validEl = e => e && e.offsetWidth > 1;
  193. const q = (css, p = d) => p.querySelector(css);
  194. const r1 = (regp, s) => regp.test(s) && RegExp.$1;
  195. const log = console.log.bind(
  196. console,
  197. MSG.console,
  198. 'color:#c3c;font-size:1.2em',
  199. GM_info.script.name,
  200. GM_info.script.homepage
  201. );
  202. const gmFuncOfCheckMenu = (title, saveName, defaultVal = true) => {
  203. const r = GM_getValue(saveName, defaultVal);
  204. if (r) title = '√ '+ title;
  205. GM_registerMenuCommand(title, () => {
  206. GM_setValue(saveName, !r);
  207. location.reload();
  208. });
  209. return r;
  210. };
  211. const sleep = ms => new Promise(resolve => { setTimeout(resolve, ms) });
  212. /* 画中画
  213. <svg viewBox="0 0 22 22"><g fill="#E6E6E6" fill-rule="evenodd"><path d="M17 4a2 2 0 012 2v6h-2V6.8a.8.8 0 00-.8-.8H4.8a.8.8 0 00-.794.7L4 6.8v8.4a.8.8 0 00.7.794l.1.006H11v2H4a2 2 0 01-2-2V6a2 2 0 012-2h13z"></path><rect x="13" y="14" width="8" height="6" rx="1"></rect></g></svg>
  214. 设置
  215. <svg viewBox="0 0 22 22">
  216. <circle cx="11" cy="11" r="2"></circle>
  217. <path d="M19.164 8.861L17.6 8.6a6.978 6.978 0 00-1.186-2.099l.574-1.533a1 1 0 00-.436-1.217l-1.997-1.153a1.001 1.001 0 00-1.272.23l-1.008 1.225a7.04 7.04 0 00-2.55.001L8.716 2.829a1 1 0 00-1.272-.23L5.447 3.751a1 1 0 00-.436 1.217l.574 1.533A6.997 6.997 0 004.4 8.6l-1.564.261A.999.999 0 002 9.847v2.306c0 .489.353.906.836.986l1.613.269a7 7 0 001.228 2.075l-.558 1.487a1 1 0 00.436 1.217l1.997 1.153c.423.244.961.147 1.272-.23l1.04-1.263a7.089 7.089 0 002.272 0l1.04 1.263a1 1 0 001.272.23l1.997-1.153a1 1 0 00.436-1.217l-.557-1.487c.521-.61.94-1.31 1.228-2.075l1.613-.269a.999.999 0 00.835-.986V9.847a.999.999 0 00-.836-.986zM11 15a4 4 0 110-8 4 4 0 010 8z"></path>
  218. </svg>
  219. next
  220. <svg viewBox="0 0 22 22"><path d="M16 5a1 1 0 00-1 1v4.615a1.431 1.431 0 00-.615-.829L7.21 5.23A1.439 1.439 0 005 6.445v9.11a1.44 1.44 0 002.21 1.215l7.175-4.555a1.436 1.436 0 00.616-.828V16a1 1 0 002 0V6C17 5.448 16.552 5 16 5z"></path></svg>
  221. 截图
  222. <svg version="1.1" viewBox="0 0 32 32"><path d="M16 23c-3.309 0-6-2.691-6-6s2.691-6 6-6 6 2.691 6 6-2.691 6-6 6zM16 13c-2.206 0-4 1.794-4 4s1.794 4 4 4c2.206 0 4-1.794 4-4s-1.794-4-4-4zM27 28h-22c-1.654 0-3-1.346-3-3v-16c0-1.654 1.346-3 3-3h3c0.552 0 1 0.448 1 1s-0.448 1-1 1h-3c-0.551 0-1 0.449-1 1v16c0 0.552 0.449 1 1 1h22c0.552 0 1-0.448 1-1v-16c0-0.551-0.448-1-1-1h-11c-0.552 0-1-0.448-1-1s0.448-1 1-1h11c1.654 0 3 1.346 3 3v16c0 1.654-1.346 3-3 3zM24 10.5c0 0.828 0.672 1.5 1.5 1.5s1.5-0.672 1.5-1.5c0-0.828-0.672-1.5-1.5-1.5s-1.5 0.672-1.5 1.5zM15 4c0 0.552-0.448 1-1 1h-4c-0.552 0-1-0.448-1-1v0c0-0.552 0.448-1 1-1h4c0.552 0 1 0.448 1 1v0z"></path></svg>
  223. const cookie = new Proxy(noopFn, {
  224. apply(target, ctx, args) { //清理cookie
  225. const keys = document.cookie.match(/[^ =;]+(?=\=)/g);
  226. if (keys) {
  227. const val = '=; expires=' + new Date(0).toUTCString() +'; domain=.; path=/';
  228. for (const k of keys) document.cookie = k + val;
  229. }
  230. // return Reflect.apply(target, ctx, args);
  231. },
  232. get(target, name) { // 读取cookie
  233. const r = r1(new RegExp(name +'=([^;]*)'), document.cookie);
  234. if (r) return decodeURIComponent(r);
  235. },
  236. set(target, name, value, receiver) { // 写入cookie
  237. let s, v, expires,
  238. oneParam = typeof value == 'string';
  239. if (oneParam) {
  240. expires = 6;
  241. v = value;
  242. } else {
  243. v = value.val;
  244. expires = value.expires || 6;
  245. delete value.expires;
  246. }
  247. s = name + '=' + encodeURIComponent(v);
  248.  
  249. if (expires && (typeof expires == 'number' || expires.toUTCString)) {
  250. let date;
  251. if (typeof expires == 'number') {
  252. date = new Date();
  253. date.setTime(expires * 24 * 3600000 + date.getTime());
  254. } else {
  255. date = expires;
  256. }
  257. s += '; expires=' + date.toUTCString();
  258. }
  259. if (!oneParam) for (const k in value) s += '; ' + k + '=' + value[k];
  260. document.cookie = s;
  261. return true;
  262. },
  263. deleteProperty(target, name, descriptor) {// 删除cookie
  264. document.cookie = name + '=; path=/; expires='+ new Date(0).toUTCString();
  265. return true;
  266. }
  267. });
  268. const onceEvent = (ctx, eName) => new Promise(resolve => ctx.addEventListener(eName, resolve));
  269. const promisify = (fn) => (...args) => new Promise((resolve, reject) => {
  270. args.push(resolve);
  271. fn.apply(this, args);
  272. }); */
  273. const hookAttachShadow = (cb) => {
  274. try {
  275. const _attachShadow = Element.prototype.attachShadow;
  276. Element.prototype.attachShadow = function(opt) {
  277. opt.mode = 'open';
  278. const shadowRoot = _attachShadow.call(this, opt);
  279. cb(shadowRoot);
  280. return shadowRoot;
  281. };
  282. } catch (e) {
  283. console.error('Hack attachShadow error', e);
  284. }
  285. };
  286. const getStyle = (o, s) => {
  287. if (o.style[s]) return o.style[s];
  288. if (getComputedStyle) {
  289. const x = getComputedStyle(o, '');
  290. s = s.replace(/([A-Z])/g,'-$1').toLowerCase();
  291. return x && x.getPropertyValue(s);
  292. }
  293. };
  294. const doClick = e => {
  295. if (typeof e === 'string') e = q(e);
  296. if (e) { e.click ? e.click() : e.dispatchEvent(new MouseEvent('click')) };
  297. };
  298. const clickDualButton = btn => { // 2合1 按钮 Element.previousElementSibling
  299. !btn.nextSibling || getStyle(btn, 'display') !== 'none' ? doClick(btn) : doClick(btn.nextSibling);
  300. };
  301. const polling = (cb, condition, stop = true) => {
  302. const fn = typeof condition === 'string' ? q.bind(null, condition) : condition;
  303. const t = setInterval(() => {
  304. const r = fn();
  305. if (r) {
  306. stop && clearInterval(t);
  307. cb(r);
  308. }
  309. }, 300);
  310. return t;
  311. };
  312. const goNextMV = () => {
  313. const s = location.pathname;
  314. const m = s.match(/(\d+)(\D*)$/);
  315. const d = +m[1] + 1;
  316. location.assign(s.slice(0, m.index) + d + m[2]);
  317. };
  318. const firefoxVer = r1(/Firefox\/(\d+)/, navigator.userAgent);
  319. const isEdge = / Edge?\//.test(navigator.userAgent);
  320. const fakeUA = ua => Object.defineProperty(navigator, 'userAgent', {
  321. value: ua,
  322. writable: false,
  323. configurable: false,
  324. enumerable: true
  325. });
  326. const getMainDomain = host => {
  327. const a = host.split('.');
  328. let i = a.length - 2;
  329. if (/^(com?|cc|tv|net|org|gov|edu)$/.test(a[i])) i--;
  330. return a[i];
  331. };
  332. const inRange = (n, min, max) => Math.max(min, n) == Math.min(n, max);
  333. const adjustRate = n => {
  334. n += v.playbackRate;
  335. if (n < 0.1) v.playbackRate = .1;
  336. else if (n > 16) v.playbackRate = 16;
  337. else v.playbackRate = +n.toFixed(2);
  338. };
  339. const adjustVolume = n => {
  340. n += v.volume;
  341. if (inRange(n, 0, 1)) v.volume = +n.toFixed(2);
  342. };
  343. const tip = (msg) => {
  344. if (!$msg?.get(0)?.offsetHeight) $msg = $('<div style="max-width:455px;min-width:333px;background:#EEE;color:#111;height:22px;top:-30px;left:50%;transform:translate(-50%, 0); border-radius:8px;border:1px solid orange;text-align:center;font-size:15px;position:fixed;z-index:2147483647"></div>').appendTo(by);
  345. if (!msg?.length) return;
  346. const len = msg.length * 15;
  347. $msg.stop(true, true).text(msg)
  348. .css({width:`${len}px`})
  349. .animate({top:'190px'})
  350. .animate({top:'+=9px'},1900)
  351. .animate({top:'-30px'});
  352. };
  353. const ua_chrome = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.3626.121 Safari/537.36';
  354. const u = getMainDomain(host);
  355. const cfg = {
  356. isLive: !1,
  357. disableDBLClick: !1,
  358. isClickOnVideo: !1,
  359. multipleV: !1, //多视频页面
  360. isNumURL: !1 //网址数字分集
  361. };
  362. const bus = new Vue();
  363. if (window.onurlchange === void 0) {
  364. history.pushState = ( f => function pushState(){
  365. const ret = f.apply(this, arguments);
  366. window.dispatchEvent(new Event('pushstate'));
  367. window.dispatchEvent(new Event('urlchange'));
  368. return ret;
  369. })(history.pushState);
  370.  
  371. history.replaceState = ( f => function replaceState(){
  372. const ret = f.apply(this, arguments);
  373. window.dispatchEvent(new Event('replacestate'));
  374. window.dispatchEvent(new Event('urlchange'));
  375. return ret;
  376. })(history.replaceState);
  377.  
  378. window.addEventListener('popstate',()=>{
  379. window.dispatchEvent(new Event('urlchange'))
  380. });
  381. };
  382.  
  383. class FullScreen {
  384. constructor(e) {
  385. let fn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen || noopFn;
  386. this.exit = fn.bind(d);
  387. fn = e.requestFullscreen || e.webkitRequestFullScreen || e.mozRequestFullScreen || e.msRequestFullScreen || noopFn;
  388. this.enter = fn.bind(e);
  389. }
  390. static isFull() {
  391. return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen ||
  392. d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement);
  393. }
  394. toggle() {
  395. FullScreen.isFull() ? this.exit() : this.enter();
  396. }
  397. }
  398.  
  399. //万能网页全屏, 参考了:https://github.com/gooyie/ykh5p
  400. class FullPage {
  401. constructor(container) {
  402. this._isFull = !1;
  403. this.container = container || FullPage.getPlayerContainer(v);
  404. GM_addStyle(
  405. `.gm-fp-body .gm-fp-zTop {
  406. position: relative !important;
  407. z-index: 2147483646 !important;
  408. }
  409. .gm-fp-wrapper, .gm-fp-body{ overflow:hidden !important; }
  410. .gm-fp-wrapper .gm-fp-innerBox {
  411. width: 100% !important;
  412. height: 100% !important;
  413. }
  414. .gm-fp-wrapper {
  415. display: block !important;
  416. position: fixed !important;
  417. width: 100% !important;
  418. height: 100% !important;
  419. padding: 0 !important;
  420. margin: 0 !important;
  421. top: 0 !important;
  422. left: 0 !important;
  423. background: #000 !important;
  424. z-index: 2147483646 !important;
  425. }`
  426. );
  427. }
  428. static getPlayerContainer(video) {
  429. let e = video, p = e.parentNode;
  430. const { clientWidth: wid, clientHeight: h } = e;
  431. do {
  432. e = p;
  433. p = e.parentNode;
  434. } while (p && p !== by && p.clientWidth-wid < 5 && p.clientHeight-h < 5);
  435. //e 为返回值,在此之后不能变了
  436. // while (p !== by) p = p.parentNode || p.host;
  437. return e;
  438. }
  439. static isFull(e) {
  440. return w.innerWidth - e.clientWidth < 5 && w.innerHeight - e.clientHeight < 5;
  441. }
  442. toggle() {
  443. // assert(this.container);
  444. if (!this.container.contains(v)) this.container = FullPage.getPlayerContainer(v);
  445. bus.$emit('switchFP', !this._isFull);
  446. by.classList.toggle('gm-fp-body');
  447. let e = v;
  448. while (e != this.container) {
  449. e.classList.toggle('gm-fp-innerBox');
  450. e = e.parentNode;
  451. }
  452. e.classList.toggle('gm-fp-wrapper');
  453. e = e.parentNode;
  454. while (e != by) {
  455. e.classList.toggle('gm-fp-zTop');
  456. e = e.parentNode;
  457. }
  458. this._isFull = !this._isFull;
  459. }
  460. }
  461.  
  462. const cacheMV = {
  463. check() {
  464. const buf = v.buffered;
  465. const i = buf.length - 1;
  466. this.iEnd = buf.end(i);
  467. return this.mode ? this.iEnd > v.duration -55 : buf.start(0) >= this.playPos || this.iEnd > v.duration -55;
  468. },
  469. finish() {
  470. v.removeEventListener('canplaythrough', this.onChache);
  471. v.currentTime = this.playPos;
  472. this.chached = !1;
  473. setTimeout(_ => v.pause(), 99);
  474. HTMLMediaElement.prototype.play = this.rawPlay;
  475. },
  476. onChache() {
  477. if (this.check()) this.finish();
  478. else v.currentTime = this.iEnd;
  479. },
  480. exec() {
  481. if (cfg.isLive || !v) return;
  482. if (v.src.startsWith('http')) {
  483. alert(MSG.cacheStoringError);
  484. return;
  485. }
  486. this.mode = confirm(MSG.cacheStoringConfirm);
  487. this.chached = true; //正在缓存
  488. v.pause();
  489. this.rawPlay = HTMLMediaElement.prototype.play;
  490. HTMLMediaElement.prototype.play = () => new Promise(noopFn);
  491. this.playPos = v.currentTime;
  492. v.addEventListener('canplaythrough', this.onChache);
  493. this.check();
  494. v.currentTime = this.iEnd;
  495. }
  496. };
  497. cacheMV.onChache = cacheMV.onChache.bind(cacheMV);
  498.  
  499. const actList = new Map();
  500. actList.set(90, _ => { //按键Z: 切换加速状态
  501. if (v.playbackRate == 1 || v.playbackRate == 0) {
  502. v.playbackRate = +localStorage.mvPlayRate || 1.3;
  503. } else {
  504. // localStorage.mvPlayRate = v.playbackRate;
  505. v.playbackRate = 1;
  506. }
  507. })
  508. .set(88, adjustRate.bind(null, -0.1)) //按键X
  509. .set(67, adjustRate.bind(null, 0.1)) //按键C
  510. .set(40, adjustVolume.bind(null, -0.1)) //↓ 降音量
  511. .set(38, adjustVolume.bind(null, 0.1)) //↑ 加音量
  512. .set(37, _ => {v.currentTime -= 5}) //按键←
  513. .set(37+1024, _ => {v.currentTime -= 20}) //按键shift+←
  514. .set(39, _ => {v.currentTime += 5}) //按键→
  515. .set(39+1024, _ => {v.currentTime += 20}) //按键shift+→
  516. .set(68, _ => {v.currentTime -= 0.03;v.pause()}) //按键D:上一帧
  517. .set(70, _ => {v.currentTime += 0.03;v.pause()}) //按键F:下一帧
  518. .set(32, _ => { //按键space
  519. if (cfg.btnPlay) clickDualButton(cfg.btnPlay);
  520. else v.paused ? v.play() : v.pause();
  521. })
  522. .set(13, _ => { //回车键。 全屏
  523. _fs ? _fs.toggle() : clickDualButton(cfg.btnFS);
  524. })
  525. .set(13+1024, _ => {//web全屏
  526. self != top ? top.postMessage({id: 'gm-h5-toggle-iframeWebFull'}, '*')
  527. : _fp ? _fp.toggle() : clickDualButton(cfg.btnFP);
  528. })
  529. .set(27+1024, noopFn) //忽略按键shift + esc
  530. .set(27, ev => { //按键esc
  531. if (FullScreen.isFull()) {
  532. _fs ? _fs.exit() : clickDualButton(cfg.btnFS);
  533. } else if (self != top) {
  534. top.postMessage({id: 'gm-h5-is-iframeWebFull'}, '*');
  535. } else if (FullPage.isFull(v)) {
  536. _fp ? _fp.toggle() : clickDualButton(cfg.btnFP);
  537. }
  538. })
  539. .set(73, _ => { //按键I:画中画模式
  540. if (!d.pictureInPictureElement) {
  541. v.requestPictureInPicture().catch(err => {
  542. alert(MSG.cantOpenPIP + err)
  543. });
  544. } else {
  545. d.exitPictureInPicture().catch(err => {
  546. alert(MSG.cantExitPIP + err)
  547. });
  548. }
  549. })
  550. .set(80, _ => { //按键P:截图
  551. const canvas = d.createElement('canvas');
  552. canvas.width = v.videoWidth;
  553. canvas.height = v.videoHeight;
  554. canvas.getContext('2d').drawImage(v, 0, 0, canvas.width, canvas.height);
  555.  
  556. canvas.toBlob((blob) => {
  557. const dataURL = URL.createObjectURL(blob);
  558. GM_download({
  559. url: dataURL,
  560. name: Date.now().toString(36) +'.png',
  561. onloadend: _ => {URL.revokeObjectURL(dataURL);}
  562. });
  563. /*
  564. const link = d.createElement('a');
  565. link.href = dataURL;
  566. link.download = Date.now().toString(36) +'.png';
  567. link.style.display = 'none';
  568. d.body.appendChild(link);
  569. link.click();
  570. link.remove(); */
  571. });
  572. })
  573. .set(77, _ => {// M 缓存视频
  574. cacheMV.chached ? cacheMV.finish() : cacheMV.exec();
  575. })
  576. .set(78, _ => {// N 下一集
  577. if (self != top) top.postMessage({id: 'gm-h5-play-next'}, '*');
  578. else if (cfg.btnNext) doClick(cfg.btnNext);
  579. else if (cfg.isNumURL) goNextMV();
  580. });
  581.  
  582. const app = {
  583. rawProps: new Map(),
  584. shellEvent() {
  585. const fn = ev => {
  586. if (ev.target.closest('svg,img,button')) return;
  587. ev.stopPropagation(); // preventDefault
  588. ev.stopImmediatePropagation();
  589. this.checkUI();
  590. actList.get(1037)(); //web全屏
  591. };
  592. const e = cfg.isClickOnVideo ? v : cfg.mvShell;
  593. e.addEventListener('mousedown', ev => {
  594. if (1 == ev.button) {
  595. ev.preventDefault();
  596. ev.stopPropagation();
  597. ev.stopImmediatePropagation();
  598. if (!cfg.isLive) {
  599. actList.has(39) ? actList.get(39)() : v.currentTime += 5;
  600. }
  601. }
  602. });
  603. !cfg.disableDBLClick && e.addEventListener('dblclick', fn);
  604. },
  605. setShell() {
  606. const e = this.getDPlayer() || this.getArtplayer() || this.getVjsPlayer() ||
  607. (cfg.shellCSS && q(cfg.shellCSS)) ||
  608. (top != self ? by : FullPage.getPlayerContainer(v));
  609. if (e && cfg.mvShell !== e) {
  610. cfg.mvShell = e;
  611. this.shellEvent();
  612. }
  613. },
  614. checkMV() {
  615. if (this.vList) {
  616. const e = this.findMV();
  617. if (e && e != v) {
  618. v = e;
  619. cfg.btnPlay = cfg.btnNext = cfg.btnFP = cfg.btnFS = _fs = _fp = null;
  620. if (!cfg.isLive && GM_getValue('remberRate', true)) {
  621. v.playbackRate = +localStorage.mvPlayRate || 1;
  622. v.addEventListener('ratechange', ev => {
  623. if (v.playbackRate && v.playbackRate != 1) localStorage.mvPlayRate = v.playbackRate;
  624. });
  625. }
  626. this.setShell();
  627. }
  628. }
  629. if (!validEl(cfg.mvShell)) {
  630. cfg.mvShell = null;
  631. this.setShell();
  632. }
  633. this.checkUI();
  634. return v;
  635. },
  636. getArtplayer() {
  637. const e = v.parentNode;
  638. if (!v.matches('.art-video') || !e.matches('.art-video-player')) return !1;
  639. cfg.btnFP = q('.art-control-fullscreenWeb', e);
  640. cfg.btnFS = q('.art-control-fullscreen', e);
  641. e.closest('body > *')?.classList.add('gm-dp-zTop');
  642. return e;
  643. },
  644. getDPlayer() {
  645. if (!v.matches('.dplayer-video')) return !1;
  646. const e = v.closest('.dplayer');
  647. if (e) {
  648. cfg.btnFP = q('.dplayer-full-in-icon > span', e);
  649. cfg.btnFS = q('.dplayer-full-icon', e);
  650. e.closest('body > *').classList.add('gm-dp-zTop');
  651. }
  652. return e;
  653. },
  654. getVjsPlayer() {
  655. const e = v.closest('.video-js');
  656. if (e) {
  657. cfg.btnFS = q('.vjs-control-bar > button.vjs-button:nth-last-of-type(1)');
  658. cfg.webfullCSS = '.vjs-control-bar > button.vjs-button[title$="全屏"]:nth-last-of-type(2)';
  659. }
  660. return e;
  661. },
  662. hotKey(e) {
  663. const t = e.target;
  664. if (e.ctrlKey || e.metaKey || e.altKey || t.contentEditable=='true' || // e.isComposing
  665. /INPUT|TEXTAREA|SELECT/.test(t.nodeName)) return;
  666. if (e.shiftKey && ![13,37,39].includes(e.keyCode)) return;
  667. if (e.shiftKey && e.keyCode == 27) return;
  668. if (cfg.isLive && [37,39,78,77,88,67,90].includes(e.keyCode)) return;
  669. if (!this.checkMV()) return;
  670. if (!e.shiftKey && cfg.mvShell && cfg.mvShell.contains(t) && [32,37,39].includes(e.keyCode)) return;
  671. const key = e.shiftKey ? e.keyCode + 1024 : e.keyCode;
  672. if (actList.has(key)) {
  673. e.stopImmediatePropagation();
  674. e.stopPropagation();
  675. e.preventDefault();
  676. actList.get(key)(e);
  677. if ([67,88,90].includes(e.keyCode)) tip(MSG.speedRate + v.playbackRate);
  678. }
  679. },
  680. checkUI() {
  681. if (cfg.webfullCSS && !validEl(cfg.btnFP)) cfg.btnFP = q(cfg.webfullCSS);
  682. if (cfg.btnFP) _fp = null;
  683. else if (!_fp && self == top) _fp = new FullPage(cfg.mvShell);
  684.  
  685. if (cfg.fullCSS && !validEl(cfg.btnFS)) cfg.btnFS = q(cfg.fullCSS);
  686. if (cfg.btnFS) _fs = null;
  687. else if (!_fs) _fs = new FullScreen(v);
  688.  
  689. if (cfg.nextCSS && (!validEl(cfg.btnNext) || !cfg.btnNext.matches(cfg.nextCSS))) cfg.btnNext = q(cfg.nextCSS);
  690. if (cfg.playCSS && !validEl(cfg.btnPlay)) cfg.btnPlay = q(cfg.playCSS);
  691. },
  692. onGrowVList() {
  693. if (this.vList.length == this.vCount) return;
  694. if (this.viewObserver) {
  695. for (let e of this.vList) {
  696. if (!this.vSet.has(e)) this.viewObserver.observe(e);
  697. }
  698. } else {
  699. const config = {
  700. rootMargin: '0px',
  701. threshold: 0.9
  702. };
  703. this.viewObserver = new IntersectionObserver(this.onIntersection.bind(this), config);
  704. for (let e of this.vList) this.viewObserver.observe(e);
  705. }
  706. this.vSet = new Set(this.vList);
  707. this.vCount = this.vList.length;
  708. },
  709. onIntersection(entries) {
  710. if (this.vList.length < 2) return;
  711. const entry = find.call(entries, k => k.isIntersecting);
  712. if (!entry || v == entry.target) return;
  713. v = entry.target;
  714. _fs = new FullScreen(v);
  715. _fp = new FullPage(v);
  716. bus.$on('switchFP', async (toFull) => {
  717. // const c = toFull ? this.vSet : this.vList;
  718. // for (const e of c) this.viewObserver.unobserve(e);
  719. sleep(200);
  720. if (!toFull) v.scrollIntoView();
  721. });
  722. bus.$emit('switchMV');
  723. },
  724. bindEvent() {
  725. clearInterval(this.timer);
  726. for (const [i,k] of this.rawProps) Reflect.defineProperty(HTMLVideoElement.prototype, i, k);
  727. this.rawProps.clear();
  728. this.rawProps = null;
  729. $(cfg.adsCSS).remove();
  730. by = d.body;
  731. log('bind event\n', v);
  732. bus.$emit('foundMV');
  733. const bRate = gmFuncOfCheckMenu(MSG.rememberRateMenuOption,'remberRate');
  734. window.addEventListener('urlchange', async (info) => { //TM event: info.url
  735. await sleep(1990);
  736. this.checkMV();
  737. if (bRate) v.playbackRate = +localStorage.mvPlayRate || 1;
  738. bus.$emit('urlchange');
  739. });
  740. if (top != self) {
  741. top.postMessage({id: 'gm-h5-init-MVframe'}, '*');
  742. window.addEventListener("message", ev => {
  743. if (!ev.source || !ev.data || !ev.data.id) return;
  744. switch (ev.data.id) {
  745. case 'gm-h5-toggle-fullScreen':
  746. _fs ? _fs.toggle() : clickDualButton(cfg.btnFS);
  747. break;
  748. }
  749. }, false);
  750. }
  751. $(v).one('canplaythrough', ev => {
  752. if (!cfg.isLive) {
  753. if (bRate) v.playbackRate = +localStorage.mvPlayRate || 1;
  754. v.addEventListener('ratechange', ev => {
  755. if (bRate && v.playbackRate && v.playbackRate != 1) localStorage.mvPlayRate = v.playbackRate;
  756. });
  757. }
  758. this.checkMV();
  759. bus.$emit('canplay');
  760. });
  761. $(by).keydown(this.hotKey.bind(this));
  762.  
  763. cfg.mvShell ? this.shellEvent() : this.setShell();
  764. this.checkUI();
  765. if (cfg.multipleV) {
  766. new MutationObserver(this.onGrowVList.bind(this)).observe(by, observeOpt);
  767. this.vCount = 0;
  768. this.onGrowVList();
  769. }
  770. // tip((GM_info.script.name_i18n?.[curLang] || GM_info.script.name) + MSG.ready);
  771. },
  772. init() {
  773. const rawAel = EventTarget.prototype.addEventListener;
  774. EventTarget.prototype.addEventListener = function(...args) {
  775. const block = (args[0] == 'dblclick' && !args[1].toString().includes('actList.get(1037)'))
  776. || (args[0] == 'ratechange' && 'baidu'== u && !args[1].toString().includes('localStorage.mvPlayRate'));
  777. if (!block) return rawAel.apply(this, args);
  778. };
  779. for (const i of this.rawProps.keys()) this.rawProps.set(i,
  780. Reflect.getOwnPropertyDescriptor(HTMLMediaElement.prototype, i));
  781. this.vList = d.getElementsByTagName('video');
  782. const fn = e => cfg.cssMV ? e.matches(cfg.cssMV) : e.offsetWidth > 9;
  783. this.findMV = find.bind(this.vList, fn);
  784. this.timer = polling(e => {
  785. v = e;
  786. this.bindEvent();
  787. }, this.findMV);
  788.  
  789. hookAttachShadow(async shadowRoot => {
  790. bus.$emit('addShadowRoot', shadowRoot);
  791. await sleep(600);
  792. if (v) return;
  793. if (v = q('video', shadowRoot)) { // v.getRootNode() == shadowRoot
  794. log('Found MV in ShadowRoot\n', v, shadowRoot);
  795. if (!cfg.shellCSS) cfg.mvShell = shadowRoot.host;
  796. this.bindEvent();
  797.  
  798. this.vList = shadowRoot.getElementsByTagName('video');
  799. this.findMV = find.bind(this.vList, fn);
  800. }
  801. });
  802. }
  803. };
  804.  
  805. let router = {
  806. ted() {
  807. cfg.fullCSS = 'button[title=Fullscreen]';
  808. },
  809. youtube() {
  810. GM_addStyle(
  811. `.gm-fp-body #player-container-inner{padding-top:0!important}
  812. .gm-fp-body #player-container-outer{
  813. max-width:100%!important;
  814. margin:0!important;
  815. }`
  816. );
  817. cfg.shellCSS = '#player';
  818. cfg.playCSS = 'button.ytp-play-button';
  819. cfg.nextCSS = 'a.ytp-next-button';
  820. cfg.fullCSS = 'button.ytp-fullscreen-button';
  821. cfg.isClickOnVideo = true;
  822. actList.delete(32);
  823. actList.set(69, actList.get(70)).delete(70); //F键 >> E键
  824. },
  825. douyin() {
  826. cfg.isLive = host.startsWith('live.');
  827. cfg.fullCSS = '.xgplayer-fullscreen';
  828. // cfg.webfullCSS = cfg.isLive ? '.xgplayer-fullscreen + xg-icon' : '.xgplayer-page-full-screen';
  829. if (!cfg.isLive) {
  830. GM_addStyle('.xgplayer-progress-cache{background-color:green!important}');
  831. actList.set(65, actList.get(90)).delete(90); //Z键 >> A键
  832. actList.set(83, actList.get(88)).delete(88); //X键 >> S键
  833. actList.set(86, actList.get(67)).delete(67); //C键 >> V键
  834. }
  835. },
  836. qq() {
  837. if (self != top &&(host == 'v.qq.com' || host == 'video.qq.com') ) throw MSG.mainPageOnly;
  838. actList.delete(32);
  839. cfg.shellCSS = '#player';
  840. cfg.nextCSS = '.txp_btn_next_u';
  841. cfg.webfullCSS = '.txp_btn_fake';
  842. cfg.fullCSS = '.txp_btn_fullscreen';
  843. // w.__PLAYER__ || w.PLAYER
  844. app.rawProps.set('playbackRate', 1);
  845. },
  846. youku() {
  847. actList.delete(37);
  848. actList.delete(39);
  849. if (host.startsWith('vku.')) {
  850. bus.$on('canplay', () => {
  851. cfg.isLive = !q('.spv_progress');
  852. });
  853. cfg.fullCSS = '.live_icon_full';
  854. } else {
  855. bus.$on('foundMV',() => { $(document).unbind('keyup') });
  856. cfg.shellCSS = '#ykPlayer';
  857. cfg.webfullCSS = '.kui-webfullscreen-icon-0';
  858. cfg.fullCSS = '.kui-fullscreen-icon-0';
  859. cfg.nextCSS = '.kui-next-icon-0';
  860. }
  861. },
  862. bilibili() {
  863. cfg.isLive = host.startsWith('live.');
  864. if (cfg.isLive) return;
  865. actList.delete(32);
  866.  
  867. bus.$on('addShadowRoot', r => {
  868. if (r.host.nodeName === 'BWP-VIDEO') {
  869. app.vList = d.getElementsByTagName('bwp-video');
  870. app.findMV = find.bind(app.vList, e => e.offsetWidth > 9);
  871. v = r.host;
  872. app.bindEvent();
  873. }
  874. });
  875. cfg.shellCSS = 'div[aria-label="哔哩哔哩播放器"]';
  876. cfg.nextCSS = '.bpx-player-ctrl-next';
  877. cfg.webfullCSS = '.bpx-player-ctrl-web';
  878. cfg.fullCSS = '.bpx-player-ctrl-full';
  879. /*
  880. const seek = function(step) {
  881. const p = this.player;
  882. p.seek(p.getCurrentTime()+ step, p.getState() === "PAUSED");
  883. };
  884. actList.set(38, _ => w.player.volume(w.player.volume()+0.1)) //加音量
  885. .set(40, _ => w.player.volume(w.player.volume()-0.1))
  886. .set(37, seek.bind(w, -5))
  887. .set(37+1024, seek.bind(w, -20)) //shift+left 快退20秒
  888. .set(39, seek.bind(w, 5))
  889. .set(39+1024, seek.bind(w, 20)) //shift+→ 快进20秒
  890. .set(70, seek.bind(w, 0.03)) //按键F:下一帧
  891. .set(68, seek.bind(w, -0.03)); //按键D:上一帧
  892. */
  893. },
  894. iqiyi() {
  895. cfg.fullCSS = '.iqp-btn-fullscreen:not(.fake__click)';
  896. cfg.nextCSS = '.iqp-btn-next';
  897. },
  898. pptv() {
  899. cfg.fullCSS = '.w-zoom-container > div';
  900. cfg.webfullCSS = '.w-expand-container > div';
  901. cfg.nextCSS = '.w-next';
  902. },
  903. mgtv() {
  904. cfg.fullCSS = 'mango-screen';
  905. cfg.webfullCSS = 'mango-webscreen > a';
  906. cfg.nextCSS = 'mango-control-playnext-btn';
  907. },
  908. ixigua() {
  909. cfg.fullCSS = 'div[aria-label="全屏"]';
  910. cfg.nextCSS = '.xgplayer-control-item.control_playnext';
  911. GM_addStyle('.gm-fp-body .xgplayer{padding-top:0!important} .gm-fp-wrapper #player_default{max-height: 100%!important} h1.title~a, .videoTitle h1~a{ padding-left:12px; color:blue; }');
  912.  
  913. bus.$on('foundMV',() => {
  914. const data = w._SSR_HYDRATED_DATA.anyVideo.gidInformation.packerData;
  915. const c = (data.video || data).videoResource.normal.video_list;
  916. const title = document.title.split(' - ')[0];
  917. const s = Object.keys(c).map(k =>
  918. `<a href="${c[k].main_url}" download="${title}_${c[k].definition}.mp4" target="_blank">${c[k].definition}</a>`
  919. ).join('   ');
  920. $('.videoTitle h1, h1.title').text(MSG.download).after(s);
  921. });
  922. },
  923. miguvideo() {
  924. cfg.nextCSS = '.next-btn';
  925. cfg.fullCSS = '.zoom-btn';
  926. cfg.shellCSS = '.mod-player';
  927. },
  928. baidu() {
  929. app.rawProps.set('playbackRate', 1);
  930. },
  931. weibo() {
  932. cfg.multipleV = path.startsWith('/u/');
  933. },
  934. acfun() {
  935. cfg.nextCSS = '.btn-next-part .control-btn';
  936. cfg.webfullCSS = '.fullscreen-web';
  937. cfg.fullCSS = '.fullscreen-screen';
  938. },
  939. ['163']() {
  940. cfg.multipleV = host.startsWith('news.');
  941. GM_addStyle('div.video,video{max-height: 100% !important;}');
  942. return host.split('.').length > 3;
  943. },
  944. sohu() {
  945. cfg.nextCSS = 'li.on[data-vid]+li a';
  946. cfg.fullCSS = '.x-fullscreen-btn';
  947. cfg.webfullCSS = '.x-pagefs-btn';
  948. },
  949. fun() {
  950. cfg.nextCSS = '.btn-item.btn-next';
  951. },
  952. le() {
  953. GM_addStyle('.gm-fp-body .le_head{display:none!important}');
  954. cfg.cssMV = '#video video';
  955. cfg.shellCSS = '#video';
  956. cfg.nextCSS = '.hv_ico_next';
  957. const delHiddenProp = _ => {
  958. if (!v.offsetWidth) Object.values(v.attributes).reverse().some(k => {
  959. if (v.getAttribute(k.name) == '') {
  960. v.removeAttribute(k.name);
  961. return true;
  962. }
  963. });
  964. };
  965. bus.$on('urlchange',delHiddenProp);
  966. bus.$once('canplay',delHiddenProp);
  967. },
  968. agedm() {
  969. actList.set(78, _ => { location.href = location.href.replace(/\d+$/, s => ++s) });
  970. },
  971. nnyy() {
  972. GM_registerMenuCommand(MSG.videoLag, () => {
  973. 'use strict';
  974. v.pause();
  975. const pos = v.currentTime;
  976. const buf = v.buffered;
  977. v.currentTime = buf.end(buf.length - 1) + 1;
  978. $(v).one('progress', ev => {
  979. v.currentTime = pos;
  980. v.play();
  981. });
  982. });
  983. cfg.nextCSS = '.playlist .on + li a';
  984. },
  985. douban() {
  986. cfg.nextCSS = 'a.next-series';
  987. },
  988. hanmidy() {
  989. cfg.nextCSS = `a[href="${path}"]+a`;
  990. }
  991. };
  992.  
  993. if (!router[u]) { //直播站点
  994. router = {
  995. douyu() {
  996. cfg.adsCSS = 'a[href*="wan.douyu.com"]';
  997. cfg.isLive = !host.startsWith('v.');
  998. if (cfg.isLive) {
  999. cfg.cssMV = '.layout-Player video';
  1000. cfg.shellCSS = '#js-player-video';
  1001. cfg.webfullCSS = '.wfs-2a8e83';
  1002. cfg.fullCSS = '.fs-781153';
  1003. cfg.playCSS = 'div[class|=play]';
  1004. path != '/' && $(ev => {
  1005. q('.u-specialStateInput').checked = true;
  1006. });
  1007. } else bus.$on('addShadowRoot', async function(r) {
  1008. if (r.host.matches('#demandcontroller-bar')) {
  1009. await sleep(600);
  1010. cfg.shellCSS = 'div[fullscreen].video';
  1011. cfg.btnFP = q('.ControllerBar-PageFull', r);
  1012. cfg.btnFS = q('.ControllerBar-WindowFull', r);
  1013. }
  1014. });
  1015. },
  1016. yy() {
  1017. cfg.isLive = !path.startsWith('/x/');
  1018. if (cfg.isLive) {
  1019. cfg.fullCSS = '.yc__fullscreen-btn';
  1020. cfg.webfullCSS = '.yc__cinema-mode-btn';
  1021. cfg.playCSS = '.yc__play-btn';
  1022. }
  1023. },
  1024. huya() {
  1025. if (firefoxVer && firefoxVer < 57) return true;
  1026. cfg.disableDBLClick = !0;
  1027. cfg.webfullCSS = '.player-fullpage-btn';
  1028. cfg.fullCSS = '.player-fullscreen-btn';
  1029. cfg.playCSS = '#player-btn';
  1030. cfg.adsCSS = '#player-subscribe-wap,#wrap-income';
  1031. polling(doClick, '.login-tips-close');
  1032. localStorage['sidebar/ads'] = '{}';
  1033. localStorage['sidebar/state'] = 0;
  1034. // localStorage.TT_ROOM_SHIELD_CFG_0_ = '{"10000":1,"20001":1,"20002":1,"20003":1,"30000":1}';
  1035. },
  1036. twitch() {
  1037. cfg.isLive = !path.startsWith('/videos/');
  1038. cfg.fullCSS = 'button[data-a-target=player-fullscreen-button]';
  1039. cfg.webfullCSS = '.player-controls__right-control-group > div:nth-child(4) > button';
  1040. cfg.playCSS = 'button[data-a-target=player-play-pause-button]';
  1041. },
  1042. longzhu() {
  1043. cfg.fullCSS = 'a.ya-screen-btn';
  1044. },
  1045. zhanqi() {
  1046. localStorage.lastPlayer = 'h5';
  1047. cfg.fullCSS = '.video-fullscreen';
  1048. }
  1049. };
  1050. if (router[u]) {
  1051. cfg.isLive = cfg.isLive || !host.startsWith('v.');
  1052. (!w.chrome || isEdge) && fakeUA(ua_chrome);
  1053. }
  1054. }
  1055.  
  1056. cfg.isLive = cfg.isLive || host.startsWith('live.');
  1057. Reflect.defineProperty(navigator, 'plugins', {
  1058. get() { return { length: 0 } }
  1059. });
  1060. GM_registerMenuCommand(MSG.helpMenuOption, alert.bind(w, MSG.helpBody));
  1061. if (!router[u] || !router[u]()) app.init();
  1062. if (!router[u] && !cfg.isNumURL) cfg.isNumURL = /[_\W]\d+(\/|\.[a-z]{3,8})?$/.test(path);