Greasy Fork is available in English.

HTML5视频播放工具

启用HTML5播放;视频截图;切换画中画;缓存视频;万能网页全屏;添加快捷键:快进、快退、暂停/播放、音量、下一集、切换(网页)全屏、上下帧、播放速度。支持视频站点:油管、TED、优.土、QQ、B站、西瓜视频、爱奇艺、A站、PPTV、芒果TV、咪咕视频、新浪、微博、网易[娱乐、云课堂、新闻]、搜狐、风行、百度云视频等;直播:斗鱼、YY、虎牙、龙珠、战旗。可增加自定义站点

目前为 2023-01-16 提交的版本。查看 最新版本

  1. /* globals jQuery, $, Vue */
  2. // ==UserScript==
  3. // @name HTML5视频播放工具
  4. // @description 启用HTML5播放;视频截图;切换画中画;缓存视频;万能网页全屏;添加快捷键:快进、快退、暂停/播放、音量、下一集、切换(网页)全屏、上下帧、播放速度。支持视频站点:油管、TED、优.土、QQ、B站、西瓜视频、爱奇艺、A站、PPTV、芒果TV、咪咕视频、新浪、微博、网易[娱乐、云课堂、新闻]、搜狐、风行、百度云视频等;直播:斗鱼、YY、虎牙、龙珠、战旗。可增加自定义站点
  5. // @homepage https://bbs.kafan.cn/thread-2093014-1-1.html
  6. // @match https://*.qq.com/*
  7. // @exclude https://user.qzone.qq.com/*
  8. // @match https://www.weiyun.com/video_*
  9. // @match https://v.youku.com/v_play/*
  10. // @match https://v.youku.com/v_show/id_*
  11. // @match https://vku.youku.com/live/*
  12. // @match https://video.tudou.com/v/*
  13. // @match https://www.iqiyi.com/*
  14. // @match https://live.bilibili.com/*
  15. // @match https://www.bilibili.com/*
  16. // @match https://www.ixigua.com/*
  17. // @match https://www.toutiao.com/video/*
  18. // @match https://www.acfun.cn/*
  19. // @match https://live.acfun.cn/live/*
  20. // @match http://v.pptv.com/show/*
  21. // @match https://v.pptv.com/show/*
  22. // @match https://www.miguvideo.com/*
  23. // @match https://tv.sohu.com/*
  24. // @match https://film.sohu.com/album/*
  25. // @match https://www.mgtv.com/*
  26. // @version 1.9.2
  27. // @match https://pan.baidu.com/*
  28. // @match https://yun.baidu.com/*
  29. // @match https://*.163.com/*
  30. // @match https://*.icourse163.org/*
  31. // @match http://video.sina.*/*
  32. // @match https://video.sina.*/*
  33. // @match http://k.sina.*/*
  34. // @match https://k.sina.*/*
  35. // @match https://weibo.com/*
  36. // @match https://*.weibo.com/*
  37. // @match https://pan.baidu.com/*
  38. // @match https://yun.baidu.com/*
  39. // @match http://v.ifeng.com/*
  40. // @match https://v.ifeng.com/*
  41. // @match http://news.mtime.com/*
  42. // @match http://video.mtime.com/*
  43. // @GM_info
  44. // @match https://www.youtube.com/watch*
  45. // @match https://www.ted.com/talks/*
  46.  
  47. // @match https://www.yy.com/*
  48. // @match https://www.huya.com/*
  49. // @match https://v.douyu.com/*
  50. // @match https://www.douyu.com/*
  51. // @match https://live.douyin.com/*
  52. // @match https://www.douyin.com/*
  53.  
  54. // @match https://www.longzhu.com/*
  55. // @match https://www.zhanqi.tv/*
  56. // @run-at document-start
  57. // @require https://cdn.staticfile.org/vue/2.6.11/vue.min.js
  58. // @require https://cdn.staticfile.org/jquery/3.6.0/jquery.min.js
  59. // @grant GM_addStyle
  60. // @include */play*
  61. // @include *play/*
  62. // @grant window.onurlchange
  63. // @grant unsafeWindow
  64. // @grant GM_registerMenuCommand
  65. // @grant GM_setValue
  66. // @grant GM_getValue
  67. // @namespace https://greasyfork.org/users/7036
  68. // ==/UserScript==
  69.  
  70. 'use strict';
  71. const w = unsafeWindow || window;
  72. const { host, pathname: path } = location;
  73. const d = document, find = [].find;
  74. let v, _fp, _fs, by; // document.body
  75. const observeOpt = {childList : true, subtree : true};
  76. const noopFn = function(){};
  77. const validEl = e => e && e.offsetWidth > 1;
  78. const q = (css, p = d) => p.querySelector(css);
  79. const delElem = e => e.remove();
  80. const r1 = (regp, s) => regp.test(s) && RegExp.$1;
  81. const log = console.log.bind(
  82. console,
  83. '%c脚本[%s] 反馈:%s\n%s',
  84. 'color:#c3c;font-size:1.2em',
  85. GM_info.script.name,
  86. GM_info.script.homepage
  87. );
  88. const gmFuncOfCheckMenu = (title, saveName, defaultVal = true) => {
  89. const r = GM_getValue(saveName, defaultVal);
  90. if (r) title = '√ '+ title;
  91. GM_registerMenuCommand(title, () => {
  92. GM_setValue(saveName, !r);
  93. location.reload();
  94. });
  95. return r;
  96. };
  97. const sleep = ms => new Promise(resolve => { setTimeout(resolve, ms) });
  98. /* 画中画
  99. <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>
  100. 设置
  101. <svg viewBox="0 0 22 22">
  102. <circle cx="11" cy="11" r="2"></circle>
  103. <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>
  104. </svg>
  105. next
  106. <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>
  107. 截图
  108. <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>
  109. const cookie = new Proxy(noopFn, {
  110. apply(target, ctx, args) { //清理cookie
  111. const keys = document.cookie.match(/[^ =;]+(?=\=)/g);
  112. if (keys) {
  113. const val = '=; expires=' + new Date(0).toUTCString() +'; domain=.; path=/';
  114. for (const k of keys) document.cookie = k + val;
  115. }
  116. // return Reflect.apply(target, ctx, args);
  117. },
  118. get(target, name) { // 读取cookie
  119. const r = r1(new RegExp(name +'=([^;]*)'), document.cookie);
  120. if (r) return decodeURIComponent(r);
  121. },
  122. set(target, name, value, receiver) { // 写入cookie
  123. let s, v, expires,
  124. oneParam = typeof value == 'string';
  125. if (oneParam) {
  126. expires = 6;
  127. v = value;
  128. } else {
  129. v = value.val;
  130. expires = value.expires || 6;
  131. delete value.expires;
  132. }
  133. s = name + '=' + encodeURIComponent(v);
  134.  
  135. if (expires && (typeof expires == 'number' || expires.toUTCString)) {
  136. let date;
  137. if (typeof expires == 'number') {
  138. date = new Date();
  139. date.setTime(expires * 24 * 3600000 + date.getTime());
  140. } else {
  141. date = expires;
  142. }
  143. s += '; expires=' + date.toUTCString();
  144. }
  145. if (!oneParam) for (const k in value) s += '; ' + k + '=' + value[k];
  146. document.cookie = s;
  147. return true;
  148. },
  149. deleteProperty(target, name, descriptor) {// 删除cookie
  150. document.cookie = name + '=; path=/; expires='+ new Date(0).toUTCString();
  151. return true;
  152. }
  153. });
  154. const onceEvent = (ctx, eName) => new Promise(resolve => ctx.addEventListener(eName, resolve));
  155. const promisify = (fn) => (...args) => new Promise((resolve, reject) => {
  156. args.push(resolve);
  157. fn.apply(this, args);
  158. }); */
  159. const hookAttachShadow = (cb) => {
  160. try {
  161. const _attachShadow = Element.prototype.attachShadow;
  162. Element.prototype.attachShadow = function(opt) {
  163. opt.mode = 'open';
  164. const shadowRoot = _attachShadow.call(this, opt);
  165. cb(shadowRoot);
  166. return shadowRoot;
  167. };
  168. } catch (e) {
  169. console.error('Hack attachShadow error', e);
  170. }
  171. };
  172. const getStyle = (o, s) => {
  173. if (o.style[s]) return o.style[s];
  174. if (getComputedStyle) {
  175. const x = getComputedStyle(o, '');
  176. s = s.replace(/([A-Z])/g,'-$1').toLowerCase();
  177. return x && x.getPropertyValue(s);
  178. }
  179. };
  180. const doClick = e => {
  181. if (typeof e === 'string') e = q(e);
  182. if (e) { e.click ? e.click() : e.dispatchEvent(new MouseEvent('click')) };
  183. };
  184. const clickDualButton = btn => { // 2合1 按钮
  185. !btn.nextSibling || getStyle(btn, 'display') !== 'none' ? doClick(btn) : doClick(btn.nextSibling);
  186. };
  187. const intervalQuery = (cb, condition, stop = true) => {
  188. const fn = typeof condition === 'string' ? q.bind(null, condition) : condition;
  189. const t = setInterval(() => {
  190. const r = fn();
  191. if (r) {
  192. stop && clearInterval(t);
  193. cb(r);
  194. }
  195. }, 300);
  196. return t;
  197. };
  198. const goNextMV = () => {
  199. const s = location.pathname;
  200. const m = s.match(/(\d+)(\D*)$/);
  201. const d = +m[1] + 1;
  202. location.assign(s.slice(0, m.index) + d + m[2]);
  203. };
  204. const firefoxVer = r1(/Firefox\/(\d+)/, navigator.userAgent);
  205. const isEdge = / Edge?\//.test(navigator.userAgent);
  206. const fakeUA = ua => Object.defineProperty(navigator, 'userAgent', {
  207. value: ua,
  208. writable: false,
  209. configurable: false,
  210. enumerable: true
  211. });
  212. const getMainDomain = host => {
  213. const a = host.split('.');
  214. let i = a.length - 2;
  215. if (/^(com?|cc|tv|net|org|gov|edu)$/.test(a[i])) i--;
  216. return a[i];
  217. };
  218. const inRange = (n, min, max) => Math.max(min, n) == Math.min(n, max);
  219. const adjustRate = n => {
  220. n += v.playbackRate;
  221. if (inRange(n, 0.1, 16)) v.playbackRate = +n.toFixed(2);
  222. };
  223. const adjustVolume = n => {
  224. n += v.volume;
  225. if (inRange(n, 0, 1)) v.volume = +n.toFixed(1);
  226. };
  227. 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';
  228. const u = getMainDomain(host);
  229. const cfg = {
  230. isLive: !1,
  231. disableDBLClick: !1,
  232. isClickOnVideo: !1,
  233. multipleV: !1, //多视频页面
  234. isNumURL: !1 //网址数字分集
  235. };
  236. const bus = new Vue();
  237. if (window.onurlchange === void 0) {
  238. history.pushState = ( f => function pushState(){
  239. const ret = f.apply(this, arguments);
  240. window.dispatchEvent(new Event('pushstate'));
  241. window.dispatchEvent(new Event('urlchange'));
  242. return ret;
  243. })(history.pushState);
  244.  
  245. history.replaceState = ( f => function replaceState(){
  246. const ret = f.apply(this, arguments);
  247. window.dispatchEvent(new Event('replacestate'));
  248. window.dispatchEvent(new Event('urlchange'));
  249. return ret;
  250. })(history.replaceState);
  251.  
  252. window.addEventListener('popstate',()=>{
  253. window.dispatchEvent(new Event('urlchange'))
  254. });
  255. };
  256.  
  257. class FullScreen {
  258. constructor(e) {
  259. let fn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen || noopFn;
  260. this.exit = fn.bind(d);
  261. fn = e.requestFullscreen || e.webkitRequestFullScreen || e.mozRequestFullScreen || e.msRequestFullScreen || noopFn;
  262. this.enter = fn.bind(e);
  263. }
  264. static isFull() {
  265. return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen ||
  266. d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement);
  267. }
  268. toggle() {
  269. FullScreen.isFull() ? this.exit() : this.enter();
  270. }
  271. }
  272.  
  273. //万能网页全屏, 参考了:https://github.com/gooyie/ykh5p
  274. class FullPage {
  275. constructor(container) {
  276. this._isFull = !1;
  277. this.container = container || FullPage.getPlayerContainer(v);
  278. GM_addStyle(
  279. `.gm-fp-body .gm-fp-zTop {
  280. position: relative !important;
  281. z-index: 2147483647 !important;
  282. }
  283. .gm-fp-wrapper, .gm-fp-body{ overflow:hidden !important; }
  284. .gm-fp-wrapper .gm-fp-innerBox {
  285. width: 100% !important;
  286. height: 100% !important;
  287. }
  288. .gm-fp-wrapper {
  289. display: block !important;
  290. position: fixed !important;
  291. width: 100% !important;
  292. height: 100% !important;
  293. top: 0 !important;
  294. left: 0 !important;
  295. background: #000 !important;
  296. z-index: 2147483647 !important;
  297. }`
  298. );
  299. }
  300. static getPlayerContainer(video) {
  301. let e = video, p = e.parentNode;
  302. const { clientWidth: wid, clientHeight: h } = e;
  303. do {
  304. e = p;
  305. p = e.parentNode;
  306. } while (p !== by && p.clientWidth-wid < 5 && p.clientHeight-h < 5);
  307. //e 为返回值,在此之后不能变了
  308. while (p !== by) p = p.parentNode || p.host;
  309. return e;
  310. }
  311. static isFull(e) {
  312. return w.innerWidth - e.clientWidth < 5 && w.innerHeight - e.clientHeight < 5;
  313. }
  314. toggle() {
  315. // assert(this.container);
  316. if (!this.container.contains(v)) this.container = FullPage.getPlayerContainer(v);
  317. bus.$emit('switchFP', !this._isFull);
  318. by.classList.toggle('gm-fp-body');
  319. let e = v;
  320. while (e != this.container) {
  321. e.classList.toggle('gm-fp-innerBox');
  322. e = e.parentNode;
  323. }
  324. e.classList.toggle('gm-fp-wrapper');
  325. e = e.parentNode;
  326. while (e != by) {
  327. e.classList.toggle('gm-fp-zTop');
  328. e = e.parentNode;
  329. }
  330. this._isFull = !this._isFull;
  331. }
  332. }
  333.  
  334. const cacheMV = {
  335. check() {
  336. const buf = v.buffered;
  337. const i = buf.length - 1;
  338. this.iEnd = buf.end(i);
  339. return buf.start(0) >= this.playPos || this.iEnd > v.duration -55;
  340. },
  341. finish() {
  342. v.removeEventListener('canplaythrough', this.onChache);
  343. v.currentTime = this.playPos;
  344. this.chached = !1;
  345. setTimeout(_ => v.pause(), 99);
  346. HTMLMediaElement.prototype.play = this.rawPlay;
  347. },
  348. onChache() {
  349. if (this.check()) this.finish();
  350. else v.currentTime = this.iEnd;
  351. },
  352. exec() {
  353. if (cfg.isLive || !v || this.chached) return;
  354. if (v.src.startsWith('http')) {
  355. alert('直接媒体类型(如MP4格式)缓存无效果!');
  356. return;
  357. }
  358. this.chached = true; //正在缓存
  359. v.pause();
  360. this.rawPlay = HTMLMediaElement.prototype.play;
  361. HTMLMediaElement.prototype.play = () => new Promise(noopFn);
  362. this.playPos = v.currentTime;
  363. v.addEventListener('canplaythrough', this.onChache);
  364. this.check();
  365. v.currentTime = this.iEnd;
  366. alert('开始缓存');
  367. }
  368. };
  369. cacheMV.onChache = cacheMV.onChache.bind(cacheMV);
  370.  
  371. const actList = new Map();
  372. actList.set(90, _ => { //按键Z: 切换加速状态
  373. if (v.playbackRate == 1) v.playbackRate = localStorage.mvPlayRate || 1.3;
  374. else {
  375. // localStorage.mvPlayRate = v.playbackRate;
  376. v.playbackRate = 1;
  377. }
  378. })
  379. .set(88, adjustRate.bind(null, -0.1)) //按键X
  380. .set(67, adjustRate.bind(null, 0.1)) //按键C
  381. .set(40, adjustVolume.bind(null, -0.1)) //↓ 降音量
  382. .set(38, adjustVolume.bind(null, 0.1)) //↑ 加音量
  383. .set(37, _ => {v.currentTime -= 5}) //按键←
  384. .set(37+1024, _ => {v.currentTime -= 20}) //按键shift+←
  385. .set(39, _ => {v.currentTime += 5}) //按键→
  386. .set(39+1024, _ => {v.currentTime += 20}) //按键shift+→
  387. .set(68, _ => {v.currentTime -= 0.03;v.pause()}) //按键D:上一帧
  388. .set(70, _ => {v.currentTime += 0.03;v.pause()}) //按键F:下一帧
  389. .set(32, _ => { //按键space
  390. if (cfg.btnPlay) clickDualButton(cfg.btnPlay);
  391. else v.paused ? v.play() : v.pause();
  392. })
  393. .set(13, _ => { //回车键。 全屏
  394. _fs ? _fs.toggle() : clickDualButton(cfg.btnFS);
  395. })
  396. .set(13+1024, _ => {//web全屏
  397. self != top ? top.postMessage({id: 'gm-h5-toggle-iframeWebFull'}, '*')
  398. : _fp ? _fp.toggle() : clickDualButton(cfg.btnFP);
  399. })
  400. .set(27+1024, noopFn) //忽略按键shift + esc
  401. .set(27, ev => { //按键esc
  402. if (FullScreen.isFull()) {
  403. _fs ? _fs.exit() : clickDualButton(cfg.btnFS);
  404. } else if (self != top) {
  405. top.postMessage({id: 'gm-h5-is-iframeWebFull'}, '*');
  406. } else if (FullPage.isFull(v)) {
  407. _fp ? _fp.toggle() : clickDualButton(cfg.btnFP);
  408. }
  409. })
  410. .set(73, _ => { //按键I:画中画模式
  411. if (!d.pictureInPictureElement) {
  412. v.requestPictureInPicture().catch(err => {
  413. alert('无法进入画中画模式!错误:\n'+ err)
  414. });
  415. } else {
  416. d.exitPictureInPicture().catch(err => {
  417. alert('无法退出画中画模式!错误:\n'+ err)
  418. });
  419. }
  420. })
  421. .set(80, _ => { //按键P:截图
  422. const canvas = d.createElement('canvas');
  423. canvas.width = v.videoWidth;
  424. canvas.height = v.videoHeight;
  425. canvas.getContext('2d').drawImage(v, 0, 0, canvas.width, canvas.height);
  426.  
  427. canvas.toBlob((blob) => {
  428. const dataURL = URL.createObjectURL(blob);
  429. const link = d.createElement('a');
  430. link.href = dataURL;
  431. link.download = Date.now().toString(36) +'.png';
  432. link.style.display = 'none';
  433. d.body.appendChild(link);
  434. link.click();
  435. link.remove();
  436. URL.revokeObjectURL(dataURL);
  437. });
  438. })
  439. .set(77, _ => {// M 缓存视频
  440. cacheMV.chached ? cacheMV.finish() : cacheMV.exec();
  441. })
  442. .set(78, _ => {// N 下一集
  443. if (self != top) top.postMessage({id: 'gm-h5-play-next'}, '*');
  444. else if (cfg.btnNext) doClick(cfg.btnNext);
  445. else if (cfg.isNumURL) goNextMV();
  446. });
  447.  
  448. const app = {
  449. rawProps: new Map(),
  450. shellEvent() {
  451. const fn = ev => {
  452. if (ev.target.closest('button')) return;
  453. ev.stopPropagation(); // preventDefault
  454. ev.stopImmediatePropagation();
  455. this.checkUI();
  456. actList.get(1037)(); //web全屏
  457. };
  458. const e = cfg.isClickOnVideo ? v : cfg.mvShell;
  459. e.addEventListener('mousedown', ev => {
  460. if (1 == ev.button) {
  461. ev.preventDefault();
  462. ev.stopPropagation();
  463. ev.stopImmediatePropagation();
  464. if (!cfg.isLive) {
  465. actList.has(39) ? actList.get(39)() : v.currentTime += 5;
  466. }
  467. }
  468. });
  469. !cfg.disableDBLClick && e.addEventListener('dblclick', fn);
  470. },
  471. setShell() {
  472. const e = this.getDPlayer() || this.getVjsPlayer() ||
  473. (cfg.shellCSS && q(cfg.shellCSS)) ||
  474. (top != self ? by : FullPage.getPlayerContainer(v));
  475. if (e && cfg.mvShell !== e) {
  476. cfg.mvShell = e;
  477. this.shellEvent();
  478. }
  479. },
  480. checkMV() {
  481. if (this.vList) {
  482. const e = this.findMV();
  483. if (e && e != v) {
  484. v = e;
  485. cfg.btnPlay = cfg.btnNext = cfg.btnFP = cfg.btnFS = _fs = _fp = null;
  486. if (!cfg.isLive) {
  487. v.playbackRate = localStorage.mvPlayRate || 1;
  488. v.addEventListener('ratechange', ev => {
  489. localStorage.mvPlayRate = v.playbackRate;
  490. });
  491. }
  492. this.setShell();
  493. }
  494. }
  495. if (!validEl(cfg.mvShell)) {
  496. cfg.mvShell = null;
  497. this.setShell();
  498. }
  499. this.checkUI();
  500. return v;
  501. },
  502. getDPlayer() {
  503. const e = v.closest('.dplayer');
  504. if (e) {
  505. cfg.btnFP = q('.dplayer-full-in-icon > span', e);
  506. cfg.btnFS = q('.dplayer-full-icon', e);
  507. e.closest('body > *').classList.add('gm-dp-zTop');
  508. }
  509. return e;
  510. },
  511. getVjsPlayer() {
  512. const e = v.closest('.video-js');
  513. if (e) {
  514. cfg.btnFS = q('.vjs-control-bar > button.vjs-button:nth-last-of-type(1)');
  515. cfg.webfullCSS = '.vjs-control-bar > button.vjs-button[title$="全屏"]:nth-last-of-type(2)';
  516. }
  517. return e;
  518. },
  519. hotKey(e) {
  520. const t = e.target;
  521. if (e.ctrlKey || e.altKey || t.contentEditable=='true' ||
  522. /INPUT|TEXTAREA|SELECT/.test(t.nodeName)) return;
  523. if (e.shiftKey && ![13,37,39].includes(e.keyCode)) return;
  524. if (cfg.isLive && [37,39,78,77,88,67,90].includes(e.keyCode)) return;
  525. if (!this.checkMV()) return;
  526. if (!e.shiftKey && cfg.mvShell && cfg.mvShell.contains(t) && [32,37,39].includes(e.keyCode)) return;
  527. const key = e.shiftKey ? e.keyCode + 1024 : e.keyCode;
  528. if (actList.has(key)) {
  529. e.stopImmediatePropagation();
  530. e.stopPropagation();
  531. e.preventDefault();
  532. actList.get(key)(e);
  533. }
  534. },
  535. checkUI() {
  536. if (cfg.webfullCSS && !validEl(cfg.btnFP)) cfg.btnFP = q(cfg.webfullCSS);
  537. if (cfg.btnFP) _fp = null;
  538. else if (!_fp && self == top) _fp = new FullPage(cfg.mvShell);
  539.  
  540. if (cfg.fullCSS && !validEl(cfg.btnFS)) cfg.btnFS = q(cfg.fullCSS);
  541. if (cfg.btnFS) _fs = null;
  542. else if (!_fs) _fs = new FullScreen(v);
  543.  
  544. if (cfg.nextCSS && (!validEl(cfg.btnNext) || !cfg.btnNext.matches(cfg.nextCSS))) cfg.btnNext = q(cfg.nextCSS);
  545. if (cfg.playCSS && !validEl(cfg.btnPlay)) cfg.btnPlay = q(cfg.playCSS);
  546. },
  547. onGrowVList() {
  548. if (this.vList.length == this.vCount) return;
  549. if (this.viewObserver) {
  550. for (let e of this.vList) {
  551. if (!this.vSet.has(e)) this.viewObserver.observe(e);
  552. }
  553. } else {
  554. const config = {
  555. rootMargin: '0px',
  556. threshold: 0.9
  557. };
  558. this.viewObserver = new IntersectionObserver(this.onIntersection.bind(this), config);
  559. for (let e of this.vList) this.viewObserver.observe(e);
  560. }
  561. this.vSet = new Set(this.vList);
  562. this.vCount = this.vList.length;
  563. },
  564. onIntersection(entries) {
  565. if (this.vList.length < 2) return;
  566. const entry = find.call(entries, k => k.isIntersecting);
  567. if (!entry || v == entry.target) return;
  568. v = entry.target;
  569. _fs = new FullScreen(v);
  570. _fp = new FullPage(v);
  571. bus.$on('switchFP', async (toFull) => {
  572. // const c = toFull ? this.vSet : this.vList;
  573. // for (const e of c) this.viewObserver.unobserve(e);
  574. sleep(200);
  575. if (!toFull) v.scrollIntoView();
  576. });
  577. bus.$emit('switchMV');
  578. },
  579. bindEvent() {
  580. for (const [i,k] of this.rawProps) Reflect.defineProperty(HTMLVideoElement.prototype, i, k);
  581. this.rawProps.clear();
  582. this.rawProps = null;
  583. $(cfg.adsCSS).remove();
  584. by = d.body;
  585. log('bind event\n', v);
  586. bus.$emit('foundMV');
  587. window.addEventListener('urlchange', async (info) => { //TM event: info.url
  588. await sleep(300);
  589. this.checkMV();
  590. v.playbackRate = localStorage.mvPlayRate || 1.3;
  591. bus.$emit('urlchange');
  592. });
  593. if (top != self) {
  594. top.postMessage({id: 'gm-h5-init-MVframe'}, '*');
  595. window.addEventListener("message", ev => {
  596. if (!ev.source || !ev.data || !ev.data.id) return;
  597. switch (ev.data.id) {
  598. case 'gm-h5-toggle-fullScreen':
  599. _fs ? _fs.toggle() : clickDualButton(cfg.btnFS);
  600. break;
  601. }
  602. }, false);
  603. }
  604. $(v).one('canplaythrough', ev => {
  605. if (!cfg.isLive) {
  606. v.playbackRate = localStorage.mvPlayRate || 1;
  607. v.addEventListener('ratechange', ev => {
  608. if (v.playbackRate != 1) localStorage.mvPlayRate = v.playbackRate;
  609. });
  610. }
  611. this.checkMV();
  612. bus.$emit('canplay');
  613. });
  614. $(by).keydown(this.hotKey.bind(this));
  615.  
  616. cfg.mvShell ? this.shellEvent() : this.setShell();
  617. this.checkUI();
  618. if (cfg.multipleV) {
  619. new MutationObserver(this.onGrowVList.bind(this)).observe(by, observeOpt);
  620. this.vCount = 0;
  621. this.onGrowVList();
  622. }
  623. },
  624. init() {
  625. const rawAel = EventTarget.prototype.addEventListener;
  626. EventTarget.prototype.addEventListener = function(...args) {
  627. let block = (args[0] == 'dblclick' && !args[1].toString().includes('actList.get(1037)'))
  628. || (args[0] == 'ratechange' && /bilibili|baidu/.test(u) && !args[1].toString().includes('localStorage.mvPlayRate'));
  629. if (!block) return rawAel.apply(this, args);
  630. };
  631. for (const i of this.rawProps.keys()) this.rawProps.set(i,
  632. Reflect.getOwnPropertyDescriptor(HTMLMediaElement.prototype, i));
  633. this.vList = d.getElementsByTagName('video'); // B站:bwp-video
  634. const fn = e => cfg.cssMV ? e.matches(cfg.cssMV) : e.offsetWidth > 9;
  635. this.findMV = find.bind(this.vList, fn);
  636. const timer = intervalQuery(e => {
  637. v = e;
  638. this.bindEvent();
  639. }, this.findMV);
  640.  
  641. hookAttachShadow(async shadowRoot => {
  642. await sleep(600);
  643. bus.$emit('addShadowRoot', shadowRoot);
  644. if (v) return;
  645. if (v = q('video', shadowRoot)) { // v.getRootNode() == shadowRoot
  646. log('Found MV in ShadowRoot\n', v, shadowRoot);
  647. if (!cfg.shellCSS) cfg.mvShell = shadowRoot.host;
  648. clearInterval(timer);
  649. this.bindEvent();
  650.  
  651. this.vList = null;
  652. this.findMV = noopFn;
  653. }
  654. });
  655. }
  656. };
  657.  
  658. let router = {
  659. ted() {
  660. cfg.fullCSS = 'button[title=Fullscreen]';
  661. },
  662. youtube() {
  663. GM_addStyle(
  664. `.gm-fp-body #player-container-inner{padding-top:0!important}
  665. .gm-fp-body #player-container-outer{
  666. max-width:100%!important;
  667. margin:0!important;
  668. }`
  669. );
  670. cfg.shellCSS = '#player';
  671. cfg.playCSS = 'button.ytp-play-button';
  672. cfg.nextCSS = 'a.ytp-next-button';
  673. cfg.fullCSS = 'button.ytp-fullscreen-button';
  674. cfg.isClickOnVideo = true;
  675. actList.delete(32);
  676. actList.set(69, actList.get(70)).delete(70); //F键 >> E键
  677. },
  678. qq() {
  679. if (self != top &&(host == 'v.qq.com' || host == 'video.qq.com') ) throw '只处理主页面';
  680. actList.delete(32);
  681. cfg.nextCSS = '.txp_btn_next_u';
  682. cfg.webfullCSS = '.txp_btn_fake';
  683. cfg.fullCSS = '.txp_btn_fullscreen';
  684. // w.__PLAYER__ || w.PLAYER
  685. app.rawProps.set('playbackRate', 1);
  686. },
  687. youku() {
  688. actList.delete(37);
  689. actList.delete(39);
  690. if (host.startsWith('vku.')) {
  691. bus.$on('canplay', () => {
  692. cfg.isLive = !q('.spv_progress');
  693. });
  694. cfg.fullCSS = '.live_icon_full';
  695. } else {
  696. bus.$on('foundMV',() => { $(document).unbind('keyup') });
  697. // localStorage.removeItem('cna');
  698. // delete cookie.cna; //全部清除: cookie(); 写入cookie: cookie.cna = 'xxxxx---xxx';
  699. cfg.shellCSS = '#ykPlayer';
  700. cfg.webfullCSS = '.kui-webfullscreen-icon-0';
  701. cfg.fullCSS = '.kui-fullscreen-icon-0';
  702. cfg.nextCSS = '.kui-next-icon-0';
  703. }
  704. },
  705. bilibili() {
  706. app.rawProps.set('playbackRate', 1)
  707. .set('currentTime', 1)
  708. .set('volume', 1);
  709. cfg.isLive = host.startsWith('live.');
  710. if (cfg.isLive) return;
  711. const isSquirtle = path.startsWith('/bangumi');
  712. if (!isSquirtle) actList.delete(32);
  713. cfg.nextCSS = isSquirtle ? '.squirtle-video-next' : '.bilibili-player-video-btn-next';
  714. cfg.webfullCSS = isSquirtle ? '.squirtle-video-pagefullscreen' : '.bpx-player-ctrl-web';
  715. cfg.fullCSS = isSquirtle ? '.squirtle-video-fullscreen' : '.bpx-player-ctrl-full';
  716. /*
  717. const seek = function(step) {
  718. const p = this.player;
  719. p.seek(p.getCurrentTime()+ step, p.getState() === "PAUSED");
  720. };
  721. actList.set(38, _ => w.player.volume(w.player.volume()+0.1)) //加音量
  722. .set(40, _ => w.player.volume(w.player.volume()-0.1))
  723. .set(37, seek.bind(w, -5))
  724. .set(37+1024, seek.bind(w, -20)) //shift+left 快退20秒
  725. .set(39, seek.bind(w, 5))
  726. .set(39+1024, seek.bind(w, 20)) //shift+→ 快进20秒
  727. .set(70, seek.bind(w, 0.03)) //按键F:下一帧
  728. .set(68, seek.bind(w, -0.03)); //按键D:上一帧
  729. */
  730. },
  731. iqiyi() {
  732. cfg.fullCSS = '.iqp-btn-fullscreen:not(.fake__click)';
  733. cfg.nextCSS = '.iqp-btn-next';
  734. },
  735. pptv() {
  736. cfg.fullCSS = '.w-zoom-container > div';
  737. cfg.webfullCSS = '.w-expand-container > div';
  738. cfg.nextCSS = '.w-next';
  739. },
  740. mgtv() {
  741. cfg.fullCSS = 'mango-screen';
  742. cfg.webfullCSS = 'mango-webscreen > a';
  743. cfg.nextCSS = 'mango-control-playnext-btn';
  744. },
  745. ixigua() {
  746. cfg.fullCSS = 'div[aria-label="全屏"]';
  747. cfg.nextCSS = '.xgplayer-control-item.control_playnext';
  748. GM_addStyle('.gm-fp-body .xgplayer{padding-top:0!important}');
  749. },
  750. miguvideo() {
  751. cfg.nextCSS = '.next-btn';
  752. cfg.fullCSS = '.zoom-btn';
  753. cfg.shellCSS = '.mod-player';
  754. },
  755. baidu() {
  756. app.rawProps.set('playbackRate', 1);
  757. },
  758. weibo() {
  759. cfg.multipleV = path.startsWith('/u/');
  760. },
  761. acfun() {
  762. cfg.nextCSS = '.btn-next-part .control-btn';
  763. cfg.webfullCSS = '.fullscreen-web';
  764. cfg.fullCSS = '.fullscreen-screen';
  765. },
  766. ['163']() {
  767. cfg.multipleV = host.startsWith('news.');
  768. GM_addStyle('div.video,video{max-height: 100% !important;}');
  769. return host.split('.').length > 3;
  770. },
  771. sohu() {
  772. cfg.nextCSS = 'li.on[data-vid]+li a';
  773. cfg.fullCSS = '.x-fullscreen-btn';
  774. cfg.webfullCSS = '.x-pagefs-btn';
  775. },
  776. fun() {
  777. cfg.nextCSS = '.btn-item.btn-next';
  778. },
  779. le() {
  780. GM_addStyle('.gm-fp-body .le_head{display:none!important}');
  781. cfg.cssMV = '#video video';
  782. cfg.shellCSS = '#video';
  783. cfg.nextCSS = '.hv_ico_next';
  784. const delHiddenProp = _ => {
  785. if (!v.offsetWidth) Object.values(v.attributes).reverse().some(k => {
  786. if (v.getAttribute(k.name) == '') {
  787. v.removeAttribute(k.name);
  788. return true;
  789. }
  790. });
  791. };
  792. bus.$on('urlchange',delHiddenProp);
  793. bus.$once('canplay',delHiddenProp);
  794. },
  795. agemys() {
  796. actList.set(78, _ => { location.href = location.href.replace(/\d+$/, s => ++s) });
  797. },
  798. dandanzan() {
  799. GM_registerMenuCommand('视频卡顿', () => {
  800. 'use strict';
  801. v.pause();
  802. const pos = v.currentTime;
  803. const buf = v.buffered;
  804. v.currentTime = buf.end(buf.length - 1) + 3;
  805. $(v).one('progress', ev => {
  806. v.currentTime = pos;
  807. v.play();
  808. });
  809. });
  810. cfg.nextCSS = '.playlist .on + li a';
  811. },
  812. hanmidy() {
  813. cfg.nextCSS = `a[href="${path}"]+a`;
  814. }
  815. };
  816. router.nunuyy5 = router.dandanzan10 = router.dandanzan;
  817.  
  818. if (!router[u]) { //直播站点
  819. router = {
  820. douyu() {
  821. cfg.adsCSS = 'a[href*="wan.douyu.com"]';
  822. cfg.isLive = !host.startsWith('v.');
  823. if (cfg.isLive) {
  824. cfg.cssMV = '.layout-Player video';
  825. cfg.shellCSS = '#js-player-video';
  826. cfg.webfullCSS = '.wfs-2a8e83';
  827. cfg.fullCSS = '.fs-781153';
  828. cfg.playCSS = 'div[class|=play]';
  829. path != '/' && $(ev => {
  830. q('.u-specialStateInput').checked = true;
  831. });
  832. } else bus.$on('addShadowRoot', function(r) {
  833. if (r.host.matches('#demandcontroller-bar')) {
  834. cfg.shellCSS = 'div[fullscreen].video';
  835. cfg.btnFP = q('.ControllerBar-PageFull', r);
  836. cfg.btnFS = q('.ControllerBar-WindowFull', r);
  837. }
  838. });
  839. },
  840. yy() {
  841. cfg.isLive = !path.startsWith('/x/');
  842. if (cfg.isLive) {
  843. cfg.fullCSS = '.yc__fullscreen-btn';
  844. cfg.webfullCSS = '.yc__cinema-mode-btn';
  845. cfg.playCSS = '.yc__play-btn';
  846. }
  847. },
  848. huya() {
  849. if (firefoxVer && firefoxVer < 57) return true;
  850. cfg.disableDBLClick = !0;
  851. cfg.webfullCSS = '.player-fullpage-btn';
  852. cfg.fullCSS = '.player-fullscreen-btn';
  853. cfg.playCSS = '#player-btn';
  854. cfg.adsCSS = '#player-subscribe-wap,#wrap-income';
  855. intervalQuery(doClick, '.login-tips-close');
  856. localStorage['sidebar/ads'] = '{}';
  857. localStorage['sidebar/state'] = 0;
  858. localStorage.TT_ROOM_SHIELD_CFG_0_ = '{"10000":1,"20001":1,"20002":1,"20003":1,"30000":1}';
  859. },
  860. longzhu() {
  861. cfg.fullCSS = 'a.ya-screen-btn';
  862. },
  863. zhanqi() {
  864. localStorage.lastPlayer = 'h5';
  865. cfg.fullCSS = '.video-fullscreen';
  866. }
  867. };
  868. if (router[u]) {
  869. cfg.isLive = cfg.isLive || !host.startsWith('v.');
  870. (!w.chrome || isEdge) && fakeUA(ua_chrome);
  871. }
  872. }
  873.  
  874. cfg.isLive = cfg.isLive || host.startsWith('live.');
  875. Reflect.defineProperty(navigator, 'plugins', {
  876. get() { return { length: 0 } }
  877. });
  878. GM_registerMenuCommand('脚本功能快捷键表' , alert.bind(w,
  879. `双击:切换(网页)全屏 鼠标中键:快进5
  880.  
  881. P:视频截图 i:切换画中画 M:(停止)缓存视频
  882. →方向键:快退、快进5秒; 方向键 + shift: 20
  883. ↓方向键:音量调节 ESC:退出(网页)全屏
  884. 空格键:暂停/播放 N:播放下一集
  885. 回车键:切换全屏; 回车键 + shift: 切换网页全屏
  886. C:加速0.1倍播放 X:减速0.1倍播放 Z:切换加速状态
  887. D:上一帧 F:下一帧(youtube.comE键)`
  888. ));
  889. if (!router[u] || !router[u]()) app.init();
  890. if (!router[u] && !cfg.isNumURL) cfg.isNumURL = /[_\W]\d+(\/|\.[a-z]{3,8})?$/.test(path);