HTML5 Video Player erweitertes Skript

Videoverbesserungsskript, unterstützt alle H5-Videowebsites, wie z. ted, instagram, twitter usw. Vollständige Tastenkombinationssteuerung, Unterstützung: Wiedergabe mit doppelter Geschwindigkeit/beschleunigte Wiedergabe, Video-Screenshots, Bild-in-Bild, Vollbild-Webseiten, Anpassung von Helligkeit, Sättigung, Kontrast, benutzerdefinierte Konfigurationsverbesserungen und andere Funktionen

Version vom 29.08.2022. Aktuellste Version

  1. // ==UserScript==
  2. // @name HTML5视频播放器增强脚本
  3. // @name:en HTML5 video player enhanced script
  4. // @name:zh HTML5视频播放器增强脚本
  5. // @name:zh-TW HTML5視頻播放器增強腳本
  6. // @name:ja HTML5ビデオプレーヤーの拡張スクリプト
  7. // @name:ko HTML5 비디오 플레이어 고급 스크립트
  8. // @name:ru HTML5 видео плеер улучшенный скрипт
  9. // @name:de HTML5 Video Player erweitertes Skript
  10. // @namespace https://github.com/xxxily/h5player
  11. // @homepage https://github.com/xxxily/h5player
  12. // @version 3.4.3
  13. // @description 视频增强脚本,支持所有H5视频网站,例如:B站、抖音、腾讯视频、优酷、爱奇艺、西瓜视频、油管(YouTube)、微博视频、知乎视频、搜狐视频、网易公开课、百度网盘、阿里云盘、ted、instagram、twitter等。全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能,为你提供愉悦的在线视频播放体验。还有视频广告快进、在线教程/教育视频倍速快学等能力
  14. // @description:en Video enhancement script, supports all H5 video websites, such as: Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, Baidu network disk, Alibaba cloud disk, ted, instagram, twitter, etc. Full shortcut key control, support: double-speed playback/accelerated playback, video screenshots, picture-in-picture, full-screen web pages, adjusting brightness, saturation, contrast
  15. // @description:zh 视频增强脚本,支持所有H5视频网站,例如:B站、抖音、腾讯视频、优酷、爱奇艺、西瓜视频、油管(YouTube)、微博视频、知乎视频、搜狐视频、网易公开课、百度网盘、阿里云盘、ted、instagram、twitter等。全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能,为你提供愉悦的在线视频播放体验。还有视频广告快进、在线教程/教育视频倍速快学等能力
  16. // @description:zh-TW 視頻增強腳本,支持所有H5視頻網站,例如:B站、抖音、騰訊視頻、優酷、愛奇藝、西瓜視頻、油管(YouTube)、微博視頻、知乎視頻、搜狐視頻、網易公開課、百度網盤、阿里雲盤、ted、instagram、twitter等。全程快捷鍵控制,支持:倍速播放/加速播放、視頻畫面截圖、畫中畫、網頁全屏、調節亮度、飽和度、對比度、自定義配置功能增強等功能,為你提供愉悅的在線視頻播放體驗。還有視頻廣告快進、在線教程/教育視頻倍速快學等能力
  17. // @description:ja ビデオ拡張スクリプトは、Bilibili、Douyin、Tencent Video、Youku、iQiyi、Xigua Video、YouTube、Weibo Video、Zhihu Video、Sohu Video、NetEase Open Course、Baidu ネットワーク ディスク、Alibaba クラウド ディスクなど、すべての H5 ビデオ Web サイトをサポートします。テッド、インスタグラム、ツイッターなど 完全なショートカット キー コントロール、サポート: 倍速再生/加速再生、ビデオ スクリーンショット、ピクチャー イン ピクチャー、フルスクリーン Web ページ、明るさ、彩度、コントラストの調整、カスタム構成の強化、その他の機能により、快適なオンラインを提供します。ビデオ再生体験。 ビデオ広告、オンライン チュートリアル/教育ビデオなどを早送りする機能もあります。
  18. // @description:ko 비디오 향상 스크립트는 Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, Baidu 네트워크 디스크, Alibaba 클라우드 디스크와 같은 모든 H5 비디오 웹사이트를 지원합니다. 테드, 인스타그램, 트위터 등 전체 바로 1가기 키 제어, 지원: 배속 재생/가속 재생, 비디오 스크린샷, PIP(Picture-in-Picture), 전체 화면 웹 페이지, 밝기, 채도, 대비, 사용자 정의 구성 향상 및 기타 기능 조정, 쾌적한 온라인 환경 제공 비디오 재생 경험. 비디오 광고, 온라인 자습서/교육 비디오 등을 빨리 감기하는 기능도 있습니다.
  19. // @description:ru Сценарий улучшения видео поддерживает все видео-сайты H5, такие как: Bilibili, Douyin, Tencent Video, Youku, iQiyi, Xigua Video, YouTube, Weibo Video, Zhihu Video, Sohu Video, NetEase Open Course, сетевой диск Baidu, облачный диск Alibaba, Тед, инстаграм, твиттер и т.д. Полное управление клавишами быстрого доступа, поддержка: воспроизведение с удвоенной скоростью/ускоренное воспроизведение, скриншоты видео, картинка в картинке, полноэкранные веб-страницы
  20. // @description:de Videoverbesserungsskript, unterstützt alle H5-Videowebsites, wie z. ted, instagram, twitter usw. Vollständige Tastenkombinationssteuerung, Unterstützung: Wiedergabe mit doppelter Geschwindigkeit/beschleunigte Wiedergabe, Video-Screenshots, Bild-in-Bild, Vollbild-Webseiten, Anpassung von Helligkeit, Sättigung, Kontrast, benutzerdefinierte Konfigurationsverbesserungen und andere Funktionen
  21. // @author ankvps
  22. // @icon https://cdn.jsdelivr.net/gh/xxxily/h5player@master/logo.png
  23. // @match *://*/*
  24. // @grant unsafeWindow
  25. // @grant GM_addStyle
  26. // @grant GM_setValue
  27. // @grant GM_getValue
  28. // @grant GM_deleteValue
  29. // @grant GM_listValues
  30. // @grant GM_addValueChangeListener
  31. // @grant GM_removeValueChangeListener
  32. // @grant GM_registerMenuCommand
  33. // @grant GM_unregisterMenuCommand
  34. // @grant GM_getTab
  35. // @grant GM_saveTab
  36. // @grant GM_getTabs
  37. // @grant GM_openInTab
  38. // @grant GM_download
  39. // @grant GM_xmlhttpRequest
  40. // @grant GM_setClipboard
  41. // @run-at document-start
  42. // @require https://unpkg.com/@popperjs/core@2.6.0/dist/umd/popper.js
  43. // @require https://unpkg.com/vue@2.6.11/dist/vue.min.js
  44. // @require https://unpkg.com/element-ui@2.13.0/lib/index.js
  45. // @resource elementUiCss https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css
  46. // @connect 127.0.0.1
  47. // @license GPL
  48. // ==/UserScript==
  49. (function (w) { if (w) { w.name = 'h5player'; } })();
  50.  
  51. /* 当前用到的快捷键 */
  52. const hasUseKey = {
  53. keyCodeList: [13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, 99, 100, 220],
  54. keyList: ['enter', 'shift', 'control', 'alt', 'escape', ' ', 'arrowleft', 'arrowright', 'arrowup', 'arrowdown', '1', '2', '3', '4', 'c', 'd', 'e', 'f', 'i', 'j', 'k', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'w', 'x', 'y', 'z', '\\', '|'],
  55. keyMap: {
  56. enter: 13,
  57. shift: 16,
  58. ctrl: 17,
  59. alt: 18,
  60. esc: 27,
  61. space: 32,
  62. '←': 37,
  63. '↑': 38,
  64. '→': 39,
  65. '↓': 40,
  66. 1: 49,
  67. 2: 50,
  68. 3: 51,
  69. 4: 52,
  70. c: 67,
  71. d: 68,
  72. e: 69,
  73. f: 70,
  74. i: 73,
  75. j: 74,
  76. k: 75,
  77. m: 77,
  78. n: 78,
  79. o: 79,
  80. p: 80,
  81. q: 81,
  82. r: 82,
  83. s: 83,
  84. t: 84,
  85. u: 85,
  86. w: 87,
  87. x: 88,
  88. y: 89,
  89. z: 90,
  90. pad1: 97,
  91. pad2: 98,
  92. pad3: 99,
  93. pad4: 100,
  94. '\\': 220
  95. }
  96. };
  97.  
  98. /**
  99. * 判断当前按键是否注册为需要用的按键
  100. * 用于减少对其它键位的干扰
  101. */
  102. function isRegisterKey (event) {
  103. const keyCode = event.keyCode;
  104. const key = event.key.toLowerCase();
  105. return hasUseKey.keyCodeList.includes(keyCode) ||
  106. hasUseKey.keyList.includes(key)
  107. }
  108.  
  109. /**
  110. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  111. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  112. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  113. * @returns {Promise<void>}
  114. */
  115. async function getPageWindow () {
  116. return new Promise(function (resolve, reject) {
  117. if (window._pageWindow) {
  118. return resolve(window._pageWindow)
  119. }
  120.  
  121. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  122.  
  123. function getWin (event) {
  124. window._pageWindow = this;
  125. // debug.log('getPageWindow succeed', event)
  126. listenEventList.forEach(eventType => {
  127. window.removeEventListener(eventType, getWin, true);
  128. });
  129. resolve(window._pageWindow);
  130. }
  131.  
  132. listenEventList.forEach(eventType => {
  133. window.addEventListener(eventType, getWin, true);
  134. });
  135.  
  136. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  137. window.dispatchEvent(new window.Event('get-page-window-event'));
  138. })
  139. }
  140. getPageWindow();
  141.  
  142. function openInTab (url, opts) {
  143. if (window.GM_openInTab) {
  144. window.GM_openInTab(url, opts || {
  145. active: true,
  146. insert: true,
  147. setParent: true
  148. });
  149. }
  150. }
  151.  
  152. function toArray (arg) {
  153. arg = Array.isArray(arg) ? arg : [arg];
  154. return arg
  155. }
  156.  
  157. const Popper = window.Popper;
  158. class Tips {
  159. constructor (opts = {}) {
  160. opts.fontSize = opts.fontSize || 16;
  161. opts.className = opts.className || 'tooltips_el';
  162. opts.content = opts.content || 'tips msg...';
  163. opts.styleRule = opts.styleRule || '';
  164. opts.show = opts.show || false;
  165. opts.popperOpts = opts.popperOpts || {};
  166. opts.showEvents = toArray(opts.showEvents || []);
  167. opts.hideEvents = toArray(opts.hideEvents || []);
  168. opts.toggleEvents = toArray(opts.toggleEvents || []);
  169.  
  170. this.popperInstance = null;
  171. this.reference = null;
  172. this.tooltip = null;
  173. this.opts = opts;
  174.  
  175. /* 当前tooltip显示还是隐藏的状态标识 */
  176. this.status = false;
  177.  
  178. if (opts.reference) {
  179. this.create(opts.reference);
  180. if (opts.show) {
  181. this.show();
  182. }
  183. }
  184. }
  185.  
  186. _createTipsDom (opts = {}) {
  187. const wrapDom = document.createElement('div');
  188. wrapDom.setAttribute('class', opts.className);
  189.  
  190. const contenDom = document.createElement('div');
  191. contenDom.setAttribute('class', 'tooltips-content');
  192. contenDom.innerHTML = opts.content;
  193. wrapDom.appendChild(contenDom);
  194.  
  195. // 过渡动画
  196. // transition: all 500ms ease;
  197. const styleDom = document.createElement('style');
  198. styleDom.appendChild(document.createTextNode(`
  199. .${opts.className} {
  200. z-index: 999999;
  201. font-size: ${opts.fontSize || 16}px;
  202. padding: 5px 10px;
  203. background: rgba(0,0,0,0.4);
  204. color:white;
  205. top: 0;
  206. left: 0;
  207. opacity: 0;
  208. border-bottom-right-radius: 5px;
  209. display: none;
  210. -webkit-font-smoothing: subpixel-antialiased;
  211. font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
  212. -webkit-user-select: none;
  213. }
  214. .${opts.className}[data-popper-reference-hidden] { visibility: hidden; pointer-events: none; }
  215. .${opts.className}[data-popper-escaped] { visibility: hidden; pointer-events: none; }
  216. ${opts.styleRule || ''}
  217. `));
  218. wrapDom.appendChild(styleDom);
  219.  
  220. return wrapDom
  221. }
  222.  
  223. /**
  224. * 创建可用的tooltip对象
  225. * @param reference {Element} -必选 提供给createPopper的reference对象
  226. * @returns {null|boolean}
  227. */
  228. create (reference) {
  229. const t = this;
  230.  
  231. /* 没引入Popper脚本或没提供参考对象或已创建实例 */
  232. if (!Popper || !reference || t.popperInstance) {
  233. return t.popperInstance || false
  234. }
  235.  
  236. t.reference = reference;
  237. t.tooltip = t._createTipsDom(t.opts);
  238.  
  239. const parentNode = reference.parentNode || reference;
  240. parentNode.appendChild(t.tooltip);
  241.  
  242. t.popperInstance = Popper.createPopper(reference, t.tooltip, t.opts.popperOpts || {});
  243. t._eventsHandler();
  244.  
  245. return t.popperInstance
  246. }
  247.  
  248. /**
  249. * 重建tooltip对象
  250. * @param reference {Element} -可选 create函数所需参数,没提供则使用之前的reference
  251. * @returns {null|boolean}
  252. */
  253. rebuild (reference) {
  254. const t = this;
  255. reference = reference || t.reference;
  256. t.destroy();
  257. return t.create(reference)
  258. }
  259.  
  260. /**
  261. * 绑定和解绑相关事件
  262. * @param unbind {Boolean} 默认是绑定相关事件,如果为true则解绑相关事件
  263. * @returns {boolean}
  264. * @private
  265. */
  266. _eventsHandler (unbind) {
  267. const t = this;
  268. if (!t.reference) return false
  269.  
  270. const handlerName = unbind ? 'removeEventListener' : 'addEventListener';
  271. const eventTypeArr = ['show', 'hide', 'toggle'];
  272. eventTypeArr.forEach(eventType => {
  273. const eventList = toArray(t.opts[eventType + 'Events']);
  274. eventList.forEach(eventName => {
  275. t.reference[handlerName](eventName, () => t[eventType]());
  276. });
  277. });
  278. }
  279.  
  280. /**
  281. * 设置tooltip的内容
  282. * @param str {String} -必选 要设置的内容,可以包含HTML内容
  283. */
  284. setContent (str) {
  285. const t = this;
  286.  
  287. if (str && t.tooltip) {
  288. const contentEl = t.tooltip.querySelector('.tooltips-content');
  289. if (contentEl) {
  290. contentEl.innerHTML = str;
  291. t.opts.content = str;
  292. }
  293. }
  294. }
  295.  
  296. /**
  297. * 设置tooltip的样式规则
  298. * @param rule {String} -必选 要设置的样式规则
  299. * @param replace {Boolean} -可选 使用当前样式规则替换之前的所有规则
  300. */
  301. setStyleRule (rule, replace) {
  302. const t = this;
  303.  
  304. if (rule && t.tooltip) {
  305. const styleEl = t.tooltip.querySelector('style') || document.createElement('style');
  306.  
  307. if (replace) {
  308. styleEl.innerHTML = '';
  309. }
  310.  
  311. styleEl.appendChild(document.createTextNode(rule));
  312. t.opts.styleRule = rule;
  313. }
  314. }
  315.  
  316. /**
  317. * 显示tooltip对象
  318. * @param str {String} -可选 修改要显示的内容
  319. */
  320. show (str) {
  321. const t = this;
  322.  
  323. if (t.reference && t.tooltip) {
  324. t.setContent(str);
  325. t.tooltip.style.display = 'block';
  326. t.tooltip.style.opacity = 1;
  327. t.status = true;
  328. }
  329. }
  330.  
  331. hide () {
  332. const t = this;
  333.  
  334. if (t.reference && t.tooltip) {
  335. t.tooltip.style.display = 'none';
  336. t.tooltip.style.opacity = 0;
  337. t.status = false;
  338. }
  339. }
  340.  
  341. toggle () {
  342. if (this.status === true) {
  343. this.hide();
  344. } else {
  345. this.show();
  346. }
  347. }
  348.  
  349. destroy () {
  350. const t = this;
  351.  
  352. t._eventsHandler(true);
  353. t.reference = null;
  354.  
  355. if (t.tooltip && t.tooltip.parentNode) {
  356. t.tooltip.parentNode.removeChild(t.tooltip);
  357. }
  358. t.tooltip = null;
  359.  
  360. t.popperInstance && t.popperInstance.destroy();
  361. t.popperInstance = null;
  362. }
  363. }
  364.  
  365. async function init () {
  366. const win = await getPageWindow();
  367. if (win) {
  368. win.Tips = Tips;
  369.  
  370. if (location.host === 'www.baidu.com') {
  371. var reference = document.querySelector('#s_kw_wrap .soutu-btn') || document.querySelector('#form .soutu-btn');
  372.  
  373. var tips = new Tips({
  374. fontSize: 12,
  375. reference: reference,
  376. className: 'test-tooltips',
  377. content: '<h1>document.querySelector(\'#s_kw_wrap .soutu-btn\')</h1>',
  378. show: true,
  379. popperOpts: {},
  380. showEvents: ['mouseenter', 'focus'],
  381. // hideEvents: ['mouseleave', 'blur'],
  382. toggleEvents: ['click']
  383. });
  384.  
  385. console.log(tips);
  386. }
  387. }
  388. }
  389. init();
  390.  
  391. class AssertionError extends Error {}
  392. AssertionError.prototype.name = 'AssertionError';
  393.  
  394. /**
  395. * Minimal assert function
  396. * @param {any} t Value to check if falsy
  397. * @param {string=} m Optional assertion error message
  398. * @throws {AssertionError}
  399. */
  400. function assert (t, m) {
  401. if (!t) {
  402. var err = new AssertionError(m);
  403. if (Error.captureStackTrace) Error.captureStackTrace(err, assert);
  404. throw err
  405. }
  406. }
  407.  
  408. /* eslint-env browser */
  409.  
  410. let ls;
  411. if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
  412. // A simple localStorage interface so that lsp works in SSR contexts. Not for persistant storage in node.
  413. const _nodeStorage = {};
  414. ls = {
  415. getItem (name) {
  416. return _nodeStorage[name] || null
  417. },
  418. setItem (name, value) {
  419. if (arguments.length < 2) throw new Error('Failed to execute \'setItem\' on \'Storage\': 2 arguments required, but only 1 present.')
  420. _nodeStorage[name] = (value).toString();
  421. },
  422. removeItem (name) {
  423. delete _nodeStorage[name];
  424. }
  425. };
  426. } else {
  427. ls = window.localStorage;
  428. }
  429.  
  430. var localStorageProxy = (name, opts = {}) => {
  431. assert(name, 'namepace required');
  432. const {
  433. defaults = {},
  434. lspReset = false,
  435. storageEventListener = true
  436. } = opts;
  437.  
  438. const state = new EventTarget();
  439. try {
  440. const restoredState = JSON.parse(ls.getItem(name)) || {};
  441. if (restoredState.lspReset !== lspReset) {
  442. ls.removeItem(name);
  443. for (const [k, v] of Object.entries({
  444. ...defaults
  445. })) {
  446. state[k] = v;
  447. }
  448. } else {
  449. for (const [k, v] of Object.entries({
  450. ...defaults,
  451. ...restoredState
  452. })) {
  453. state[k] = v;
  454. }
  455. }
  456. } catch (e) {
  457. console.error(e);
  458. ls.removeItem(name);
  459. }
  460.  
  461. state.lspReset = lspReset;
  462.  
  463. if (storageEventListener && typeof window !== 'undefined' && typeof window.addEventListener !== 'undefined') {
  464. window.addEventListener('storage', (ev) => {
  465. // Replace state with whats stored on localStorage... it is newer.
  466. for (const k of Object.keys(state)) {
  467. delete state[k];
  468. }
  469. const restoredState = JSON.parse(ls.getItem(name)) || {};
  470. for (const [k, v] of Object.entries({
  471. ...defaults,
  472. ...restoredState
  473. })) {
  474. state[k] = v;
  475. }
  476. opts.lspReset = restoredState.lspReset;
  477. state.dispatchEvent(new Event('update'));
  478. });
  479. }
  480.  
  481. function boundHandler (rootRef) {
  482. return {
  483. get (obj, prop) {
  484. if (typeof obj[prop] === 'object' && obj[prop] !== null) {
  485. return new Proxy(obj[prop], boundHandler(rootRef))
  486. } else if (typeof obj[prop] === 'function' && obj === rootRef && prop !== 'constructor') {
  487. // this returns bound EventTarget functions
  488. return obj[prop].bind(obj)
  489. } else {
  490. return obj[prop]
  491. }
  492. },
  493. set (obj, prop, value) {
  494. obj[prop] = value;
  495. try {
  496. ls.setItem(name, JSON.stringify(rootRef));
  497. rootRef.dispatchEvent(new Event('update'));
  498. return true
  499. } catch (e) {
  500. console.error(e);
  501. return false
  502. }
  503. }
  504. }
  505. }
  506.  
  507. return new Proxy(state, boundHandler(state))
  508. };
  509.  
  510. const defaultConfig = {
  511. autoPlay: true,
  512.  
  513. /* transform样式规则 */
  514. transform: {
  515. /* 放大缩小系数 */
  516. scale: 1,
  517.  
  518. /* 水平位移参数 */
  519. translate: {
  520. x: 0,
  521. y: 0
  522. },
  523.  
  524. /* 旋转角度 */
  525. rotate: 0,
  526.  
  527. /* 水平镜像翻转, 0 或 180 */
  528. rotateY: 0,
  529. /* 垂直镜像翻转, 0 或 180 */
  530. rotateX: 0
  531. }
  532. };
  533.  
  534. const config = localStorageProxy('_h5playerConfig_', {
  535. defaults: defaultConfig,
  536. lspReset: false,
  537. storageEventListener: false
  538. });
  539.  
  540. /* 保存重要的原始函数,防止被外部脚本污染 */
  541. const originalMethods = {
  542. Object: {
  543. defineProperty: Object.defineProperty,
  544. defineProperties: Object.defineProperties
  545. },
  546. setInterval: window.setInterval,
  547. setTimeout: window.setTimeout
  548. };
  549.  
  550. /*!
  551. * @name utils.js
  552. * @description 数据类型相关的方法
  553. * @version 0.0.1
  554. * @author Blaze
  555. * @date 22/03/2019 22:46
  556. * @github https://github.com/xxxily
  557. */
  558.  
  559. /**
  560. * 准确地获取对象的具体类型 参见:https://www.talkingcoder.com/article/6333557442705696719
  561. * @param obj { all } -必选 要判断的对象
  562. * @returns {*} 返回判断的具体类型
  563. */
  564. function getType (obj) {
  565. if (obj == null) {
  566. return String(obj)
  567. }
  568. return typeof obj === 'object' || typeof obj === 'function'
  569. ? (obj.constructor && obj.constructor.name && obj.constructor.name.toLowerCase()) ||
  570. /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase()
  571. : typeof obj
  572. }
  573.  
  574. const isType = (obj, typeName) => getType(obj) === typeName;
  575. const isObj = obj => isType(obj, 'object');
  576.  
  577. /**
  578. * 任务配置中心 Task Control Center
  579. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  580. * */
  581.  
  582. class TCC {
  583. constructor (taskConf, doTaskFunc) {
  584. this.conf = taskConf || {
  585. /**
  586. * 配置示例
  587. * 父级键名对应的是一级域名,
  588. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  589. * 所有子级的键值都支持使用选择器触发或函数调用
  590. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  591. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  592. * */
  593. 'demo.demo': {
  594. fullScreen: '.fullscreen-btn',
  595. exitFullScreen: '.exit-fullscreen-btn',
  596. webFullScreen: function () {},
  597. exitWebFullScreen: '.exit-fullscreen-btn',
  598. autoPlay: '.player-start-btn',
  599. pause: '.player-pause',
  600. play: '.player-play',
  601. switchPlayStatus: '.player-play',
  602. playbackRate: function () {},
  603. currentTime: function () {},
  604. addCurrentTime: '.add-currenttime',
  605. subtractCurrentTime: '.subtract-currenttime',
  606. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  607. shortcuts: {
  608. /* 注册要执行自定义回调操作的快捷键 */
  609. register: [
  610. 'ctrl+shift+alt+c',
  611. 'ctrl+shift+c',
  612. 'ctrl+alt+c',
  613. 'ctrl+c',
  614. 'c'
  615. ],
  616. /* 自定义快捷键的回调操作 */
  617. callback: function (h5Player, taskConf, data) {
  618. const { event, player } = data;
  619. console.log(event, player);
  620. }
  621. },
  622. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  623. include: /^.*/,
  624. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  625. exclude: /\t/
  626. }
  627. };
  628.  
  629. // 通过doTaskFunc回调定义配置该如何执行任务
  630. this.doTaskFunc = doTaskFunc instanceof Function ? doTaskFunc : function () {};
  631. }
  632.  
  633. /**
  634. * 获取域名 , 目前实现方式不好,需改造,对地区性域名(如com.cn)、三级及以上域名支持不好
  635. * */
  636. getDomain () {
  637. const host = window.location.host;
  638. let domain = host;
  639. const tmpArr = host.split('.');
  640. if (tmpArr.length > 2) {
  641. tmpArr.shift();
  642. domain = tmpArr.join('.');
  643. }
  644. return domain
  645. }
  646.  
  647. /**
  648. * 格式化配置任务
  649. * @param isAll { boolean } -可选 默认只格式当前域名或host下的配置任务,传入true则将所有域名下的任务配置都进行格式化
  650. */
  651. formatTCC (isAll) {
  652. const t = this;
  653. const keys = Object.keys(t.conf);
  654. const domain = t.getDomain();
  655. const host = window.location.host;
  656.  
  657. function formatter (item) {
  658. const defObj = {
  659. include: /^.*/,
  660. exclude: /\t/
  661. };
  662. item.include = item.include || defObj.include;
  663. item.exclude = item.exclude || defObj.exclude;
  664. return item
  665. }
  666.  
  667. const result = {};
  668. keys.forEach(function (key) {
  669. let item = t[key];
  670. if (isObj(item)) {
  671. if (isAll) {
  672. item = formatter(item);
  673. result[key] = item;
  674. } else {
  675. if (key === host || key === domain) {
  676. item = formatter(item);
  677. result[key] = item;
  678. }
  679. }
  680. }
  681. });
  682. return result
  683. }
  684.  
  685. /* 判断所提供的配置任务是否适用于当前URL */
  686. isMatch (taskConf) {
  687. const url = window.location.href;
  688. let isMatch = false;
  689. if (!taskConf.include && !taskConf.exclude) {
  690. isMatch = true;
  691. } else {
  692. if (taskConf.include && taskConf.include.test(url)) {
  693. isMatch = true;
  694. }
  695. if (taskConf.exclude && taskConf.exclude.test(url)) {
  696. isMatch = false;
  697. }
  698. }
  699. return isMatch
  700. }
  701.  
  702. /**
  703. * 获取任务配置,只能获取到当前域名下的任务配置信息
  704. * @param taskName {string} -可选 指定具体任务,默认返回所有类型的任务配置
  705. */
  706. getTaskConfig () {
  707. const t = this;
  708. if (!t._hasFormatTCC_) {
  709. t.formatTCC();
  710. t._hasFormatTCC_ = true;
  711. }
  712. const domain = t.getDomain();
  713. const taskConf = t.conf[window.location.host] || t.conf[domain];
  714.  
  715. if (taskConf && t.isMatch(taskConf)) {
  716. return taskConf
  717. }
  718.  
  719. return {}
  720. }
  721.  
  722. /**
  723. * 执行当前页面下的相应任务
  724. * @param taskName {object|string} -必选,可直接传入任务配置对象,也可用是任务名称的字符串信息,自己去查找是否有任务需要执行
  725. * @param data {object} -可选,传给回调函数的数据
  726. */
  727. doTask (taskName, data) {
  728. const t = this;
  729. let isDo = false;
  730. if (!taskName) return isDo
  731. const taskConf = isObj(taskName) ? taskName : t.getTaskConfig();
  732.  
  733. if (!isObj(taskConf) || !taskConf[taskName]) return isDo
  734.  
  735. const task = taskConf[taskName];
  736.  
  737. if (task) {
  738. isDo = t.doTaskFunc(taskName, taskConf, data);
  739. }
  740.  
  741. return isDo
  742. }
  743. }
  744.  
  745. /**
  746. * 元素监听器
  747. * @param selector -必选
  748. * @param fn -必选,元素存在时的回调
  749. * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素
  750. * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html
  751. */
  752. function ready (selector, fn, shadowRoot) {
  753. const win = window;
  754. const docRoot = shadowRoot || win.document.documentElement;
  755. if (!docRoot) return false
  756. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  757. const listeners = docRoot._MutationListeners || [];
  758.  
  759. function $ready (selector, fn) {
  760. // 储存选择器和回调函数
  761. listeners.push({
  762. selector: selector,
  763. fn: fn
  764. });
  765.  
  766. /* 增加监听对象 */
  767. if (!docRoot._MutationListeners || !docRoot._MutationObserver) {
  768. docRoot._MutationListeners = listeners;
  769. docRoot._MutationObserver = new MutationObserver(() => {
  770. for (let i = 0; i < docRoot._MutationListeners.length; i++) {
  771. const item = docRoot._MutationListeners[i];
  772. check(item.selector, item.fn);
  773. }
  774. });
  775.  
  776. docRoot._MutationObserver.observe(docRoot, {
  777. childList: true,
  778. subtree: true
  779. });
  780. }
  781.  
  782. // 检查节点是否已经在DOM中
  783. check(selector, fn);
  784. }
  785.  
  786. function check (selector, fn) {
  787. const elements = docRoot.querySelectorAll(selector);
  788. for (let i = 0; i < elements.length; i++) {
  789. const element = elements[i];
  790. element._MutationReadyList_ = element._MutationReadyList_ || [];
  791. if (!element._MutationReadyList_.includes(fn)) {
  792. element._MutationReadyList_.push(fn);
  793. fn.call(element, element);
  794. }
  795. }
  796. }
  797.  
  798. const selectorArr = Array.isArray(selector) ? selector : [selector];
  799. selectorArr.forEach(selector => $ready(selector, fn));
  800. }
  801.  
  802. /**
  803. * 某些网页用了attachShadow closed mode,需要open才能获取video标签,例如百度云盘
  804. * 解决参考:
  805. * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed
  806. * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension
  807. */
  808. function hackAttachShadow () {
  809. if (window._hasHackAttachShadow_) return
  810. try {
  811. window._shadowDomList_ = [];
  812. window.Element.prototype._attachShadow = window.Element.prototype.attachShadow;
  813. window.Element.prototype.attachShadow = function () {
  814. const arg = arguments;
  815. if (arg[0] && arg[0].mode) {
  816. // 强制使用 open mode
  817. arg[0].mode = 'open';
  818. }
  819. const shadowRoot = this._attachShadow.apply(this, arg);
  820. // 存一份shadowDomList
  821. window._shadowDomList_.push(shadowRoot);
  822.  
  823. /* 让shadowRoot里面的元素有机会访问shadowHost */
  824. shadowRoot._shadowHost = this;
  825.  
  826. // 在document下面添加 addShadowRoot 自定义事件
  827. const shadowEvent = new window.CustomEvent('addShadowRoot', {
  828. shadowRoot,
  829. detail: {
  830. shadowRoot,
  831. message: 'addShadowRoot',
  832. time: new Date()
  833. },
  834. bubbles: true,
  835. cancelable: true
  836. });
  837. document.dispatchEvent(shadowEvent);
  838.  
  839. return shadowRoot
  840. };
  841. window._hasHackAttachShadow_ = true;
  842. } catch (e) {
  843. console.error('hackAttachShadow error by h5player plug-in', e);
  844. }
  845. }
  846.  
  847. const quickSort = function (arr) {
  848. if (arr.length <= 1) { return arr }
  849. var pivotIndex = Math.floor(arr.length / 2);
  850. var pivot = arr.splice(pivotIndex, 1)[0];
  851. var left = [];
  852. var right = [];
  853. for (var i = 0; i < arr.length; i++) {
  854. if (arr[i] < pivot) {
  855. left.push(arr[i]);
  856. } else {
  857. right.push(arr[i]);
  858. }
  859. }
  860. return quickSort(left).concat([pivot], quickSort(right))
  861. };
  862.  
  863. function hideDom (selector, delay) {
  864. setTimeout(function () {
  865. const dom = document.querySelector(selector);
  866. if (dom) {
  867. dom.style.opacity = 0;
  868. }
  869. }, delay || 1000 * 5);
  870. }
  871.  
  872. /**
  873. * 向上查找操作
  874. * @param dom {Element} -必选 初始dom元素
  875. * @param fn {function} -必选 每一级ParentNode的回调操作
  876. * 如果函数返回true则表示停止向上查找动作
  877. */
  878. function eachParentNode (dom, fn) {
  879. let parent = dom.parentNode;
  880. while (parent) {
  881. const isEnd = fn(parent, dom);
  882. parent = parent.parentNode;
  883. if (isEnd) {
  884. break
  885. }
  886. }
  887. }
  888.  
  889. /**
  890. * 动态加载css内容
  891. * @param cssText {String} -必选 样式的文本内容
  892. * @param id {String} -可选 指定样式文本的id号,如果已存在对应id号则不会再次插入
  893. * @param insetTo {Dom} -可选 指定插入到哪
  894. * @returns {HTMLStyleElement}
  895. */
  896. function loadCSSText (cssText, id, insetTo) {
  897. if (id && document.getElementById(id)) {
  898. return false
  899. }
  900.  
  901. const style = document.createElement('style');
  902. const head = insetTo || document.head || document.getElementsByTagName('head')[0];
  903. style.appendChild(document.createTextNode(cssText));
  904. head.appendChild(style);
  905.  
  906. if (id) {
  907. style.setAttribute('id', id);
  908. }
  909.  
  910. return style
  911. }
  912.  
  913. /**
  914. * 判断当前元素是否为可编辑元素
  915. * @param target
  916. * @returns Boolean
  917. */
  918. function isEditableTarget (target) {
  919. const isEditable = target.getAttribute && target.getAttribute('contenteditable') === 'true';
  920. const isInputDom = /INPUT|TEXTAREA|SELECT/.test(target.nodeName);
  921. return isEditable || isInputDom
  922. }
  923.  
  924. /**
  925. * 判断某个元素是否处于shadowDom里面
  926. * 参考:https://www.coder.work/article/299700
  927. * @param node
  928. * @returns {boolean}
  929. */
  930. function isInShadow (node, returnShadowRoot) {
  931. for (; node; node = node.parentNode) {
  932. if (node.toString() === '[object ShadowRoot]') {
  933. if (returnShadowRoot) {
  934. return node
  935. } else {
  936. return true
  937. }
  938. }
  939. }
  940. return false
  941. }
  942.  
  943. /**
  944. * 判断某个元素是否处于可视区域,适用于被动调用情况,需要高性能,请使用IntersectionObserver
  945. * 参考:https://github.com/febobo/web-interview/issues/84
  946. * @param element
  947. * @returns {boolean}
  948. */
  949. function isInViewPort (element) {
  950. const viewWidth = window.innerWidth || document.documentElement.clientWidth;
  951. const viewHeight = window.innerHeight || document.documentElement.clientHeight;
  952. const {
  953. top,
  954. right,
  955. bottom,
  956. left
  957. } = element.getBoundingClientRect();
  958.  
  959. return (
  960. top >= 0 &&
  961. left >= 0 &&
  962. right <= viewWidth &&
  963. bottom <= viewHeight
  964. )
  965. }
  966.  
  967. /* ua信息伪装 */
  968. function fakeUA (ua) {
  969. Object.defineProperty(navigator, 'userAgent', {
  970. value: ua,
  971. writable: false,
  972. configurable: false,
  973. enumerable: true
  974. });
  975. }
  976.  
  977. /* ua信息来源:https://developers.whatismybrowser.com */
  978. const userAgentMap = {
  979. android: {
  980. chrome: 'Mozilla/5.0 (Linux; Android 9; SM-G960F Build/PPR1.180610.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.157 Mobile Safari/537.36',
  981. firefox: 'Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0'
  982. },
  983. iPhone: {
  984. safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1',
  985. chrome: 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.121 Mobile/15E148 Safari/605.1'
  986. },
  987. iPad: {
  988. safari: 'Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1',
  989. chrome: 'Mozilla/5.0 (iPad; CPU OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/74.0.3729.155 Mobile/15E148 Safari/605.1'
  990. }
  991. };
  992.  
  993. /**
  994. * 判断是否处于Iframe中
  995. * @returns {boolean}
  996. */
  997. function isInIframe () {
  998. return window !== window.top
  999. }
  1000.  
  1001. /**
  1002. * 判断是否处于跨域限制的Iframe中
  1003. * @returns {boolean}
  1004. */
  1005. function isInCrossOriginFrame () {
  1006. let result = true;
  1007. try {
  1008. if (window.top.localStorage || window.top.location.href) {
  1009. result = false;
  1010. }
  1011. } catch (e) {
  1012. result = true;
  1013. }
  1014. return result
  1015. }
  1016.  
  1017. /**
  1018. * 简单的节流函数
  1019. * @param fn
  1020. * @param interval
  1021. * @returns {Function}
  1022. */
  1023. function throttle (fn, interval = 80) {
  1024. let timeout = null;
  1025. return function () {
  1026. if (timeout) return false
  1027. timeout = setTimeout(() => {
  1028. timeout = null;
  1029. }, interval);
  1030. fn.apply(this, arguments);
  1031. }
  1032. }
  1033.  
  1034. const $q = document.querySelector.bind(document);
  1035.  
  1036. /**
  1037. * 任务配置中心 Task Control Center
  1038. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  1039. * */
  1040.  
  1041. const taskConf = {
  1042. /**
  1043. * 配置示例
  1044. * 父级键名对应的是一级域名,
  1045. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  1046. * 所有子级的键值都支持使用选择器触发或函数调用
  1047. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  1048. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  1049. * */
  1050. 'demo.demo': {
  1051. fullScreen: '.fullscreen-btn',
  1052. exitFullScreen: '.exit-fullscreen-btn',
  1053. webFullScreen: function () {},
  1054. exitWebFullScreen: '.exit-fullscreen-btn',
  1055. autoPlay: '.player-start-btn',
  1056. pause: '.player-pause',
  1057. play: '.player-play',
  1058. switchPlayStatus: '.player-play',
  1059. playbackRate: function () {},
  1060. currentTime: function () {},
  1061. addCurrentTime: '.add-currenttime',
  1062. subtractCurrentTime: '.subtract-currenttime',
  1063. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  1064. shortcuts: {
  1065. /* 注册要执行自定义回调操作的快捷键 */
  1066. register: [
  1067. 'ctrl+shift+alt+c',
  1068. 'ctrl+shift+c',
  1069. 'ctrl+alt+c',
  1070. 'ctrl+c',
  1071. 'c'
  1072. ],
  1073. /* 自定义快捷键的回调操作 */
  1074. callback: function (h5Player, taskConf, data) {
  1075. const { event, player } = data;
  1076. console.log(event, player);
  1077. }
  1078. },
  1079. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  1080. include: /^.*/,
  1081. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  1082. exclude: /\t/
  1083. },
  1084. 'youtube.com': {
  1085. webFullScreen: 'button.ytp-size-button',
  1086. fullScreen: 'button.ytp-fullscreen-button',
  1087. next: '.ytp-next-button',
  1088. shortcuts: {
  1089. register: [
  1090. 'escape'
  1091. ],
  1092. callback: function (h5Player, taskConf, data) {
  1093. const { event } = data;
  1094. if (event.keyCode === 27) {
  1095. /* 取消播放下一个推荐的视频 */
  1096. if (document.querySelector('.ytp-upnext').style.display !== 'none') {
  1097. document.querySelector('.ytp-upnext-cancel-button').click();
  1098. }
  1099. }
  1100. }
  1101. }
  1102. },
  1103. 'netflix.com': {
  1104. fullScreen: 'button.button-nfplayerFullscreen',
  1105. addCurrentTime: 'button.button-nfplayerFastForward',
  1106. subtractCurrentTime: 'button.button-nfplayerBackTen'
  1107. },
  1108. 'bilibili.com': {
  1109. fullScreen: function () {
  1110. const fullScreen = $q('.bpx-player-ctrl-full') || $q('.squirtle-video-fullscreen') || $q('.bilibili-player-video-btn-fullscreen');
  1111. if (fullScreen) {
  1112. fullScreen.click();
  1113. return true
  1114. }
  1115. },
  1116. webFullScreen: function () {
  1117. const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen');
  1118. const webFullscreenEnter = $q('.bpx-player-ctrl-web-enter') || $q('.squirtle-pagefullscreen-inactive');
  1119. const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active');
  1120. if (oldWebFullscreen || (webFullscreenEnter && webFullscreenLeave)) {
  1121. const webFullscreen = oldWebFullscreen || (getComputedStyle(webFullscreenLeave).display === 'none' ? webFullscreenEnter : webFullscreenLeave);
  1122. webFullscreen.click();
  1123.  
  1124. /* 取消弹幕框聚焦,干扰了快捷键的操作 */
  1125. setTimeout(function () {
  1126. const danmaku = $q('.bpx-player-dm-input') || $q('.bilibili-player-video-danmaku-input');
  1127. danmaku && danmaku.blur();
  1128. }, 1000 * 0.1);
  1129.  
  1130. return true
  1131. }
  1132. },
  1133. autoPlay: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'],
  1134. switchPlayStatus: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'],
  1135. next: ['.bpx-player-ctrl-next', '.squirtle-video-next', '.bilibili-player-video-btn-next'],
  1136. init: function (h5Player, taskConf) {},
  1137. shortcuts: {
  1138. register: [
  1139. 'escape'
  1140. ],
  1141. callback: function (h5Player, taskConf, data) {
  1142. const { event } = data;
  1143. if (event.keyCode === 27) {
  1144. /* 退出网页全屏 */
  1145. const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen');
  1146. if (oldWebFullscreen && oldWebFullscreen.classList.contains('closed')) {
  1147. oldWebFullscreen.click();
  1148. } else {
  1149. const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active');
  1150. if (getComputedStyle(webFullscreenLeave).display !== 'none') {
  1151. webFullscreenLeave.click();
  1152. }
  1153. }
  1154. }
  1155. }
  1156. }
  1157. },
  1158. 't.bilibili.com': {
  1159. fullScreen: 'button[name="fullscreen-button"]'
  1160. },
  1161. 'live.bilibili.com': {
  1162. init: function () {
  1163. if (!JSON._stringifySource_) {
  1164. JSON._stringifySource_ = JSON.stringify;
  1165.  
  1166. JSON.stringify = function (arg1) {
  1167. try {
  1168. return JSON._stringifySource_.apply(this, arguments)
  1169. } catch (e) {
  1170. console.error('JSON.stringify 解释出错:', e, arg1);
  1171. }
  1172. };
  1173. }
  1174. },
  1175. fullScreen: '.bilibili-live-player-video-controller-fullscreen-btn button',
  1176. webFullScreen: '.bilibili-live-player-video-controller-web-fullscreen-btn button',
  1177. switchPlayStatus: '.bilibili-live-player-video-controller-start-btn button'
  1178. },
  1179. 'acfun.cn': {
  1180. fullScreen: '[data-bind-key="screenTip"]',
  1181. webFullScreen: '[data-bind-key="webTip"]',
  1182. switchPlayStatus: function (h5player) {
  1183. /* 无法抢得控制权,只好延迟判断要不要干预 */
  1184. const player = h5player.player();
  1185. const status = player.paused;
  1186. setTimeout(function () {
  1187. if (status === player.paused) {
  1188. if (player.paused) {
  1189. player.play();
  1190. } else {
  1191. player.pause();
  1192. }
  1193. }
  1194. }, 200);
  1195. }
  1196. },
  1197. 'ixigua.com': {
  1198. fullScreen: 'xg-fullscreen.xgplayer-fullscreen',
  1199. webFullScreen: 'xg-cssfullscreen.xgplayer-cssfullscreen'
  1200. },
  1201. 'tv.sohu.com': {
  1202. fullScreen: 'button[data-title="网页全屏"]',
  1203. webFullScreen: 'button[data-title="全屏"]'
  1204. },
  1205. 'iqiyi.com': {
  1206. fullScreen: '.iqp-btn-fullscreen',
  1207. webFullScreen: '.iqp-btn-webscreen',
  1208. next: '.iqp-btn-next',
  1209. init: function (h5Player, taskConf) {
  1210. // 隐藏水印
  1211. hideDom('.iqp-logo-box');
  1212. // 移除暂停广告
  1213. window.GM_addStyle(`
  1214. div[templatetype="common_pause"]{ display:none }
  1215. .iqp-logo-box{ display:none !important }
  1216. `);
  1217. }
  1218. },
  1219. 'youku.com': {
  1220. fullScreen: '.control-fullscreen-icon',
  1221. next: '.control-next-video',
  1222. init: function (h5Player, taskConf) {
  1223. // 隐藏水印
  1224. hideDom('.youku-layer-logo');
  1225. }
  1226. },
  1227. 'ted.com': {
  1228. fullScreen: 'button.Fullscreen'
  1229. },
  1230. 'qq.com': {
  1231. pause: '.container_inner .txp-shadow-mod',
  1232. play: '.container_inner .txp-shadow-mod',
  1233. shortcuts: {
  1234. register: ['c', 'x', 'z', '1', '2', '3', '4'],
  1235. callback: function (h5Player, taskConf, data) {
  1236. const { event } = data;
  1237. const key = event.key.toLowerCase();
  1238. const keyName = 'customShortcuts_' + key;
  1239.  
  1240. if (!h5Player[keyName]) {
  1241. /* 第一次按下快捷键使用默认逻辑进行调速 */
  1242. h5Player[keyName] = {
  1243. time: Date.now(),
  1244. playbackRate: h5Player.playbackRate
  1245. };
  1246. return false
  1247. } else {
  1248. /* 第一次操作后的200ms内的操作都是由默认逻辑进行调速 */
  1249. if (Date.now() - h5Player[keyName].time < 200) {
  1250. return false
  1251. }
  1252.  
  1253. /* 判断是否需进行降级处理,利用sessionStorage进行调速 */
  1254. if (h5Player[keyName] === h5Player.playbackRate || h5Player[keyName] === true) {
  1255. if (window.sessionStorage.playbackRate && /(c|x|z|1|2|3|4)/.test(key)) {
  1256. const curSpeed = Number(window.sessionStorage.playbackRate);
  1257. const perSpeed = curSpeed - 0.1 >= 0 ? curSpeed - 0.1 : 0.1;
  1258. const nextSpeed = curSpeed + 0.1 <= 4 ? curSpeed + 0.1 : 4;
  1259. let targetSpeed = curSpeed;
  1260. switch (key) {
  1261. case 'z' :
  1262. targetSpeed = 1;
  1263. break
  1264. case 'c' :
  1265. targetSpeed = nextSpeed;
  1266. break
  1267. case 'x' :
  1268. targetSpeed = perSpeed;
  1269. break
  1270. default :
  1271. targetSpeed = Number(key);
  1272. break
  1273. }
  1274.  
  1275. window.sessionStorage.playbackRate = targetSpeed;
  1276. h5Player.setCurrentTime(0.01, true);
  1277. h5Player.setPlaybackRate(targetSpeed, true);
  1278. return true
  1279. }
  1280.  
  1281. /* 标识默认调速方案失效,需启用sessionStorage调速方案 */
  1282. h5Player[keyName] = true;
  1283. } else {
  1284. /* 标识默认调速方案生效 */
  1285. h5Player[keyName] = false;
  1286. }
  1287. }
  1288. }
  1289. },
  1290. fullScreen: 'txpdiv[data-report="window-fullscreen"]',
  1291. webFullScreen: 'txpdiv[data-report="browser-fullscreen"]',
  1292. next: 'txpdiv[data-report="play-next"]',
  1293. init: function (h5Player, taskConf) {
  1294. // 隐藏水印
  1295. hideDom('.txp-watermark');
  1296. hideDom('.txp-watermark-action');
  1297. },
  1298. include: /(v.qq|sports.qq)/
  1299. },
  1300. 'pan.baidu.com': {
  1301. fullScreen: function (h5Player, taskConf) {
  1302. h5Player.player().parentNode.querySelector('.vjs-fullscreen-control').click();
  1303. }
  1304. },
  1305. // 'pornhub.com': {
  1306. // fullScreen: 'div[class*="icon-fullscreen"]',
  1307. // webFullScreen: 'div[class*="icon-size-large"]'
  1308. // },
  1309. 'facebook.com': {
  1310. fullScreen: function (h5Player, taskConf) {
  1311. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  1312. if (actionBtn && actionBtn.length > 3) {
  1313. /* 模拟点击倒数第二个按钮 */
  1314. actionBtn[actionBtn.length - 2].click();
  1315. return true
  1316. }
  1317. },
  1318. webFullScreen: function (h5Player, taskConf) {
  1319. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  1320. if (actionBtn && actionBtn.length > 3) {
  1321. /* 模拟点击倒数第二个按钮 */
  1322. actionBtn[actionBtn.length - 2].click();
  1323. return true
  1324. }
  1325. },
  1326. shortcuts: {
  1327. /* 在视频模式下按esc键,自动返回上一层界面 */
  1328. register: [
  1329. 'escape'
  1330. ],
  1331. /* 自定义快捷键的回调操作 */
  1332. callback: function (h5Player, taskConf, data) {
  1333. eachParentNode(h5Player.player(), function (parentNode) {
  1334. if (parentNode.getAttribute('data-fullscreen-container') === 'true') {
  1335. const goBackBtn = parentNode.parentNode.querySelector('div>a>i>u');
  1336. if (goBackBtn) {
  1337. goBackBtn.parentNode.parentNode.click();
  1338. }
  1339. return true
  1340. }
  1341. });
  1342. }
  1343. }
  1344. },
  1345. 'douyu.com': {
  1346. fullScreen: function (h5Player, taskConf) {
  1347. const player = h5Player.player();
  1348. const container = player._fullScreen_.getContainer();
  1349. if (player._isFullScreen_) {
  1350. container.querySelector('div[title="退出窗口全屏"]').click();
  1351. } else {
  1352. container.querySelector('div[title="窗口全屏"]').click();
  1353. }
  1354. player._isFullScreen_ = !player._isFullScreen_;
  1355. return true
  1356. },
  1357. webFullScreen: function (h5Player, taskConf) {
  1358. const player = h5Player.player();
  1359. const container = player._fullScreen_.getContainer();
  1360. if (player._isWebFullScreen_) {
  1361. container.querySelector('div[title="退出网页全屏"]').click();
  1362. } else {
  1363. container.querySelector('div[title="网页全屏"]').click();
  1364. }
  1365. player._isWebFullScreen_ = !player._isWebFullScreen_;
  1366. return true
  1367. }
  1368. },
  1369. 'open.163.com': {
  1370. init: function (h5Player, taskConf) {
  1371. const player = h5Player.player();
  1372. /**
  1373. * 不设置CORS标识,这样才能跨域截图
  1374. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
  1375. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes
  1376. */
  1377. player.setAttribute('crossOrigin', 'anonymous');
  1378. }
  1379. },
  1380. 'agefans.tv': {
  1381. init: function (h5Player, taskConf) {
  1382. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  1383. }
  1384. },
  1385. 'chaoxing.com': {
  1386. fullScreen: '.vjs-fullscreen-control'
  1387. },
  1388. 'yixi.tv': {
  1389. init: function (h5Player, taskConf) {
  1390. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  1391. }
  1392. },
  1393. 'douyin.com': {
  1394. fullScreen: '.xgplayer-fullscreen',
  1395. webFullScreen: '.xgplayer-page-full-screen',
  1396. init: function (h5Player, taskConf) {
  1397. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  1398. }
  1399. },
  1400. 'live.douyin.com': {
  1401. fullScreen: '.xgplayer-fullscreen',
  1402. webFullScreen: '.xgplayer-page-full-screen',
  1403. init: function (h5Player, taskConf) {
  1404. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  1405. }
  1406. }
  1407. };
  1408.  
  1409. function h5PlayerTccInit (h5Player) {
  1410. return new TCC(taskConf, function (taskName, taskConf, data) {
  1411. const task = taskConf[taskName];
  1412. const wrapDom = h5Player.getPlayerWrapDom();
  1413.  
  1414. if (taskName === 'shortcuts') {
  1415. if (isObj(task) && task.callback instanceof Function) {
  1416. return task.callback(h5Player, taskConf, data)
  1417. }
  1418. } else if (task instanceof Function) {
  1419. try {
  1420. return task(h5Player, taskConf, data)
  1421. } catch (e) {
  1422. console.error('TCC自定义函数任务执行失败:', h5Player, taskConf, data);
  1423. return false
  1424. }
  1425. } else {
  1426. const selectorList = Array.isArray(task) ? task : [task];
  1427. for (let i = 0; i < selectorList.length; i++) {
  1428. const selector = selectorList[i];
  1429.  
  1430. /* 触发选择器上的点击事件 */
  1431. if (wrapDom && wrapDom.querySelector(selector)) {
  1432. // 在video的父元素里查找,是为了尽可能兼容多实例下的逻辑
  1433. wrapDom.querySelector(selector).click();
  1434. return true
  1435. } else if (document.querySelector(selector)) {
  1436. document.querySelector(selector).click();
  1437. return true
  1438. }
  1439. }
  1440. }
  1441. })
  1442. }
  1443.  
  1444. /* ua伪装配置 */
  1445. const fakeConfig = {
  1446. // 'tv.cctv.com': userAgentMap.iPhone.chrome,
  1447. // 'v.qq.com': userAgentMap.iPad.chrome,
  1448. 'open.163.com': userAgentMap.iPhone.chrome,
  1449. 'm.open.163.com': userAgentMap.iPhone.chrome
  1450. };
  1451.  
  1452. function setFakeUA (ua) {
  1453. const host = window.location.host;
  1454. ua = ua || fakeConfig[host];
  1455.  
  1456. /**
  1457. * 动态判断是否需要进行ua伪装
  1458. * 下面方案暂时不可用
  1459. * 由于部分网站跳转至移动端后域名不一致,形成跨域问题
  1460. * 导致无法同步伪装配置而不断死循环跳转
  1461. * eg. open.163.com
  1462. * */
  1463. // let customUA = window.localStorage.getItem('_h5_player_user_agent_')
  1464. // debug.log(customUA, window.location.href, window.navigator.userAgent, document.referrer)
  1465. // if (customUA) {
  1466. // fakeUA(customUA)
  1467. // alert(customUA)
  1468. // } else {
  1469. // alert('ua false')
  1470. // }
  1471.  
  1472. ua && fakeUA(ua);
  1473. }
  1474.  
  1475. /**
  1476. * 元素全屏API,同时兼容网页全屏
  1477. */
  1478.  
  1479. hackAttachShadow();
  1480. class FullScreen {
  1481. constructor (dom, pageMode) {
  1482. this.dom = dom;
  1483. this.shadowRoot = null;
  1484. this.fullStatus = false;
  1485. // 默认全屏模式,如果传入pageMode则表示进行的是页面全屏操作
  1486. this.pageMode = pageMode || false;
  1487. const fullPageStyle = `
  1488. ._webfullscreen_box_size_ {
  1489. width: 100% !important;
  1490. height: 100% !important;
  1491. }
  1492. ._webfullscreen_ {
  1493. display: block !important;
  1494. position: fixed !important;
  1495. width: 100% !important;
  1496. height: 100% !important;
  1497. top: 0 !important;
  1498. left: 0 !important;
  1499. background: #000 !important;
  1500. z-index: 999999 !important;
  1501. }
  1502. ._webfullscreen_zindex_ {
  1503. z-index: 999999 !important;
  1504. }
  1505. `;
  1506. /* 将样式插入到全局页面中 */
  1507. if (!window._hasInitFullPageStyle_) {
  1508. window.GM_addStyle(fullPageStyle);
  1509. window._hasInitFullPageStyle_ = true;
  1510. }
  1511.  
  1512. /* 将样式插入到shadowRoot中 */
  1513. const shadowRoot = isInShadow(dom, true);
  1514. if (shadowRoot) {
  1515. this.shadowRoot = shadowRoot;
  1516. loadCSSText(fullPageStyle, 'fullPageStyle', shadowRoot);
  1517. }
  1518.  
  1519. const t = this;
  1520. window.addEventListener('keyup', (event) => {
  1521. const key = event.key.toLowerCase();
  1522. if (key === 'escape') {
  1523. if (t.isFull()) {
  1524. t.exit();
  1525. } else if (t.isFullScreen()) {
  1526. t.exitFullScreen();
  1527. }
  1528. }
  1529. }, true);
  1530.  
  1531. this.getContainer();
  1532. }
  1533.  
  1534. eachParentNode (dom, fn) {
  1535. let parent = dom.parentNode;
  1536. while (parent && parent.classList) {
  1537. const isEnd = fn(parent, dom);
  1538. parent = parent.parentNode;
  1539. if (isEnd) {
  1540. break
  1541. }
  1542. }
  1543. }
  1544.  
  1545. getContainer () {
  1546. const t = this;
  1547. if (t._container_) return t._container_
  1548.  
  1549. const d = t.dom;
  1550. const domBox = d.getBoundingClientRect();
  1551. let container = d;
  1552. t.eachParentNode(d, function (parentNode) {
  1553. const noParentNode = !parentNode || !parentNode.getBoundingClientRect;
  1554. if (noParentNode || parentNode.getAttribute('data-fullscreen-container')) {
  1555. container = parentNode;
  1556. return true
  1557. }
  1558.  
  1559. const parentBox = parentNode.getBoundingClientRect();
  1560. const isInsideTheBox = parentBox.width <= domBox.width && parentBox.height <= domBox.height;
  1561. if (isInsideTheBox) {
  1562. container = parentNode;
  1563. } else {
  1564. return true
  1565. }
  1566. });
  1567.  
  1568. container.setAttribute('data-fullscreen-container', 'true');
  1569. t._container_ = container;
  1570. return container
  1571. }
  1572.  
  1573. isFull () {
  1574. return this.dom.classList.contains('_webfullscreen_') || this.fullStatus
  1575. }
  1576.  
  1577. isFullScreen () {
  1578. const d = document;
  1579. return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen ||
  1580. d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement)
  1581. }
  1582.  
  1583. enterFullScreen () {
  1584. const c = this.getContainer();
  1585. const enterFn = c.requestFullscreen || c.webkitRequestFullScreen || c.mozRequestFullScreen || c.msRequestFullScreen;
  1586. enterFn && enterFn.call(c);
  1587. }
  1588.  
  1589. enter () {
  1590. const t = this;
  1591. if (t.isFull()) return
  1592. const container = t.getContainer();
  1593. let needSetIndex = false;
  1594. if (t.dom === container) {
  1595. needSetIndex = true;
  1596. }
  1597.  
  1598. function addFullscreenStyleToParentNode (node) {
  1599. t.eachParentNode(node, function (parentNode) {
  1600. parentNode.classList.add('_webfullscreen_');
  1601. if (container === parentNode || needSetIndex) {
  1602. needSetIndex = true;
  1603. parentNode.classList.add('_webfullscreen_zindex_');
  1604. }
  1605. });
  1606. }
  1607. addFullscreenStyleToParentNode(t.dom);
  1608.  
  1609. /* 判断dom自身是否需要加上webfullscreen样式 */
  1610. if (t.dom.parentNode) {
  1611. const domBox = t.dom.getBoundingClientRect();
  1612. const domParentBox = t.dom.parentNode.getBoundingClientRect();
  1613. if (domParentBox.width - domBox.width >= 5) {
  1614. t.dom.classList.add('_webfullscreen_');
  1615. }
  1616.  
  1617. if (t.shadowRoot && t.shadowRoot._shadowHost) {
  1618. const shadowHost = t.shadowRoot._shadowHost;
  1619. const shadowHostBox = shadowHost.getBoundingClientRect();
  1620. if (shadowHostBox.width <= domBox.width) {
  1621. shadowHost.classList.add('_webfullscreen_');
  1622. addFullscreenStyleToParentNode(shadowHost);
  1623. }
  1624. }
  1625. }
  1626.  
  1627. const fullScreenMode = !t.pageMode;
  1628. if (fullScreenMode) {
  1629. t.enterFullScreen();
  1630. }
  1631.  
  1632. this.fullStatus = true;
  1633. }
  1634.  
  1635. exitFullScreen () {
  1636. const d = document;
  1637. const exitFn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen;
  1638. exitFn && exitFn.call(d);
  1639. }
  1640.  
  1641. exit () {
  1642. const t = this;
  1643.  
  1644. function removeFullscreenStyleToParentNode (node) {
  1645. t.eachParentNode(node, function (parentNode) {
  1646. parentNode.classList.remove('_webfullscreen_');
  1647. parentNode.classList.remove('_webfullscreen_zindex_');
  1648. });
  1649. }
  1650. removeFullscreenStyleToParentNode(t.dom);
  1651.  
  1652. t.dom.classList.remove('_webfullscreen_');
  1653.  
  1654. if (t.shadowRoot && t.shadowRoot._shadowHost) {
  1655. const shadowHost = t.shadowRoot._shadowHost;
  1656. shadowHost.classList.remove('_webfullscreen_');
  1657. removeFullscreenStyleToParentNode(shadowHost);
  1658. }
  1659.  
  1660. const fullScreenMode = !t.pageMode;
  1661. if (fullScreenMode || t.isFullScreen()) {
  1662. t.exitFullScreen();
  1663. }
  1664. this.fullStatus = false;
  1665. }
  1666.  
  1667. toggle () {
  1668. this.isFull() ? this.exit() : this.enter();
  1669. }
  1670. }
  1671.  
  1672. /*!
  1673. * @name videoCapturer.js
  1674. * @version 0.0.1
  1675. * @author Blaze
  1676. * @date 2019/9/21 12:03
  1677. * @github https://github.com/xxxily
  1678. */
  1679.  
  1680. async function setClipboard (blob) {
  1681. if (navigator.clipboard) {
  1682. navigator.clipboard.write([
  1683. // eslint-disable-next-line no-undef
  1684. new ClipboardItem({
  1685. [blob.type]: blob
  1686. })
  1687. ]).then(() => {
  1688. alert('clipboard suc');
  1689. });
  1690. }
  1691. }
  1692.  
  1693. var videoCapturer = {
  1694. /**
  1695. * 进行截图操作
  1696. * @param video {dom} -必选 video dom 标签
  1697. * @returns {boolean}
  1698. */
  1699. capture (video, download, title) {
  1700. if (!video) return false
  1701. const t = this;
  1702. const currentTime = `${Math.floor(video.currentTime / 60)}'${(video.currentTime % 60).toFixed(3)}''`;
  1703. const captureTitle = title || `${document.title}_${currentTime}`;
  1704.  
  1705. /* 截图核心逻辑 */
  1706. video.setAttribute('crossorigin', 'anonymous');
  1707. const canvas = document.createElement('canvas');
  1708. canvas.width = video.videoWidth;
  1709. canvas.height = video.videoHeight;
  1710. const context = canvas.getContext('2d');
  1711. context.drawImage(video, 0, 0, canvas.width, canvas.height);
  1712.  
  1713. if (download) {
  1714. t.download(canvas, captureTitle, video);
  1715. } else {
  1716. t.previe(canvas, captureTitle);
  1717. }
  1718.  
  1719. return canvas
  1720. },
  1721. /**
  1722. * 预览截取到的画面内容
  1723. * @param canvas
  1724. */
  1725. previe (canvas, title) {
  1726. canvas.style = 'max-width:100%';
  1727. const previewPage = window.open('', '_blank');
  1728. previewPage.document.title = `capture previe - ${title || 'Untitled'}`;
  1729. previewPage.document.body.style.textAlign = 'center';
  1730. previewPage.document.body.style.background = '#000';
  1731. previewPage.document.body.appendChild(canvas);
  1732. },
  1733. /**
  1734. * canvas 下载截取到的内容
  1735. * @param canvas
  1736. */
  1737. download (canvas, title, video) {
  1738. title = title || 'videoCapturer_' + Date.now();
  1739.  
  1740. try {
  1741. canvas.toBlob(function (blob) {
  1742. const el = document.createElement('a');
  1743. el.download = `${title}.jpg`;
  1744. el.href = URL.createObjectURL(blob);
  1745. el.click();
  1746.  
  1747. /* 尝试复制到剪贴板 */
  1748. setClipboard(blob);
  1749. }, 'image/jpeg', 0.99);
  1750. } catch (e) {
  1751. videoCapturer.previe(canvas, title);
  1752. console.error('视频源受CORS标识限制,无法直接下载截图,见:\n https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS');
  1753. console.error(video, e);
  1754. }
  1755. }
  1756. };
  1757.  
  1758. /**
  1759. * 鼠标事件观测对象
  1760. * 用于实现鼠标事件的穿透响应,有别于pointer-events:none
  1761. * pointer-events:none是设置当前层允许穿透
  1762. * 而MouseObserver是:即使不知道target上面存在多少层遮挡,一样可以响应鼠标事件
  1763. */
  1764.  
  1765. class MouseObserver {
  1766. constructor (observeOpt) {
  1767. // eslint-disable-next-line no-undef
  1768. this.observer = new IntersectionObserver((infoList) => {
  1769. infoList.forEach((info) => {
  1770. info.target.IntersectionObserverEntry = info;
  1771. });
  1772. }, observeOpt || {});
  1773.  
  1774. this.observeList = [];
  1775. }
  1776.  
  1777. _observe (target) {
  1778. let hasObserve = false;
  1779. for (let i = 0; i < this.observeList.length; i++) {
  1780. const el = this.observeList[i];
  1781. if (target === el) {
  1782. hasObserve = true;
  1783. break
  1784. }
  1785. }
  1786.  
  1787. if (!hasObserve) {
  1788. this.observer.observe(target);
  1789. this.observeList.push(target);
  1790. }
  1791. }
  1792.  
  1793. _unobserve (target) {
  1794. this.observer.unobserve(target);
  1795. const newObserveList = [];
  1796. this.observeList.forEach((el) => {
  1797. if (el !== target) {
  1798. newObserveList.push(el);
  1799. }
  1800. });
  1801. this.observeList = newObserveList;
  1802. }
  1803.  
  1804. /**
  1805. * 增加事件绑定
  1806. * @param target {element} -必选 要绑定事件的dom对象
  1807. * @param type {string} -必选 要绑定的事件,只支持鼠标事件
  1808. * @param listener {function} -必选 符合触发条件时的响应函数
  1809. */
  1810. on (target, type, listener, options) {
  1811. const t = this;
  1812. t._observe(target);
  1813.  
  1814. if (!target.MouseObserverEvent) {
  1815. target.MouseObserverEvent = {};
  1816. }
  1817. target.MouseObserverEvent[type] = true;
  1818.  
  1819. if (!t._mouseObserver_) {
  1820. t._mouseObserver_ = {};
  1821. }
  1822.  
  1823. if (!t._mouseObserver_[type]) {
  1824. t._mouseObserver_[type] = [];
  1825.  
  1826. window.addEventListener(type, (event) => {
  1827. t.observeList.forEach((target) => {
  1828. const isVisibility = target.IntersectionObserverEntry && target.IntersectionObserverEntry.intersectionRatio > 0;
  1829. const isReg = target.MouseObserverEvent[event.type] === true;
  1830. if (isVisibility && isReg) {
  1831. /* 判断是否符合触发侦听器事件条件 */
  1832. const bound = target.getBoundingClientRect();
  1833. const offsetX = event.x - bound.x;
  1834. const offsetY = event.y - bound.y;
  1835. const isNeedTap = offsetX <= bound.width && offsetX >= 0 && offsetY <= bound.height && offsetY >= 0;
  1836.  
  1837. if (isNeedTap) {
  1838. /* 执行监听回调 */
  1839. const listenerList = t._mouseObserver_[type];
  1840. listenerList.forEach((listener) => {
  1841. if (listener instanceof Function) {
  1842. listener.call(t, event, {
  1843. x: offsetX,
  1844. y: offsetY
  1845. }, target);
  1846. }
  1847. });
  1848. }
  1849. }
  1850. });
  1851. }, options);
  1852. }
  1853.  
  1854. /* 将监听回调加入到事件队列 */
  1855. if (listener instanceof Function) {
  1856. t._mouseObserver_[type].push(listener);
  1857. }
  1858. }
  1859.  
  1860. /**
  1861. * 解除事件绑定
  1862. * @param target {element} -必选 要解除事件的dom对象
  1863. * @param type {string} -必选 要解除的事件,只支持鼠标事件
  1864. * @param listener {function} -必选 绑定事件时的响应函数
  1865. * @returns {boolean}
  1866. */
  1867. off (target, type, listener) {
  1868. const t = this;
  1869. if (!target || !type || !listener || !t._mouseObserver_ || !t._mouseObserver_[type] || !target.MouseObserverEvent || !target.MouseObserverEvent[type]) return false
  1870.  
  1871. const newListenerList = [];
  1872. const listenerList = t._mouseObserver_[type];
  1873. let isMatch = false;
  1874. listenerList.forEach((listenerItem) => {
  1875. if (listenerItem === listener) {
  1876. isMatch = true;
  1877. } else {
  1878. newListenerList.push(listenerItem);
  1879. }
  1880. });
  1881.  
  1882. if (isMatch) {
  1883. t._mouseObserver_[type] = newListenerList;
  1884.  
  1885. /* 侦听器已被完全移除 */
  1886. if (newListenerList.length === 0) {
  1887. delete target.MouseObserverEvent[type];
  1888. }
  1889.  
  1890. /* 当MouseObserverEvent为空对象时移除观测对象 */
  1891. if (JSON.stringify(target.MouseObserverEvent[type]) === '{}') {
  1892. t._unobserve(target);
  1893. }
  1894. }
  1895. }
  1896. }
  1897.  
  1898. /**
  1899. * 简单的i18n库
  1900. */
  1901.  
  1902. class I18n {
  1903. constructor (config) {
  1904. this._languages = {};
  1905. this._locale = this.getClientLang();
  1906. this._defaultLanguage = '';
  1907. this.init(config);
  1908. }
  1909.  
  1910. init (config) {
  1911. if (!config) return false
  1912.  
  1913. const t = this;
  1914. t._locale = config.locale || t._locale;
  1915. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1916. t._languages = config.languages || t._languages;
  1917. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  1918. }
  1919.  
  1920. use () {}
  1921.  
  1922. t (path) {
  1923. const t = this;
  1924. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  1925.  
  1926. /* 版本回退 */
  1927. if (!result && t._locale !== t._defaultLanguage) {
  1928. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  1929. }
  1930.  
  1931. return result || ''
  1932. }
  1933.  
  1934. /* 当前语言值 */
  1935. language () {
  1936. return this._locale
  1937. }
  1938.  
  1939. languages () {
  1940. return this._languages
  1941. }
  1942.  
  1943. changeLanguage (locale) {
  1944. if (this._languages[locale]) {
  1945. this._languages = locale;
  1946. return locale
  1947. } else {
  1948. return false
  1949. }
  1950. }
  1951.  
  1952. /**
  1953. * 根据文本路径获取对象里面的值
  1954. * @param obj {Object} -必选 要操作的对象
  1955. * @param path {String} -必选 路径信息
  1956. * @returns {*}
  1957. */
  1958. getValByPath (obj, path) {
  1959. path = path || '';
  1960. const pathArr = path.split('.');
  1961. let result = obj;
  1962.  
  1963. /* 递归提取结果值 */
  1964. for (let i = 0; i < pathArr.length; i++) {
  1965. if (!result) break
  1966. result = result[pathArr[i]];
  1967. }
  1968.  
  1969. return result
  1970. }
  1971.  
  1972. /* 获取客户端当前的语言环境 */
  1973. getClientLang () {
  1974. return navigator.languages ? navigator.languages[0] : navigator.language
  1975. }
  1976. }
  1977.  
  1978. var zhCN = {
  1979. about: '关于',
  1980. issues: '反馈',
  1981. setting: '设置',
  1982. hotkeys: '快捷键',
  1983. donate: '赞赏',
  1984. disableInitAutoPlay: '禁止在此网站自动播放视频',
  1985. enableInitAutoPlay: '允许在此网站自动播放视频',
  1986. tipsMsg: {
  1987. playspeed: '播放速度:',
  1988. forward: '前进:',
  1989. backward: '后退:',
  1990. seconds: '秒',
  1991. volume: '音量:',
  1992. nextframe: '定位:下一帧',
  1993. previousframe: '定位:上一帧',
  1994. stopframe: '定格帧画面:',
  1995. play: '播放',
  1996. pause: '暂停',
  1997. arpl: '允许自动恢复播放进度',
  1998. drpl: '禁止自动恢复播放进度',
  1999. brightness: '图像亮度:',
  2000. contrast: '图像对比度:',
  2001. saturation: '图像饱和度:',
  2002. hue: '图像色相:',
  2003. blur: '图像模糊度:',
  2004. imgattrreset: '图像属性:复位',
  2005. imgrotate: '画面旋转:',
  2006. onplugin: '启用h5Player插件',
  2007. offplugin: '禁用h5Player插件',
  2008. globalmode: '全局模式:',
  2009. playbackrestored: '为你恢复上次播放进度',
  2010. playbackrestoreoff: '恢复播放进度功能已禁用,按 SHIFT+R 可开启该功能',
  2011. horizontal: '水平位移:',
  2012. vertical: '垂直位移:',
  2013. horizontalMirror: '水平镜像',
  2014. verticalMirror: '垂直镜像',
  2015. videozoom: '视频缩放率:'
  2016. }
  2017. };
  2018.  
  2019. var enUS = {
  2020. about: 'about',
  2021. issues: 'issues',
  2022. setting: 'setting',
  2023. hotkeys: 'hotkeys',
  2024. donate: 'donate',
  2025. disableInitAutoPlay: 'Prohibit autoplay of videos on this site',
  2026. enableInitAutoPlay: 'Allow autoplay videos on this site',
  2027. tipsMsg: {
  2028. playspeed: 'Speed: ',
  2029. forward: 'Forward: ',
  2030. backward: 'Backward: ',
  2031. seconds: 'sec',
  2032. volume: 'Volume: ',
  2033. nextframe: 'Next frame',
  2034. previousframe: 'Previous frame',
  2035. stopframe: 'Stopframe: ',
  2036. play: 'Play',
  2037. pause: 'Pause',
  2038. arpl: 'Allow auto resume playback progress',
  2039. drpl: 'Disable auto resume playback progress',
  2040. brightness: 'Brightness: ',
  2041. contrast: 'Contrast: ',
  2042. saturation: 'Saturation: ',
  2043. hue: 'HUE: ',
  2044. blur: 'Blur: ',
  2045. imgattrreset: 'Attributes: reset',
  2046. imgrotate: 'Picture rotation: ',
  2047. onplugin: 'ON h5Player plugin',
  2048. offplugin: 'OFF h5Player plugin',
  2049. globalmode: 'Global mode: ',
  2050. playbackrestored: 'Restored the last playback progress for you',
  2051. playbackrestoreoff: 'The function of restoring the playback progress is disabled. Press SHIFT+R to turn on the function',
  2052. horizontal: 'Horizontal displacement: ',
  2053. vertical: 'Vertical displacement: ',
  2054. horizontalMirror: 'Horizontal mirror',
  2055. verticalMirror: 'vertical mirror',
  2056. videozoom: 'Video zoom: '
  2057. },
  2058. demo: 'demo-test'
  2059. };
  2060.  
  2061. var ru = {
  2062. about: 'около',
  2063. issues: 'обратная связь',
  2064. setting: 'установка',
  2065. hotkeys: 'горячие клавиши',
  2066. donate: 'пожертвовать',
  2067. disableInitAutoPlay: 'Запретить автовоспроизведение видео на этом сайте',
  2068. enableInitAutoPlay: 'Разрешить автоматическое воспроизведение видео на этом сайте',
  2069. tipsMsg: {
  2070. playspeed: 'Скорость: ',
  2071. forward: 'Вперёд: ',
  2072. backward: 'Назад: ',
  2073. seconds: ' сек',
  2074. volume: 'Громкость: ',
  2075. nextframe: 'Следующий кадр',
  2076. previousframe: 'Предыдущий кадр',
  2077. stopframe: 'Стоп-кадр: ',
  2078. play: 'Запуск',
  2079. pause: 'Пауза',
  2080. arpl: 'Разрешить автоматическое возобновление прогресса воспроизведения',
  2081. drpl: 'Запретить автоматическое возобновление прогресса воспроизведения',
  2082. brightness: 'Яркость: ',
  2083. contrast: 'Контраст: ',
  2084. saturation: 'Насыщенность: ',
  2085. hue: 'Оттенок: ',
  2086. blur: 'Размытие: ',
  2087. imgattrreset: 'Атрибуты: сброс',
  2088. imgrotate: 'Поворот изображения: ',
  2089. onplugin: 'ВКЛ: плагин воспроизведения',
  2090. offplugin: 'ВЫКЛ: плагин воспроизведения',
  2091. globalmode: 'Глобальный режим:',
  2092. playbackrestored: 'Восстановлен последний прогресс воспроизведения',
  2093. playbackrestoreoff: 'Функция восстановления прогресса воспроизведения отключена. Нажмите SHIFT + R, чтобы включить функцию',
  2094. horizontal: 'Горизонтальное смещение: ',
  2095. vertical: 'Вертикальное смещение: ',
  2096. horizontalMirror: 'Горизонтальное зеркало',
  2097. verticalMirror: 'вертикальное зеркало',
  2098. videozoom: 'Увеличить видео: '
  2099. }
  2100. };
  2101.  
  2102. var zhTW = {
  2103. about: '關於',
  2104. issues: '反饋',
  2105. setting: '設置',
  2106. hotkeys: '快捷鍵',
  2107. donate: '讚賞',
  2108. disableInitAutoPlay: '禁止在此網站自動播放視頻',
  2109. enableInitAutoPlay: '允許在此網站自動播放視頻',
  2110. tipsMsg: {
  2111. playspeed: '播放速度:',
  2112. forward: '向前:',
  2113. backward: '向後:',
  2114. seconds: '秒',
  2115. volume: '音量:',
  2116. nextframe: '定位:下一幀',
  2117. previousframe: '定位:上一幀',
  2118. stopframe: '定格幀畫面:',
  2119. play: '播放',
  2120. pause: '暫停',
  2121. arpl: '允許自動恢復播放進度',
  2122. drpl: '禁止自動恢復播放進度',
  2123. brightness: '圖像亮度:',
  2124. contrast: '圖像對比度:',
  2125. saturation: '圖像飽和度:',
  2126. hue: '圖像色相:',
  2127. blur: '圖像模糊度:',
  2128. imgattrreset: '圖像屬性:復位',
  2129. imgrotate: '畫面旋轉:',
  2130. onplugin: '啟用h5Player插件',
  2131. offplugin: '禁用h5Player插件',
  2132. globalmode: '全局模式:',
  2133. playbackrestored: '為你恢復上次播放進度',
  2134. playbackrestoreoff: '恢復播放進度功能已禁用,按 SHIFT+R 可開啟該功能',
  2135. horizontal: '水平位移:',
  2136. vertical: '垂直位移:',
  2137. horizontalMirror: '水平鏡像',
  2138. verticalMirror: '垂直鏡像',
  2139. videozoom: '視頻縮放率:'
  2140. }
  2141. };
  2142.  
  2143. const messages = {
  2144. 'zh-CN': zhCN,
  2145. zh: zhCN,
  2146. 'zh-HK': zhTW,
  2147. 'zh-TW': zhTW,
  2148. 'en-US': enUS,
  2149. en: enUS,
  2150. ru: ru
  2151. };
  2152.  
  2153. const i18n = new I18n({
  2154. defaultLanguage: 'en',
  2155. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  2156. // locale: 'zh-TW',
  2157. languages: messages
  2158. });
  2159.  
  2160. /* 用于获取全局唯一的id */
  2161. function getId () {
  2162. let gID = window.GM_getValue('_global_id_');
  2163. if (!gID) gID = 0;
  2164. gID = Number(gID) + 1;
  2165. window.GM_setValue('_global_id_', gID);
  2166. return gID
  2167. }
  2168.  
  2169. let curTabId = null;
  2170.  
  2171. /**
  2172. * 获取当前TAB标签的Id号,可用于iframe确定自己是否处于同一TAB标签下
  2173. * @returns {Promise<any>}
  2174. */
  2175. function getTabId () {
  2176. return new Promise((resolve, reject) => {
  2177. window.GM_getTab(function (obj) {
  2178. if (!obj.tabId) {
  2179. obj.tabId = getId();
  2180. window.GM_saveTab(obj);
  2181. }
  2182. /* 每次获取都更新当前Tab的id号 */
  2183. curTabId = obj.tabId;
  2184. resolve(obj.tabId);
  2185. });
  2186. })
  2187. }
  2188.  
  2189. /* 一开始就初始化好curTabId,这样后续就不需要异步获取Tabid,部分场景下需要用到 */
  2190. getTabId();
  2191.  
  2192. /*!
  2193. * @name monkeyMsg.js
  2194. * @version 0.0.1
  2195. * @author Blaze
  2196. * @date 2019/9/21 14:22
  2197. */
  2198. // import debug from './debug'
  2199.  
  2200. /**
  2201. * 将对象数据里面可存储到GM_setValue里面的值提取出来
  2202. * @param obj {objcet} -必选 打算要存储的对象数据
  2203. * @param deep {number} -可选 如果对象层级非常深,则须限定递归的层级,默认最高不能超过3级
  2204. * @returns {{}}
  2205. */
  2206. function extractDatafromOb (obj, deep) {
  2207. deep = deep || 1;
  2208. if (deep > 3) return {}
  2209.  
  2210. const result = {};
  2211. if (typeof obj === 'object') {
  2212. for (const key in obj) {
  2213. const val = obj[key];
  2214. const valType = typeof val;
  2215. if (valType === 'number' || valType === 'string' || valType === 'boolean') {
  2216. Object.defineProperty(result, key, {
  2217. value: val,
  2218. writable: true,
  2219. configurable: true,
  2220. enumerable: true
  2221. });
  2222. } else if (valType === 'object' && Object.prototype.propertyIsEnumerable.call(obj, key)) {
  2223. /* 进行递归提取 */
  2224. result[key] = extractDatafromOb(val, deep + 1);
  2225. } else if (valType === 'array') {
  2226. result[key] = val;
  2227. }
  2228. }
  2229. }
  2230. return result
  2231. }
  2232.  
  2233. const monkeyMsg = {
  2234. /**
  2235. * 发送消息,除了正常发送信息外,还会补充各类必要的信息
  2236. * @param name {string} -必选 要发送给那个字段,接收时要一致才能监听的正确
  2237. * @param data {Any} -必选 要发送的数据
  2238. * @param throttleInterval -可选,因为会出现莫名奇妙的重复发送情况,为了消除重复发送带来的副作用,所以引入节流限制逻辑,即限制某个时间间隔内只能发送一次,多余的次数自动抛弃掉,默认80ms
  2239. * @returns {Promise<void>}
  2240. */
  2241. send (name, data, throttleInterval = 80) {
  2242. /* 阻止频繁发送修改事件 */
  2243. const oldMsg = window.GM_getValue(name);
  2244. if (oldMsg && oldMsg.updateTime) {
  2245. const interval = Math.abs(Date.now() - oldMsg.updateTime);
  2246. if (interval < throttleInterval) {
  2247. return false
  2248. }
  2249. }
  2250.  
  2251. const msg = {
  2252. /* 发送过来的数据 */
  2253. data,
  2254. /* 补充标签ID,用于判断是否同处一个tab标签下 */
  2255. tabId: curTabId || 'undefined',
  2256. /* 补充消息的页面来源的标题信息 */
  2257. title: document.title,
  2258. /* 补充消息的页面来源信息 */
  2259. referrer: extractDatafromOb(window.location),
  2260. /* 最近一次更新该数据的时间 */
  2261. updateTime: Date.now()
  2262. };
  2263. if (typeof data === 'object') {
  2264. msg.data = extractDatafromOb(data);
  2265. }
  2266. window.GM_setValue(name, msg);
  2267.  
  2268. // debug.info(`[monkeyMsg-send][${name}]`, msg)
  2269. },
  2270. set: (name, data) => monkeyMsg.send(name, data),
  2271. get: (name) => window.GM_getValue(name),
  2272. on: (name, fn) => window.GM_addValueChangeListener(name, function (name, oldVal, newVal, remote) {
  2273. // debug.info(`[monkeyMsg-on][${name}]`, oldVal, newVal, remote)
  2274.  
  2275. /* 补充消息来源是否出自同一个Tab的判断字段 */
  2276. newVal.originTab = newVal.tabId === curTabId;
  2277.  
  2278. fn instanceof Function && fn.apply(null, arguments);
  2279. }),
  2280. off: (listenerId) => window.GM_removeValueChangeListener(listenerId),
  2281.  
  2282. /**
  2283. * 进行monkeyMsg的消息广播,该广播每两秒钟发送一次,其它任意页面可通接收到的广播信息来更新一些变量信息
  2284. * 主要用以解决通过setInterval或setTimeout因页面可视状态和性能策略导致的不运行或执行频率异常而不能正确更新变量状态的问题
  2285. * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API
  2286. * 广播也不能100%保证不受性能策略的影响,但只要有一个网页处于前台运行,则就能正常工作
  2287. * @param handler {Function} -必选 接收到广播信息时的回调函数
  2288. * @returns
  2289. */
  2290. broadcast (handler) {
  2291. const broadcastName = '__monkeyMsgBroadcast__';
  2292. monkeyMsg._monkeyMsgBroadcastHandler_ = monkeyMsg._monkeyMsgBroadcastHandler_ || [];
  2293. handler instanceof Function && monkeyMsg._monkeyMsgBroadcastHandler_.push(handler);
  2294.  
  2295. monkeyMsg.on(broadcastName, function () {
  2296. monkeyMsg._monkeyMsgBroadcastHandler_.forEach(handler => {
  2297. handler.apply(null, arguments);
  2298. });
  2299. });
  2300.  
  2301. setInterval(function () {
  2302. /* 通过限定时间间隔来防止多个页面批量发起广播信息 */
  2303. const data = monkeyMsg.get(broadcastName);
  2304. if (data && Date.now() - data.updateTime < 1000 * 2) {
  2305. return false
  2306. }
  2307.  
  2308. monkeyMsg.send(broadcastName, {});
  2309. }, 1000 * 2);
  2310.  
  2311. return broadcastName
  2312. }
  2313. };
  2314.  
  2315. /*!
  2316. * @name crossTabCtl.js
  2317. * @description 跨Tab控制脚本逻辑
  2318. * @version 0.0.1
  2319. * @author Blaze
  2320. * @date 2019/11/21 上午11:56
  2321. * @github https://github.com/xxxily
  2322. */
  2323.  
  2324. const crossTabCtl = {
  2325. /* 意外退出的时候leavepictureinpicture事件并不会被调用,所以只能通过轮询来更新画中画信息 */
  2326. updatePictureInPictureInfo () {
  2327. setInterval(function () {
  2328. if (document.pictureInPictureElement) {
  2329. monkeyMsg.send('globalPictureInPictureInfo', {
  2330. usePictureInPicture: true
  2331. });
  2332. }
  2333. }, 1000 * 1.5);
  2334.  
  2335. /**
  2336. * 通过setInterval来更新globalPictureInPictureInfo会受页面可见性和性能策略影响而得不到更新
  2337. * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API
  2338. * 所以通过增加monkeyMsg广播机制来校准globalPictureInPictureInfo状态
  2339. */
  2340. monkeyMsg.broadcast(function () {
  2341. // console.log('[monkeyMsg][broadcast]', ...arguments)
  2342. if (document.pictureInPictureElement) {
  2343. monkeyMsg.send('globalPictureInPictureInfo', {
  2344. usePictureInPicture: true
  2345. });
  2346. }
  2347. });
  2348. },
  2349. /* 判断当前是否开启了画中画功能 */
  2350. hasOpenPictureInPicture () {
  2351. const data = monkeyMsg.get('globalPictureInPictureInfo');
  2352.  
  2353. /* 画中画的全局信息更新时间差在3s内,才认为当前开启了画中画模式,否则有可能意外退出,而没修改usePictureInPicture的值,造成误判 */
  2354. if (data && data.data) {
  2355. if (data.data.usePictureInPicture) {
  2356. return Math.abs(Date.now() - data.updateTime) < 1000 * 3
  2357. } else {
  2358. /**
  2359. * 检测到画中画已经被关闭,但还没关闭太久的话,允许有个短暂的时间段内让用户跨TAB控制视频
  2360. * 例如:暂停视频播放
  2361. */
  2362. return Math.abs(Date.now() - data.updateTime) < 1000 * 15
  2363. }
  2364. }
  2365.  
  2366. return false
  2367. },
  2368. /**
  2369. * 判断是否需要发送跨Tab控制按键信息
  2370. */
  2371. isNeedSendCrossTabCtlEvent () {
  2372. const t = crossTabCtl;
  2373.  
  2374. /* 画中画开启后,判断不在同一个Tab才发送事件 */
  2375. const data = monkeyMsg.get('globalPictureInPictureInfo');
  2376. if (t.hasOpenPictureInPicture() && data.tabId !== curTabId) {
  2377. return true
  2378. } else {
  2379. return false
  2380. }
  2381. },
  2382. crossTabKeydownEvent (event) {
  2383. const t = crossTabCtl;
  2384. /* 处于可编辑元素中不执行任何快捷键 */
  2385. if (isEditableTarget(event.target)) return
  2386. if (t.isNeedSendCrossTabCtlEvent() && isRegisterKey(event)) {
  2387. // 阻止事件冒泡和默认事件
  2388. event.stopPropagation();
  2389. event.preventDefault();
  2390.  
  2391. /* 广播按键消息,进行跨Tab控制 */
  2392. // keydownEvent里已经包含了globalKeydownEvent事件
  2393. // monkeyMsg.send('globalKeydownEvent', event)
  2394.  
  2395. return true
  2396. }
  2397. },
  2398. bindCrossTabEvent () {
  2399. const t = crossTabCtl;
  2400. if (t._hasBindEvent_) return
  2401. document.removeEventListener('keydown', t.crossTabKeydownEvent);
  2402. document.addEventListener('keydown', t.crossTabKeydownEvent, true);
  2403. t._hasBindEvent_ = true;
  2404. },
  2405. init () {
  2406. const t = crossTabCtl;
  2407. t.updatePictureInPictureInfo();
  2408. t.bindCrossTabEvent();
  2409. }
  2410. };
  2411.  
  2412. class Debug {
  2413. constructor (msg, printTime = false) {
  2414. const t = this;
  2415. msg = msg || 'debug message:';
  2416. t.log = t.createDebugMethod('log', null, msg);
  2417. t.error = t.createDebugMethod('error', null, msg);
  2418. t.info = t.createDebugMethod('info', null, msg);
  2419. t.warn = t.createDebugMethod('warn', null, msg);
  2420. }
  2421.  
  2422. create (msg) {
  2423. return new Debug(msg)
  2424. }
  2425.  
  2426. createDebugMethod (name, color, tipsMsg) {
  2427. name = name || 'info';
  2428.  
  2429. const bgColorMap = {
  2430. info: '#2274A5',
  2431. log: '#95B46A',
  2432. warn: '#F5A623',
  2433. error: '#D33F49'
  2434. };
  2435.  
  2436. const printTime = this.printTime;
  2437.  
  2438. return function () {
  2439. if (!window._debugMode_) {
  2440. return false
  2441. }
  2442.  
  2443. const msg = tipsMsg || 'debug message:';
  2444.  
  2445. const arg = Array.from(arguments);
  2446. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  2447.  
  2448. if (printTime) {
  2449. const curTime = new Date();
  2450. const H = curTime.getHours();
  2451. const M = curTime.getMinutes();
  2452. const S = curTime.getSeconds();
  2453. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  2454. } else {
  2455. arg.unshift(`%c ${msg} `);
  2456. }
  2457.  
  2458. window.console[name].apply(window.console, arg);
  2459. }
  2460. }
  2461.  
  2462. isDebugMode () {
  2463. return Boolean(window._debugMode_)
  2464. }
  2465. }
  2466.  
  2467. var Debug$1 = new Debug();
  2468.  
  2469. var debug = Debug$1.create('h5player message:');
  2470.  
  2471. /*!
  2472. * @name index.js
  2473. * @description hookJs JS AOP切面编程辅助库
  2474. * @version 0.0.1
  2475. * @author Blaze
  2476. * @date 2020/10/22 17:40
  2477. * @github https://github.com/xxxily
  2478. */
  2479.  
  2480. const win = typeof window === 'undefined' ? global : window;
  2481. const toStr = Function.prototype.call.bind(Object.prototype.toString);
  2482. /* 特殊场景,如果把Boolean也hook了,很容易导致调用溢出,所以是需要使用原生Boolean */
  2483. const toBoolean = Boolean.originMethod ? Boolean.originMethod : Boolean;
  2484. const util = {
  2485. toStr,
  2486. isObj: obj => toStr(obj) === '[object Object]',
  2487. /* 判断是否为引用类型,用于更宽泛的场景 */
  2488. isRef: obj => typeof obj === 'object',
  2489. isReg: obj => toStr(obj) === '[object RegExp]',
  2490. isFn: obj => obj instanceof Function,
  2491. isAsyncFn: fn => toStr(fn) === '[object AsyncFunction]',
  2492. isPromise: obj => toStr(obj) === '[object Promise]',
  2493. firstUpperCase: str => str.replace(/^\S/, s => s.toUpperCase()),
  2494. toArr: arg => Array.from(Array.isArray(arg) ? arg : [arg]),
  2495.  
  2496. debug: {
  2497. log () {
  2498. let log = win.console.log;
  2499. /* 如果log也被hook了,则使用未被hook前的log函数 */
  2500. if (log.originMethod) { log = log.originMethod; }
  2501. if (win._debugMode_) {
  2502. log.apply(win.console, arguments);
  2503. }
  2504. }
  2505. },
  2506. /* 获取包含自身、继承、可枚举、不可枚举的键名 */
  2507. getAllKeys (obj) {
  2508. const tmpArr = [];
  2509. for (const key in obj) { tmpArr.push(key); }
  2510. const allKeys = Array.from(new Set(tmpArr.concat(Reflect.ownKeys(obj))));
  2511. return allKeys
  2512. }
  2513. };
  2514.  
  2515. class HookJs {
  2516. constructor (useProxy) {
  2517. this.useProxy = useProxy || false;
  2518. this.hookPropertiesKeyName = '_hookProperties' + Date.now();
  2519. }
  2520.  
  2521. hookJsPro () {
  2522. return new HookJs(true)
  2523. }
  2524.  
  2525. _addHook (hookMethod, fn, type, classHook) {
  2526. const hookKeyName = type + 'Hooks';
  2527. const hookMethodProperties = hookMethod[this.hookPropertiesKeyName];
  2528. if (!hookMethodProperties[hookKeyName]) {
  2529. hookMethodProperties[hookKeyName] = [];
  2530. }
  2531.  
  2532. /* 注册(储存)要被调用的hook函数,同时防止重复注册 */
  2533. let hasSameHook = false;
  2534. for (let i = 0; i < hookMethodProperties[hookKeyName].length; i++) {
  2535. if (fn === hookMethodProperties[hookKeyName][i]) {
  2536. hasSameHook = true;
  2537. break
  2538. }
  2539. }
  2540.  
  2541. if (!hasSameHook) {
  2542. fn.classHook = classHook || false;
  2543. hookMethodProperties[hookKeyName].push(fn);
  2544. }
  2545. }
  2546.  
  2547. _runHooks (parentObj, methodName, originMethod, hookMethod, target, ctx, args, classHook, hookPropertiesKeyName) {
  2548. const hookMethodProperties = hookMethod[hookPropertiesKeyName];
  2549. const beforeHooks = hookMethodProperties.beforeHooks || [];
  2550. const afterHooks = hookMethodProperties.afterHooks || [];
  2551. const errorHooks = hookMethodProperties.errorHooks || [];
  2552. const hangUpHooks = hookMethodProperties.hangUpHooks || [];
  2553. const replaceHooks = hookMethodProperties.replaceHooks || [];
  2554. const execInfo = {
  2555. result: null,
  2556. error: null,
  2557. args: args,
  2558. type: ''
  2559. };
  2560.  
  2561. function runHooks (hooks, type) {
  2562. let hookResult = null;
  2563. execInfo.type = type || '';
  2564. if (Array.isArray(hooks)) {
  2565. hooks.forEach(fn => {
  2566. if (util.isFn(fn) && classHook === fn.classHook) {
  2567. hookResult = fn(args, parentObj, methodName, originMethod, execInfo, ctx);
  2568. }
  2569. });
  2570. }
  2571. return hookResult
  2572. }
  2573.  
  2574. const runTarget = (function () {
  2575. if (classHook) {
  2576. return function () {
  2577. // eslint-disable-next-line new-cap
  2578. return new target(...args)
  2579. }
  2580. } else {
  2581. return function () {
  2582. return target.apply(ctx, args)
  2583. }
  2584. }
  2585. })();
  2586.  
  2587. const beforeHooksResult = runHooks(beforeHooks, 'before');
  2588. /* 支持终止后续调用的指令 */
  2589. if (beforeHooksResult && beforeHooksResult === 'STOP-INVOKE') {
  2590. return beforeHooksResult
  2591. }
  2592.  
  2593. if (hangUpHooks.length || replaceHooks.length) {
  2594. /**
  2595. * 当存在hangUpHooks或replaceHooks的时候是不会触发原来函数的
  2596. * 本质上来说hangUpHooks和replaceHooks是一样的,只是外部的定义描述不一致和分类不一致而已
  2597. */
  2598. runHooks(hangUpHooks, 'hangUp');
  2599. runHooks(replaceHooks, 'replace');
  2600. } else {
  2601. if (errorHooks.length) {
  2602. try {
  2603. execInfo.result = runTarget();
  2604. } catch (err) {
  2605. execInfo.error = err;
  2606. const errorHooksResult = runHooks(errorHooks, 'error');
  2607. /* 支持执行错误后不抛出异常的指令 */
  2608. if (errorHooksResult && errorHooksResult === 'SKIP-ERROR') ; else {
  2609. throw err
  2610. }
  2611. }
  2612. } else {
  2613. execInfo.result = runTarget();
  2614. }
  2615. }
  2616.  
  2617. /**
  2618. * 执行afterHooks,如果返回的是Promise,理论上应该进行进一步的细分处理
  2619. * 但添加细分处理逻辑后发现性能下降得比较厉害,且容易出现各种异常,所以决定不在hook里处理Promise情况
  2620. * 下面是原Promise处理逻辑,添加后会导致以下网站卡死或无法访问:
  2621. * wenku.baidu.com
  2622. * https://pubs.rsc.org/en/content/articlelanding/2021/sc/d1sc01881g#!divAbstract
  2623. * https://www.elsevier.com/connect/coronavirus-information-center
  2624. */
  2625. // if (execInfo.result && execInfo.result.then && util.isPromise(execInfo.result)) {
  2626. // execInfo.result.then(function (data) {
  2627. // execInfo.result = data
  2628. // runHooks(afterHooks, 'after')
  2629. // return Promise.resolve.apply(ctx, arguments)
  2630. // }).catch(function (err) {
  2631. // execInfo.error = err
  2632. // runHooks(errorHooks, 'error')
  2633. // return Promise.reject.apply(ctx, arguments)
  2634. // })
  2635. // }
  2636.  
  2637. runHooks(afterHooks, 'after');
  2638.  
  2639. return execInfo.result
  2640. }
  2641.  
  2642. _proxyMethodcGenerator (parentObj, methodName, originMethod, classHook, context, proxyHandler) {
  2643. const t = this;
  2644. const useProxy = t.useProxy;
  2645. let hookMethod = null;
  2646.  
  2647. /* 存在缓存则使用缓存的hookMethod */
  2648. if (t.isHook(originMethod)) {
  2649. hookMethod = originMethod;
  2650. } else if (originMethod[t.hookPropertiesKeyName] && t.isHook(originMethod[t.hookPropertiesKeyName].hookMethod)) {
  2651. hookMethod = originMethod[t.hookPropertiesKeyName].hookMethod;
  2652. }
  2653.  
  2654. if (hookMethod) {
  2655. if (!hookMethod[t.hookPropertiesKeyName].isHook) {
  2656. /* 重新标注被hook状态 */
  2657. hookMethod[t.hookPropertiesKeyName].isHook = true;
  2658. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  2659. }
  2660. return hookMethod
  2661. }
  2662.  
  2663. /* 使用Proxy模式进行hook可以获得更多特性,但性能也会稍差一些 */
  2664. if (useProxy && Proxy) {
  2665. /* 注意:使用Proxy代理,hookMethod和originMethod将共用同一对象 */
  2666. const handler = { ...proxyHandler };
  2667.  
  2668. /* 下面的写法确定了proxyHandler是无法覆盖construct和apply操作的 */
  2669. if (classHook) {
  2670. handler.construct = function (target, args, newTarget) {
  2671. context = context || this;
  2672. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, context, args, true, t.hookPropertiesKeyName)
  2673. };
  2674. } else {
  2675. handler.apply = function (target, ctx, args) {
  2676. ctx = context || ctx;
  2677. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, ctx, args, false, t.hookPropertiesKeyName)
  2678. };
  2679. }
  2680.  
  2681. hookMethod = new Proxy(originMethod, handler);
  2682. } else {
  2683. hookMethod = function () {
  2684. /**
  2685. * 注意此处不能通过 context = context || this
  2686. * 然后通过把context当ctx传递过去
  2687. * 这将导致ctx引用错误
  2688. */
  2689. const ctx = context || this;
  2690. return t._runHooks(parentObj, methodName, originMethod, hookMethod, originMethod, ctx, arguments, classHook, t.hookPropertiesKeyName)
  2691. };
  2692.  
  2693. /* 确保子对象和原型链跟originMethod保持一致 */
  2694. const keys = Reflect.ownKeys(originMethod);
  2695. keys.forEach(keyName => {
  2696. try {
  2697. Object.defineProperty(hookMethod, keyName, {
  2698. get: function () {
  2699. return originMethod[keyName]
  2700. },
  2701. set: function (val) {
  2702. originMethod[keyName] = val;
  2703. }
  2704. });
  2705. } catch (err) {
  2706. // 设置defineProperty的时候出现异常,可能导致hookMethod部分功能确实,也可能不受影响
  2707. util.debug.log(`[proxyMethodcGenerator] hookMethod defineProperty abnormal. hookMethod:${methodName}, definePropertyName:${keyName}`, err);
  2708. }
  2709. });
  2710. hookMethod.prototype = originMethod.prototype;
  2711. }
  2712.  
  2713. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName] = {};
  2714.  
  2715. hookMethodProperties.originMethod = originMethod;
  2716. hookMethodProperties.hookMethod = hookMethod;
  2717. hookMethodProperties.isHook = true;
  2718. hookMethodProperties.classHook = classHook;
  2719.  
  2720. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  2721.  
  2722. return hookMethod
  2723. }
  2724.  
  2725. _getObjKeysByRule (obj, rule) {
  2726. let excludeRule = null;
  2727. let result = rule;
  2728.  
  2729. if (util.isObj(rule) && rule.include) {
  2730. excludeRule = rule.exclude;
  2731. rule = rule.include;
  2732. result = rule;
  2733. }
  2734.  
  2735. /**
  2736. * for in、Object.keys与Reflect.ownKeys的区别见:
  2737. * https://es6.ruanyifeng.com/#docs/object#%E5%B1%9E%E6%80%A7%E7%9A%84%E9%81%8D%E5%8E%86
  2738. */
  2739. if (rule === '*') {
  2740. result = Object.keys(obj);
  2741. } else if (rule === '**') {
  2742. result = Reflect.ownKeys(obj);
  2743. } else if (rule === '***') {
  2744. result = util.getAllKeys(obj);
  2745. } else if (util.isReg(rule)) {
  2746. result = util.getAllKeys(obj).filter(keyName => rule.test(keyName));
  2747. }
  2748.  
  2749. /* 如果存在排除规则,则需要进行排除 */
  2750. if (excludeRule) {
  2751. result = Array.isArray(result) ? result : [result];
  2752. if (util.isReg(excludeRule)) {
  2753. result = result.filter(keyName => !excludeRule.test(keyName));
  2754. } else if (Array.isArray(excludeRule)) {
  2755. result = result.filter(keyName => !excludeRule.includes(keyName));
  2756. } else {
  2757. result = result.filter(keyName => excludeRule !== keyName);
  2758. }
  2759. }
  2760.  
  2761. return util.toArr(result)
  2762. }
  2763.  
  2764. /**
  2765. * 判断某个函数是否已经被hook
  2766. * @param fn {Function} -必选 要判断的函数
  2767. * @returns {boolean}
  2768. */
  2769. isHook (fn) {
  2770. if (!fn || !fn[this.hookPropertiesKeyName]) {
  2771. return false
  2772. }
  2773. const hookMethodProperties = fn[this.hookPropertiesKeyName];
  2774. return util.isFn(hookMethodProperties.originMethod) && fn !== hookMethodProperties.originMethod
  2775. }
  2776.  
  2777. /**
  2778. * 判断对象下的某个值是否具备hook的条件
  2779. * 注意:具备hook条件和能否直接修改值是两回事,
  2780. * 在进行hook的时候还要检查descriptor.writable是否为false
  2781. * 如果为false则要修改成true才能hook成功
  2782. * @param parentObj
  2783. * @param keyName
  2784. * @returns {boolean}
  2785. */
  2786. isAllowHook (parentObj, keyName) {
  2787. /* 有些对象会设置getter,让读取值的时候就抛错,所以需要try catch 判断能否正常读取属性 */
  2788. try { if (!parentObj[keyName]) return false } catch (e) { return false }
  2789. const descriptor = Object.getOwnPropertyDescriptor(parentObj, keyName);
  2790. return !(descriptor && descriptor.configurable === false)
  2791. }
  2792.  
  2793. /**
  2794. * hook 核心函数
  2795. * @param parentObj {Object} -必选 被hook函数依赖的父对象
  2796. * @param hookMethods {Object|Array|RegExp|string} -必选 被hook函数的函数名或函数名的匹配规则
  2797. * @param fn {Function} -必选 hook之后的回调方法
  2798. * @param type {String} -可选 默认before,指定运行hook函数回调的时机,可选字符串:before、after、replace、error、hangUp
  2799. * @param classHook {Boolean} -可选 默认false,指定是否为针对new(class)操作的hook
  2800. * @param context {Object} -可选 指定运行被hook函数时的上下文对象
  2801. * @param proxyHandler {Object} -可选 仅当用Proxy进行hook时有效,默认使用的是Proxy的apply handler进行hook,如果你有特殊需求也可以配置自己的handler以实现更复杂的功能
  2802. * 附注:不使用Proxy进行hook,可以获得更高性能,但也意味着通用性更差些,对于要hook HTMLElement.prototype、EventTarget.prototype这些对象里面的非实例的函数往往会失败而导致被hook函数执行出错
  2803. * @returns {boolean}
  2804. */
  2805. hook (parentObj, hookMethods, fn, type, classHook, context, proxyHandler) {
  2806. classHook = toBoolean(classHook);
  2807. type = type || 'before';
  2808.  
  2809. if ((!util.isRef(parentObj) && !util.isFn(parentObj)) || !util.isFn(fn) || !hookMethods) {
  2810. return false
  2811. }
  2812.  
  2813. const t = this;
  2814.  
  2815. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  2816. hookMethods.forEach(methodName => {
  2817. if (!t.isAllowHook(parentObj, methodName)) {
  2818. util.debug.log(`${util.toStr(parentObj)} [${methodName}] does not support modification`);
  2819. return false
  2820. }
  2821.  
  2822. const descriptor = Object.getOwnPropertyDescriptor(parentObj, methodName);
  2823. if (descriptor && descriptor.writable === false) {
  2824. Object.defineProperty(parentObj, methodName, { writable: true });
  2825. }
  2826.  
  2827. const originMethod = parentObj[methodName];
  2828. let hookMethod = null;
  2829.  
  2830. /* 非函数无法进行hook操作 */
  2831. if (!util.isFn(originMethod)) {
  2832. return false
  2833. }
  2834.  
  2835. hookMethod = t._proxyMethodcGenerator(parentObj, methodName, originMethod, classHook, context, proxyHandler);
  2836.  
  2837. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  2838. if (hookMethodProperties.classHook !== classHook) {
  2839. util.debug.log(`${util.toStr(parentObj)} [${methodName}] Cannot support functions hook and classes hook at the same time `);
  2840. return false
  2841. }
  2842.  
  2843. /* 使用hookMethod接管需要被hook的方法 */
  2844. if (parentObj[methodName] !== hookMethod) {
  2845. parentObj[methodName] = hookMethod;
  2846. }
  2847.  
  2848. t._addHook(hookMethod, fn, type, classHook);
  2849. });
  2850. }
  2851.  
  2852. /* 专门针对new操作的hook,本质上是hook函数的别名,可以少传classHook这个参数,并且明确语义 */
  2853. hookClass (parentObj, hookMethods, fn, type, context, proxyHandler) {
  2854. return this.hook(parentObj, hookMethods, fn, type, true, context, proxyHandler)
  2855. }
  2856.  
  2857. /**
  2858. * 取消对某个函数的hook
  2859. * @param parentObj {Object} -必选 要取消被hook函数依赖的父对象
  2860. * @param hookMethods {Object|Array|RegExp|string} -必选 要取消被hook函数的函数名或函数名的匹配规则
  2861. * @param type {String} -可选 默认before,指定要取消的hook类型,可选字符串:before、after、replace、error、hangUp,如果不指定该选项则取消所有类型下的所有回调
  2862. * @param fn {Function} -必选 取消指定的hook回调函数,如果不指定该选项则取消对应type类型下的所有回调
  2863. * @returns {boolean}
  2864. */
  2865. unHook (parentObj, hookMethods, type, fn) {
  2866. if (!util.isRef(parentObj) || !hookMethods) {
  2867. return false
  2868. }
  2869.  
  2870. const t = this;
  2871. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  2872. hookMethods.forEach(methodName => {
  2873. if (!t.isAllowHook(parentObj, methodName)) {
  2874. return false
  2875. }
  2876.  
  2877. const hookMethod = parentObj[methodName];
  2878.  
  2879. if (!t.isHook(hookMethod)) {
  2880. return false
  2881. }
  2882.  
  2883. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  2884. const originMethod = hookMethodProperties.originMethod;
  2885.  
  2886. if (type) {
  2887. const hookKeyName = type + 'Hooks';
  2888. const hooks = hookMethodProperties[hookKeyName] || [];
  2889.  
  2890. if (fn) {
  2891. /* 删除指定类型下的指定hook函数 */
  2892. for (let i = 0; i < hooks.length; i++) {
  2893. if (fn === hooks[i]) {
  2894. hookMethodProperties[hookKeyName].splice(i, 1);
  2895. util.debug.log(`[unHook ${hookKeyName} func] ${util.toStr(parentObj)} ${methodName}`, fn);
  2896. break
  2897. }
  2898. }
  2899. } else {
  2900. /* 删除指定类型下的所有hook函数 */
  2901. if (Array.isArray(hookMethodProperties[hookKeyName])) {
  2902. hookMethodProperties[hookKeyName] = [];
  2903. util.debug.log(`[unHook all ${hookKeyName}] ${util.toStr(parentObj)} ${methodName}`);
  2904. }
  2905. }
  2906. } else {
  2907. /* 彻底还原被hook的函数 */
  2908. if (util.isFn(originMethod)) {
  2909. parentObj[methodName] = originMethod;
  2910. delete parentObj[methodName][t.hookPropertiesKeyName];
  2911.  
  2912. // Object.keys(hookMethod).forEach(keyName => {
  2913. // if (/Hooks$/.test(keyName) && Array.isArray(hookMethod[keyName])) {
  2914. // hookMethod[keyName] = []
  2915. // }
  2916. // })
  2917. //
  2918. // hookMethod.isHook = false
  2919. // parentObj[methodName] = originMethod
  2920. // delete parentObj[methodName].originMethod
  2921. // delete parentObj[methodName].hookMethod
  2922. // delete parentObj[methodName].isHook
  2923. // delete parentObj[methodName].isClassHook
  2924.  
  2925. util.debug.log(`[unHook method] ${util.toStr(parentObj)} ${methodName}`);
  2926. }
  2927. }
  2928. });
  2929. }
  2930.  
  2931. /* 源函数运行前的hook */
  2932. before (obj, hookMethods, fn, classHook, context, proxyHandler) {
  2933. return this.hook(obj, hookMethods, fn, 'before', classHook, context, proxyHandler)
  2934. }
  2935.  
  2936. /* 源函数运行后的hook */
  2937. after (obj, hookMethods, fn, classHook, context, proxyHandler) {
  2938. return this.hook(obj, hookMethods, fn, 'after', classHook, context, proxyHandler)
  2939. }
  2940.  
  2941. /* 替换掉要hook的函数,不再运行源函数,换成运行其他逻辑 */
  2942. replace (obj, hookMethods, fn, classHook, context, proxyHandler) {
  2943. return this.hook(obj, hookMethods, fn, 'replace', classHook, context, proxyHandler)
  2944. }
  2945.  
  2946. /* 源函数运行出错时的hook */
  2947. error (obj, hookMethods, fn, classHook, context, proxyHandler) {
  2948. return this.hook(obj, hookMethods, fn, 'error', classHook, context, proxyHandler)
  2949. }
  2950.  
  2951. /* 底层实现逻辑与replace一样,都是替换掉要hook的函数,不再运行源函数,只不过是为了明确语义,将源函数挂起不再执行,原则上也不再执行其他逻辑,如果要执行其他逻辑请使用replace hook */
  2952. hangUp (obj, hookMethods, fn, classHook, context, proxyHandler) {
  2953. return this.hook(obj, hookMethods, fn, 'hangUp', classHook, context, proxyHandler)
  2954. }
  2955. }
  2956.  
  2957. var hookJs = new HookJs();
  2958.  
  2959. /**
  2960. * 禁止对playbackRate进行锁定
  2961. * 部分播放器会阻止修改playbackRate
  2962. * 通过hackDefineProperty来反阻止playbackRate的修改
  2963. * 参考: https://greasyfork.org/zh-CN/scripts/372673
  2964. */
  2965.  
  2966. function hackDefineProperCore (target, key, option) {
  2967. if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) {
  2968. option.configurable = true;
  2969. }
  2970.  
  2971. if (target instanceof HTMLVideoElement) {
  2972. const unLockProperties = ['playbackRate', 'currentTime', 'volume', 'muted'];
  2973. if (unLockProperties.includes(key)) {
  2974. if (!option.configurable) {
  2975. debug.log(`禁止对${key}进行锁定`);
  2976. option.configurable = true;
  2977. key = key + '_hack';
  2978. }
  2979. }
  2980. }
  2981.  
  2982. return [target, key, option]
  2983. }
  2984.  
  2985. function hackDefineProperOnError (args, parentObj, methodName, originMethod, execInfo, ctx) {
  2986. debug.error(`${methodName} error:`, execInfo.error);
  2987.  
  2988. /* 忽略执行异常 */
  2989. return 'SKIP-ERROR'
  2990. }
  2991.  
  2992. function hackDefineProperty () {
  2993. hookJs.before(Object, 'defineProperty', function (args, parentObj, methodName, originMethod, execInfo, ctx) {
  2994. const option = args[2];
  2995. const ele = args[0];
  2996. const key = args[1];
  2997. const afterArgs = hackDefineProperCore(ele, key, option);
  2998. afterArgs.forEach((arg, i) => {
  2999. args[i] = arg;
  3000. });
  3001. });
  3002.  
  3003. hookJs.before(Object, 'defineProperties', function (args, parentObj, methodName, originMethod, execInfo, ctx) {
  3004. const properties = args[1];
  3005. const ele = args[0];
  3006. if (ele && ele instanceof Element) {
  3007. Object.keys(properties).forEach(key => {
  3008. const option = properties[key];
  3009. const afterArgs = hackDefineProperCore(ele, key, option);
  3010. args[0] = afterArgs[0];
  3011. delete properties[key];
  3012. properties[afterArgs[1]] = afterArgs[2];
  3013. });
  3014. }
  3015. });
  3016.  
  3017. hookJs.error(Object, 'defineProperty', hackDefineProperOnError);
  3018. hookJs.error(Object, 'defineProperties', hackDefineProperOnError);
  3019. }
  3020.  
  3021. /*!
  3022. * @name menuCommand.js
  3023. * @version 0.0.1
  3024. * @author Blaze
  3025. * @date 2019/9/21 14:22
  3026. */
  3027.  
  3028. const monkeyMenu = {
  3029. menuIds: {},
  3030. on (title, fn, accessKey) {
  3031. if (title instanceof Function) {
  3032. title = title();
  3033. }
  3034.  
  3035. if (window.GM_registerMenuCommand) {
  3036. const menuId = window.GM_registerMenuCommand(title, fn, accessKey);
  3037.  
  3038. this.menuIds[menuId] = {
  3039. title,
  3040. fn,
  3041. accessKey
  3042. };
  3043.  
  3044. return menuId
  3045. }
  3046. },
  3047.  
  3048. off (id) {
  3049. if (window.GM_unregisterMenuCommand) {
  3050. delete this.menuIds[id];
  3051. return window.GM_unregisterMenuCommand(id)
  3052. }
  3053. },
  3054.  
  3055. clear () {
  3056. Object.keys(this.menuIds).forEach(id => {
  3057. this.off(id);
  3058. });
  3059. },
  3060.  
  3061. /**
  3062. * 通过菜单配置进行批量注册,注册前会清空之前注册过的所有菜单
  3063. * @param {array|function} menuOpts 菜单配置,如果是函数则会调用该函数获取菜单配置,并且当菜单被点击后会重新创建菜单,实现菜单的动态更新
  3064. */
  3065. build (menuOpts) {
  3066. this.clear();
  3067.  
  3068. if (Array.isArray(menuOpts)) {
  3069. menuOpts.forEach(menu => {
  3070. if (menu.disable === true) { return }
  3071. this.on(menu.title, menu.fn, menu.accessKey);
  3072. });
  3073. } else if (menuOpts instanceof Function) {
  3074. const menuList = menuOpts();
  3075. if (Array.isArray(menuList)) {
  3076. this._menuBuilder_ = menuOpts;
  3077.  
  3078. menuList.forEach(menu => {
  3079. if (menu.disable === true) { return }
  3080.  
  3081. const menuFn = () => {
  3082. try {
  3083. menu.fn.apply(menu, arguments);
  3084. } catch (e) {
  3085. console.error('[monkeyMenu]', menu.title, e);
  3086. }
  3087.  
  3088. // 每次菜单点击后,重新注册菜单,这样可以确保菜单的状态是最新的
  3089. setTimeout(() => {
  3090. // console.log('[monkeyMenu rebuild]', menu.title)
  3091. this.build(this._menuBuilder_);
  3092. }, 100);
  3093. };
  3094.  
  3095. this.on(menu.title, menuFn, menu.accessKey);
  3096. });
  3097. } else {
  3098. console.error('monkeyMenu build error, no menuList return', menuOpts);
  3099. }
  3100. }
  3101. }
  3102. };
  3103.  
  3104. /*!
  3105. * @name menuManager.js
  3106. * @description 菜单管理器
  3107. * @version 0.0.1
  3108. * @author xxxily
  3109. * @date 2022/08/11 10:05
  3110. * @github https://github.com/xxxily
  3111. */
  3112.  
  3113. function refreshPage (msg) {
  3114. debug.log('[config]', JSON.stringify(config, null, 2));
  3115.  
  3116. msg = msg || '配置已更改,马上刷新页面让配置生效?';
  3117. const status = confirm(msg);
  3118. if (status) {
  3119. window.location.reload();
  3120. }
  3121. }
  3122.  
  3123. let monkeyMenuList = [
  3124. {
  3125. title: '还原默认配置',
  3126. disable: true,
  3127. fn: () => {
  3128. localStorage.removeItem('_h5playerConfig_');
  3129. refreshPage();
  3130. }
  3131. },
  3132. {
  3133. title: i18n.t('hotkeys'),
  3134. fn: () => {
  3135. openInTab('https://github.com/xxxily/h5player#%E5%BF%AB%E6%8D%B7%E9%94%AE%E5%88%97%E8%A1%A8');
  3136. }
  3137. },
  3138. {
  3139. title: i18n.t('issues'),
  3140. fn: () => {
  3141. openInTab('https://github.com/xxxily/h5player/issues');
  3142. }
  3143. },
  3144. {
  3145. title: i18n.t('donate'),
  3146. fn: () => {
  3147. openInTab('https://cdn.jsdelivr.net/gh/xxxily/h5player@master/donate.png');
  3148. }
  3149. },
  3150. {
  3151. title: i18n.t('setting'),
  3152. disable: true,
  3153. fn: () => {
  3154. window.alert('功能开发中,敬请期待...');
  3155. }
  3156. }
  3157. ];
  3158.  
  3159. /* 菜单构造函数(必须是函数才能在点击后动态更新菜单状态) */
  3160. function menuBuilder () {
  3161. return monkeyMenuList
  3162. }
  3163.  
  3164. /* 注册动态菜单 */
  3165. function menuRegister () {
  3166. monkeyMenu.build(menuBuilder);
  3167. }
  3168.  
  3169. /**
  3170. * 增加菜单项
  3171. * @param {Object|Array} menuOpts 菜单的配置项目,多个配置项目用数组表示
  3172. */
  3173. function addMenu (menuOpts, before) {
  3174. menuOpts = Array.isArray(menuOpts) ? menuOpts : [menuOpts];
  3175. menuOpts = menuOpts.filter(item => item.title && !item.disabled);
  3176.  
  3177. if (before) {
  3178. /* 将菜单追加到其它菜单的前面 */
  3179. monkeyMenuList = menuOpts.concat(monkeyMenuList);
  3180. } else {
  3181. monkeyMenuList = monkeyMenuList.concat(menuOpts);
  3182. }
  3183.  
  3184. /* 重新注册菜单 */
  3185. menuRegister();
  3186. }
  3187.  
  3188. window._debugMode_ = true;
  3189.  
  3190. let TCC$1 = null;
  3191. const h5Player = {
  3192. /* 提示文本的字号 */
  3193. fontSize: 12,
  3194. enable: true,
  3195. globalMode: true,
  3196. playerInstance: null,
  3197. scale: 1,
  3198. translate: {
  3199. x: 0,
  3200. y: 0
  3201. },
  3202. rotate: 0,
  3203.  
  3204. /* 水平镜像翻转, 0 或 180 */
  3205. rotateY: 0,
  3206. /* 垂直镜像翻转, 0 或 180 */
  3207. rotateX: 0,
  3208.  
  3209. playbackRate: 1,
  3210. lastPlaybackRate: 1,
  3211. /* 快进快退步长 */
  3212. skipStep: 5,
  3213.  
  3214. /* 监听鼠标活动的观察对象 */
  3215. mouseObserver: new MouseObserver(),
  3216.  
  3217. /* 获取当前播放器的实例 */
  3218. player: function () {
  3219. const t = this;
  3220. return t.playerInstance || t.getPlayerList()[0]
  3221. },
  3222. /* 每个网页可能存在的多个video播放器 */
  3223. getPlayerList: function () {
  3224. const list = [];
  3225. function findPlayer (context) {
  3226. context.querySelectorAll('video').forEach(function (player) {
  3227. list.push(player);
  3228. });
  3229. }
  3230. findPlayer(document);
  3231.  
  3232. // 被封装在 shadow dom 里面的video
  3233. if (window._shadowDomList_) {
  3234. window._shadowDomList_.forEach(function (shadowRoot) {
  3235. findPlayer(shadowRoot);
  3236. });
  3237. }
  3238.  
  3239. return list
  3240. },
  3241. getPlayerWrapDom: function () {
  3242. const t = this;
  3243. const player = t.player();
  3244. if (!player) return
  3245.  
  3246. let wrapDom = null;
  3247. const playerBox = player.getBoundingClientRect();
  3248. eachParentNode(player, function (parent) {
  3249. if (parent === document || !parent.getBoundingClientRect) return
  3250. const parentBox = parent.getBoundingClientRect();
  3251. if (parentBox.width && parentBox.height) {
  3252. if (parentBox.width === playerBox.width && parentBox.height === playerBox.height) {
  3253. wrapDom = parent;
  3254. }
  3255. }
  3256. });
  3257. return wrapDom
  3258. },
  3259.  
  3260. /* 挂载到页面上的window对象,用于调试 */
  3261. async mountToGlobal () {
  3262. try {
  3263. const pageWindow = await getPageWindow();
  3264. if (pageWindow) {
  3265. pageWindow._h5Player = h5Player || 'null';
  3266. if (window.top !== window) {
  3267. pageWindow._h5PlayerInFrame = h5Player || 'null';
  3268. }
  3269. pageWindow._window = window || '';
  3270. debug.log('h5Player对象已成功挂载到全局');
  3271. }
  3272. } catch (e) {
  3273. debug.error(e);
  3274. }
  3275. },
  3276.  
  3277. /**
  3278. * 初始化播放器实例
  3279. * @param isSingle 是否为单实例video标签
  3280. */
  3281. initPlayerInstance (isSingle) {
  3282. const t = this;
  3283. if (!t.playerInstance) return
  3284.  
  3285. const player = t.playerInstance;
  3286. t.initPlaybackRate();
  3287. t.isFoucs();
  3288. t.proxyPlayerInstance(player);
  3289.  
  3290. // player.setAttribute('preload', 'auto')
  3291.  
  3292. /* 增加通用全屏,网页全屏api */
  3293. player._fullScreen_ = new FullScreen(player);
  3294. player._fullPageScreen_ = new FullScreen(player, true);
  3295.  
  3296. if (!player._hasCanplayEvent_) {
  3297. player.addEventListener('canplay', function (event) {
  3298. t.initAutoPlay(player);
  3299. });
  3300. player._hasCanplayEvent_ = true;
  3301. }
  3302.  
  3303. /* 播放的时候进行相关同步操作 */
  3304. if (!player._hasPlayingInitEvent_) {
  3305. let setPlaybackRateOnPlayingCount = 0;
  3306. player.addEventListener('playing', function (event) {
  3307. if (setPlaybackRateOnPlayingCount === 0) {
  3308. /* 同步之前设定的播放速度 */
  3309. t.setPlaybackRate();
  3310.  
  3311. if (isSingle === true) {
  3312. /* 恢复播放进度和进行进度记录 */
  3313. t.setPlayProgress(player);
  3314. setTimeout(function () {
  3315. t.playProgressRecorder(player);
  3316. }, 1000 * 3);
  3317. }
  3318. } else {
  3319. t.setPlaybackRate(null, true);
  3320. }
  3321. setPlaybackRateOnPlayingCount += 1;
  3322. });
  3323. player._hasPlayingInitEvent_ = true;
  3324. }
  3325.  
  3326. /* 进行自定义初始化操作 */
  3327. const taskConf = TCC$1.getTaskConfig();
  3328. if (taskConf.init) {
  3329. TCC$1.doTask('init', player);
  3330. }
  3331.  
  3332. /* 注册鼠标响应事件 */
  3333. t.mouseObserver.on(player, 'click', function (event, offset, target) {
  3334. // debug.log('捕捉到鼠标点击事件:', event, offset, target)
  3335. });
  3336.  
  3337. /* 画中画事件监听 */
  3338. player.addEventListener('enterpictureinpicture', () => {
  3339. monkeyMsg.send('globalPictureInPictureInfo', {
  3340. usePictureInPicture: true
  3341. });
  3342. debug.log('enterpictureinpicture', player);
  3343. });
  3344. player.addEventListener('leavepictureinpicture', () => {
  3345. t.leavepictureinpictureTime = Date.now();
  3346.  
  3347. monkeyMsg.send('globalPictureInPictureInfo', {
  3348. usePictureInPicture: false
  3349. });
  3350. debug.log('leavepictureinpicture', player);
  3351. });
  3352.  
  3353. if (debug.isDebugMode()) {
  3354. t.mountToGlobal();
  3355. player.addEventListener('loadeddata', function () {
  3356. debug.log('video dom:', player);
  3357. debug.log('video url:', player.src);
  3358. debug.log('video duration:', player.duration);
  3359. });
  3360.  
  3361. player.addEventListener('durationchange', function () {
  3362. debug.log('video durationchange:', player.duration);
  3363. });
  3364. }
  3365. },
  3366.  
  3367. /* 刚关闭画中画不久,此段时间内允许跨TAB控制 */
  3368. isLeavepictureinpictureAwhile () {
  3369. const t = this;
  3370. return t.leavepictureinpictureTime && (Date.now() - t.leavepictureinpictureTime < 1000 * 10)
  3371. },
  3372.  
  3373. /**
  3374. * 对播放器实例的方法或属性进行代理
  3375. * @param player
  3376. */
  3377. proxyPlayerInstance (player) {
  3378. if (!player) return
  3379.  
  3380. /* 要代理的方法或属性列表 */
  3381. const proxyList = [
  3382. 'play',
  3383. 'pause'
  3384. ];
  3385.  
  3386. proxyList.forEach(key => {
  3387. const originKey = 'origin_' + key;
  3388. if (Reflect.has(player, key) && !Reflect.has(player, originKey)) {
  3389. player[originKey] = player[key];
  3390. const proxy = new Proxy(player[key], {
  3391. apply (target, ctx, args) {
  3392. debug.log(key + '被调用');
  3393.  
  3394. /* 处理挂起逻辑 */
  3395. const hangUpInfo = player._hangUpInfo_ || {};
  3396. const hangUpDetail = hangUpInfo[key] || hangUpInfo['hangUp_' + key];
  3397. const needHangUp = hangUpDetail && hangUpDetail.timeout >= Date.now();
  3398. if (needHangUp) {
  3399. debug.log(key + '已被挂起,本次调用将被忽略');
  3400. return false
  3401. }
  3402.  
  3403. return target.apply(ctx || player, args)
  3404. }
  3405. });
  3406.  
  3407. player[key] = proxy;
  3408. }
  3409. });
  3410.  
  3411. if (!player._hangUp_) {
  3412. player._hangUpInfo_ = {};
  3413. /**
  3414. * 挂起player某个函数的调用
  3415. * @param name {String} -必选 player方法或属性名,名字写对外,还须要该方法或属性被代理了才能进行挂起,否则这将是个无效的调用
  3416. * @param timeout {Number} -可选 挂起多长时间,默认200ms
  3417. * @private
  3418. */
  3419. player._hangUp_ = function (name, timeout) {
  3420. timeout = Number(timeout) || 200;
  3421. debug.log('_hangUp_', name, timeout);
  3422. player._hangUpInfo_[name] = {
  3423. timeout: Date.now() + timeout
  3424. };
  3425. };
  3426. }
  3427. },
  3428. initPlaybackRate () {
  3429. const t = this;
  3430. t.playbackRate = t.getPlaybackRate();
  3431. },
  3432. getPlaybackRate () {
  3433. const t = this;
  3434. let playbackRate = t.playbackRate;
  3435. if (!isInCrossOriginFrame()) {
  3436. playbackRate = window.localStorage.getItem('_h5_player_playback_rate_') || t.playbackRate;
  3437. }
  3438. return Number(Number(playbackRate).toFixed(1))
  3439. },
  3440. /* 设置播放速度 */
  3441. setPlaybackRate: function (num, notips) {
  3442. const taskConf = TCC$1.getTaskConfig();
  3443. if (taskConf.playbackRate) {
  3444. TCC$1.doTask('playbackRate');
  3445. return
  3446. }
  3447.  
  3448. const t = this;
  3449. const player = t.player();
  3450. let curPlaybackRate;
  3451. if (num) {
  3452. num = Number(num);
  3453. if (Number.isNaN(num)) {
  3454. debug.error('h5player: 播放速度转换出错');
  3455. return false
  3456. }
  3457.  
  3458. if (num <= 0) {
  3459. num = 0.1;
  3460. } else if (num > 16) {
  3461. num = 16;
  3462. }
  3463.  
  3464. num = Number(num.toFixed(1));
  3465. curPlaybackRate = num;
  3466. } else {
  3467. curPlaybackRate = t.getPlaybackRate();
  3468. }
  3469.  
  3470. /* 记录播放速度的信息 */
  3471. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_playback_rate_', curPlaybackRate);
  3472.  
  3473. t.playbackRate = curPlaybackRate;
  3474.  
  3475. delete player.playbackRate;
  3476. player.playbackRate = curPlaybackRate;
  3477. try {
  3478. originalMethods.Object.defineProperty.call(Object, player, 'playbackRate', {
  3479. configurable: true,
  3480. get: function () {
  3481. return curPlaybackRate
  3482. },
  3483. set: function () {}
  3484. });
  3485. } catch (e) {
  3486. debug.error('解锁playbackRate失败', e);
  3487. }
  3488.  
  3489. /* 本身处于1倍播放速度的时候不再提示 */
  3490. if (!num && curPlaybackRate === 1) {
  3491. return true
  3492. } else {
  3493. !notips && t.tips(i18n.t('tipsMsg.playspeed') + player.playbackRate);
  3494. }
  3495. },
  3496. /* 恢复播放速度,还原到1倍速度、或恢复到上次的倍速 */
  3497. resetPlaybackRate: function (player) {
  3498. const t = this;
  3499. player = player || t.player();
  3500.  
  3501. const oldPlaybackRate = Number(player.playbackRate);
  3502. const playbackRate = oldPlaybackRate === 1 ? t.lastPlaybackRate : 1;
  3503. if (oldPlaybackRate !== 1) {
  3504. t.lastPlaybackRate = oldPlaybackRate;
  3505. }
  3506.  
  3507. t.setPlaybackRate(playbackRate);
  3508. },
  3509. /**
  3510. * 初始化自动播放逻辑
  3511. * 必须是配置了自动播放按钮选择器得的才会进行自动播放
  3512. */
  3513. initAutoPlay: function (p) {
  3514. const t = this;
  3515. const player = p || t.player();
  3516. const taskConf = TCC$1.getTaskConfig();
  3517.  
  3518. // 在轮询重试的时候,如果实例变了,或处于隐藏页面中则不进行自动播放操作
  3519. if ((!p && t.hasInitAutoPlay) || !player || (p && p !== t.player()) || document.hidden) {
  3520. return false
  3521. }
  3522.  
  3523. /* 注册开启禁止自动播放的控制菜单 */
  3524. if (taskConf.autoPlay) {
  3525. addMenu({
  3526. title: () => config.autoPlay ? i18n.t('disableInitAutoPlay') : i18n.t('enableInitAutoPlay'),
  3527. fn: () => {
  3528. const confirm = window.confirm(config.autoPlay ? i18n.t('disableInitAutoPlay') : i18n.t('enableInitAutoPlay'));
  3529. if (confirm) {
  3530. config.autoPlay = !config.autoPlay;
  3531. }
  3532. }
  3533. });
  3534. }
  3535.  
  3536. /**
  3537. * 元素不在可视范围,不允许进行初始化自动播放逻辑
  3538. * 由于iframe下元素的可视范围判断不准确,所以iframe下也禁止初始化自动播放逻辑
  3539. * TODO 待优化
  3540. */
  3541. if (!isInViewPort(player) || isInIframe()) {
  3542. return false
  3543. }
  3544.  
  3545. if (!taskConf.autoPlay || window.localStorage.getItem('_disableInitAutoPlay_')) {
  3546. return false
  3547. }
  3548.  
  3549. t.hasInitAutoPlay = true;
  3550.  
  3551. if (player && taskConf.autoPlay && player.paused) {
  3552. TCC$1.doTask('autoPlay');
  3553. if (player.paused) {
  3554. // 轮询重试
  3555. if (!player._initAutoPlayCount_) {
  3556. player._initAutoPlayCount_ = 1;
  3557. }
  3558. player._initAutoPlayCount_ += 1;
  3559. if (player._initAutoPlayCount_ >= 10) {
  3560. return false
  3561. }
  3562. setTimeout(function () {
  3563. t.initAutoPlay(player);
  3564. }, 200);
  3565. }
  3566. }
  3567. },
  3568. setWebFullScreen: function () {
  3569. const t = this;
  3570. const player = t.player();
  3571. const isDo = TCC$1.doTask('webFullScreen');
  3572. if (!isDo && player && player._fullPageScreen_) {
  3573. player._fullPageScreen_.toggle();
  3574. }
  3575. },
  3576. /* 设置播放进度 */
  3577. setCurrentTime: function (num, notips) {
  3578. if (!num) return
  3579. num = Number(num);
  3580. const _num = Math.abs(Number(num.toFixed(1)));
  3581.  
  3582. const t = this;
  3583. const player = t.player();
  3584. const taskConf = TCC$1.getTaskConfig();
  3585. if (taskConf.currentTime) {
  3586. TCC$1.doTask('currentTime');
  3587. return
  3588. }
  3589.  
  3590. if (num > 0) {
  3591. if (taskConf.addCurrentTime) {
  3592. TCC$1.doTask('addCurrentTime');
  3593. } else {
  3594. player.currentTime += _num;
  3595. !notips && t.tips(i18n.t('tipsMsg.forward') + _num + i18n.t('tipsMsg.seconds'));
  3596. }
  3597. } else {
  3598. if (taskConf.subtractCurrentTime) {
  3599. TCC$1.doTask('subtractCurrentTime');
  3600. } else {
  3601. player.currentTime -= _num;
  3602. !notips && t.tips(i18n.t('tipsMsg.backward') + _num + i18n.t('tipsMsg.seconds'));
  3603. }
  3604. }
  3605. },
  3606. /* 设置声音大小 */
  3607. setVolume: function (num) {
  3608. if (!num) return
  3609. const t = this;
  3610. const player = t.player();
  3611.  
  3612. num = Number(num);
  3613. const _num = Math.abs(Number(num.toFixed(2)));
  3614. const curVol = player.volume;
  3615. let newVol = curVol;
  3616.  
  3617. if (num > 0) {
  3618. newVol += _num;
  3619. if (newVol > 1) {
  3620. newVol = 1;
  3621. }
  3622. } else {
  3623. newVol -= _num;
  3624. if (newVol < 0) {
  3625. newVol = 0;
  3626. }
  3627. }
  3628.  
  3629. player.volume = newVol;
  3630.  
  3631. /* 调节音量的时候顺便把静音模式关闭 */
  3632. player.muted = false;
  3633.  
  3634. t.tips(i18n.t('tipsMsg.volume') + parseInt(player.volume * 100) + '%');
  3635. },
  3636.  
  3637. /* 设置视频画面的缩放与位移 */
  3638. setTransform (scale, translate, transformStateGuard) {
  3639. const t = this;
  3640. const player = t.player();
  3641. scale = t.scale = typeof scale === 'undefined' ? t.scale : Number(scale).toFixed(1);
  3642. translate = t.translate = translate || t.translate;
  3643.  
  3644. const mirror = t.rotateX === 180 ? `rotateX(${t.rotateX}deg)` : (t.rotateY === 180 ? `rotateY(${t.rotateY}deg)` : '');
  3645. player.style.transform = `scale(${scale}) translate(${translate.x}px, ${translate.y}px) rotate(${t.rotate}deg) ${mirror}`;
  3646. let tipsMsg = i18n.t('tipsMsg.videozoom') + `${scale * 100}%`;
  3647. if (translate.x) {
  3648. tipsMsg += ` ${i18n.t('tipsMsg.horizontal')}${t.translate.x}px`;
  3649. }
  3650. if (translate.y) {
  3651. tipsMsg += ` ${i18n.t('tipsMsg.vertical')}${t.translate.y}px`;
  3652. }
  3653. if (transformStateGuard === true) ; else {
  3654. t.tips(tipsMsg);
  3655. }
  3656.  
  3657. /* 始终保持transform样式的正常 */
  3658. if (!t._transformStateGuard_) {
  3659. t._transformStateGuard_ = setInterval(() => {
  3660. t.setTransform(t.scale, t.translate, true);
  3661. }, 1000);
  3662. }
  3663. },
  3664.  
  3665. /* 设置镜像翻转 */
  3666. setMirror (vertical = false) {
  3667. const t = this;
  3668. const player = t.player();
  3669.  
  3670. let tipsMsg = '';
  3671. let mirror = t.rotateY === 0 ? 'rotateY(180deg)' : 'rotateY(0deg)';
  3672. if (vertical) {
  3673. t.rotateX = t.rotateX === 0 ? 180 : 0;
  3674. mirror = `rotateX(${t.rotateX}deg)`;
  3675. tipsMsg += ` ${i18n.t('tipsMsg.verticalMirror')} ${t.rotateX}deg`;
  3676. } else {
  3677. t.rotateY = t.rotateY === 0 ? 180 : 0;
  3678. mirror = `rotateY(${t.rotateY}deg)`;
  3679. tipsMsg += ` ${i18n.t('tipsMsg.horizontalMirror')} ${t.rotateY}deg`;
  3680. }
  3681.  
  3682. player.style.transform = `scale(${t.scale}) translate(${t.translate.x}px, ${t.translate.y}px) ${mirror}`;
  3683. t.tips(tipsMsg);
  3684. },
  3685.  
  3686. /**
  3687. * 定格帧画面
  3688. * @param perFps {Number} -可选 默认 1,即定格到下一帧,如果是-1则为定格到上一帧
  3689. */
  3690. freezeFrame (perFps) {
  3691. perFps = perFps || 1;
  3692. const t = this;
  3693. const player = t.player();
  3694.  
  3695. /* 跳帧 */
  3696. player.currentTime += Number(perFps / t.fps);
  3697.  
  3698. /* 定格画面 */
  3699. if (!player.paused) player.pause();
  3700.  
  3701. /* 有些播放器发现画面所在位置变了会自动进行播放,所以此时需要对播放操作进行挂起 */
  3702. player._hangUp_ && player._hangUp_('play', 400);
  3703.  
  3704. if (perFps === 1) {
  3705. t.tips(i18n.t('tipsMsg.nextframe'));
  3706. } else if (perFps === -1) {
  3707. t.tips(i18n.t('tipsMsg.previousframe'));
  3708. } else {
  3709. t.tips(i18n.t('tipsMsg.stopframe') + perFps);
  3710. }
  3711. },
  3712.  
  3713. /**
  3714. * 切换画中画功能
  3715. */
  3716. togglePictureInPicture () {
  3717. const player = this.player();
  3718. if (window._isPictureInPicture_ && document.pictureInPictureElement) {
  3719. document.exitPictureInPicture().then(() => {
  3720. window._isPictureInPicture_ = null;
  3721. }).catch((e) => {
  3722. window._isPictureInPicture_ = null;
  3723. debug.error('[togglePictureInPicture]', e);
  3724. });
  3725. } else {
  3726. player.requestPictureInPicture && player.requestPictureInPicture().then(() => {
  3727. window._isPictureInPicture_ = true;
  3728. }).catch((e) => {
  3729. window._isPictureInPicture_ = null;
  3730. debug.error('[togglePictureInPicture]', e);
  3731. });
  3732. }
  3733. },
  3734.  
  3735. /* 播放下一个视频,默认是没有这个功能的,只有在TCC里配置了next字段才会有该功能 */
  3736. setNextVideo () {
  3737. const isDo = TCC$1.doTask('next');
  3738. if (!isDo) {
  3739. debug.log('当前网页不支持一键播放下个视频功能~');
  3740. }
  3741. },
  3742.  
  3743. setFakeUA (ua) {
  3744. ua = ua || userAgentMap.iPhone.safari;
  3745.  
  3746. /* 记录设定的ua信息 */
  3747. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_user_agent_', ua);
  3748. fakeUA(ua);
  3749. },
  3750.  
  3751. /* ua伪装切换开关 */
  3752. switchFakeUA (ua) {
  3753. const customUA = isInCrossOriginFrame() ? null : window.localStorage.getItem('_h5_player_user_agent_');
  3754. if (customUA) {
  3755. !isInCrossOriginFrame() && window.localStorage.removeItem('_h5_player_user_agent_');
  3756. } else {
  3757. this.setFakeUA(ua);
  3758. }
  3759.  
  3760. debug.log('ua', navigator.userAgent);
  3761. },
  3762.  
  3763. /* 切换播放状态 */
  3764. switchPlayStatus () {
  3765. const t = this;
  3766. const player = t.player();
  3767. const taskConf = TCC$1.getTaskConfig();
  3768. if (taskConf.switchPlayStatus) {
  3769. TCC$1.doTask('switchPlayStatus');
  3770. return
  3771. }
  3772.  
  3773. if (player.paused) {
  3774. if (taskConf.play) {
  3775. TCC$1.doTask('play');
  3776. } else {
  3777. player.play();
  3778. t.tips(i18n.t('tipsMsg.play'));
  3779. }
  3780. } else {
  3781. if (taskConf.pause) {
  3782. TCC$1.doTask('pause');
  3783. } else {
  3784. player.pause();
  3785. t.tips(i18n.t('tipsMsg.pause'));
  3786. }
  3787. }
  3788. },
  3789.  
  3790. isAllowRestorePlayProgress: function () {
  3791. const keyName = '_allowRestorePlayProgress_' + window.location.host;
  3792. const allowRestorePlayProgressVal = window.GM_getValue(keyName);
  3793. return !allowRestorePlayProgressVal || allowRestorePlayProgressVal === 'true'
  3794. },
  3795. /* 切换自动恢复播放进度的状态 */
  3796. switchRestorePlayProgressStatus: function () {
  3797. const t = h5Player;
  3798. let isAllowRestorePlayProgress = t.isAllowRestorePlayProgress();
  3799. /* 进行值反转 */
  3800. isAllowRestorePlayProgress = !isAllowRestorePlayProgress;
  3801. const keyName = '_allowRestorePlayProgress_' + window.location.host;
  3802. window.GM_setValue(keyName, String(isAllowRestorePlayProgress));
  3803.  
  3804. /* 操作提示 */
  3805. if (isAllowRestorePlayProgress) {
  3806. t.tips(i18n.t('tipsMsg.arpl'));
  3807. t.setPlayProgress(t.player());
  3808. } else {
  3809. t.tips(i18n.t('tipsMsg.drpl'));
  3810. }
  3811. },
  3812. tipsClassName: 'html_player_enhance_tips',
  3813. getTipsContainer: function () {
  3814. const t = h5Player;
  3815. const player = t.player();
  3816. // 使用getContainer获取到的父节点弊端太多,暂时弃用
  3817. // const _tispContainer_ = player._tispContainer_ || getContainer(player);
  3818.  
  3819. let tispContainer = player.parentNode || player;
  3820.  
  3821. /* 如果父节点为无长宽的元素,则再往上查找一级 */
  3822. const containerBox = tispContainer.getBoundingClientRect();
  3823. if ((!containerBox.width || !containerBox.height) && tispContainer.parentNode) {
  3824. tispContainer = tispContainer.parentNode;
  3825. }
  3826.  
  3827. return tispContainer
  3828. },
  3829. tips: function (str) {
  3830. const t = h5Player;
  3831. const player = t.player();
  3832. if (!player) {
  3833. debug.log('h5Player Tips:', str);
  3834. return true
  3835. }
  3836.  
  3837. const parentNode = t.getTipsContainer();
  3838.  
  3839. if (parentNode === player) {
  3840. debug.info('无法获取tips的包裹容器异常:', player, str);
  3841. return false
  3842. }
  3843.  
  3844. // 修复部分提示按钮位置异常问题
  3845. const defStyle = parentNode.getAttribute('style') || '';
  3846. let backupStyle = parentNode.getAttribute('style-backup') || '';
  3847. if (!backupStyle) {
  3848. parentNode.setAttribute('style-backup', defStyle || 'style-backup:none');
  3849. backupStyle = defStyle;
  3850. }
  3851.  
  3852. const newStyleArr = backupStyle.split(';');
  3853.  
  3854. const oldPosition = parentNode.getAttribute('def-position') || window.getComputedStyle(parentNode).position;
  3855. if (parentNode.getAttribute('def-position') === null) {
  3856. parentNode.setAttribute('def-position', oldPosition || '');
  3857. }
  3858. if (['static', 'inherit', 'initial', 'unset', ''].includes(oldPosition)) {
  3859. newStyleArr.push('position: relative');
  3860. }
  3861.  
  3862. const playerBox = player.getBoundingClientRect();
  3863. const parentNodeBox = parentNode.getBoundingClientRect();
  3864. /* 不存在高宽时,给包裹节点一个最小高宽,才能保证提示能正常显示 */
  3865. if (!parentNodeBox.width || !parentNodeBox.height) {
  3866. newStyleArr.push('min-width:' + playerBox.width + 'px');
  3867. newStyleArr.push('min-height:' + playerBox.height + 'px');
  3868. }
  3869.  
  3870. parentNode.setAttribute('style', newStyleArr.join(';'));
  3871.  
  3872. const newPlayerBox = player.getBoundingClientRect();
  3873. if (Math.abs(newPlayerBox.height - playerBox.height) > 50) {
  3874. parentNode.setAttribute('style', backupStyle);
  3875. // debug.info('应用新样式后给播放器高宽造成了严重的偏差,样式已被还原:', player, playerBox, newPlayerBox)
  3876. }
  3877.  
  3878. const tipsSelector = '.' + t.tipsClassName;
  3879. let tipsDom = parentNode.querySelector(tipsSelector);
  3880.  
  3881. /* 提示dom未初始化的,则进行初始化 */
  3882. if (!tipsDom) {
  3883. t.initTips();
  3884. tipsDom = parentNode.querySelector(tipsSelector);
  3885. if (!tipsDom) {
  3886. debug.log('init h5player tips dom error...');
  3887. return false
  3888. }
  3889. }
  3890.  
  3891. const style = tipsDom.style;
  3892. tipsDom.innerText = str;
  3893.  
  3894. for (var i = 0; i < 3; i++) {
  3895. if (this.on_off[i]) clearTimeout(this.on_off[i]);
  3896. }
  3897.  
  3898. function showTips () {
  3899. style.display = 'block';
  3900. t.on_off[0] = setTimeout(function () {
  3901. style.opacity = 1;
  3902. }, 50);
  3903. t.on_off[1] = setTimeout(function () {
  3904. // 隐藏提示框和还原样式
  3905. style.opacity = 0;
  3906. style.display = 'none';
  3907. if (backupStyle) {
  3908. parentNode.setAttribute('style', backupStyle);
  3909. }
  3910. }, 2000);
  3911. }
  3912.  
  3913. if (style.display === 'block') {
  3914. style.display = 'none';
  3915. clearTimeout(this.on_off[3]);
  3916. t.on_off[2] = setTimeout(function () {
  3917. showTips();
  3918. }, 100);
  3919. } else {
  3920. showTips();
  3921. }
  3922. },
  3923. /* 设置提示DOM的样式 */
  3924. initTips: function () {
  3925. const t = h5Player;
  3926. const parentNode = t.getTipsContainer();
  3927. if (parentNode.querySelector('.' + t.tipsClassName)) return
  3928.  
  3929. // top: 50%;
  3930. // left: 50%;
  3931. // transform: translate(-50%,-50%);
  3932. const tipsStyle = `
  3933. position: absolute;
  3934. z-index: 999999;
  3935. font-size: ${t.fontSize || 16}px;
  3936. padding: 5px 10px;
  3937. background: rgba(0,0,0,0.4);
  3938. color:white;
  3939. top: 0;
  3940. left: 0;
  3941. transition: all 500ms ease;
  3942. opacity: 0;
  3943. border-bottom-right-radius: 5px;
  3944. display: none;
  3945. -webkit-font-smoothing: subpixel-antialiased;
  3946. font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
  3947. -webkit-user-select: none;
  3948. `;
  3949. const tips = document.createElement('div');
  3950. tips.setAttribute('style', tipsStyle);
  3951. tips.setAttribute('class', t.tipsClassName);
  3952. parentNode.appendChild(tips);
  3953. },
  3954. on_off: new Array(3),
  3955. fps: 30,
  3956. /* 滤镜效果 */
  3957. filter: {
  3958. key: [1, 1, 1, 0, 0],
  3959. setup: function () {
  3960. var view = 'brightness({0}) contrast({1}) saturate({2}) hue-rotate({3}deg) blur({4}px)';
  3961. for (var i = 0; i < 5; i++) {
  3962. view = view.replace('{' + i + '}', String(this.key[i]));
  3963. this.key[i] = Number(this.key[i]);
  3964. }
  3965. h5Player.player().style.filter = view;
  3966. },
  3967. reset: function () {
  3968. this.key[0] = 1;
  3969. this.key[1] = 1;
  3970. this.key[2] = 1;
  3971. this.key[3] = 0;
  3972. this.key[4] = 0;
  3973. this.setup();
  3974. }
  3975. },
  3976. _isFoucs: false,
  3977.  
  3978. /* 播放器的聚焦事件 */
  3979. isFoucs: function () {
  3980. const t = h5Player;
  3981. const player = t.player();
  3982. if (!player) return
  3983.  
  3984. player.onmouseenter = function (e) {
  3985. h5Player._isFoucs = true;
  3986. };
  3987. player.onmouseleave = function (e) {
  3988. h5Player._isFoucs = false;
  3989. };
  3990. },
  3991. /* 播放器事件响应器 */
  3992. palyerTrigger: function (player, event) {
  3993. if (!player || !event) return
  3994. const t = h5Player;
  3995. const keyCode = event.keyCode;
  3996. const key = event.key.toLowerCase();
  3997.  
  3998. if (event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
  3999. // 网页全屏
  4000. if (key === 'enter') {
  4001. t.setWebFullScreen();
  4002. }
  4003.  
  4004. // 进入或退出画中画模式
  4005. if (key === 'p') {
  4006. t.togglePictureInPicture();
  4007. }
  4008.  
  4009. // 截图并下载保存
  4010. if (key === 's') {
  4011. videoCapturer.capture(player, true);
  4012.  
  4013. /* 暂停画面 */
  4014. if (!player.paused && !document.pictureInPictureElement && document.visibilityState !== 'visible') {
  4015. t.freezeFrame();
  4016. }
  4017. }
  4018.  
  4019. if (key === 'r') {
  4020. t.switchRestorePlayProgressStatus();
  4021. }
  4022.  
  4023. if (key === 'm') {
  4024. /* 垂直镜像翻转 */
  4025. t.setMirror(true);
  4026. }
  4027.  
  4028. // 视频画面缩放相关事件
  4029. const allowKeys = ['x', 'c', 'z', 'arrowright', 'arrowleft', 'arrowup', 'arrowdown'];
  4030. if (!allowKeys.includes(key)) return
  4031.  
  4032. t.scale = Number(t.scale);
  4033. switch (key) {
  4034. // shift+X:视频缩小 -0.1
  4035. case 'x' :
  4036. t.scale -= 0.1;
  4037. break
  4038. // shift+C:视频放大 +0.1
  4039. case 'c' :
  4040. t.scale += 0.1;
  4041. break
  4042. // shift+Z:视频恢复正常大小
  4043. case 'z' :
  4044. t.scale = 1;
  4045. t.translate = { x: 0, y: 0 };
  4046. break
  4047. case 'arrowright' :
  4048. t.translate.x += 10;
  4049. break
  4050. case 'arrowleft' :
  4051. t.translate.x -= 10;
  4052. break
  4053. case 'arrowup' :
  4054. t.translate.y -= 10;
  4055. break
  4056. case 'arrowdown' :
  4057. t.translate.y += 10;
  4058. break
  4059. }
  4060. t.setTransform(t.scale, t.translate);
  4061.  
  4062. // 阻止事件冒泡
  4063. event.stopPropagation();
  4064. event.preventDefault();
  4065. return true
  4066. }
  4067.  
  4068. // ctrl+方向键右→:快进30秒
  4069. if (event.ctrlKey && keyCode === 39) {
  4070. t.setCurrentTime(t.skipStep * 6);
  4071. }
  4072. // ctrl+方向键左←:后退30秒
  4073. if (event.ctrlKey && keyCode === 37) {
  4074. t.setCurrentTime(-t.skipStep * 6);
  4075. }
  4076.  
  4077. // ctrl+方向键上↑:音量升高 20%
  4078. if (event.ctrlKey && keyCode === 38) {
  4079. t.setVolume(0.2);
  4080. }
  4081. // 方向键下↓:音量降低 20%
  4082. if (event.ctrlKey && keyCode === 40) {
  4083. t.setVolume(-0.2);
  4084. }
  4085.  
  4086. // 防止其它无关组合键冲突
  4087. if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) return
  4088.  
  4089. // 方向键右→:快进5秒
  4090. if (keyCode === 39) {
  4091. t.setCurrentTime(t.skipStep);
  4092. }
  4093. // 方向键左←:后退5秒
  4094. if (keyCode === 37) {
  4095. t.setCurrentTime(-t.skipStep);
  4096. }
  4097.  
  4098. // 方向键上↑:音量升高 10%
  4099. if (keyCode === 38) {
  4100. t.setVolume(0.1);
  4101. }
  4102. // 方向键下↓:音量降低 10%
  4103. if (keyCode === 40) {
  4104. t.setVolume(-0.1);
  4105. }
  4106.  
  4107. // 空格键:暂停/播放
  4108. if (keyCode === 32) {
  4109. t.switchPlayStatus();
  4110. }
  4111.  
  4112. // 按键X:减速播放 -0.1
  4113. if (keyCode === 88) {
  4114. t.setPlaybackRate(player.playbackRate - 0.1);
  4115. }
  4116. // 按键C:加速播放 +0.1
  4117. if (keyCode === 67) {
  4118. t.setPlaybackRate(player.playbackRate + 0.1);
  4119. }
  4120. // 按键Z:正常速度播放
  4121. if (keyCode === 90) {
  4122. t.resetPlaybackRate();
  4123. }
  4124.  
  4125. // 按1-4设置播放速度 49-52;97-100
  4126. if ((keyCode >= 49 && keyCode <= 52) || (keyCode >= 97 && keyCode <= 100)) {
  4127. t.setPlaybackRate(event.key);
  4128. }
  4129.  
  4130. // 按键F:下一帧
  4131. if (keyCode === 70) {
  4132. if (window.location.hostname === 'www.netflix.com') {
  4133. /* netflix 的F键是全屏的意思 */
  4134. return
  4135. }
  4136. t.freezeFrame(1);
  4137. }
  4138. // 按键D:上一帧
  4139. if (keyCode === 68) {
  4140. t.freezeFrame(-1);
  4141. }
  4142.  
  4143. // 按键E:亮度增加%
  4144. if (keyCode === 69) {
  4145. t.filter.key[0] += 0.1;
  4146. t.filter.key[0] = t.filter.key[0].toFixed(2);
  4147. t.filter.setup();
  4148. t.tips(i18n.t('tipsMsg.brightness') + parseInt(t.filter.key[0] * 100) + '%');
  4149. }
  4150. // 按键W:亮度减少%
  4151. if (keyCode === 87) {
  4152. if (t.filter.key[0] > 0) {
  4153. t.filter.key[0] -= 0.1;
  4154. t.filter.key[0] = t.filter.key[0].toFixed(2);
  4155. t.filter.setup();
  4156. }
  4157. t.tips(i18n.t('tipsMsg.brightness') + parseInt(t.filter.key[0] * 100) + '%');
  4158. }
  4159.  
  4160. // 按键T:对比度增加%
  4161. if (keyCode === 84) {
  4162. t.filter.key[1] += 0.1;
  4163. t.filter.key[1] = t.filter.key[1].toFixed(2);
  4164. t.filter.setup();
  4165. t.tips(i18n.t('tipsMsg.contrast') + parseInt(t.filter.key[1] * 100) + '%');
  4166. }
  4167. // 按键R:对比度减少%
  4168. if (keyCode === 82) {
  4169. if (t.filter.key[1] > 0) {
  4170. t.filter.key[1] -= 0.1;
  4171. t.filter.key[1] = t.filter.key[1].toFixed(2);
  4172. t.filter.setup();
  4173. }
  4174. t.tips(i18n.t('tipsMsg.contrast') + parseInt(t.filter.key[1] * 100) + '%');
  4175. }
  4176.  
  4177. // 按键U:饱和度增加%
  4178. if (keyCode === 85) {
  4179. t.filter.key[2] += 0.1;
  4180. t.filter.key[2] = t.filter.key[2].toFixed(2);
  4181. t.filter.setup();
  4182. t.tips(i18n.t('tipsMsg.saturation') + parseInt(t.filter.key[2] * 100) + '%');
  4183. }
  4184. // 按键Y:饱和度减少%
  4185. if (keyCode === 89) {
  4186. if (t.filter.key[2] > 0) {
  4187. t.filter.key[2] -= 0.1;
  4188. t.filter.key[2] = t.filter.key[2].toFixed(2);
  4189. t.filter.setup();
  4190. }
  4191. t.tips(i18n.t('tipsMsg.saturation') + parseInt(t.filter.key[2] * 100) + '%');
  4192. }
  4193.  
  4194. // 按键O:色相增加 1 度
  4195. if (keyCode === 79) {
  4196. t.filter.key[3] += 1;
  4197. t.filter.setup();
  4198. t.tips(i18n.t('tipsMsg.hue') + t.filter.key[3] + '度');
  4199. }
  4200. // 按键I:色相减少 1 度
  4201. if (keyCode === 73) {
  4202. t.filter.key[3] -= 1;
  4203. t.filter.setup();
  4204. t.tips(i18n.t('tipsMsg.hue') + t.filter.key[3] + '度');
  4205. }
  4206.  
  4207. // 按键K:模糊增加 1 px
  4208. if (keyCode === 75) {
  4209. t.filter.key[4] += 1;
  4210. t.filter.setup();
  4211. t.tips(i18n.t('tipsMsg.blur') + t.filter.key[4] + 'PX');
  4212. }
  4213. // 按键J:模糊减少 1 px
  4214. if (keyCode === 74) {
  4215. if (t.filter.key[4] > 0) {
  4216. t.filter.key[4] -= 1;
  4217. t.filter.setup();
  4218. }
  4219. t.tips(i18n.t('tipsMsg.blur') + t.filter.key[4] + 'PX');
  4220. }
  4221.  
  4222. // 按键Q:图像复位
  4223. if (keyCode === 81) {
  4224. t.scale = 1;
  4225. t.translate = { x: 0, y: 0 };
  4226. t.rotate = 0;
  4227. t.rotateX = 0;
  4228. t.rotateY = 0;
  4229. t.setTransform();
  4230.  
  4231. t.filter.reset();
  4232. t.tips(i18n.t('tipsMsg.imgattrreset'));
  4233. }
  4234.  
  4235. // 按键S:画面旋转 90 度
  4236. if (keyCode === 83) {
  4237. t.rotate += 90;
  4238. if (t.rotate % 360 === 0) t.rotate = 0;
  4239. player.style.transform = `scale(${t.scale}) translate(${t.translate.x}px, ${t.translate.y}px) rotate( ${t.rotate}deg)`;
  4240. t.tips(i18n.t('tipsMsg.imgrotate') + t.rotate + '°');
  4241. }
  4242.  
  4243. /* 水平镜像翻转 */
  4244. if (keyCode === 77) {
  4245. t.setMirror();
  4246. }
  4247.  
  4248. // 按键回车,进入全屏
  4249. if (keyCode === 13) {
  4250. const isDo = TCC$1.doTask('fullScreen');
  4251. if (!isDo && player._fullScreen_) {
  4252. player._fullScreen_.toggle();
  4253. }
  4254. }
  4255.  
  4256. if (key === 'n') {
  4257. t.setNextVideo();
  4258. }
  4259.  
  4260. // 阻止事件冒泡
  4261. event.stopPropagation();
  4262. event.preventDefault();
  4263. return true
  4264. },
  4265.  
  4266. /* 运行自定义的快捷键操作,如果运行了会返回true */
  4267. runCustomShortcuts: function (player, event) {
  4268. if (!player || !event) return
  4269. const key = event.key.toLowerCase();
  4270. const taskConf = TCC$1.getTaskConfig();
  4271. const confIsCorrect = isObj(taskConf.shortcuts) &&
  4272. Array.isArray(taskConf.shortcuts.register) &&
  4273. taskConf.shortcuts.callback instanceof Function;
  4274.  
  4275. /* 判断当前触发的快捷键是否已被注册 */
  4276. function isRegister () {
  4277. const list = taskConf.shortcuts.register;
  4278.  
  4279. /* 当前触发的组合键 */
  4280. const combineKey = [];
  4281. if (event.ctrlKey) {
  4282. combineKey.push('ctrl');
  4283. }
  4284. if (event.shiftKey) {
  4285. combineKey.push('shift');
  4286. }
  4287. if (event.altKey) {
  4288. combineKey.push('alt');
  4289. }
  4290. if (event.metaKey) {
  4291. combineKey.push('command');
  4292. }
  4293.  
  4294. combineKey.push(key);
  4295.  
  4296. /* 通过循环判断当前触发的组合键和已注册的组合键是否完全一致 */
  4297. let hasReg = false;
  4298. list.forEach((shortcut) => {
  4299. const regKey = shortcut.split('+');
  4300. if (combineKey.length === regKey.length) {
  4301. let allMatch = true;
  4302. regKey.forEach((key) => {
  4303. if (!combineKey.includes(key)) {
  4304. allMatch = false;
  4305. }
  4306. });
  4307. if (allMatch) {
  4308. hasReg = true;
  4309. }
  4310. }
  4311. });
  4312.  
  4313. return hasReg
  4314. }
  4315.  
  4316. if (confIsCorrect && isRegister()) {
  4317. // 执行自定义快捷键操作
  4318. const isDo = TCC$1.doTask('shortcuts', {
  4319. event,
  4320. player,
  4321. h5Player
  4322. });
  4323.  
  4324. if (isDo) {
  4325. event.stopPropagation();
  4326. event.preventDefault();
  4327. }
  4328.  
  4329. return isDo
  4330. } else {
  4331. return false
  4332. }
  4333. },
  4334.  
  4335. /* 按键响应方法 */
  4336. keydownEvent: function (event) {
  4337. const t = h5Player;
  4338. const keyCode = event.keyCode;
  4339. // const key = event.key.toLowerCase()
  4340. const player = t.player();
  4341.  
  4342. /* 处于可编辑元素中不执行任何快捷键 */
  4343. if (isEditableTarget(event.target)) return
  4344.  
  4345. /* shift+f 切换UA伪装 */
  4346. if (event.shiftKey && keyCode === 70) {
  4347. t.switchFakeUA();
  4348. }
  4349.  
  4350. /* 未用到的按键不进行任何事件监听 */
  4351. if (!isRegisterKey(event)) return
  4352.  
  4353. /* 广播按键消息,进行跨域控制 */
  4354. monkeyMsg.send('globalKeydownEvent', event, 0);
  4355.  
  4356. if (!player) {
  4357. if (t.hasCrossOriginVideoDetected) {
  4358. // debug.log('当前页面检出了跨域受限的视频,仍需阻止默认事件和事件冒泡')
  4359. event.stopPropagation();
  4360. event.preventDefault();
  4361. return true
  4362. }
  4363.  
  4364. // debug.log('无可用的播放,不执行相关操作')
  4365. return
  4366. }
  4367.  
  4368. /* 切换插件的可用状态 */
  4369. if (event.ctrlKey && keyCode === 32) {
  4370. t.enable = !t.enable;
  4371. if (t.enable) {
  4372. t.tips(i18n.t('tipsMsg.onplugin'));
  4373. } else {
  4374. t.tips(i18n.t('tipsMsg.offplugin'));
  4375. }
  4376. }
  4377.  
  4378. if (!t.enable) {
  4379. debug.log('h5Player 已禁用~');
  4380. return false
  4381. }
  4382.  
  4383. // 按ctrl+\ 键进入聚焦或取消聚焦状态,用于视频标签被遮挡的场景
  4384. if (event.ctrlKey && keyCode === 220) {
  4385. t.globalMode = !t.globalMode;
  4386. if (t.globalMode) {
  4387. t.tips(i18n.t('tipsMsg.globalmode') + ' ON');
  4388. } else {
  4389. t.tips(i18n.t('tipsMsg.globalmode') + ' OFF');
  4390. }
  4391. }
  4392.  
  4393. /* 非全局模式下,不聚焦则不执行快捷键的操作 */
  4394. if (!t.globalMode && !t._isFoucs) return
  4395.  
  4396. /* 判断是否执行了自定义快捷键操作,如果是则不再响应后面默认定义操作 */
  4397. if (t.runCustomShortcuts(player, event) === true) return
  4398.  
  4399. /* 响应播放器相关操作 */
  4400. t.palyerTrigger(player, event);
  4401. },
  4402.  
  4403. /**
  4404. * 获取播放进度
  4405. * @param player -可选 对应的h5 播放器对象, 如果不传,则获取到的是整个播放进度表,传则获取当前播放器的播放进度
  4406. */
  4407. getPlayProgress: function (player) {
  4408. let progressMap = isInCrossOriginFrame() ? null : window.localStorage.getItem('_h5_player_play_progress_');
  4409. if (!progressMap) {
  4410. progressMap = {};
  4411. } else {
  4412. try {
  4413. progressMap = JSON.parse(progressMap);
  4414. } catch (e) {
  4415. progressMap = {};
  4416. }
  4417. }
  4418.  
  4419. if (!player) {
  4420. return progressMap
  4421. } else {
  4422. let keyName = window.location.href || player.src;
  4423. keyName += player.duration;
  4424. if (progressMap[keyName]) {
  4425. return progressMap[keyName].progress
  4426. } else {
  4427. return player.currentTime
  4428. }
  4429. }
  4430. },
  4431. /* 播放进度记录器 */
  4432. playProgressRecorder: function (player) {
  4433. const t = h5Player;
  4434. clearTimeout(player._playProgressTimer_);
  4435. function recorder (player) {
  4436. player._playProgressTimer_ = setTimeout(function () {
  4437. if (!t.isAllowRestorePlayProgress()) {
  4438. recorder(player);
  4439. return true
  4440. }
  4441.  
  4442. const progressMap = t.getPlayProgress() || {};
  4443. const list = Object.keys(progressMap);
  4444.  
  4445. let keyName = window.location.href || player.src;
  4446. keyName += player.duration;
  4447.  
  4448. /* 只保存最近10个视频的播放进度 */
  4449. if (list.length > 10) {
  4450. /* 根据更新的时间戳,取出最早添加播放进度的记录项 */
  4451. let timeList = [];
  4452. list.forEach(function (keyName) {
  4453. progressMap[keyName] && progressMap[keyName].t && timeList.push(progressMap[keyName].t);
  4454. });
  4455. timeList = quickSort(timeList);
  4456. const timestamp = timeList[0];
  4457.  
  4458. /* 删除最早添加的记录项 */
  4459. list.forEach(function (keyName) {
  4460. if (progressMap[keyName].t === timestamp) {
  4461. delete progressMap[keyName];
  4462. }
  4463. });
  4464. }
  4465.  
  4466. /* 记录当前播放进度 */
  4467. progressMap[keyName] = {
  4468. progress: player.currentTime,
  4469. t: new Date().getTime()
  4470. };
  4471.  
  4472. /* 存储播放进度表 */
  4473. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_play_progress_', JSON.stringify(progressMap));
  4474.  
  4475. /* 循环侦听 */
  4476. recorder(player);
  4477. }, 1000 * 2);
  4478. }
  4479. recorder(player);
  4480. },
  4481. /* 设置播放进度 */
  4482. setPlayProgress: function (player, time) {
  4483. const t = h5Player;
  4484. if (!player) return
  4485.  
  4486. const curTime = Number(t.getPlayProgress(player));
  4487. if (!curTime || Number.isNaN(curTime)) return
  4488.  
  4489. if (t.isAllowRestorePlayProgress()) {
  4490. player.currentTime = curTime || player.currentTime;
  4491. if (curTime > 3) {
  4492. t.tips(i18n.t('tipsMsg.playbackrestored'));
  4493. }
  4494. } else {
  4495. t.tips(i18n.t('tipsMsg.playbackrestoreoff'));
  4496. }
  4497. },
  4498. /**
  4499. * 检测h5播放器是否存在
  4500. * @param callback
  4501. */
  4502. detecH5Player: function () {
  4503. const t = this;
  4504. const playerList = t.getPlayerList();
  4505.  
  4506. if (playerList.length) {
  4507. debug.log('检测到HTML5视频!');
  4508.  
  4509. /* 单video实例标签的情况 */
  4510. if (playerList.length === 1) {
  4511. t.playerInstance = playerList[0];
  4512. t.initPlayerInstance(true);
  4513. } else {
  4514. /* 多video实例标签的情况 */
  4515. playerList.forEach(function (player) {
  4516. /* 鼠标移到其上面的时候重新指定实例 */
  4517. if (player._hasMouseRedirectEvent_) return
  4518. player.addEventListener('mouseenter', function (event) {
  4519. t.playerInstance = event.target;
  4520. t.initPlayerInstance(false);
  4521. });
  4522. player._hasMouseRedirectEvent_ = true;
  4523.  
  4524. /* 播放器开始播放的时候重新指向实例 */
  4525. if (player._hasPlayingRedirectEvent_) return
  4526. player.addEventListener('playing', function (event) {
  4527. t.playerInstance = event.target;
  4528. t.initPlayerInstance(false);
  4529.  
  4530. /* 同步之前设定的播放速度 */
  4531. t.setPlaybackRate();
  4532. });
  4533. player._hasPlayingRedirectEvent_ = true;
  4534. });
  4535. }
  4536.  
  4537. if (isInCrossOriginFrame()) {
  4538. /* 广播检测到H5Player的消息 */
  4539. monkeyMsg.send('videoDetected', {
  4540. src: t.playerInstance.src
  4541. });
  4542. }
  4543. }
  4544. },
  4545.  
  4546. /* 响应来自按键消息的广播 */
  4547. bindFakeEvent () {
  4548. const t = this;
  4549. if (t._hasBindFakeEvent_) return
  4550.  
  4551. /* 触发来自消息广播的模拟事件,实现跨域、跨Tab控制视频播放 */
  4552. let triggerFakeEvent = function (name, oldVal, newVal, remote) {
  4553. const player = t.player();
  4554. if (player) {
  4555. const fakeEvent = newVal.data;
  4556. fakeEvent.stopPropagation = () => {};
  4557. fakeEvent.preventDefault = () => {};
  4558. t.palyerTrigger(player, fakeEvent);
  4559.  
  4560. debug.log('已响应跨Tab/跨域按键控制信息:', newVal);
  4561. }
  4562. };
  4563.  
  4564. /**
  4565. * 操作节流控制,减少按键消息频率,
  4566. * 注意,开启节流控制后导致复合按键(如:shift+s)没法生效
  4567. */
  4568. if (!crossTabCtl.hasOpenPictureInPicture() && !t.hasCrossOriginVideoDetected) {
  4569. triggerFakeEvent = throttle(triggerFakeEvent, 80);
  4570. }
  4571.  
  4572. /* 注册响应来自按键消息的广播的事件 */
  4573. monkeyMsg.on('globalKeydownEvent', async (name, oldVal, newVal, remote) => {
  4574. if (remote) {
  4575. if (isInCrossOriginFrame()) {
  4576. /**
  4577. * 同处跨域受限页面,且都处于可见状态,大概率处于同一个Tab标签里,但不是100%
  4578. * tabId一致则100%为同一标签下
  4579. */
  4580. if (document.visibilityState === 'visible' && newVal.originTab) {
  4581. triggerFakeEvent(name, oldVal, newVal, remote);
  4582. }
  4583. } else if (crossTabCtl.hasOpenPictureInPicture()) {
  4584. /* 跨Tab控制画中画里面的视频播放 */
  4585. if (!newVal.originTab && (document.pictureInPictureElement || t.isLeavepictureinpictureAwhile())) {
  4586. triggerFakeEvent(name, oldVal, newVal, remote);
  4587. }
  4588. }
  4589. }
  4590. });
  4591.  
  4592. t._hasBindFakeEvent_ = true;
  4593. },
  4594.  
  4595. /* 绑定相关事件 */
  4596. bindEvent: function () {
  4597. const t = this;
  4598. if (t._hasBindEvent_) return
  4599.  
  4600. document.removeEventListener('keydown', t.keydownEvent);
  4601. document.addEventListener('keydown', t.keydownEvent, true);
  4602.  
  4603. /* 兼容iframe操作 */
  4604. if (isInIframe() && !isInCrossOriginFrame()) {
  4605. window.top.document.removeEventListener('keydown', t.keydownEvent);
  4606. window.top.document.addEventListener('keydown', t.keydownEvent, true);
  4607. }
  4608.  
  4609. t._hasBindEvent_ = true;
  4610. },
  4611. init: function (global) {
  4612. var t = this;
  4613.  
  4614. if (!global) {
  4615. /* 检测是否存在H5播放器 */
  4616. t.detecH5Player();
  4617. return true
  4618. }
  4619.  
  4620. setFakeUA();
  4621.  
  4622. /* 初始化任务配置中心 */
  4623. TCC$1 = h5PlayerTccInit(t);
  4624.  
  4625. /* 绑定键盘事件 */
  4626. t.bindEvent();
  4627. t.bindFakeEvent();
  4628.  
  4629. /* 响应来自跨域受限的视频检出事件 */
  4630. monkeyMsg.on('videoDetected', async (name, oldVal, newVal, remote) => {
  4631. if (newVal.originTab) {
  4632. t.hasCrossOriginVideoDetected = true;
  4633. }
  4634.  
  4635. debug.log('[hasCrossOriginVideoDetected]', t, name, oldVal, newVal, remote);
  4636. });
  4637.  
  4638. /* 当页面处于可视化状态时,初始化自定义播放逻辑 */
  4639. document.addEventListener('visibilitychange', function () {
  4640. h5Player.initAutoPlay();
  4641. });
  4642. }
  4643. };
  4644.  
  4645. async function h5PlayerInit () {
  4646. try {
  4647. /* 禁止对playbackRate等属性进行锁定 */
  4648. hackDefineProperty();
  4649.  
  4650. /* 禁止对shadowdom使用close模式 */
  4651. hackAttachShadow();
  4652.  
  4653. /* 对所有事件进行接管 */
  4654. // hackEventListener()
  4655. } catch (e) {
  4656. console.error('h5player hack error', e);
  4657. }
  4658.  
  4659. menuRegister();
  4660.  
  4661. try {
  4662. /* 初始化全局所需的相关方法 */
  4663. h5Player.init(true);
  4664.  
  4665. /* 检测到有视频标签就进行初始化 */
  4666. ready('video', function () {
  4667. h5Player.init();
  4668. });
  4669.  
  4670. /* 兼容B站的bwp播放器的支持 */
  4671. ready('bwp-video', function () {
  4672. h5Player.init();
  4673. });
  4674.  
  4675. /* 检测shadow dom 下面的video */
  4676. document.addEventListener('addShadowRoot', function (e) {
  4677. const shadowRoot = e.detail.shadowRoot;
  4678. ready('video', function (element) {
  4679. h5Player.init();
  4680. }, shadowRoot);
  4681.  
  4682. /* 兼容B站的bwp播放器的支持 */
  4683. ready('bwp-video', function (element) {
  4684. h5Player.init();
  4685. }, shadowRoot);
  4686. });
  4687.  
  4688. /* 初始化跨Tab控制逻辑 */
  4689. crossTabCtl.init();
  4690.  
  4691. debug.log('h5Player init suc', window);
  4692.  
  4693. if (isInCrossOriginFrame()) {
  4694. debug.log('当前处于跨域受限的iframe中,h5Player部分功能可能无法正常开启', window.location.href);
  4695. }
  4696. } catch (e) {
  4697. debug.error('h5Player init fail', e);
  4698. }
  4699. }
  4700.  
  4701. function init$1 (retryCount = 0) {
  4702. if (!window.document.documentElement) {
  4703. setTimeout(() => {
  4704. if (retryCount < 200) {
  4705. init$1(retryCount + 1);
  4706. } else {
  4707. console.error('[h5player message:]', 'not documentElement detected!', window);
  4708. }
  4709. }, 10);
  4710.  
  4711. return false
  4712. } else if (retryCount > 0) {
  4713. console.warn('[h5player message:]', 'documentElement detected!', retryCount, window);
  4714. }
  4715.  
  4716. h5PlayerInit();
  4717. }
  4718.  
  4719. init$1(0);