1. // ==UserScript==
  2. // @name HTML5视频播放器增强脚本
  3. // @name:en HTML5 video player enhanced script
  4. // @name:zh HTML5视频播放器增强脚本
  5. // @name:zh-CN HTML5视频播放器增强脚本
  6. // @name:zh-TW HTML5視頻播放器增強腳本
  7. // @name:ja HTML5ビデオプレーヤーの拡張スクリプト
  8. // @name:ko HTML5 비디오 플레이어 고급 스크립트
  9. // @name:ru HTML5 видео плеер улучшенный скрипт
  10. // @name:de HTML5 Video Player erweitertes Skript
  11. // @namespace https://github.com/xxxily/h5player
  12. // @homepage https://github.com/xxxily/h5player
  13. // @version 3.3.1
  14. // @description HTML5视频播放增强脚本,支持所有H5视频播放网站,全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能。
  15. // @description:en HTML5 video playback enhanced script, supports all H5 video playback websites, full-length shortcut key control, supports: double-speed playback / accelerated playback, video screenshots, picture-in-picture, full-page webpage, brightness, saturation, contrast, custom configuration enhancement And other functions.
  16. // @description:zh HTML5视频播放增强脚本,支持所有H5视频播放网站,全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能。
  17. // @description:zh-CN HTML5视频播放增强脚本,支持所有H5视频播放网站,全程快捷键控制,支持:倍速播放/加速播放、视频画面截图、画中画、网页全屏、调节亮度、饱和度、对比度、自定义配置功能增强等功能。
  18. // @description:zh-TW HTML5視頻播放增強腳本,支持所有H5視頻播放網站,全程快捷鍵控制,支持:倍速播放/加速播放、視頻畫面截圖、畫中畫、網頁全屏、調節亮度、飽和度、對比度、自定義配置功能增強等功能。
  19. // @description:ja HTML5ビデオ再生拡張スクリプト、すべてのH5ビデオ再生Webサイト、フルレングスのショートカットキーコントロールをサポート、サポート:倍速再生/加速再生、ビデオスクリーンショット、ピクチャーインピクチャー、フルページWebページ、明るさ、彩度、コントラスト、カスタム構成拡張 そして他の機能。
  20. // @description:ko HTML5 비디오 재생 고급 스크립트, 모든 H5 비디오 재생 웹 사이트 지원, 전체 길이 바로 가기 키 제어 지원 : 2 배속 재생 / 가속 재생, 비디오 스크린 샷, PIP (picture-in-picture), 전체 페이지 웹 페이지, 밝기, 채도, 대비, 사용자 정의 구성 향상 그리고 다른 기능들.
  21. // @description:ru HTML5 улучшенный сценарий воспроизведения видео, поддерживает все веб-сайты воспроизведения видео H5, полноразмерное управление с помощью сочетания клавиш, поддерживает: двухскоростное воспроизведение / ускоренное воспроизведение, скриншоты видео, картинка в картинке, полностраничную веб-страницу, яркость, насыщенность, контрастность, улучшение пользовательской конфигурации И другие функции.
  22. // @description:de Verbessertes Skript für die HTML5-Videowiedergabe, unterstützt alle H5-Videowiedergabewebsites, Tastenkombination in voller Länge, unterstützt: Wiedergabe mit doppelter Geschwindigkeit / beschleunigte Wiedergabe, Video-Screenshots, Bild-in-Bild, ganzseitige Webseite, Helligkeit, Sättigung, Kontrast, benutzerdefinierte Konfigurationsverbesserung Und andere Funktionen.
  23. // @author ankvps
  24. // @icon https://raw.githubusercontent.com/xxxily/h5player/master/logo.png
  25. // @match http://*/*
  26. // @match https://*/*
  27. // @grant unsafeWindow
  28. // @grant GM_addStyle
  29. // @grant GM_setValue
  30. // @grant GM_getValue
  31. // @grant GM_deleteValue
  32. // @grant GM_listValues
  33. // @grant GM_addValueChangeListener
  34. // @grant GM_removeValueChangeListener
  35. // @grant GM_registerMenuCommand
  36. // @grant GM_unregisterMenuCommand
  37. // @grant GM_getTab
  38. // @grant GM_saveTab
  39. // @grant GM_getTabs
  40. // @grant GM_openInTab
  41. // @grant GM_download
  42. // @grant GM_xmlhttpRequest
  43. // @run-at document-start
  44. // @require https://unpkg.com/vue@2.6.11/dist/vue.min.js
  45. // @require https://unpkg.com/element-ui@2.13.0/lib/index.js
  46. // @resource elementUiCss https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css
  47. // @connect 127.0.0.1
  48. // @license GPL
  49. // ==/UserScript==
  50. (function (w) { if (w) { w.name = 'h5player'; } })();
  51.  
  52. /*!
  53. * @name utils.js
  54. * @description 数据类型相关的方法
  55. * @version 0.0.1
  56. * @author Blaze
  57. * @date 22/03/2019 22:46
  58. * @github https://github.com/xxxily
  59. */
  60.  
  61. /**
  62. * 准确地获取对象的具体类型 参见:https://www.talkingcoder.com/article/6333557442705696719
  63. * @param obj { all } -必选 要判断的对象
  64. * @returns {*} 返回判断的具体类型
  65. */
  66. function getType (obj) {
  67. if (obj == null) {
  68. return String(obj)
  69. }
  70. return typeof obj === 'object' || typeof obj === 'function'
  71. ? (obj.constructor && obj.constructor.name && obj.constructor.name.toLowerCase()) ||
  72. /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase()
  73. : typeof obj
  74. }
  75.  
  76. const isType = (obj, typeName) => getType(obj) === typeName;
  77. const isObj = obj => isType(obj, 'object');
  78.  
  79. /**
  80. * 任务配置中心 Task Control Center
  81. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  82. * */
  83.  
  84. class TCC {
  85. constructor (taskConf, doTaskFunc) {
  86. this.conf = taskConf || {
  87. /**
  88. * 配置示例
  89. * 父级键名对应的是一级域名,
  90. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  91. * 所有子级的键值都支持使用选择器触发或函数调用
  92. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  93. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  94. * */
  95. 'demo.demo': {
  96. fullScreen: '.fullscreen-btn',
  97. exitFullScreen: '.exit-fullscreen-btn',
  98. webFullScreen: function () {},
  99. exitWebFullScreen: '.exit-fullscreen-btn',
  100. autoPlay: '.player-start-btn',
  101. pause: '.player-pause',
  102. play: '.player-play',
  103. switchPlayStatus: '.player-play',
  104. playbackRate: function () {},
  105. currentTime: function () {},
  106. addCurrentTime: '.add-currenttime',
  107. subtractCurrentTime: '.subtract-currenttime',
  108. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  109. shortcuts: {
  110. /* 注册要执行自定义回调操作的快捷键 */
  111. register: [
  112. 'ctrl+shift+alt+c',
  113. 'ctrl+shift+c',
  114. 'ctrl+alt+c',
  115. 'ctrl+c',
  116. 'c'
  117. ],
  118. /* 自定义快捷键的回调操作 */
  119. callback: function (h5Player, taskConf, data) {
  120. const { event, player } = data;
  121. console.log(event, player);
  122. }
  123. },
  124. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  125. include: /^.*/,
  126. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  127. exclude: /\t/
  128. }
  129. };
  130.  
  131. // 通过doTaskFunc回调定义配置该如何执行任务
  132. this.doTaskFunc = doTaskFunc instanceof Function ? doTaskFunc : function () {};
  133. }
  134.  
  135. /**
  136. * 获取域名 , 目前实现方式不好,需改造,对地区性域名(如com.cn)、三级及以上域名支持不好
  137. * */
  138. getDomain () {
  139. const host = window.location.host;
  140. let domain = host;
  141. const tmpArr = host.split('.');
  142. if (tmpArr.length > 2) {
  143. tmpArr.shift();
  144. domain = tmpArr.join('.');
  145. }
  146. return domain
  147. }
  148.  
  149. /**
  150. * 格式化配置任务
  151. * @param isAll { boolean } -可选 默认只格式当前域名或host下的配置任务,传入true则将所有域名下的任务配置都进行格式化
  152. */
  153. formatTCC (isAll) {
  154. const t = this;
  155. const keys = Object.keys(t.conf);
  156. const domain = t.getDomain();
  157. const host = window.location.host;
  158.  
  159. function formatter (item) {
  160. const defObj = {
  161. include: /^.*/,
  162. exclude: /\t/
  163. };
  164. item.include = item.include || defObj.include;
  165. item.exclude = item.exclude || defObj.exclude;
  166. return item
  167. }
  168.  
  169. const result = {};
  170. keys.forEach(function (key) {
  171. let item = t[key];
  172. if (isObj(item)) {
  173. if (isAll) {
  174. item = formatter(item);
  175. result[key] = item;
  176. } else {
  177. if (key === host || key === domain) {
  178. item = formatter(item);
  179. result[key] = item;
  180. }
  181. }
  182. }
  183. });
  184. return result
  185. }
  186.  
  187. /* 判断所提供的配置任务是否适用于当前URL */
  188. isMatch (taskConf) {
  189. const url = window.location.href;
  190. let isMatch = false;
  191. if (!taskConf.include && !taskConf.exclude) {
  192. isMatch = true;
  193. } else {
  194. if (taskConf.include && taskConf.include.test(url)) {
  195. isMatch = true;
  196. }
  197. if (taskConf.exclude && taskConf.exclude.test(url)) {
  198. isMatch = false;
  199. }
  200. }
  201. return isMatch
  202. }
  203.  
  204. /**
  205. * 获取任务配置,只能获取到当前域名下的任务配置信息
  206. * @param taskName {string} -可选 指定具体任务,默认返回所有类型的任务配置
  207. */
  208. getTaskConfig () {
  209. const t = this;
  210. if (!t._hasFormatTCC_) {
  211. t.formatTCC();
  212. t._hasFormatTCC_ = true;
  213. }
  214. const domain = t.getDomain();
  215. const taskConf = t.conf[window.location.host] || t.conf[domain];
  216.  
  217. if (taskConf && t.isMatch(taskConf)) {
  218. return taskConf
  219. }
  220.  
  221. return {}
  222. }
  223.  
  224. /**
  225. * 执行当前页面下的相应任务
  226. * @param taskName {object|string} -必选,可直接传入任务配置对象,也可用是任务名称的字符串信息,自己去查找是否有任务需要执行
  227. * @param data {object} -可选,传给回调函数的数据
  228. */
  229. doTask (taskName, data) {
  230. const t = this;
  231. let isDo = false;
  232. if (!taskName) return isDo
  233. const taskConf = isObj(taskName) ? taskName : t.getTaskConfig();
  234.  
  235. if (!isObj(taskConf) || !taskConf[taskName]) return isDo
  236.  
  237. const task = taskConf[taskName];
  238.  
  239. if (task) {
  240. isDo = t.doTaskFunc(taskName, taskConf, data);
  241. }
  242.  
  243. return isDo
  244. }
  245. }
  246.  
  247. /**
  248. * 元素监听器
  249. * @param selector -必选
  250. * @param fn -必选,元素存在时的回调
  251. * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素
  252. * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html
  253. */
  254. function ready (selector, fn, shadowRoot) {
  255. const listeners = [];
  256. const win = window;
  257. const doc = shadowRoot || win.document;
  258. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  259. let observer;
  260.  
  261. function $ready (selector, fn) {
  262. // 储存选择器和回调函数
  263. listeners.push({
  264. selector: selector,
  265. fn: fn
  266. });
  267. if (!observer) {
  268. // 监听document变化
  269. observer = new MutationObserver(check);
  270. observer.observe(shadowRoot || doc.documentElement, {
  271. childList: true,
  272. subtree: true
  273. });
  274. }
  275. // 检查该节点是否已经在DOM中
  276. check();
  277. }
  278.  
  279. function check () {
  280. for (let i = 0; i < listeners.length; i++) {
  281. var listener = listeners[i];
  282. var elements = doc.querySelectorAll(listener.selector);
  283. for (let j = 0; j < elements.length; j++) {
  284. var element = elements[j];
  285. if (!element._isMutationReady_) {
  286. element._isMutationReady_ = true;
  287. listener.fn.call(element, element);
  288. }
  289. }
  290. }
  291. }
  292.  
  293. $ready(selector, fn);
  294. }
  295.  
  296. /**
  297. * 某些网页用了attachShadow closed mode,需要open才能获取video标签,例如百度云盘
  298. * 解决参考:
  299. * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed
  300. * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension
  301. */
  302. function hackAttachShadow () {
  303. if (window._hasHackAttachShadow_) return
  304. try {
  305. window._shadowDomList_ = [];
  306. window.Element.prototype._attachShadow = window.Element.prototype.attachShadow;
  307. window.Element.prototype.attachShadow = function () {
  308. const arg = arguments;
  309. if (arg[0] && arg[0].mode) {
  310. // 强制使用 open mode
  311. arg[0].mode = 'open';
  312. }
  313. const shadowRoot = this._attachShadow.apply(this, arg);
  314. // 存一份shadowDomList
  315. window._shadowDomList_.push(shadowRoot);
  316.  
  317. // 在document下面添加 addShadowRoot 自定义事件
  318. const shadowEvent = new window.CustomEvent('addShadowRoot', {
  319. shadowRoot,
  320. detail: {
  321. shadowRoot,
  322. message: 'addShadowRoot',
  323. time: new Date()
  324. },
  325. bubbles: true,
  326. cancelable: true
  327. });
  328. document.dispatchEvent(shadowEvent);
  329.  
  330. return shadowRoot
  331. };
  332. window._hasHackAttachShadow_ = true;
  333. } catch (e) {
  334. console.error('hackAttachShadow error by h5player plug-in');
  335. }
  336. }
  337.  
  338. /**
  339. * 事件侦听hack
  340. * @param config.debug {Boolean} -可选 开启调试模式,调试模式下会把所有注册的事件都挂载到 window._listenerList_ 对象下,用于调试分析
  341. * @param config.proxyNodeType {String|Array} -可选 对某些类型的dom标签的事件进行代理处理
  342. * 请不要对一些非常常见的标签进行事件代理,过多的代理会造成严重的性能消耗
  343. */
  344. function hackEventListener (config) {
  345. config = config || {
  346. debug: false,
  347. proxyAll: false,
  348. proxyNodeType: []
  349. };
  350.  
  351. /* 对proxyNodeType数据进行预处理,将里面的字符变成大写 */
  352. let proxyNodeType = Array.isArray(config.proxyNodeType) ? config.proxyNodeType : [config.proxyNodeType];
  353. const tmpArr = [];
  354. proxyNodeType.forEach(type => {
  355. if (typeof type === 'string') {
  356. tmpArr.push(type.toUpperCase());
  357. }
  358. });
  359. proxyNodeType = tmpArr;
  360.  
  361. const EVENT = window.EventTarget.prototype;
  362. if (EVENT._addEventListener) return
  363. EVENT._addEventListener = EVENT.addEventListener;
  364. EVENT._removeEventListener = EVENT.removeEventListener;
  365. // 挂载到全局用于调试
  366. window._listenerList_ = window._listenerList_ || {};
  367.  
  368. // hack addEventListener
  369. EVENT.addEventListener = function () {
  370. const t = this;
  371. const arg = arguments;
  372. const type = arg[0];
  373. const listener = arg[1];
  374.  
  375. if (!listener) {
  376. return false
  377. }
  378.  
  379. /* 把sourceopen事件干掉,则好多网站视频都将播放不了 */
  380. // if (/sourceopen/gi.test(type)) {
  381. // console.log('------------------------------')
  382. // console.log(type, listener)
  383. // return false
  384. // }
  385.  
  386. /**
  387. * 使用了Symbol之后,某些页面下会和 raven-js发生冲突,所以必须进行 try catch
  388. * TODO 如何解决该问题待研究,测试页面:https://xueqiu.com/S/SZ300498
  389. */
  390. try {
  391. /**
  392. * 对监听函数进行代理
  393. * 为了降低对性能的影响,此处只对特定的标签的事件进行代理
  394. */
  395. const listenerSymbol = Symbol.for(listener);
  396. let listenerProxy = null;
  397. if (config.proxyAll || proxyNodeType.includes(t.nodeName)) {
  398. try {
  399. listenerProxy = new Proxy(listener, {
  400. apply (target, ctx, args) {
  401. // const event = args[0]
  402. // console.log(event.type, event, target)
  403.  
  404. /* 让外部通过 _listenerProxyApplyHandler_ 控制事件的执行 */
  405. if (t._listenerProxyApplyHandler_ instanceof Function) {
  406. const handlerResult = t._listenerProxyApplyHandler_(target, ctx, args, arg);
  407. if (handlerResult !== undefined) {
  408. return handlerResult
  409. }
  410. }
  411.  
  412. return target.apply(ctx, args)
  413. }
  414. });
  415.  
  416. /* 挂载listenerProxy到自身,方便快速查找 */
  417. listener[listenerSymbol] = listenerProxy;
  418.  
  419. /* 使用listenerProxy替代本来应该进行侦听的listener */
  420. arg[1] = listenerProxy;
  421. } catch (e) {
  422. // console.error('listenerProxy error:', e)
  423. }
  424. }
  425. t._addEventListener.apply(t, arg);
  426. t._listeners = t._listeners || {};
  427. t._listeners[type] = t._listeners[type] || [];
  428. const listenerObj = {
  429. target: t,
  430. type,
  431. listener,
  432. listenerProxy,
  433. options: arg[2],
  434. addTime: new Date().getTime()
  435. };
  436. t._listeners[type].push(listenerObj);
  437.  
  438. /* 挂载到全局对象用于观测调试 */
  439. if (config.debug) {
  440. window._listenerList_[type] = window._listenerList_[type] || [];
  441. window._listenerList_[type].push(listenerObj);
  442. }
  443. } catch (e) {
  444. t._addEventListener.apply(t, arg);
  445. // console.error(e)
  446. }
  447. };
  448.  
  449. // hack removeEventListener
  450. EVENT.removeEventListener = function () {
  451. const arg = arguments;
  452. const type = arg[0];
  453. const listener = arg[1];
  454.  
  455. if (!listener) {
  456. return false
  457. }
  458.  
  459. try {
  460. /* 对arg[1]重新赋值,以便正确卸载对应的监听函数 */
  461. const listenerSymbol = Symbol.for(listener);
  462. arg[1] = listener[listenerSymbol] || listener;
  463.  
  464. this._removeEventListener.apply(this, arg);
  465. this._listeners = this._listeners || {};
  466. this._listeners[type] = this._listeners[type] || [];
  467.  
  468. const result = [];
  469. this._listeners[type].forEach(listenerObj => {
  470. if (listenerObj.listener !== listener) {
  471. result.push(listenerObj);
  472. }
  473. });
  474. this._listeners[type] = result;
  475.  
  476. /* 从全局列表中移除 */
  477. if (config.debug) {
  478. const result = [];
  479. const listenerTypeList = window._listenerList_[type] || [];
  480. listenerTypeList.forEach(listenerObj => {
  481. if (listenerObj.listener !== listener) {
  482. result.push(listenerObj);
  483. }
  484. });
  485. window._listenerList_[type] = result;
  486. }
  487. } catch (e) {
  488. this._removeEventListener.apply(this, arg);
  489. console.error(e);
  490. }
  491. };
  492.  
  493. /* 对document下的事件侦听方法进行hack */
  494. try {
  495. if (document.addEventListener !== EVENT.addEventListener) {
  496. document.addEventListener = EVENT.addEventListener;
  497. }
  498. if (document.removeEventListener !== EVENT.removeEventListener) {
  499. document.removeEventListener = EVENT.removeEventListener;
  500. }
  501.  
  502. // if (window.addEventListener !== EVENT.addEventListener) {
  503. // window.addEventListener = EVENT.addEventListener
  504. // }
  505. // if (window.removeEventListener !== EVENT.removeEventListener) {
  506. // window.removeEventListener = EVENT.removeEventListener
  507. // }
  508. } catch (e) {
  509. console.error(e);
  510. }
  511. }
  512.  
  513. const quickSort = function (arr) {
  514. if (arr.length <= 1) { return arr }
  515. var pivotIndex = Math.floor(arr.length / 2);
  516. var pivot = arr.splice(pivotIndex, 1)[0];
  517. var left = [];
  518. var right = [];
  519. for (var i = 0; i < arr.length; i++) {
  520. if (arr[i] < pivot) {
  521. left.push(arr[i]);
  522. } else {
  523. right.push(arr[i]);
  524. }
  525. }
  526. return quickSort(left).concat([pivot], quickSort(right))
  527. };
  528.  
  529. function hideDom (selector, delay) {
  530. setTimeout(function () {
  531. const dom = document.querySelector(selector);
  532. if (dom) {
  533. dom.style.opacity = 0;
  534. }
  535. }, delay || 1000 * 5);
  536. }
  537.  
  538. /**
  539. * 向上查找操作
  540. * @param dom {Element} -必选 初始dom元素
  541. * @param fn {function} -必选 每一级ParentNode的回调操作
  542. * 如果函数返回true则表示停止向上查找动作
  543. */
  544. function eachParentNode (dom, fn) {
  545. let parent = dom.parentNode;
  546. while (parent) {
  547. const isEnd = fn(parent, dom);
  548. parent = parent.parentNode;
  549. if (isEnd) {
  550. break
  551. }
  552. }
  553. }
  554.  
  555. /**
  556. * 判断当前元素是否为可编辑元素
  557. * @param target
  558. * @returns Boolean
  559. */
  560. function isEditableTarget (target) {
  561. const isEditable = target.getAttribute && target.getAttribute('contenteditable') === 'true';
  562. const isInputDom = /INPUT|TEXTAREA|SELECT/.test(target.nodeName);
  563. return isEditable || isInputDom
  564. }
  565.  
  566. /* ua信息伪装 */
  567. function fakeUA (ua) {
  568. Object.defineProperty(navigator, 'userAgent', {
  569. value: ua,
  570. writable: false,
  571. configurable: false,
  572. enumerable: true
  573. });
  574. }
  575.  
  576. /* ua信息来源:https://developers.whatismybrowser.com */
  577. const userAgentMap = {
  578. android: {
  579. 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',
  580. firefox: 'Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0'
  581. },
  582. iPhone: {
  583. 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',
  584. 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'
  585. },
  586. iPad: {
  587. 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',
  588. 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'
  589. }
  590. };
  591.  
  592. /**
  593. * 判断是否处于Iframe中
  594. * @returns {boolean}
  595. */
  596. function isInIframe () {
  597. return window !== window.top
  598. }
  599.  
  600. /**
  601. * 判断是否处于跨域限制的Iframe中
  602. * @returns {boolean}
  603. */
  604. function isInCrossOriginFrame () {
  605. let result = true;
  606. try {
  607. if (window.top.localStorage || window.top.location.href) {
  608. result = false;
  609. }
  610. } catch (e) {
  611. result = true;
  612. }
  613. return result
  614. }
  615.  
  616. /**
  617. * 简单的节流函数
  618. * @param fn
  619. * @param interval
  620. * @returns {Function}
  621. */
  622. function throttle (fn, interval = 80) {
  623. let timeout = null;
  624. return function () {
  625. if (timeout) return false
  626. timeout = setTimeout(() => {
  627. timeout = null;
  628. }, interval);
  629. fn.apply(this, arguments);
  630. }
  631. }
  632.  
  633. /**
  634. * 任务配置中心 Task Control Center
  635. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  636. * */
  637.  
  638. const taskConf = {
  639. /**
  640. * 配置示例
  641. * 父级键名对应的是一级域名,
  642. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  643. * 所有子级的键值都支持使用选择器触发或函数调用
  644. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  645. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  646. * */
  647. 'demo.demo': {
  648. fullScreen: '.fullscreen-btn',
  649. exitFullScreen: '.exit-fullscreen-btn',
  650. webFullScreen: function () {},
  651. exitWebFullScreen: '.exit-fullscreen-btn',
  652. autoPlay: '.player-start-btn',
  653. pause: '.player-pause',
  654. play: '.player-play',
  655. switchPlayStatus: '.player-play',
  656. playbackRate: function () {},
  657. currentTime: function () {},
  658. addCurrentTime: '.add-currenttime',
  659. subtractCurrentTime: '.subtract-currenttime',
  660. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  661. shortcuts: {
  662. /* 注册要执行自定义回调操作的快捷键 */
  663. register: [
  664. 'ctrl+shift+alt+c',
  665. 'ctrl+shift+c',
  666. 'ctrl+alt+c',
  667. 'ctrl+c',
  668. 'c'
  669. ],
  670. /* 自定义快捷键的回调操作 */
  671. callback: function (h5Player, taskConf, data) {
  672. const { event, player } = data;
  673. console.log(event, player);
  674. }
  675. },
  676. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  677. include: /^.*/,
  678. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  679. exclude: /\t/
  680. },
  681. 'youtube.com': {
  682. webFullScreen: 'button.ytp-size-button',
  683. fullScreen: 'button.ytp-fullscreen-button',
  684. next: '.ytp-next-button',
  685. shortcuts: {
  686. register: [
  687. 'escape'
  688. ],
  689. callback: function (h5Player, taskConf, data) {
  690. const { event } = data;
  691. if (event.keyCode === 27) {
  692. /* 取消播放下一个推荐的视频 */
  693. if (document.querySelector('.ytp-upnext').style.display !== 'none') {
  694. document.querySelector('.ytp-upnext-cancel-button').click();
  695. }
  696. }
  697. }
  698. }
  699. },
  700. 'netflix.com': {
  701. fullScreen: 'button.button-nfplayerFullscreen',
  702. addCurrentTime: 'button.button-nfplayerFastForward',
  703. subtractCurrentTime: 'button.button-nfplayerBackTen'
  704. },
  705. 'bilibili.com': {
  706. // fullScreen: '[data-text="进入全屏"]',
  707. // webFullScreen: '[data-text="网页全屏"]',
  708. fullScreen: '.bilibili-player-video-btn-fullscreen',
  709. webFullScreen: function () {
  710. const webFullscreen = document.querySelector('.bilibili-player-video-web-fullscreen');
  711. if (webFullscreen) {
  712. webFullscreen.click();
  713.  
  714. /* 取消弹幕框聚焦,干扰了快捷键的操作 */
  715. setTimeout(function () {
  716. document.querySelector('.bilibili-player-video-danmaku-input').blur();
  717. }, 1000 * 0.1);
  718.  
  719. return true
  720. }
  721. },
  722. // autoPlay: '.bilibili-player-video-btn-start',
  723. switchPlayStatus: '.bilibili-player-video-btn-start',
  724. next: '.bilibili-player-video-btn-next',
  725. init: function (h5Player, taskConf) {},
  726. shortcuts: {
  727. register: [
  728. 'escape'
  729. ],
  730. callback: function (h5Player, taskConf, data) {
  731. const { event } = data;
  732. if (event.keyCode === 27) {
  733. /* 退出网页全屏 */
  734. const webFullscreen = document.querySelector('.bilibili-player-video-web-fullscreen');
  735. if (webFullscreen.classList.contains('closed')) {
  736. webFullscreen.click();
  737. }
  738. }
  739. }
  740. }
  741. },
  742. 't.bilibili.com': {
  743. fullScreen: 'button[name="fullscreen-button"]'
  744. },
  745. 'live.bilibili.com': {
  746. init: function () {
  747. if (!JSON._stringifySource_) {
  748. JSON._stringifySource_ = JSON.stringify;
  749.  
  750. JSON.stringify = function (arg1) {
  751. try {
  752. return JSON._stringifySource_.apply(this, arguments)
  753. } catch (e) {
  754. console.error('JSON.stringify 解释出错:', e, arg1);
  755. }
  756. };
  757. }
  758. },
  759. fullScreen: '.bilibili-live-player-video-controller-fullscreen-btn button',
  760. webFullScreen: '.bilibili-live-player-video-controller-web-fullscreen-btn button',
  761. switchPlayStatus: '.bilibili-live-player-video-controller-start-btn button'
  762. },
  763. 'acfun.cn': {
  764. fullScreen: '[data-bind-key="screenTip"]',
  765. webFullScreen: '[data-bind-key="webTip"]',
  766. switchPlayStatus: function (h5player) {
  767. /* 无法抢得控制权,只好延迟判断要不要干预 */
  768. const player = h5player.player();
  769. const status = player.paused;
  770. setTimeout(function () {
  771. if (status === player.paused) {
  772. if (player.paused) {
  773. player.play();
  774. } else {
  775. player.pause();
  776. }
  777. }
  778. }, 200);
  779. }
  780. },
  781. 'iqiyi.com': {
  782. fullScreen: '.iqp-btn-fullscreen',
  783. webFullScreen: '.iqp-btn-webscreen',
  784. next: '.iqp-btn-next',
  785. init: function (h5Player, taskConf) {
  786. // 隐藏水印
  787. hideDom('.iqp-logo-box');
  788. // 移除暂停广告
  789. window.GM_addStyle(`
  790. div[templatetype="common_pause"]{ display:none }
  791. .iqp-logo-box{ display:none !important }
  792. `);
  793. }
  794. },
  795. 'youku.com': {
  796. fullScreen: '.control-fullscreen-icon',
  797. next: '.control-next-video',
  798. init: function (h5Player, taskConf) {
  799. // 隐藏水印
  800. hideDom('.youku-layer-logo');
  801. }
  802. },
  803. 'ted.com': {
  804. fullScreen: 'button.Fullscreen'
  805. },
  806. 'qq.com': {
  807. pause: '.container_inner .txp-shadow-mod',
  808. play: '.container_inner .txp-shadow-mod',
  809. shortcuts: {
  810. register: ['c', 'x', 'z', '1', '2', '3', '4'],
  811. callback: function (h5Player, taskConf, data) {
  812. const { event } = data;
  813. const key = event.key.toLowerCase();
  814. const speedItems = document.querySelectorAll('.container_inner txpdiv[data-role="txp-button-speed-list"] .txp_menuitem');
  815.  
  816. /* 利用sessionStorage下的playbackRate进行设置 */
  817. if (window.sessionStorage.playbackRate && /(c|x|z|1|2|3|4)/.test(key)) {
  818. const curSpeed = Number(window.sessionStorage.playbackRate);
  819. const perSpeed = curSpeed - 0.1 >= 0 ? curSpeed - 0.1 : 0.1;
  820. const nextSpeed = curSpeed + 0.1 <= 4 ? curSpeed + 0.1 : 4;
  821. let targetSpeed = curSpeed;
  822. switch (key) {
  823. case 'z' :
  824. targetSpeed = 1;
  825. break
  826. case 'c' :
  827. targetSpeed = nextSpeed;
  828. break
  829. case 'x' :
  830. targetSpeed = perSpeed;
  831. break
  832. default :
  833. targetSpeed = Number(key);
  834. break
  835. }
  836.  
  837. window.sessionStorage.playbackRate = targetSpeed;
  838. h5Player.setCurrentTime(0.01, true);
  839. h5Player.setPlaybackRate(targetSpeed, true);
  840. return true
  841. }
  842.  
  843. /* 模拟点击触发 */
  844. if (speedItems.length >= 3 && /(c|x|z)/.test(key)) {
  845. let curIndex = 1;
  846. speedItems.forEach((item, index) => {
  847. if (item.classList.contains('txp_current')) {
  848. curIndex = index;
  849. }
  850. });
  851. const perIndex = curIndex - 1 >= 0 ? curIndex - 1 : 0;
  852. const nextIndex = curIndex + 1 < speedItems.length ? curIndex + 1 : speedItems.length - 1;
  853.  
  854. let target = speedItems[1];
  855. switch (key) {
  856. case 'z' :
  857. target = speedItems[1];
  858. break
  859. case 'c' :
  860. target = speedItems[nextIndex];
  861. break
  862. case 'x' :
  863. target = speedItems[perIndex];
  864. break
  865. }
  866.  
  867. target.click();
  868. const speedNum = Number(target.innerHTML.replace('x'));
  869. h5Player.setPlaybackRate(speedNum);
  870. return true
  871. }
  872. }
  873. },
  874. fullScreen: 'txpdiv[data-report="window-fullscreen"]',
  875. webFullScreen: 'txpdiv[data-report="browser-fullscreen"]',
  876. next: 'txpdiv[data-report="play-next"]',
  877. init: function (h5Player, taskConf) {
  878. // 隐藏水印
  879. hideDom('.txp-watermark');
  880. hideDom('.txp-watermark-action');
  881. },
  882. include: /(v.qq|sports.qq)/
  883. },
  884. 'pan.baidu.com': {
  885. fullScreen: function (h5Player, taskConf) {
  886. h5Player.player().parentNode.querySelector('.vjs-fullscreen-control').click();
  887. }
  888. },
  889. // 'pornhub.com': {
  890. // fullScreen: 'div[class*="icon-fullscreen"]',
  891. // webFullScreen: 'div[class*="icon-size-large"]'
  892. // },
  893. 'facebook.com': {
  894. fullScreen: function (h5Player, taskConf) {
  895. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  896. if (actionBtn && actionBtn.length > 3) {
  897. /* 模拟点击倒数第二个按钮 */
  898. actionBtn[actionBtn.length - 2].click();
  899. return true
  900. }
  901. },
  902. webFullScreen: function (h5Player, taskConf) {
  903. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  904. if (actionBtn && actionBtn.length > 3) {
  905. /* 模拟点击倒数第二个按钮 */
  906. actionBtn[actionBtn.length - 2].click();
  907. return true
  908. }
  909. },
  910. shortcuts: {
  911. /* 在视频模式下按esc键,自动返回上一层界面 */
  912. register: [
  913. 'escape'
  914. ],
  915. /* 自定义快捷键的回调操作 */
  916. callback: function (h5Player, taskConf, data) {
  917. eachParentNode(h5Player.player(), function (parentNode) {
  918. if (parentNode.getAttribute('data-fullscreen-container') === 'true') {
  919. const goBackBtn = parentNode.parentNode.querySelector('div>a>i>u');
  920. if (goBackBtn) {
  921. goBackBtn.parentNode.parentNode.click();
  922. }
  923. return true
  924. }
  925. });
  926. }
  927. }
  928. },
  929. 'douyu.com': {
  930. fullScreen: function (h5Player, taskConf) {
  931. const player = h5Player.player();
  932. const container = player._fullScreen_.getContainer();
  933. if (player._isFullScreen_) {
  934. container.querySelector('div[title="退出窗口全屏"]').click();
  935. } else {
  936. container.querySelector('div[title="窗口全屏"]').click();
  937. }
  938. player._isFullScreen_ = !player._isFullScreen_;
  939. return true
  940. },
  941. webFullScreen: function (h5Player, taskConf) {
  942. const player = h5Player.player();
  943. const container = player._fullScreen_.getContainer();
  944. if (player._isWebFullScreen_) {
  945. container.querySelector('div[title="退出网页全屏"]').click();
  946. } else {
  947. container.querySelector('div[title="网页全屏"]').click();
  948. }
  949. player._isWebFullScreen_ = !player._isWebFullScreen_;
  950. return true
  951. }
  952. },
  953. 'open.163.com': {
  954. init: function (h5Player, taskConf) {
  955. const player = h5Player.player();
  956. /**
  957. * 不设置CORS标识,这样才能跨域截图
  958. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
  959. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes
  960. */
  961. player.setAttribute('crossOrigin', 'anonymous');
  962. }
  963. },
  964. 'agefans.tv': {
  965. init: function (h5Player, taskConf) {
  966. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  967. }
  968. },
  969. 'chaoxing.com': {
  970. fullScreen: '.vjs-fullscreen-control'
  971. },
  972. 'yixi.tv': {
  973. init: function (h5Player, taskConf) {
  974. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  975. }
  976. }
  977. };
  978.  
  979. function h5PlayerTccInit (h5Player) {
  980. return new TCC(taskConf, function (taskName, taskConf, data) {
  981. const task = taskConf[taskName];
  982. const wrapDom = h5Player.getPlayerWrapDom();
  983.  
  984. if (taskName === 'shortcuts') {
  985. if (isObj(task) && task.callback instanceof Function) {
  986. return task.callback(h5Player, taskConf, data)
  987. }
  988. } else if (task instanceof Function) {
  989. try {
  990. return task(h5Player, taskConf, data)
  991. } catch (e) {
  992. console.error('TCC自定义函数任务执行失败:', h5Player, taskConf, data);
  993. return false
  994. }
  995. } else {
  996. /* 触发选择器上的点击事件 */
  997. if (wrapDom && wrapDom.querySelector(task)) {
  998. // 在video的父元素里查找,是为了尽可能兼容多实例下的逻辑
  999. wrapDom.querySelector(task).click();
  1000. return true
  1001. } else if (document.querySelector(task)) {
  1002. document.querySelector(task).click();
  1003. return true
  1004. }
  1005. }
  1006. })
  1007. }
  1008.  
  1009. /* ua伪装配置 */
  1010. const fakeConfig = {
  1011. // 'tv.cctv.com': userAgentMap.iPhone.chrome,
  1012. // 'v.qq.com': userAgentMap.iPad.chrome,
  1013. 'open.163.com': userAgentMap.iPhone.chrome,
  1014. 'm.open.163.com': userAgentMap.iPhone.chrome
  1015. };
  1016.  
  1017. /**
  1018. * 元素全屏API,同时兼容网页全屏
  1019. */
  1020.  
  1021. class FullScreen {
  1022. constructor (dom, pageMode) {
  1023. this.dom = dom;
  1024. // 默认全屏模式,如果传入pageMode则表示进行的是页面全屏操作
  1025. this.pageMode = pageMode || false;
  1026. const fullPageStyle = `
  1027. ._webfullscreen_ {
  1028. display: block !important;
  1029. position: fixed !important;
  1030. width: 100% !important;
  1031. height: 100% !important;
  1032. top: 0 !important;
  1033. left: 0 !important;
  1034. background: #000 !important;
  1035. z-index: 999999 !important;
  1036. }
  1037. ._webfullscreen_zindex_ {
  1038. z-index: 999999 !important;
  1039. }
  1040. `;
  1041. if (!window._hasInitFullPageStyle_) {
  1042. window.GM_addStyle(fullPageStyle);
  1043. window._hasInitFullPageStyle_ = true;
  1044. }
  1045.  
  1046. window.addEventListener('keyup', (event) => {
  1047. const key = event.key.toLowerCase();
  1048. if (key === 'escape') {
  1049. if (this.isFull()) {
  1050. this.exit();
  1051. } else if (this.isFullScreen()) {
  1052. this.exitFullScreen();
  1053. }
  1054. }
  1055. }, true);
  1056.  
  1057. this.getContainer();
  1058. }
  1059.  
  1060. eachParentNode (dom, fn) {
  1061. let parent = dom.parentNode;
  1062. while (parent && parent.classList) {
  1063. const isEnd = fn(parent, dom);
  1064. parent = parent.parentNode;
  1065. if (isEnd) {
  1066. break
  1067. }
  1068. }
  1069. }
  1070.  
  1071. getContainer () {
  1072. const t = this;
  1073. if (t._container_) return t._container_
  1074.  
  1075. const d = t.dom;
  1076. const domBox = d.getBoundingClientRect();
  1077. let container = d;
  1078. t.eachParentNode(d, function (parentNode) {
  1079. const noParentNode = !parentNode || !parentNode.getBoundingClientRect;
  1080. if (noParentNode || parentNode.getAttribute('data-fullscreen-container')) {
  1081. container = parentNode;
  1082. return true
  1083. }
  1084.  
  1085. const parentBox = parentNode.getBoundingClientRect();
  1086. const isInsideTheBox = parentBox.width <= domBox.width && parentBox.height <= domBox.height;
  1087. if (isInsideTheBox) {
  1088. container = parentNode;
  1089. } else {
  1090. return true
  1091. }
  1092. });
  1093.  
  1094. container.setAttribute('data-fullscreen-container', 'true');
  1095. t._container_ = container;
  1096. return container
  1097. }
  1098.  
  1099. isFull () {
  1100. return this.dom.classList.contains('_webfullscreen_')
  1101. }
  1102.  
  1103. isFullScreen () {
  1104. const d = document;
  1105. return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen ||
  1106. d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement)
  1107. }
  1108.  
  1109. enterFullScreen () {
  1110. const c = this.getContainer();
  1111. const enterFn = c.requestFullscreen || c.webkitRequestFullScreen || c.mozRequestFullScreen || c.msRequestFullScreen;
  1112. enterFn && enterFn.call(c);
  1113. }
  1114.  
  1115. enter () {
  1116. const t = this;
  1117. if (t.isFull()) return
  1118. const container = t.getContainer();
  1119. let needSetIndex = false;
  1120. if (t.dom === container) {
  1121. needSetIndex = true;
  1122. }
  1123. this.eachParentNode(t.dom, function (parentNode) {
  1124. parentNode.classList.add('_webfullscreen_');
  1125. if (container === parentNode || needSetIndex) {
  1126. needSetIndex = true;
  1127. parentNode.classList.add('_webfullscreen_zindex_');
  1128. }
  1129. });
  1130. t.dom.classList.add('_webfullscreen_');
  1131. const fullScreenMode = !t.pageMode;
  1132. if (fullScreenMode) {
  1133. t.enterFullScreen();
  1134. }
  1135. }
  1136.  
  1137. exitFullScreen () {
  1138. const d = document;
  1139. const exitFn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen;
  1140. exitFn && exitFn.call(d);
  1141. }
  1142.  
  1143. exit () {
  1144. const t = this;
  1145. t.dom.classList.remove('_webfullscreen_');
  1146. this.eachParentNode(t.dom, function (parentNode) {
  1147. parentNode.classList.remove('_webfullscreen_');
  1148. parentNode.classList.remove('_webfullscreen_zindex_');
  1149. });
  1150. const fullScreenMode = !t.pageMode;
  1151. if (fullScreenMode || t.isFullScreen()) {
  1152. t.exitFullScreen();
  1153. }
  1154. }
  1155.  
  1156. toggle () {
  1157. this.isFull() ? this.exit() : this.enter();
  1158. }
  1159. }
  1160.  
  1161. /*!
  1162. * @name videoCapturer.js
  1163. * @version 0.0.1
  1164. * @author Blaze
  1165. * @date 2019/9/21 12:03
  1166. * @github https://github.com/xxxily
  1167. */
  1168.  
  1169. var videoCapturer = {
  1170. /**
  1171. * 进行截图操作
  1172. * @param video {dom} -必选 video dom 标签
  1173. * @returns {boolean}
  1174. */
  1175. capture (video, download, title) {
  1176. if (!video) return false
  1177. const t = this;
  1178. const currentTime = `${Math.floor(video.currentTime / 60)}'${(video.currentTime % 60).toFixed(3)}''`;
  1179. const captureTitle = title || `${document.title}_${currentTime}`;
  1180.  
  1181. /* 截图核心逻辑 */
  1182. video.setAttribute('crossorigin', 'anonymous');
  1183. const canvas = document.createElement('canvas');
  1184. canvas.width = video.videoWidth;
  1185. canvas.height = video.videoHeight;
  1186. const context = canvas.getContext('2d');
  1187. context.drawImage(video, 0, 0, canvas.width, canvas.height);
  1188.  
  1189. if (download) {
  1190. t.download(canvas, captureTitle, video);
  1191. } else {
  1192. t.previe(canvas, captureTitle);
  1193. }
  1194.  
  1195. return canvas
  1196. },
  1197. /**
  1198. * 预览截取到的画面内容
  1199. * @param canvas
  1200. */
  1201. previe (canvas, title) {
  1202. canvas.style = 'max-width:100%';
  1203. const previewPage = window.open('', '_blank');
  1204. previewPage.document.title = `capture previe - ${title || 'Untitled'}`;
  1205. previewPage.document.body.style.textAlign = 'center';
  1206. previewPage.document.body.style.background = '#000';
  1207. previewPage.document.body.appendChild(canvas);
  1208. },
  1209. /**
  1210. * canvas 下载截取到的内容
  1211. * @param canvas
  1212. */
  1213. download (canvas, title, video) {
  1214. title = title || 'videoCapturer_' + Date.now();
  1215. try {
  1216. canvas.toBlob(function (blob) {
  1217. const el = document.createElement('a');
  1218. el.download = `${title}.jpg`;
  1219. el.href = URL.createObjectURL(blob);
  1220. el.click();
  1221. }, 'image/jpeg', 0.99);
  1222. } catch (e) {
  1223. window.alert('视频源受CORS标识限制,无法下载截图,\n您可向作者反馈信息,以便完善网站兼容逻辑');
  1224. console.log('video object:', video);
  1225. console.error('video crossorigin:', video.getAttribute('crossorigin'));
  1226. console.error(e);
  1227. }
  1228. }
  1229. };
  1230.  
  1231. /**
  1232. * 鼠标事件观测对象
  1233. * 用于实现鼠标事件的穿透响应,有别于pointer-events:none
  1234. * pointer-events:none是设置当前层允许穿透
  1235. * 而MouseObserver是:即使不知道target上面存在多少层遮挡,一样可以响应鼠标事件
  1236. */
  1237.  
  1238. class MouseObserver {
  1239. constructor (observeOpt) {
  1240. // eslint-disable-next-line no-undef
  1241. this.observer = new IntersectionObserver((infoList) => {
  1242. infoList.forEach((info) => {
  1243. info.target.IntersectionObserverEntry = info;
  1244. });
  1245. }, observeOpt || {});
  1246.  
  1247. this.observeList = [];
  1248. }
  1249.  
  1250. _observe (target) {
  1251. let hasObserve = false;
  1252. for (let i = 0; i < this.observeList.length; i++) {
  1253. const el = this.observeList[i];
  1254. if (target === el) {
  1255. hasObserve = true;
  1256. break
  1257. }
  1258. }
  1259.  
  1260. if (!hasObserve) {
  1261. this.observer.observe(target);
  1262. this.observeList.push(target);
  1263. }
  1264. }
  1265.  
  1266. _unobserve (target) {
  1267. this.observer.unobserve(target);
  1268. const newObserveList = [];
  1269. this.observeList.forEach((el) => {
  1270. if (el !== target) {
  1271. newObserveList.push(el);
  1272. }
  1273. });
  1274. this.observeList = newObserveList;
  1275. }
  1276.  
  1277. /**
  1278. * 增加事件绑定
  1279. * @param target {element} -必选 要绑定事件的dom对象
  1280. * @param type {string} -必选 要绑定的事件,只支持鼠标事件
  1281. * @param listener {function} -必选 符合触发条件时的响应函数
  1282. */
  1283. on (target, type, listener, options) {
  1284. const t = this;
  1285. t._observe(target);
  1286.  
  1287. if (!target.MouseObserverEvent) {
  1288. target.MouseObserverEvent = {};
  1289. }
  1290. target.MouseObserverEvent[type] = true;
  1291.  
  1292. if (!t._mouseObserver_) {
  1293. t._mouseObserver_ = {};
  1294. }
  1295.  
  1296. if (!t._mouseObserver_[type]) {
  1297. t._mouseObserver_[type] = [];
  1298.  
  1299. window.addEventListener(type, (event) => {
  1300. t.observeList.forEach((target) => {
  1301. const isVisibility = target.IntersectionObserverEntry && target.IntersectionObserverEntry.intersectionRatio > 0;
  1302. const isReg = target.MouseObserverEvent[event.type] === true;
  1303. if (isVisibility && isReg) {
  1304. /* 判断是否符合触发侦听器事件条件 */
  1305. const bound = target.getBoundingClientRect();
  1306. const offsetX = event.x - bound.x;
  1307. const offsetY = event.y - bound.y;
  1308. const isNeedTap = offsetX <= bound.width && offsetX >= 0 && offsetY <= bound.height && offsetY >= 0;
  1309.  
  1310. if (isNeedTap) {
  1311. /* 执行监听回调 */
  1312. const listenerList = t._mouseObserver_[type];
  1313. listenerList.forEach((listener) => {
  1314. if (listener instanceof Function) {
  1315. listener.call(t, event, {
  1316. x: offsetX,
  1317. y: offsetY
  1318. }, target);
  1319. }
  1320. });
  1321. }
  1322. }
  1323. });
  1324. }, options);
  1325. }
  1326.  
  1327. /* 将监听回调加入到事件队列 */
  1328. if (listener instanceof Function) {
  1329. t._mouseObserver_[type].push(listener);
  1330. }
  1331. }
  1332.  
  1333. /**
  1334. * 解除事件绑定
  1335. * @param target {element} -必选 要解除事件的dom对象
  1336. * @param type {string} -必选 要解除的事件,只支持鼠标事件
  1337. * @param listener {function} -必选 绑定事件时的响应函数
  1338. * @returns {boolean}
  1339. */
  1340. off (target, type, listener) {
  1341. const t = this;
  1342. if (!target || !type || !listener || !t._mouseObserver_ || !t._mouseObserver_[type] || !target.MouseObserverEvent || !target.MouseObserverEvent[type]) return false
  1343.  
  1344. const newListenerList = [];
  1345. const listenerList = t._mouseObserver_[type];
  1346. let isMatch = false;
  1347. listenerList.forEach((listenerItem) => {
  1348. if (listenerItem === listener) {
  1349. isMatch = true;
  1350. } else {
  1351. newListenerList.push(listenerItem);
  1352. }
  1353. });
  1354.  
  1355. if (isMatch) {
  1356. t._mouseObserver_[type] = newListenerList;
  1357.  
  1358. /* 侦听器已被完全移除 */
  1359. if (newListenerList.length === 0) {
  1360. delete target.MouseObserverEvent[type];
  1361. }
  1362.  
  1363. /* 当MouseObserverEvent为空对象时移除观测对象 */
  1364. if (JSON.stringify(target.MouseObserverEvent[type]) === '{}') {
  1365. t._unobserve(target);
  1366. }
  1367. }
  1368. }
  1369. }
  1370.  
  1371. /**
  1372. * 简单的i18n库
  1373. */
  1374.  
  1375. class I18n {
  1376. constructor (config) {
  1377. this._languages = {};
  1378. this._locale = this.getClientLang();
  1379. this._defaultLanguage = '';
  1380. this.init(config);
  1381. }
  1382.  
  1383. init (config) {
  1384. if (!config) return false
  1385.  
  1386. const t = this;
  1387. t._locale = config.locale || t._locale;
  1388. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1389. t._languages = config.languages || t._languages;
  1390. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  1391. }
  1392.  
  1393. use () {}
  1394.  
  1395. t (path) {
  1396. const t = this;
  1397. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  1398.  
  1399. /* 版本回退 */
  1400. if (!result && t._locale !== t._defaultLanguage) {
  1401. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  1402. }
  1403.  
  1404. return result || ''
  1405. }
  1406.  
  1407. /* 当前语言值 */
  1408. language () {
  1409. return this._locale
  1410. }
  1411.  
  1412. languages () {
  1413. return this._languages
  1414. }
  1415.  
  1416. changeLanguage (locale) {
  1417. if (this._languages[locale]) {
  1418. this._languages = locale;
  1419. return locale
  1420. } else {
  1421. return false
  1422. }
  1423. }
  1424.  
  1425. /**
  1426. * 根据文本路径获取对象里面的值
  1427. * @param obj {Object} -必选 要操作的对象
  1428. * @param path {String} -必选 路径信息
  1429. * @returns {*}
  1430. */
  1431. getValByPath (obj, path) {
  1432. path = path || '';
  1433. const pathArr = path.split('.');
  1434. let result = obj;
  1435.  
  1436. /* 递归提取结果值 */
  1437. for (let i = 0; i < pathArr.length; i++) {
  1438. if (!result) break
  1439. result = result[pathArr[i]];
  1440. }
  1441.  
  1442. return result
  1443. }
  1444.  
  1445. /* 获取客户端当前的语言环境 */
  1446. getClientLang () {
  1447. return navigator.languages ? navigator.languages[0] : navigator.language
  1448. }
  1449. }
  1450.  
  1451. /* 用于获取全局唯一的id */
  1452. function getId () {
  1453. let gID = window.GM_getValue('_global_id_');
  1454. if (!gID) gID = 0;
  1455. gID = Number(gID) + 1;
  1456. window.GM_setValue('_global_id_', gID);
  1457. return gID
  1458. }
  1459.  
  1460. let curTabId = null;
  1461.  
  1462. /**
  1463. * 获取当前TAB标签的Id号,可用于iframe确定自己是否处于同一TAB标签下
  1464. * @returns {Promise<any>}
  1465. */
  1466. function getTabId () {
  1467. return new Promise((resolve, reject) => {
  1468. window.GM_getTab(function (obj) {
  1469. if (!obj.tabId) {
  1470. obj.tabId = getId();
  1471. window.GM_saveTab(obj);
  1472. }
  1473. /* 每次获取都更新当前Tab的id号 */
  1474. curTabId = obj.tabId;
  1475. resolve(obj.tabId);
  1476. });
  1477. })
  1478. }
  1479.  
  1480. /* 一开始就初始化好curTabId,这样后续就不需要异步获取Tabid,部分场景下需要用到 */
  1481. getTabId();
  1482.  
  1483. /*!
  1484. * @name menuCommand.js
  1485. * @version 0.0.1
  1486. * @author Blaze
  1487. * @date 2019/9/21 14:22
  1488. */
  1489.  
  1490. const monkeyMenu = {
  1491. on (title, fn, accessKey) {
  1492. return window.GM_registerMenuCommand && window.GM_registerMenuCommand(title, fn, accessKey)
  1493. },
  1494. off (id) {
  1495. return window.GM_unregisterMenuCommand && window.GM_unregisterMenuCommand(id)
  1496. },
  1497. /* 切换类型的菜单功能 */
  1498. switch (title, fn, defVal) {
  1499. const t = this;
  1500. t.on(title, fn);
  1501. }
  1502. };
  1503.  
  1504. /*!
  1505. * @name monkeyMsg.js
  1506. * @version 0.0.1
  1507. * @author Blaze
  1508. * @date 2019/9/21 14:22
  1509. */
  1510.  
  1511. /**
  1512. * 将对象数据里面可存储到GM_setValue里面的值提取出来
  1513. * @param obj {objcet} -必选 打算要存储的对象数据
  1514. * @param deep {number} -可选 如果对象层级非常深,则须限定递归的层级,默认最高不能超过3级
  1515. * @returns {{}}
  1516. */
  1517. function extractDatafromOb (obj, deep) {
  1518. deep = deep || 1;
  1519. if (deep > 3) return {}
  1520.  
  1521. const result = {};
  1522. if (typeof obj === 'object') {
  1523. for (const key in obj) {
  1524. const val = obj[key];
  1525. const valType = typeof val;
  1526. if (valType === 'number' || valType === 'string' || valType === 'boolean') {
  1527. Object.defineProperty(result, key, {
  1528. value: val,
  1529. writable: true,
  1530. configurable: true,
  1531. enumerable: true
  1532. });
  1533. } else if (valType === 'object' && Object.prototype.propertyIsEnumerable.call(obj, key)) {
  1534. /* 进行递归提取 */
  1535. result[key] = extractDatafromOb(val, deep + 1);
  1536. } else if (valType === 'array') {
  1537. result[key] = val;
  1538. }
  1539. }
  1540. }
  1541. return result
  1542. }
  1543.  
  1544. const monkeyMsg = {
  1545. /**
  1546. * 发送消息,除了正常发送信息外,还会补充各类必要的信息
  1547. * @param name {string} -必选 要发送给那个字段,接收时要一致才能监听的正确
  1548. * @param data {Any} -必选 要发送的数据
  1549. * @param throttleInterval -可选,因为会出现莫名奇妙的重复发送情况,为了消除重复发送带来的副作用,所以引入节流限制逻辑,即限制某个时间间隔内只能发送一次,多余的次数自动抛弃掉,默认80ms
  1550. * @returns {Promise<void>}
  1551. */
  1552. send (name, data, throttleInterval = 80) {
  1553. /* 阻止频繁发送修改事件 */
  1554. const oldMsg = window.GM_getValue(name);
  1555. if (oldMsg && oldMsg.updateTime) {
  1556. const interval = Math.abs(Date.now() - oldMsg.updateTime);
  1557. if (interval < throttleInterval) {
  1558. return false
  1559. }
  1560. }
  1561.  
  1562. const msg = {
  1563. /* 发送过来的数据 */
  1564. data,
  1565. /* 补充标签ID,用于判断是否同处一个tab标签下 */
  1566. tabId: curTabId || 'undefined',
  1567. /* 补充消息的页面来源的标题信息 */
  1568. title: document.title,
  1569. /* 补充消息的页面来源信息 */
  1570. referrer: extractDatafromOb(window.location),
  1571. /* 最近一次更新该数据的时间 */
  1572. updateTime: Date.now()
  1573. };
  1574. if (typeof data === 'object') {
  1575. msg.data = extractDatafromOb(data);
  1576. }
  1577. window.GM_setValue(name, msg);
  1578. },
  1579. set: (name, data) => monkeyMsg.send(name, data),
  1580. get: (name) => window.GM_getValue(name),
  1581. on: (name, fn) => window.GM_addValueChangeListener(name, fn),
  1582. off: (listenerId) => window.GM_removeValueChangeListener(listenerId)
  1583. };
  1584.  
  1585. class Debug {
  1586. constructor (msg) {
  1587. const t = this;
  1588. msg = msg || 'debug message:';
  1589. t.log = t.createDebugMethod('log', null, msg);
  1590. t.error = t.createDebugMethod('error', null, msg);
  1591. t.info = t.createDebugMethod('info', null, msg);
  1592. }
  1593.  
  1594. create (msg) {
  1595. return new Debug(msg)
  1596. }
  1597.  
  1598. createDebugMethod (name, color, tipsMsg) {
  1599. name = name || 'info';
  1600.  
  1601. const bgColorMap = {
  1602. info: '#2274A5',
  1603. log: '#95B46A',
  1604. error: '#D33F49'
  1605. };
  1606.  
  1607. return function () {
  1608. if (!window._debugMode_) {
  1609. return false
  1610. }
  1611.  
  1612. const curTime = new Date();
  1613. const H = curTime.getHours();
  1614. const M = curTime.getMinutes();
  1615. const S = curTime.getSeconds();
  1616. const msg = tipsMsg || 'debug message:';
  1617.  
  1618. const arg = Array.from(arguments);
  1619. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  1620. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  1621. window.console[name].apply(window.console, arg);
  1622. }
  1623. }
  1624.  
  1625. isDebugMode () {
  1626. return Boolean(window._debugMode_)
  1627. }
  1628. }
  1629.  
  1630. var Debug$1 = new Debug();
  1631.  
  1632. var debug = Debug$1.create('h5player message:');
  1633.  
  1634. /* 当前用到的快捷键 */
  1635. const hasUseKey = {
  1636. keyCodeList: [13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, 73, 74, 75, 78, 79, 80, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, 99, 100, 220],
  1637. 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', '\\', '|'],
  1638. keyMap: {
  1639. enter: 13,
  1640. shift: 16,
  1641. ctrl: 17,
  1642. alt: 18,
  1643. esc: 27,
  1644. space: 32,
  1645. '←': 37,
  1646. '↑': 38,
  1647. '→': 39,
  1648. '↓': 40,
  1649. 1: 49,
  1650. 2: 50,
  1651. 3: 51,
  1652. 4: 52,
  1653. c: 67,
  1654. d: 68,
  1655. e: 69,
  1656. f: 70,
  1657. i: 73,
  1658. j: 74,
  1659. k: 75,
  1660. n: 78,
  1661. o: 79,
  1662. p: 80,
  1663. q: 81,
  1664. r: 82,
  1665. s: 83,
  1666. t: 84,
  1667. u: 85,
  1668. w: 87,
  1669. x: 88,
  1670. y: 89,
  1671. z: 90,
  1672. pad1: 97,
  1673. pad2: 98,
  1674. pad3: 99,
  1675. pad4: 100,
  1676. '\\': 220
  1677. }
  1678. };
  1679.  
  1680. /**
  1681. * 判断当前按键是否注册为需要用的按键
  1682. * 用于减少对其它键位的干扰
  1683. */
  1684. function isRegisterKey (event) {
  1685. const keyCode = event.keyCode;
  1686. const key = event.key.toLowerCase();
  1687. return hasUseKey.keyCodeList.includes(keyCode) ||
  1688. hasUseKey.keyList.includes(key)
  1689. }
  1690.  
  1691. /**
  1692. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  1693. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  1694. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  1695. * @returns {Promise<void>}
  1696. */
  1697. async function getPageWindow () {
  1698. return new Promise(function (resolve, reject) {
  1699. if (window._pageWindow) {
  1700. return resolve(window._pageWindow)
  1701. }
  1702.  
  1703. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  1704.  
  1705. function getWin (event) {
  1706. window._pageWindow = this;
  1707. // debug.log('getPageWindow succeed', event)
  1708. listenEventList.forEach(eventType => {
  1709. window.removeEventListener(eventType, getWin, true);
  1710. });
  1711. resolve(window._pageWindow);
  1712. }
  1713.  
  1714. listenEventList.forEach(eventType => {
  1715. window.addEventListener(eventType, getWin, true);
  1716. });
  1717.  
  1718. /* 自行派发事件以便用最短的时候获得pageWindow对象 */
  1719. window.dispatchEvent(new window.Event('get-page-window-event'));
  1720. })
  1721. }
  1722. getPageWindow();
  1723.  
  1724. /*!
  1725. * @name crossTabCtl.js
  1726. * @description 跨Tab控制脚本逻辑
  1727. * @version 0.0.1
  1728. * @author Blaze
  1729. * @date 2019/11/21 上午11:56
  1730. * @github https://github.com/xxxily
  1731. */
  1732.  
  1733. const crossTabCtl = {
  1734. /* 由于没有专门的监控方法,所以只能通过轮询来更新画中画信息 */
  1735. updatePictureInPictureInfo () {
  1736. setInterval(function () {
  1737. if (document.pictureInPictureElement) {
  1738. monkeyMsg.send('globalPictureInPictureInfo', {
  1739. hasGlobalPictureInPictureElement: true
  1740. });
  1741. }
  1742. }, 1000 * 1.5);
  1743. },
  1744. /* 判断当前是否开启了画中画功能 */
  1745. hasOpenPictureInPicture () {
  1746. const data = monkeyMsg.get('globalPictureInPictureInfo');
  1747. /* 画中画的全局信息更新时间差在3s内,才认为当前开启了画中画模式 */
  1748. return data && Math.abs(Date.now() - data.updateTime) < 1000 * 3
  1749. },
  1750. /**
  1751. * 判断是否需要发送跨Tab控制按键信息
  1752. */
  1753. isNeedSendCrossTabCtlEvent () {
  1754. const t = crossTabCtl;
  1755. if (t.hasOpenPictureInPicture()) {
  1756. /* 画中画开启后,判断不在同一个Tab才发送事件 */
  1757. const data = monkeyMsg.get('globalPictureInPictureInfo');
  1758. if (data.tabId !== curTabId) {
  1759. return true
  1760. }
  1761. }
  1762. },
  1763. crossTabKeydownEvent (event) {
  1764. const t = crossTabCtl;
  1765. /* 处于可编辑元素中不执行任何快捷键 */
  1766. if (isEditableTarget(event.target)) return
  1767. if (t.isNeedSendCrossTabCtlEvent() && isRegisterKey(event)) {
  1768. // 阻止事件冒泡和默认事件
  1769. event.stopPropagation();
  1770. event.preventDefault();
  1771.  
  1772. /* 广播按键消息,进行跨Tab控制 */
  1773. monkeyMsg.send('globalKeydownEvent', event);
  1774. debug.log('已发送跨Tab按键控制信息:', event);
  1775. return true
  1776. }
  1777. },
  1778. bindCrossTabEvent () {
  1779. const t = crossTabCtl;
  1780. if (t._hasBindEvent_) return
  1781. document.removeEventListener('keydown', t.crossTabKeydownEvent);
  1782. document.addEventListener('keydown', t.crossTabKeydownEvent, true);
  1783. t._hasBindEvent_ = true;
  1784. },
  1785. init () {
  1786. const t = crossTabCtl;
  1787. t.updatePictureInPictureInfo();
  1788. t.bindCrossTabEvent();
  1789. }
  1790. };
  1791.  
  1792. var zhCN = {
  1793. about: '关于',
  1794. issues: '反馈',
  1795. setting: '设置',
  1796. tipsMsg: {
  1797. playspeed: '播放速度:',
  1798. forward: '前进:',
  1799. backward: '后退:',
  1800. seconds: '秒',
  1801. volume: '音量:',
  1802. nextframe: '定位:下一帧',
  1803. previousframe: '定位:上一帧',
  1804. stopframe: '定格帧画面:',
  1805. play: '播放',
  1806. pause: '暂停',
  1807. arpl: '允许自动恢复播放进度',
  1808. drpl: '禁止自动恢复播放进度',
  1809. brightness: '图像亮度:',
  1810. contrast: '图像对比度:',
  1811. saturation: '图像饱和度:',
  1812. hue: '图像色相:',
  1813. blur: '图像模糊度:',
  1814. imgattrreset: '图像属性:复位',
  1815. imgrotate: '画面旋转:',
  1816. onplugin: '启用h5Player插件',
  1817. offplugin: '禁用h5Player插件',
  1818. globalmode: '全局模式:',
  1819. playbackrestored: '为你恢复上次播放进度',
  1820. playbackrestoreoff: '恢复播放进度功能已禁用,按 SHIFT+R 可开启该功能',
  1821. horizontal: '水平位移:',
  1822. vertical: '垂直位移:',
  1823. videozoom: '视频缩放率:'
  1824. }
  1825. };
  1826.  
  1827. var enUS = {
  1828. about: 'about',
  1829. issues: 'issues',
  1830. setting: 'setting',
  1831. tipsMsg: {
  1832. playspeed: 'Speed: ',
  1833. forward: 'Forward: ',
  1834. backward: 'Backward: ',
  1835. seconds: 'sec',
  1836. volume: 'Volume: ',
  1837. nextframe: 'Next frame',
  1838. previousframe: 'Previous frame',
  1839. stopframe: 'Stopframe: ',
  1840. play: 'Play',
  1841. pause: 'Pause',
  1842. arpl: 'Allow auto resume playback progress',
  1843. drpl: 'Disable auto resume playback progress',
  1844. brightness: 'Brightness: ',
  1845. contrast: 'Contrast: ',
  1846. saturation: 'Saturation: ',
  1847. hue: 'HUE: ',
  1848. blur: 'Blur: ',
  1849. imgattrreset: 'Attributes: reset',
  1850. imgrotate: 'Picture rotation: ',
  1851. onplugin: 'ON h5Player plugin',
  1852. offplugin: 'OFF h5Player plugin',
  1853. globalmode: 'Global mode: ',
  1854. playbackrestored: 'Restored the last playback progress for you',
  1855. playbackrestoreoff: 'The function of restoring the playback progress is disabled. Press SHIFT+R to turn on the function',
  1856. horizontal: 'Horizontal displacement: ',
  1857. vertical: 'Vertical displacement: ',
  1858. videozoom: 'Video zoom: '
  1859. },
  1860. demo: 'demo-test'
  1861. };
  1862.  
  1863. var ru = {
  1864. about: 'около',
  1865. issues: 'обратная связь',
  1866. setting: 'установка',
  1867. tipsMsg: {
  1868. playspeed: 'Скорость: ',
  1869. forward: 'Вперёд: ',
  1870. backward: 'Назад: ',
  1871. seconds: ' сек',
  1872. volume: 'Громкость: ',
  1873. nextframe: 'Следующий кадр',
  1874. previousframe: 'Предыдущий кадр',
  1875. stopframe: 'Стоп-кадр: ',
  1876. play: 'Запуск',
  1877. pause: 'Пауза',
  1878. arpl: 'Разрешить автоматическое возобновление прогресса воспроизведения',
  1879. drpl: 'Запретить автоматическое возобновление прогресса воспроизведения',
  1880. brightness: 'Яркость: ',
  1881. contrast: 'Контраст: ',
  1882. saturation: 'Насыщенность: ',
  1883. hue: 'Оттенок: ',
  1884. blur: 'Размытие: ',
  1885. imgattrreset: 'Атрибуты: сброс',
  1886. imgrotate: 'Поворот изображения: ',
  1887. onplugin: 'ВКЛ: плагин воспроизведения',
  1888. offplugin: 'ВЫКЛ: плагин воспроизведения',
  1889. globalmode: 'Глобальный режим:',
  1890. playbackrestored: 'Восстановлен последний прогресс воспроизведения',
  1891. playbackrestoreoff: 'Функция восстановления прогресса воспроизведения отключена. Нажмите SHIFT + R, чтобы включить функцию',
  1892. horizontal: 'Горизонтальное смещение: ',
  1893. vertical: 'Вертикальное смещение: ',
  1894. videozoom: 'Увеличить видео: '
  1895. }
  1896. };
  1897.  
  1898. var zhTW = {
  1899. about: '關於',
  1900. issues: '反饋',
  1901. setting: '設置',
  1902. tipsMsg: {
  1903. playspeed: '播放速度:',
  1904. forward: '向前:',
  1905. backward: '向後:',
  1906. seconds: '秒',
  1907. volume: '音量:',
  1908. nextframe: '定位:下一幀',
  1909. previousframe: '定位:上一幀',
  1910. stopframe: '定格幀畫面:',
  1911. play: '播放',
  1912. pause: '暫停',
  1913. arpl: '允許自動恢復播放進度',
  1914. drpl: '禁止自動恢復播放進度',
  1915. brightness: '圖像亮度:',
  1916. contrast: '圖像對比度:',
  1917. saturation: '圖像飽和度:',
  1918. hue: '圖像色相:',
  1919. blur: '圖像模糊度:',
  1920. imgattrreset: '圖像屬性:復位',
  1921. imgrotate: '畫面旋轉:',
  1922. onplugin: '啟用h5Player插件',
  1923. offplugin: '禁用h5Player插件',
  1924. globalmode: '全局模式:',
  1925. playbackrestored: '為你恢復上次播放進度',
  1926. playbackrestoreoff: '恢復播放進度功能已禁用,按 SHIFT+R 可開啟該功能',
  1927. horizontal: '水平位移:',
  1928. vertical: '垂直位移:',
  1929. videozoom: '視頻縮放率:'
  1930. }
  1931. };
  1932.  
  1933. const messages = {
  1934. 'zh-CN': zhCN,
  1935. zh: zhCN,
  1936. 'zh-HK': zhTW,
  1937. 'zh-TW': zhTW,
  1938. 'en-US': enUS,
  1939. en: enUS,
  1940. ru: ru
  1941. };
  1942.  
  1943. (async function () {
  1944. debug.log('h5Player init');
  1945.  
  1946. const i18n = new I18n({
  1947. defaultLanguage: 'en',
  1948. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  1949. // locale: 'zh-TW',
  1950. languages: messages
  1951. });
  1952.  
  1953. const mouseObserver = new MouseObserver();
  1954.  
  1955. // monkeyMenu.on('i18n.t('setting')', function () {
  1956. // window.alert('功能开发中,敬请期待...')
  1957. // })
  1958. monkeyMenu.on(i18n.t('about'), function () {
  1959. window.GM_openInTab('https://github.com/xxxily/h5player', {
  1960. active: true,
  1961. insert: true,
  1962. setParent: true
  1963. });
  1964. });
  1965. monkeyMenu.on(i18n.t('issues'), function () {
  1966. window.GM_openInTab('https://github.com/xxxily/h5player/issues', {
  1967. active: true,
  1968. insert: true,
  1969. setParent: true
  1970. });
  1971. });
  1972.  
  1973. hackAttachShadow();
  1974. hackEventListener({
  1975. // proxyAll: true,
  1976. proxyNodeType: ['video'],
  1977. debug: debug.isDebugMode()
  1978. });
  1979.  
  1980. let TCC = null;
  1981. const h5Player = {
  1982. /* 提示文本的字号 */
  1983. fontSize: 12,
  1984. enable: true,
  1985. globalMode: true,
  1986. playerInstance: null,
  1987. scale: 1,
  1988. translate: {
  1989. x: 0,
  1990. y: 0
  1991. },
  1992. playbackRate: 1,
  1993. lastPlaybackRate: 1,
  1994. /* 快进快退步长 */
  1995. skipStep: 5,
  1996. /* 获取当前播放器的实例 */
  1997. player: function () {
  1998. const t = this;
  1999. return t.playerInstance || t.getPlayerList()[0]
  2000. },
  2001. /* 每个网页可能存在的多个video播放器 */
  2002. getPlayerList: function () {
  2003. const list = [];
  2004. function findPlayer (context) {
  2005. context.querySelectorAll('video').forEach(function (player) {
  2006. list.push(player);
  2007. });
  2008. }
  2009. findPlayer(document);
  2010.  
  2011. // 被封装在 shadow dom 里面的video
  2012. if (window._shadowDomList_) {
  2013. window._shadowDomList_.forEach(function (shadowRoot) {
  2014. findPlayer(shadowRoot);
  2015. });
  2016. }
  2017.  
  2018. return list
  2019. },
  2020. getPlayerWrapDom: function () {
  2021. const t = this;
  2022. const player = t.player();
  2023. if (!player) return
  2024.  
  2025. let wrapDom = null;
  2026. const playerBox = player.getBoundingClientRect();
  2027. eachParentNode(player, function (parent) {
  2028. if (parent === document || !parent.getBoundingClientRect) return
  2029. const parentBox = parent.getBoundingClientRect();
  2030. if (parentBox.width && parentBox.height) {
  2031. if (parentBox.width === playerBox.width && parentBox.height === playerBox.height) {
  2032. wrapDom = parent;
  2033. }
  2034. }
  2035. });
  2036. return wrapDom
  2037. },
  2038.  
  2039. /* 挂载到页面上的window对象,用于调试 */
  2040. async mountToGlobal () {
  2041. try {
  2042. const pageWindow = await getPageWindow();
  2043. if (pageWindow) {
  2044. pageWindow._h5Player = h5Player || 'null';
  2045. if (window.top !== window) {
  2046. pageWindow._h5PlayerInFrame = h5Player || 'null';
  2047. }
  2048. pageWindow._window = window || '';
  2049. debug.log('h5Player对象已成功挂载到全局');
  2050. }
  2051. } catch (e) {
  2052. debug.error(e);
  2053. }
  2054. },
  2055.  
  2056. /**
  2057. * 初始化播放器实例
  2058. * @param isSingle 是否为单实例video标签
  2059. */
  2060. initPlayerInstance (isSingle) {
  2061. const t = this;
  2062. if (!t.playerInstance) return
  2063.  
  2064. const player = t.playerInstance;
  2065. t.initPlaybackRate();
  2066. t.isFoucs();
  2067. t.proxyPlayerInstance(player);
  2068.  
  2069. // player.addEventListener('durationchange', () => {
  2070. // debug.log('当前视频长度:', player.duration)
  2071. // })
  2072. // player.setAttribute('preload', 'auto')
  2073.  
  2074. /* 增加通用全屏,网页全屏api */
  2075. player._fullScreen_ = new FullScreen(player);
  2076. player._fullPageScreen_ = new FullScreen(player, true);
  2077.  
  2078. /* 注册播放器的事件代理处理器 */
  2079. player._listenerProxyApplyHandler_ = t.playerEventHandler;
  2080.  
  2081. if (!player._hasCanplayEvent_) {
  2082. player.addEventListener('canplay', function (event) {
  2083. t.initAutoPlay(player);
  2084. });
  2085. player._hasCanplayEvent_ = true;
  2086. }
  2087.  
  2088. /* 播放的时候进行相关同步操作 */
  2089. if (!player._hasPlayingInitEvent_) {
  2090. let setPlaybackRateOnPlayingCount = 0;
  2091. player.addEventListener('playing', function (event) {
  2092. if (setPlaybackRateOnPlayingCount === 0) {
  2093. /* 同步之前设定的播放速度 */
  2094. t.setPlaybackRate();
  2095.  
  2096. if (isSingle === true) {
  2097. /* 恢复播放进度和进行进度记录 */
  2098. t.setPlayProgress(player);
  2099. setTimeout(function () {
  2100. t.playProgressRecorder(player);
  2101. }, 1000 * 3);
  2102. }
  2103. } else {
  2104. t.setPlaybackRate(null, true);
  2105. }
  2106. setPlaybackRateOnPlayingCount += 1;
  2107. });
  2108. player._hasPlayingInitEvent_ = true;
  2109. }
  2110.  
  2111. /* 进行自定义初始化操作 */
  2112. const taskConf = TCC.getTaskConfig();
  2113. if (taskConf.init) {
  2114. TCC.doTask('init', player);
  2115. }
  2116.  
  2117. /* 注册鼠标响应事件 */
  2118. mouseObserver.on(player, 'click', function (event, offset, target) {
  2119. // debug.log('捕捉到鼠标点击事件:', event, offset, target)
  2120. });
  2121.  
  2122. debug.isDebugMode() && t.mountToGlobal();
  2123. },
  2124.  
  2125. /**
  2126. * 对播放器实例的方法或属性进行代理
  2127. * @param player
  2128. */
  2129. proxyPlayerInstance (player) {
  2130. if (!player) return
  2131.  
  2132. /* 要代理的方法或属性列表 */
  2133. const proxyList = [
  2134. 'play',
  2135. 'pause'
  2136. ];
  2137.  
  2138. proxyList.forEach(key => {
  2139. const originKey = 'origin_' + key;
  2140. if (Reflect.has(player, key) && !Reflect.has(player, originKey)) {
  2141. player[originKey] = player[key];
  2142. const proxy = new Proxy(player[key], {
  2143. apply (target, ctx, args) {
  2144. debug.log(key + '被调用');
  2145.  
  2146. /* 处理挂起逻辑 */
  2147. const hangUpInfo = player._hangUpInfo_ || {};
  2148. const hangUpDetail = hangUpInfo[key] || hangUpInfo['hangUp_' + key];
  2149. const needHangUp = hangUpDetail && hangUpDetail.timeout >= Date.now();
  2150. if (needHangUp) {
  2151. debug.log(key + '已被挂起,本次调用将被忽略');
  2152. return false
  2153. }
  2154.  
  2155. return target.apply(ctx || player, args)
  2156. }
  2157. });
  2158.  
  2159. player[key] = proxy;
  2160. }
  2161. });
  2162.  
  2163. if (!player._hangUp_) {
  2164. player._hangUpInfo_ = {};
  2165. /**
  2166. * 挂起player某个函数的调用
  2167. * @param name {String} -必选 player方法或属性名,名字写对外,还须要该方法或属性被代理了才能进行挂起,否则这将是个无效的调用
  2168. * @param timeout {Number} -可选 挂起多长时间,默认200ms
  2169. * @private
  2170. */
  2171. player._hangUp_ = function (name, timeout) {
  2172. timeout = Number(timeout) || 200;
  2173. debug.log('_hangUp_', name, timeout);
  2174. player._hangUpInfo_[name] = {
  2175. timeout: Date.now() + timeout
  2176. };
  2177. };
  2178. }
  2179. },
  2180. initPlaybackRate () {
  2181. const t = this;
  2182. t.playbackRate = t.getPlaybackRate();
  2183. },
  2184. getPlaybackRate () {
  2185. const t = this;
  2186. let playbackRate = t.playbackRate;
  2187. if (!isInCrossOriginFrame()) {
  2188. playbackRate = window.localStorage.getItem('_h5_player_playback_rate_') || t.playbackRate;
  2189. }
  2190. return Number(Number(playbackRate).toFixed(1))
  2191. },
  2192. /* 设置播放速度 */
  2193. setPlaybackRate: function (num, notips) {
  2194. const taskConf = TCC.getTaskConfig();
  2195. if (taskConf.playbackRate) {
  2196. TCC.doTask('playbackRate');
  2197. return
  2198. }
  2199.  
  2200. const t = this;
  2201. const player = t.player();
  2202. let curPlaybackRate;
  2203. if (num) {
  2204. num = Number(num);
  2205. if (Number.isNaN(num)) {
  2206. debug.error('h5player: 播放速度转换出错');
  2207. return false
  2208. }
  2209.  
  2210. if (num <= 0) {
  2211. num = 0.1;
  2212. } else if (num > 16) {
  2213. num = 16;
  2214. }
  2215.  
  2216. num = Number(num.toFixed(1));
  2217. curPlaybackRate = num;
  2218. } else {
  2219. curPlaybackRate = t.getPlaybackRate();
  2220. }
  2221.  
  2222. /* 记录播放速度的信息 */
  2223. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_playback_rate_', curPlaybackRate);
  2224.  
  2225. t.playbackRate = curPlaybackRate;
  2226. player.playbackRate = curPlaybackRate;
  2227.  
  2228. /* 本身处于1倍播放速度的时候不再提示 */
  2229. if (!num && curPlaybackRate === 1) {
  2230. return true
  2231. } else {
  2232. !notips && t.tips(i18n.t('tipsMsg.playspeed') + player.playbackRate);
  2233. }
  2234. },
  2235. /* 恢复播放速度,还原到1倍速度、或恢复到上次的倍速 */
  2236. resetPlaybackRate: function (player) {
  2237. const t = this;
  2238. player = player || t.player();
  2239.  
  2240. const oldPlaybackRate = Number(player.playbackRate);
  2241. const playbackRate = oldPlaybackRate === 1 ? t.lastPlaybackRate : 1;
  2242. if (oldPlaybackRate !== 1) {
  2243. t.lastPlaybackRate = oldPlaybackRate;
  2244. }
  2245.  
  2246. player.playbackRate = playbackRate;
  2247. t.setPlaybackRate(player.playbackRate);
  2248. },
  2249. /**
  2250. * 初始化自动播放逻辑
  2251. * 必须是配置了自动播放按钮选择器得的才会进行自动播放
  2252. */
  2253. initAutoPlay: function (p) {
  2254. const t = this;
  2255. const player = p || t.player();
  2256.  
  2257. // 在轮询重试的时候,如果实例变了,或处于隐藏页面中则不进行自动播放操作
  2258. if (!player || (p && p !== t.player()) || document.hidden) return
  2259.  
  2260. const taskConf = TCC.getTaskConfig();
  2261. if (player && taskConf.autoPlay && player.paused) {
  2262. TCC.doTask('autoPlay');
  2263. if (player.paused) {
  2264. // 轮询重试
  2265. if (!player._initAutoPlayCount_) {
  2266. player._initAutoPlayCount_ = 1;
  2267. }
  2268. player._initAutoPlayCount_ += 1;
  2269. if (player._initAutoPlayCount_ >= 10) {
  2270. return false
  2271. }
  2272. setTimeout(function () {
  2273. t.initAutoPlay(player);
  2274. }, 200);
  2275. }
  2276. }
  2277. },
  2278. setWebFullScreen: function () {
  2279. const t = this;
  2280. const player = t.player();
  2281. const isDo = TCC.doTask('webFullScreen');
  2282. if (!isDo && player && player._fullPageScreen_) {
  2283. player._fullPageScreen_.toggle();
  2284. }
  2285. },
  2286. /* 设置播放进度 */
  2287. setCurrentTime: function (num, notips) {
  2288. if (!num) return
  2289. num = Number(num);
  2290. const _num = Math.abs(Number(num.toFixed(1)));
  2291.  
  2292. const t = this;
  2293. const player = t.player();
  2294. const taskConf = TCC.getTaskConfig();
  2295. if (taskConf.currentTime) {
  2296. TCC.doTask('currentTime');
  2297. return
  2298. }
  2299.  
  2300. if (num > 0) {
  2301. if (taskConf.addCurrentTime) {
  2302. TCC.doTask('addCurrentTime');
  2303. } else {
  2304. player.currentTime += _num;
  2305. !notips && t.tips(i18n.t('tipsMsg.forward') + _num + i18n.t('tipsMsg.seconds'));
  2306. }
  2307. } else {
  2308. if (taskConf.subtractCurrentTime) {
  2309. TCC.doTask('subtractCurrentTime');
  2310. } else {
  2311. player.currentTime -= _num;
  2312. !notips && t.tips(i18n.t('tipsMsg.backward') + _num + i18n.t('tipsMsg.seconds'));
  2313. }
  2314. }
  2315. },
  2316. /* 设置声音大小 */
  2317. setVolume: function (num) {
  2318. if (!num) return
  2319. const t = this;
  2320. const player = t.player();
  2321.  
  2322. num = Number(num);
  2323. const _num = Math.abs(Number(num.toFixed(2)));
  2324. const curVol = player.volume;
  2325. let newVol = curVol;
  2326.  
  2327. if (num > 0) {
  2328. newVol += _num;
  2329. if (newVol > 1) {
  2330. newVol = 1;
  2331. }
  2332. } else {
  2333. newVol -= _num;
  2334. if (newVol < 0) {
  2335. newVol = 0;
  2336. }
  2337. }
  2338.  
  2339. player.volume = newVol;
  2340.  
  2341. /* 条件音量的时候顺便把静音模式关闭 */
  2342. player.muted = false;
  2343.  
  2344. t.tips(i18n.t('tipsMsg.volume') + parseInt(player.volume * 100) + '%');
  2345. },
  2346.  
  2347. /* 设置视频画面的缩放与位移 */
  2348. setTransform (scale, translate) {
  2349. const t = this;
  2350. const player = t.player();
  2351. scale = t.scale = typeof scale === 'undefined' ? t.scale : Number(scale).toFixed(1);
  2352. translate = t.translate = translate || t.translate;
  2353. player.style.transform = `scale(${scale}) translate(${translate.x}px, ${translate.y}px) rotate(${t.rotate}deg)`;
  2354. let tipsMsg = i18n.t('tipsMsg.videozoom') + `${scale * 100}%`;
  2355. if (translate.x) {
  2356. tipsMsg += ` ${i18n.t('tipsMsg.horizontal')}${t.translate.x}px`;
  2357. }
  2358. if (translate.y) {
  2359. tipsMsg += ` ${i18n.t('tipsMsg.vertical')}${t.translate.y}px`;
  2360. }
  2361. t.tips(tipsMsg);
  2362. },
  2363.  
  2364. /**
  2365. * 定格帧画面
  2366. * @param perFps {Number} -可选 默认 1,即定格到下一帧,如果是-1则为定格到上一帧
  2367. */
  2368. freezeFrame (perFps) {
  2369. perFps = perFps || 1;
  2370. const t = this;
  2371. const player = t.player();
  2372.  
  2373. /* 跳帧 */
  2374. player.currentTime += Number(perFps / t.fps);
  2375.  
  2376. /* 定格画面 */
  2377. if (!player.paused) player.pause();
  2378.  
  2379. /* 有些播放器发现画面所在位置变了会自动进行播放,所以此时需要对播放操作进行挂起 */
  2380. player._hangUp_ && player._hangUp_('play', 400);
  2381.  
  2382. if (perFps === 1) {
  2383. t.tips(i18n.t('tipsMsg.nextframe'));
  2384. } else if (perFps === -1) {
  2385. t.tips(i18n.t('tipsMsg.previousframe'));
  2386. } else {
  2387. t.tips(i18n.t('tipsMsg.stopframe') + perFps);
  2388. }
  2389. },
  2390.  
  2391. /**
  2392. * 切换画中画功能
  2393. */
  2394. togglePictureInPicture () {
  2395. const player = this.player();
  2396. if (window._isPictureInPicture_) {
  2397. document.exitPictureInPicture().then(() => {
  2398. window._isPictureInPicture_ = null;
  2399. }).catch(() => {
  2400. window._isPictureInPicture_ = null;
  2401. });
  2402. } else {
  2403. player.requestPictureInPicture && player.requestPictureInPicture().then(() => {
  2404. window._isPictureInPicture_ = true;
  2405. }).catch(() => {
  2406. window._isPictureInPicture_ = null;
  2407. });
  2408. }
  2409. },
  2410.  
  2411. /* 播放下一个视频,默认是没有这个功能的,只有在TCC里配置了next字段才会有该功能 */
  2412. setNextVideo () {
  2413. const isDo = TCC.doTask('next');
  2414. if (!isDo) {
  2415. debug.log('当前网页不支持一键播放下个视频功能~');
  2416. }
  2417. },
  2418.  
  2419. setFakeUA (ua) {
  2420. ua = ua || userAgentMap.iPhone.safari;
  2421.  
  2422. /* 记录设定的ua信息 */
  2423. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_user_agent_', ua);
  2424. fakeUA(ua);
  2425. },
  2426.  
  2427. /* ua伪装切换开关 */
  2428. switchFakeUA (ua) {
  2429. const customUA = isInCrossOriginFrame() ? null : window.localStorage.getItem('_h5_player_user_agent_');
  2430. if (customUA) {
  2431. !isInCrossOriginFrame() && window.localStorage.removeItem('_h5_player_user_agent_');
  2432. } else {
  2433. this.setFakeUA(ua);
  2434. }
  2435.  
  2436. debug.log('ua', navigator.userAgent);
  2437. },
  2438.  
  2439. /* 切换播放状态 */
  2440. switchPlayStatus () {
  2441. const t = this;
  2442. const player = t.player();
  2443. const taskConf = TCC.getTaskConfig();
  2444. if (taskConf.switchPlayStatus) {
  2445. TCC.doTask('switchPlayStatus');
  2446. return
  2447. }
  2448.  
  2449. if (player.paused) {
  2450. if (taskConf.play) {
  2451. TCC.doTask('play');
  2452. } else {
  2453. player.play();
  2454. t.tips(i18n.t('tipsMsg.play'));
  2455. }
  2456. } else {
  2457. if (taskConf.pause) {
  2458. TCC.doTask('pause');
  2459. } else {
  2460. player.pause();
  2461. t.tips(i18n.t('tipsMsg.pause'));
  2462. }
  2463. }
  2464. },
  2465.  
  2466. isAllowRestorePlayProgress: function () {
  2467. const keyName = '_allowRestorePlayProgress_' + window.location.host;
  2468. const allowRestorePlayProgressVal = window.GM_getValue(keyName);
  2469. return !allowRestorePlayProgressVal || allowRestorePlayProgressVal === 'true'
  2470. },
  2471. /* 切换自动恢复播放进度的状态 */
  2472. switchRestorePlayProgressStatus: function () {
  2473. const t = h5Player;
  2474. let isAllowRestorePlayProgress = t.isAllowRestorePlayProgress();
  2475. /* 进行值反转 */
  2476. isAllowRestorePlayProgress = !isAllowRestorePlayProgress;
  2477. const keyName = '_allowRestorePlayProgress_' + window.location.host;
  2478. window.GM_setValue(keyName, String(isAllowRestorePlayProgress));
  2479.  
  2480. /* 操作提示 */
  2481. if (isAllowRestorePlayProgress) {
  2482. t.tips(i18n.t('tipsMsg.arpl'));
  2483. t.setPlayProgress(t.player());
  2484. } else {
  2485. t.tips(i18n.t('tipsMsg.drpl'));
  2486. }
  2487. },
  2488. tipsClassName: 'html_player_enhance_tips',
  2489. getTipsContainer: function () {
  2490. const t = h5Player;
  2491. const player = t.player();
  2492. // 使用getContainer获取到的父节点弊端太多,暂时弃用
  2493. // const _tispContainer_ = player._tispContainer_ || getContainer(player);
  2494.  
  2495. let tispContainer = player._tispContainer_ || player.parentNode;
  2496. /* 如果父节点为无长宽的元素,则再往上查找一级 */
  2497. const containerBox = tispContainer.getBoundingClientRect();
  2498. if ((!containerBox.width || !containerBox.height) && tispContainer.parentNode) {
  2499. tispContainer = tispContainer.parentNode;
  2500. }
  2501.  
  2502. if (!player._tispContainer_) { player._tispContainer_ = tispContainer; }
  2503. return tispContainer
  2504. },
  2505. tips: function (str) {
  2506. const t = h5Player;
  2507. const player = t.player();
  2508. if (!player) {
  2509. debug.log('h5Player Tips:', str);
  2510. return true
  2511. }
  2512.  
  2513. const parentNode = t.getTipsContainer();
  2514.  
  2515. // 修复部分提示按钮位置异常问题
  2516. const defStyle = parentNode.getAttribute('style') || '';
  2517. let backupStyle = parentNode.getAttribute('style-backup') || '';
  2518. if (!backupStyle) {
  2519. parentNode.setAttribute('style-backup', defStyle || 'style-backup:none');
  2520. backupStyle = defStyle;
  2521. }
  2522.  
  2523. const newStyleArr = backupStyle.split(';');
  2524.  
  2525. const oldPosition = parentNode.getAttribute('def-position') || window.getComputedStyle(parentNode).position;
  2526. if (parentNode.getAttribute('def-position') === null) {
  2527. parentNode.setAttribute('def-position', oldPosition || '');
  2528. }
  2529. if (['static', 'inherit', 'initial', 'unset', ''].includes(oldPosition)) {
  2530. newStyleArr.push('position: relative');
  2531. }
  2532.  
  2533. const playerBox = player.getBoundingClientRect();
  2534. const parentNodeBox = parentNode.getBoundingClientRect();
  2535. /* 不存在高宽时,给包裹节点一个最小高宽,才能保证提示能正常显示 */
  2536. if (!parentNodeBox.width || !parentNodeBox.height) {
  2537. newStyleArr.push('min-width:' + playerBox.width + 'px');
  2538. newStyleArr.push('min-height:' + playerBox.height + 'px');
  2539. }
  2540.  
  2541. parentNode.setAttribute('style', newStyleArr.join(';'));
  2542.  
  2543. const tipsSelector = '.' + t.tipsClassName;
  2544. let tipsDom = parentNode.querySelector(tipsSelector);
  2545.  
  2546. /* 提示dom未初始化的,则进行初始化 */
  2547. if (!tipsDom) {
  2548. t.initTips();
  2549. tipsDom = parentNode.querySelector(tipsSelector);
  2550. if (!tipsDom) {
  2551. debug.log('init h5player tips dom error...');
  2552. return false
  2553. }
  2554. }
  2555.  
  2556. const style = tipsDom.style;
  2557. tipsDom.innerText = str;
  2558.  
  2559. for (var i = 0; i < 3; i++) {
  2560. if (this.on_off[i]) clearTimeout(this.on_off[i]);
  2561. }
  2562.  
  2563. function showTips () {
  2564. style.display = 'block';
  2565. t.on_off[0] = setTimeout(function () {
  2566. style.opacity = 1;
  2567. }, 50);
  2568. t.on_off[1] = setTimeout(function () {
  2569. // 隐藏提示框和还原样式
  2570. style.opacity = 0;
  2571. style.display = 'none';
  2572. if (backupStyle && backupStyle !== 'style-backup:none') {
  2573. parentNode.setAttribute('style', backupStyle);
  2574. }
  2575. }, 2000);
  2576. }
  2577.  
  2578. if (style.display === 'block') {
  2579. style.display = 'none';
  2580. clearTimeout(this.on_off[3]);
  2581. t.on_off[2] = setTimeout(function () {
  2582. showTips();
  2583. }, 100);
  2584. } else {
  2585. showTips();
  2586. }
  2587. },
  2588. /* 设置提示DOM的样式 */
  2589. initTips: function () {
  2590. const t = h5Player;
  2591. const parentNode = t.getTipsContainer();
  2592. if (parentNode.querySelector('.' + t.tipsClassName)) return
  2593.  
  2594. // top: 50%;
  2595. // left: 50%;
  2596. // transform: translate(-50%,-50%);
  2597. const tipsStyle = `
  2598. position: absolute;
  2599. z-index: 999999;
  2600. font-size: ${t.fontSize || 16}px;
  2601. padding: 5px 10px;
  2602. background: rgba(0,0,0,0.4);
  2603. color:white;
  2604. top: 0;
  2605. left: 0;
  2606. transition: all 500ms ease;
  2607. opacity: 0;
  2608. border-bottom-right-radius: 5px;
  2609. display: none;
  2610. -webkit-font-smoothing: subpixel-antialiased;
  2611. font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
  2612. -webkit-user-select: none;
  2613. `;
  2614. const tips = document.createElement('div');
  2615. tips.setAttribute('style', tipsStyle);
  2616. tips.setAttribute('class', t.tipsClassName);
  2617. parentNode.appendChild(tips);
  2618. },
  2619. on_off: new Array(3),
  2620. rotate: 0,
  2621. fps: 30,
  2622. /* 滤镜效果 */
  2623. filter: {
  2624. key: [1, 1, 1, 0, 0],
  2625. setup: function () {
  2626. var view = 'brightness({0}) contrast({1}) saturate({2}) hue-rotate({3}deg) blur({4}px)';
  2627. for (var i = 0; i < 5; i++) {
  2628. view = view.replace('{' + i + '}', String(this.key[i]));
  2629. this.key[i] = Number(this.key[i]);
  2630. }
  2631. h5Player.player().style.filter = view;
  2632. },
  2633. reset: function () {
  2634. this.key[0] = 1;
  2635. this.key[1] = 1;
  2636. this.key[2] = 1;
  2637. this.key[3] = 0;
  2638. this.key[4] = 0;
  2639. this.setup();
  2640. }
  2641. },
  2642. _isFoucs: false,
  2643.  
  2644. /* 播放器的聚焦事件 */
  2645. isFoucs: function () {
  2646. const t = h5Player;
  2647. const player = t.player();
  2648. if (!player) return
  2649.  
  2650. player.onmouseenter = function (e) {
  2651. h5Player._isFoucs = true;
  2652. };
  2653. player.onmouseleave = function (e) {
  2654. h5Player._isFoucs = false;
  2655. };
  2656. },
  2657. /* 播放器事件响应器 */
  2658. palyerTrigger: function (player, event) {
  2659. if (!player || !event) return
  2660. const t = h5Player;
  2661. const keyCode = event.keyCode;
  2662. const key = event.key.toLowerCase();
  2663.  
  2664. if (event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
  2665. // 网页全屏
  2666. if (key === 'enter') {
  2667. t.setWebFullScreen();
  2668. }
  2669.  
  2670. // 进入或退出画中画模式
  2671. if (key === 'p') {
  2672. t.togglePictureInPicture();
  2673. }
  2674.  
  2675. // 截图并下载保存
  2676. if (key === 's') {
  2677. videoCapturer.capture(player, true);
  2678. }
  2679.  
  2680. if (key === 'r') {
  2681. t.switchRestorePlayProgressStatus();
  2682. }
  2683.  
  2684. // 视频画面缩放相关事件
  2685. const allowKeys = ['x', 'c', 'z', 'arrowright', 'arrowleft', 'arrowup', 'arrowdown'];
  2686. if (!allowKeys.includes(key)) return
  2687.  
  2688. t.scale = Number(t.scale);
  2689. switch (key) {
  2690. // shift+X:视频缩小 -0.1
  2691. case 'x' :
  2692. t.scale -= 0.1;
  2693. break
  2694. // shift+C:视频放大 +0.1
  2695. case 'c' :
  2696. t.scale += 0.1;
  2697. break
  2698. // shift+Z:视频恢复正常大小
  2699. case 'z' :
  2700. t.scale = 1;
  2701. t.translate = { x: 0, y: 0 };
  2702. break
  2703. case 'arrowright' :
  2704. t.translate.x += 10;
  2705. break
  2706. case 'arrowleft' :
  2707. t.translate.x -= 10;
  2708. break
  2709. case 'arrowup' :
  2710. t.translate.y -= 10;
  2711. break
  2712. case 'arrowdown' :
  2713. t.translate.y += 10;
  2714. break
  2715. }
  2716. t.setTransform(t.scale, t.translate);
  2717.  
  2718. // 阻止事件冒泡
  2719. event.stopPropagation();
  2720. event.preventDefault();
  2721. return true
  2722. }
  2723.  
  2724. // ctrl+方向键右→:快进30秒
  2725. if (event.ctrlKey && keyCode === 39) {
  2726. t.setCurrentTime(t.skipStep * 6);
  2727. }
  2728. // ctrl+方向键左←:后退30秒
  2729. if (event.ctrlKey && keyCode === 37) {
  2730. t.setCurrentTime(-t.skipStep * 6);
  2731. }
  2732.  
  2733. // ctrl+方向键上↑:音量升高 20%
  2734. if (event.ctrlKey && keyCode === 38) {
  2735. t.setVolume(0.2);
  2736. }
  2737. // 方向键下↓:音量降低 20%
  2738. if (event.ctrlKey && keyCode === 40) {
  2739. t.setVolume(-0.2);
  2740. }
  2741.  
  2742. // 防止其它无关组合键冲突
  2743. if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) return
  2744.  
  2745. // 方向键右→:快进5秒
  2746. if (keyCode === 39) {
  2747. t.setCurrentTime(t.skipStep);
  2748. }
  2749. // 方向键左←:后退5秒
  2750. if (keyCode === 37) {
  2751. t.setCurrentTime(-t.skipStep);
  2752. }
  2753.  
  2754. // 方向键上↑:音量升高 10%
  2755. if (keyCode === 38) {
  2756. t.setVolume(0.1);
  2757. }
  2758. // 方向键下↓:音量降低 10%
  2759. if (keyCode === 40) {
  2760. t.setVolume(-0.1);
  2761. }
  2762.  
  2763. // 空格键:暂停/播放
  2764. if (keyCode === 32) {
  2765. t.switchPlayStatus();
  2766. }
  2767.  
  2768. // 按键X:减速播放 -0.1
  2769. if (keyCode === 88) {
  2770. t.setPlaybackRate(player.playbackRate - 0.1);
  2771. }
  2772. // 按键C:加速播放 +0.1
  2773. if (keyCode === 67) {
  2774. t.setPlaybackRate(player.playbackRate + 0.1);
  2775. }
  2776. // 按键Z:正常速度播放
  2777. if (keyCode === 90) {
  2778. t.resetPlaybackRate();
  2779. }
  2780.  
  2781. // 按1-4设置播放速度 49-52;97-100
  2782. if ((keyCode >= 49 && keyCode <= 52) || (keyCode >= 97 && keyCode <= 100)) {
  2783. t.setPlaybackRate(event.key);
  2784. }
  2785.  
  2786. // 按键F:下一帧
  2787. if (keyCode === 70) {
  2788. if (window.location.hostname === 'www.netflix.com') {
  2789. /* netflix 的F键是全屏的意思 */
  2790. return
  2791. }
  2792. t.freezeFrame(1);
  2793. }
  2794. // 按键D:上一帧
  2795. if (keyCode === 68) {
  2796. t.freezeFrame(-1);
  2797. }
  2798.  
  2799. // 按键E:亮度增加%
  2800. if (keyCode === 69) {
  2801. t.filter.key[0] += 0.1;
  2802. t.filter.key[0] = t.filter.key[0].toFixed(2);
  2803. t.filter.setup();
  2804. t.tips(i18n.t('tipsMsg.brightness') + parseInt(t.filter.key[0] * 100) + '%');
  2805. }
  2806. // 按键W:亮度减少%
  2807. if (keyCode === 87) {
  2808. if (t.filter.key[0] > 0) {
  2809. t.filter.key[0] -= 0.1;
  2810. t.filter.key[0] = t.filter.key[0].toFixed(2);
  2811. t.filter.setup();
  2812. }
  2813. t.tips(i18n.t('tipsMsg.brightness') + parseInt(t.filter.key[0] * 100) + '%');
  2814. }
  2815.  
  2816. // 按键T:对比度增加%
  2817. if (keyCode === 84) {
  2818. t.filter.key[1] += 0.1;
  2819. t.filter.key[1] = t.filter.key[1].toFixed(2);
  2820. t.filter.setup();
  2821. t.tips(i18n.t('tipsMsg.contrast') + parseInt(t.filter.key[1] * 100) + '%');
  2822. }
  2823. // 按键R:对比度减少%
  2824. if (keyCode === 82) {
  2825. if (t.filter.key[1] > 0) {
  2826. t.filter.key[1] -= 0.1;
  2827. t.filter.key[1] = t.filter.key[1].toFixed(2);
  2828. t.filter.setup();
  2829. }
  2830. t.tips(i18n.t('tipsMsg.contrast') + parseInt(t.filter.key[1] * 100) + '%');
  2831. }
  2832.  
  2833. // 按键U:饱和度增加%
  2834. if (keyCode === 85) {
  2835. t.filter.key[2] += 0.1;
  2836. t.filter.key[2] = t.filter.key[2].toFixed(2);
  2837. t.filter.setup();
  2838. t.tips(i18n.t('tipsMsg.saturation') + parseInt(t.filter.key[2] * 100) + '%');
  2839. }
  2840. // 按键Y:饱和度减少%
  2841. if (keyCode === 89) {
  2842. if (t.filter.key[2] > 0) {
  2843. t.filter.key[2] -= 0.1;
  2844. t.filter.key[2] = t.filter.key[2].toFixed(2);
  2845. t.filter.setup();
  2846. }
  2847. t.tips(i18n.t('tipsMsg.saturation') + parseInt(t.filter.key[2] * 100) + '%');
  2848. }
  2849.  
  2850. // 按键O:色相增加 1 度
  2851. if (keyCode === 79) {
  2852. t.filter.key[3] += 1;
  2853. t.filter.setup();
  2854. t.tips(i18n.t('tipsMsg.hue') + t.filter.key[3] + '度');
  2855. }
  2856. // 按键I:色相减少 1 度
  2857. if (keyCode === 73) {
  2858. t.filter.key[3] -= 1;
  2859. t.filter.setup();
  2860. t.tips(i18n.t('tipsMsg.hue') + t.filter.key[3] + '度');
  2861. }
  2862.  
  2863. // 按键K:模糊增加 1 px
  2864. if (keyCode === 75) {
  2865. t.filter.key[4] += 1;
  2866. t.filter.setup();
  2867. t.tips(i18n.t('tipsMsg.blur') + t.filter.key[4] + 'PX');
  2868. }
  2869. // 按键J:模糊减少 1 px
  2870. if (keyCode === 74) {
  2871. if (t.filter.key[4] > 0) {
  2872. t.filter.key[4] -= 1;
  2873. t.filter.setup();
  2874. }
  2875. t.tips(i18n.t('tipsMsg.blur') + t.filter.key[4] + 'PX');
  2876. }
  2877.  
  2878. // 按键Q:图像复位
  2879. if (keyCode === 81) {
  2880. t.filter.reset();
  2881. t.tips(i18n.t('tipsMsg.imgattrreset'));
  2882. }
  2883.  
  2884. // 按键S:画面旋转 90 度
  2885. if (keyCode === 83) {
  2886. t.rotate += 90;
  2887. if (t.rotate % 360 === 0) t.rotate = 0;
  2888. player.style.transform = `scale(${t.scale}) translate(${t.translate.x}px, ${t.translate.y}px) rotate( ${t.rotate}deg)`;
  2889. t.tips(i18n.t('tipsMsg.imgrotate') + t.rotate + '°');
  2890. }
  2891.  
  2892. // 按键回车,进入全屏
  2893. if (keyCode === 13) {
  2894. const isDo = TCC.doTask('fullScreen');
  2895. if (!isDo && player._fullScreen_) {
  2896. player._fullScreen_.toggle();
  2897. }
  2898. }
  2899.  
  2900. if (key === 'n') {
  2901. t.setNextVideo();
  2902. }
  2903.  
  2904. // 阻止事件冒泡
  2905. event.stopPropagation();
  2906. event.preventDefault();
  2907. return true
  2908. },
  2909.  
  2910. /* 运行自定义的快捷键操作,如果运行了会返回true */
  2911. runCustomShortcuts: function (player, event) {
  2912. if (!player || !event) return
  2913. const key = event.key.toLowerCase();
  2914. const taskConf = TCC.getTaskConfig();
  2915. const confIsCorrect = isObj(taskConf.shortcuts) &&
  2916. Array.isArray(taskConf.shortcuts.register) &&
  2917. taskConf.shortcuts.callback instanceof Function;
  2918.  
  2919. /* 判断当前触发的快捷键是否已被注册 */
  2920. function isRegister () {
  2921. const list = taskConf.shortcuts.register;
  2922.  
  2923. /* 当前触发的组合键 */
  2924. const combineKey = [];
  2925. if (event.ctrlKey) {
  2926. combineKey.push('ctrl');
  2927. }
  2928. if (event.shiftKey) {
  2929. combineKey.push('shift');
  2930. }
  2931. if (event.altKey) {
  2932. combineKey.push('alt');
  2933. }
  2934. if (event.metaKey) {
  2935. combineKey.push('command');
  2936. }
  2937.  
  2938. combineKey.push(key);
  2939.  
  2940. /* 通过循环判断当前触发的组合键和已注册的组合键是否完全一致 */
  2941. let hasReg = false;
  2942. list.forEach((shortcut) => {
  2943. const regKey = shortcut.split('+');
  2944. if (combineKey.length === regKey.length) {
  2945. let allMatch = true;
  2946. regKey.forEach((key) => {
  2947. if (!combineKey.includes(key)) {
  2948. allMatch = false;
  2949. }
  2950. });
  2951. if (allMatch) {
  2952. hasReg = true;
  2953. }
  2954. }
  2955. });
  2956.  
  2957. return hasReg
  2958. }
  2959.  
  2960. if (confIsCorrect && isRegister()) {
  2961. // 执行自定义快捷键操作
  2962. const isDo = TCC.doTask('shortcuts', {
  2963. event,
  2964. player,
  2965. h5Player
  2966. });
  2967.  
  2968. if (isDo) {
  2969. event.stopPropagation();
  2970. event.preventDefault();
  2971. }
  2972.  
  2973. return isDo
  2974. } else {
  2975. return false
  2976. }
  2977. },
  2978.  
  2979. /* 按键响应方法 */
  2980. keydownEvent: function (event) {
  2981. const t = h5Player;
  2982. const keyCode = event.keyCode;
  2983. // const key = event.key.toLowerCase()
  2984. const player = t.player();
  2985.  
  2986. /* 处于可编辑元素中不执行任何快捷键 */
  2987. if (isEditableTarget(event.target)) return
  2988.  
  2989. /* shift+f 切换UA伪装 */
  2990. if (event.shiftKey && keyCode === 70) {
  2991. t.switchFakeUA();
  2992. }
  2993.  
  2994. /* 未用到的按键不进行任何事件监听 */
  2995. if (!isRegisterKey(event)) return
  2996.  
  2997. /* 广播按键消息,进行跨域控制 */
  2998. monkeyMsg.send('globalKeydownEvent', event);
  2999.  
  3000. if (!player) {
  3001. // debug.log('无可用的播放,不执行相关操作')
  3002. return
  3003. }
  3004.  
  3005. /* 切换插件的可用状态 */
  3006. if (event.ctrlKey && keyCode === 32) {
  3007. t.enable = !t.enable;
  3008. if (t.enable) {
  3009. t.tips(i18n.t('tipsMsg.onplugin'));
  3010. } else {
  3011. t.tips(i18n.t('tipsMsg.offplugin'));
  3012. }
  3013. }
  3014.  
  3015. if (!t.enable) {
  3016. debug.log('h5Player 已禁用~');
  3017. return false
  3018. }
  3019.  
  3020. // 按ctrl+\ 键进入聚焦或取消聚焦状态,用于视频标签被遮挡的场景
  3021. if (event.ctrlKey && keyCode === 220) {
  3022. t.globalMode = !t.globalMode;
  3023. if (t.globalMode) {
  3024. t.tips(i18n.t('tipsMsg.globalmode') + ' ON');
  3025. } else {
  3026. t.tips(i18n.t('tipsMsg.globalmode') + ' OFF');
  3027. }
  3028. }
  3029.  
  3030. /* 非全局模式下,不聚焦则不执行快捷键的操作 */
  3031. if (!t.globalMode && !t._isFoucs) return
  3032.  
  3033. /* 判断是否执行了自定义快捷键操作,如果是则不再响应后面默认定义操作 */
  3034. if (t.runCustomShortcuts(player, event) === true) return
  3035.  
  3036. /* 响应播放器相关操作 */
  3037. t.palyerTrigger(player, event);
  3038. },
  3039.  
  3040. /**
  3041. * 获取播放进度
  3042. * @param player -可选 对应的h5 播放器对象, 如果不传,则获取到的是整个播放进度表,传则获取当前播放器的播放进度
  3043. */
  3044. getPlayProgress: function (player) {
  3045. let progressMap = isInCrossOriginFrame() ? null : window.localStorage.getItem('_h5_player_play_progress_');
  3046. if (!progressMap) {
  3047. progressMap = {};
  3048. } else {
  3049. try {
  3050. progressMap = JSON.parse(progressMap);
  3051. } catch (e) {
  3052. progressMap = {};
  3053. }
  3054. }
  3055.  
  3056. if (!player) {
  3057. return progressMap
  3058. } else {
  3059. let keyName = window.location.href || player.src;
  3060. keyName += player.duration;
  3061. if (progressMap[keyName]) {
  3062. return progressMap[keyName].progress
  3063. } else {
  3064. return player.currentTime
  3065. }
  3066. }
  3067. },
  3068. /* 播放进度记录器 */
  3069. playProgressRecorder: function (player) {
  3070. const t = h5Player;
  3071. clearTimeout(player._playProgressTimer_);
  3072. function recorder (player) {
  3073. player._playProgressTimer_ = setTimeout(function () {
  3074. if (!t.isAllowRestorePlayProgress()) {
  3075. recorder(player);
  3076. return true
  3077. }
  3078.  
  3079. const progressMap = t.getPlayProgress() || {};
  3080. const list = Object.keys(progressMap);
  3081.  
  3082. let keyName = window.location.href || player.src;
  3083. keyName += player.duration;
  3084.  
  3085. /* 只保存最近10个视频的播放进度 */
  3086. if (list.length > 10) {
  3087. /* 根据更新的时间戳,取出最早添加播放进度的记录项 */
  3088. let timeList = [];
  3089. list.forEach(function (keyName) {
  3090. progressMap[keyName] && progressMap[keyName].t && timeList.push(progressMap[keyName].t);
  3091. });
  3092. timeList = quickSort(timeList);
  3093. const timestamp = timeList[0];
  3094.  
  3095. /* 删除最早添加的记录项 */
  3096. list.forEach(function (keyName) {
  3097. if (progressMap[keyName].t === timestamp) {
  3098. delete progressMap[keyName];
  3099. }
  3100. });
  3101. }
  3102.  
  3103. /* 记录当前播放进度 */
  3104. progressMap[keyName] = {
  3105. progress: player.currentTime,
  3106. t: new Date().getTime()
  3107. };
  3108.  
  3109. /* 存储播放进度表 */
  3110. !isInCrossOriginFrame() && window.localStorage.setItem('_h5_player_play_progress_', JSON.stringify(progressMap));
  3111.  
  3112. /* 循环侦听 */
  3113. recorder(player);
  3114. }, 1000 * 2);
  3115. }
  3116. recorder(player);
  3117. },
  3118. /* 设置播放进度 */
  3119. setPlayProgress: function (player, time) {
  3120. const t = h5Player;
  3121. if (!player) return
  3122.  
  3123. const curTime = Number(t.getPlayProgress(player));
  3124. if (!curTime || Number.isNaN(curTime)) return
  3125.  
  3126. if (t.isAllowRestorePlayProgress()) {
  3127. player.currentTime = curTime || player.currentTime;
  3128. if (curTime > 3) {
  3129. t.tips(i18n.t('tipsMsg.playbackrestored'));
  3130. }
  3131. } else {
  3132. t.tips(i18n.t('tipsMsg.playbackrestoreoff'));
  3133. }
  3134. },
  3135. /**
  3136. * 检测h5播放器是否存在
  3137. * @param callback
  3138. */
  3139. detecH5Player: function () {
  3140. const t = this;
  3141. const playerList = t.getPlayerList();
  3142.  
  3143. if (playerList.length) {
  3144. debug.log('检测到HTML5视频!');
  3145.  
  3146. /* 单video实例标签的情况 */
  3147. if (playerList.length === 1) {
  3148. t.playerInstance = playerList[0];
  3149. t.initPlayerInstance(true);
  3150. } else {
  3151. /* 多video实例标签的情况 */
  3152. playerList.forEach(function (player) {
  3153. /* 鼠标移到其上面的时候重新指定实例 */
  3154. if (player._hasMouseRedirectEvent_) return
  3155. player.addEventListener('mouseenter', function (event) {
  3156. t.playerInstance = event.target;
  3157. t.initPlayerInstance(false);
  3158. });
  3159. player._hasMouseRedirectEvent_ = true;
  3160.  
  3161. /* 播放器开始播放的时候重新指向实例 */
  3162. if (player._hasPlayingRedirectEvent_) return
  3163. player.addEventListener('playing', function (event) {
  3164. t.playerInstance = event.target;
  3165. t.initPlayerInstance(false);
  3166.  
  3167. /* 同步之前设定的播放速度 */
  3168. t.setPlaybackRate();
  3169. });
  3170. player._hasPlayingRedirectEvent_ = true;
  3171. });
  3172. }
  3173. }
  3174. },
  3175. /* 指定取消响应某些事件的列表 */
  3176. _hangUpPlayerEventList_: [],
  3177. /**
  3178. * 挂起播放器的某些事件,注意:挂起时间过长容易出现较多副作用
  3179. * @param eventType {String|Array} -必选 要挂起的事件类型,可以是单个事件也可以是多个事件
  3180. * @param timeout {Number} -可选 调用挂起事件函数后,多久后失效,恢复正常事件响应,默认200ms
  3181. */
  3182. hangUpPlayerEvent (eventType, timeout) {
  3183. const t = h5Player;
  3184. t._hangUpPlayerEventList_ = t._hangUpPlayerEventList_ || [];
  3185. eventType = Array.isArray(eventType) ? eventType : [eventType];
  3186. timeout = timeout || 200;
  3187.  
  3188. eventType.forEach(type => {
  3189. if (!t._hangUpPlayerEventList_.includes(type)) {
  3190. t._hangUpPlayerEventList_.push(type);
  3191. }
  3192. });
  3193.  
  3194. clearTimeout(t._hangUpPlayerEventTimer_);
  3195. t._hangUpPlayerEventTimer_ = setTimeout(function () {
  3196. const newList = [];
  3197. t._hangUpPlayerEventList_.forEach(cancelType => {
  3198. if (!eventType.includes(cancelType)) {
  3199. newList.push(cancelType);
  3200. }
  3201. });
  3202. t._hangUpPlayerEventList_ = newList;
  3203. }, timeout);
  3204. },
  3205. /**
  3206. * 播放器里的所有事件代理处理器
  3207. * @param target
  3208. * @param ctx
  3209. * @param args
  3210. * @param listenerArgs
  3211. */
  3212. playerEventHandler (target, ctx, args, listenerArgs) {
  3213. const t = h5Player;
  3214. const eventType = listenerArgs[0];
  3215.  
  3216. /* 取消对某些事件的响应 */
  3217. if (t._hangUpPlayerEventList_.includes(eventType) || t._hangUpPlayerEventList_.includes('all')) {
  3218. debug.log(`播放器[${eventType}]事件被取消`);
  3219. return false
  3220. }
  3221. },
  3222. /* 绑定相关事件 */
  3223. bindEvent: function () {
  3224. const t = this;
  3225. if (t._hasBindEvent_) return
  3226.  
  3227. document.removeEventListener('keydown', t.keydownEvent);
  3228. document.addEventListener('keydown', t.keydownEvent, true);
  3229.  
  3230. /* 兼容iframe操作 */
  3231. if (isInIframe() && !isInCrossOriginFrame()) {
  3232. window.top.document.removeEventListener('keydown', t.keydownEvent);
  3233. window.top.document.addEventListener('keydown', t.keydownEvent, true);
  3234. }
  3235.  
  3236. /* 响应来自按键消息的广播 */
  3237. monkeyMsg.on('globalKeydownEvent', async (name, oldVal, newVal, remote) => {
  3238. const tabId = await getTabId();
  3239. const triggerFakeEvent = throttle(function () {
  3240. /* 模拟触发快捷键事件,实现跨域、跨Tab控制 */
  3241. const player = t.player();
  3242. if (player) {
  3243. const fakeEvent = newVal.data;
  3244. fakeEvent.stopPropagation = () => {};
  3245. fakeEvent.preventDefault = () => {};
  3246. t.palyerTrigger(player, fakeEvent);
  3247. debug.log('模拟触发操作成功');
  3248. }
  3249. }, 80);
  3250.  
  3251. if (remote) {
  3252. if (isInCrossOriginFrame()) {
  3253. /**
  3254. * 同处跨域受限页面,且都处于可见状态,大概率处于同一个Tab标签里,但不是100%
  3255. * tabId一致则100%为同一标签下
  3256. */
  3257. if (newVal.tabId === tabId && document.visibilityState === 'visible') {
  3258. triggerFakeEvent();
  3259. }
  3260. } else if (crossTabCtl.hasOpenPictureInPicture() && document.pictureInPictureElement) {
  3261. /* 跨Tab控制画中画里面的视频播放 */
  3262. if (tabId !== newVal.tabId) {
  3263. triggerFakeEvent();
  3264. debug.log('已接收到跨Tab按键控制信息:', newVal);
  3265. }
  3266. }
  3267. }
  3268. });
  3269.  
  3270. t._hasBindEvent_ = true;
  3271. },
  3272. init: function (global) {
  3273. var t = this;
  3274. if (global) {
  3275. /* 绑定键盘事件 */
  3276. t.bindEvent();
  3277.  
  3278. /**
  3279. * 判断是否需要进行ua伪装
  3280. * 下面方案暂时不可用
  3281. * 由于部分网站跳转至移动端后域名不一致,形成跨域问题
  3282. * 导致无法同步伪装配置而不断死循环跳转
  3283. * eg. open.163.com
  3284. * */
  3285. // let customUA = window.localStorage.getItem('_h5_player_user_agent_')
  3286. // debug.log(customUA, window.location.href, window.navigator.userAgent, document.referrer)
  3287. // if (customUA) {
  3288. // t.setFakeUA(customUA)
  3289. // alert(customUA)
  3290. // } else {
  3291. // alert('ua false')
  3292. // }
  3293.  
  3294. /* 对配置了ua伪装的域名进行伪装 */
  3295. const host = window.location.host;
  3296. if (fakeConfig[host]) {
  3297. t.setFakeUA(fakeConfig[host]);
  3298. }
  3299. } else {
  3300. /* 检测是否存在H5播放器 */
  3301. t.detecH5Player();
  3302. }
  3303. },
  3304. load: false
  3305. };
  3306.  
  3307. /* 初始化任务配置中心 */
  3308. TCC = h5PlayerTccInit(h5Player);
  3309.  
  3310. try {
  3311. /* 初始化全局所需的相关方法 */
  3312. h5Player.init(true);
  3313.  
  3314. /* 检测到有视频标签就进行初始化 */
  3315. ready('video', function () {
  3316. h5Player.init();
  3317. });
  3318.  
  3319. /* 检测shadow dom 下面的video */
  3320. document.addEventListener('addShadowRoot', function (e) {
  3321. const shadowRoot = e.detail.shadowRoot;
  3322. ready('video', function (element) {
  3323. h5Player.init();
  3324. }, shadowRoot);
  3325. });
  3326.  
  3327. if (isInCrossOriginFrame()) {
  3328. debug.log('当前处于跨域受限的Iframe中,h5Player相关功能可能无法正常开启');
  3329. }
  3330.  
  3331. /* 初始化跨Tab控制逻辑 */
  3332. crossTabCtl.init();
  3333. } catch (e) {
  3334. debug.error(e);
  3335. }
  3336.  
  3337. // h5playerUi.init()
  3338.  
  3339. // debugCode.init(h5Player)
  3340.  
  3341. // document.addEventListener('visibilitychange', function () {
  3342. // if (!document.hidden) {
  3343. // h5Player.initAutoPlay()
  3344. // }
  3345. // })
  3346. })();