🕭✇HTML5 videoplayer enhanced +AUTOPROTECT v3'7'9-patch

Video enhancement script with no annoyances and security issues/

  1. // ==UserScript==
  2. // @name 🕭✇HTML5 videoplayer enhanced +AUTOPROTECT v3'7'9-patch
  3. // @name:zh 🕭✇HTML5视频播放器增强脚本
  4. // @name:zh-TW 🕭✇HTML5視頻播放器增強腳本
  5. // @namespace https://github.com/xxxily/h5player
  6. // @homepage https://github.com/xxxily/h5player
  7. // @version 3.7.9-patch3.3
  8. // @description Video enhancement script with no annoyances and security issues/
  9. // @description:zh 视频增强脚本,支持所有H5视频网站/
  10. // @description:zh-TW 視頻增強腳本,支持所有H5視頻網站/
  11. // @author ankvps, SAPIENT, PERDEXA
  12. // @icon https://www.vdocipher.com/blog/wp-content/uploads/2020/09/VIDEO-ENCRYPTION.png
  13. // @match *://*/*
  14. // @exclude *://yiyan.baidu.com/*
  15. // @exclude *://*.art
  16. // @exclude *://*.ai
  17. // @exclude *://*.bing.com/search*
  18. // @grant unsafeWindow
  19. // @grant GM_addStyle
  20. // @grant GM_setValue
  21. // @grant GM_getValue
  22. // @grant GM_deleteValue
  23. // @grant GM_listValues
  24. // @grant GM_addValueChangeListener
  25. // @grant GM_removeValueChangeListener
  26. // @grant GM_registerMenuCommand
  27. // @grant GM_unregisterMenuCommand
  28. // @grant GM_getTab
  29. // @grant GM_saveTab
  30. // @grant GM_getTabs
  31. // @grant GM_openInTab
  32. // @grant GM_setClipboard
  33. // @run-at document-start
  34. // @license GPLv3
  35. // ==UserScript==
  36. // ...
  37. // ==/UserScript==
  38.  
  39. (function (w) {
  40. if (w) {
  41. w.name = 'h5player';
  42. }
  43. })();
  44.  
  45. /**
  46. * 元素监听器
  47. * @param selector -必选
  48. * @param fn -必选,元素存在时的回调
  49. * @param shadowRoot -可选 指定监听某个shadowRoot下面的DOM元素
  50. * 参考:https://javascript.ruanyifeng.com/dom/mutationobserver.html
  51. */
  52. function ready (selector, fn, shadowRoot) {
  53. const win = window;
  54. const docRoot = shadowRoot || win.document.documentElement;
  55. if (!docRoot) return false
  56. const MutationObserver = win.MutationObserver || win.WebKitMutationObserver;
  57. const listeners = docRoot._MutationListeners || [];
  58.  
  59. function $ready (selector, fn) {
  60. // 储存选择器和回调函数
  61. listeners.push({
  62. selector: selector,
  63. fn: fn
  64. });
  65.  
  66. /* 增加监听对象 */
  67. if (!docRoot._MutationListeners || !docRoot._MutationObserver) {
  68. docRoot._MutationListeners = listeners;
  69. docRoot._MutationObserver = new MutationObserver(() => {
  70. for (let i = 0; i < docRoot._MutationListeners.length; i++) {
  71. const item = docRoot._MutationListeners[i];
  72. check(item.selector, item.fn);
  73. }
  74. });
  75.  
  76. docRoot._MutationObserver.observe(docRoot, {
  77. childList: true,
  78. subtree: true
  79. });
  80. }
  81.  
  82. // 检查节点是否已经在DOM中
  83. check(selector, fn);
  84. }
  85.  
  86. function check (selector, fn) {
  87. const elements = docRoot.querySelectorAll(selector);
  88. for (let i = 0; i < elements.length; i++) {
  89. const element = elements[i];
  90. element._MutationReadyList_ = element._MutationReadyList_ || [];
  91. if (!element._MutationReadyList_.includes(fn)) {
  92. element._MutationReadyList_.push(fn);
  93. fn.call(element, element);
  94. }
  95. }
  96. }
  97.  
  98. const selectorArr = Array.isArray(selector) ? selector : [selector];
  99. selectorArr.forEach(selector => $ready(selector, fn));
  100. }
  101.  
  102. // Check if there's a video or stream in the page
  103. window.addEventListener('DOMContentLoaded', (event) => {
  104. if (document.querySelector('video')) {
  105. // If a video or stream is found, continue with the script
  106. // ... rest of the code
  107. } else {
  108. console.log("No video or stream found on the page.");
  109. }
  110. });
  111.  
  112. /** * 某些网页用了attachShadow closed mode,需要open才能获取video标签,例如百度云盘
  113. * 解决参考:
  114. * https://developers.google.com/web/fundamentals/web-components/shadowdom?hl=zh-cn#closed
  115. * https://stackoverflow.com/questions/54954383/override-element-prototype-attachshadow-using-chrome-extension
  116. */
  117. function hackAttachShadow () {
  118. if (window._hasHackAttachShadow_) return
  119. try {
  120. window._shadowDomList_ = [];
  121. window.Element.prototype._attachShadow = window.Element.prototype.attachShadow;
  122. window.Element.prototype.attachShadow = function () {
  123. const arg = arguments;
  124. if (arg[0] && arg[0].mode) {
  125. // 强制使用 open mode
  126. arg[0].mode = 'open';
  127. }
  128. const shadowRoot = this._attachShadow.apply(this, arg);
  129. // 存一份shadowDomList
  130. window._shadowDomList_.push(shadowRoot);
  131.  
  132. /* 让shadowRoot里面的元素有机会访问shadowHost */
  133. shadowRoot._shadowHost = this;
  134.  
  135. // 在document下面添加 addShadowRoot 自定义事件
  136. const shadowEvent = new window.CustomEvent('addShadowRoot', {
  137. shadowRoot,
  138. detail: {
  139. shadowRoot,
  140. message: 'addShadowRoot',
  141. time: new Date()
  142. },
  143. bubbles: true,
  144. cancelable: true
  145. });
  146. document.dispatchEvent(shadowEvent);
  147.  
  148. return shadowRoot
  149. };
  150. window._hasHackAttachShadow_ = true;
  151. } catch (e) {
  152. console.error('hackAttachShadow error by h5player plug-in', e);
  153. }
  154. }
  155.  
  156. /*!
  157. * @name original.js
  158. * @description 存储部分重要的原生函数,防止被外部污染,此逻辑应尽可能前置,否则存储的将是污染后的函数
  159. * @version 0.0.1
  160. * @author xxxily
  161. * @date 2022/10/16 10:32
  162. * @github https://github.com/xxxily
  163. */
  164.  
  165. const original = {
  166. // 防止defineProperty和defineProperties被AOP脚本重写
  167. Object: {
  168. defineProperty: Object.defineProperty,
  169. defineProperties: Object.defineProperties
  170. },
  171.  
  172. // 防止此类玩法:https://juejin.cn/post/6865910564817010702
  173. Proxy,
  174.  
  175. Map,
  176. map: {
  177. clear: Map.prototype.clear,
  178. set: Map.prototype.set,
  179. has: Map.prototype.has,
  180. get: Map.prototype.get
  181. },
  182.  
  183. console: {
  184. log: console.log,
  185. info: console.info,
  186. error: console.error,
  187. warn: console.warn,
  188. table: console.table
  189. },
  190.  
  191. ShadowRoot,
  192. HTMLMediaElement,
  193. CustomEvent,
  194. // appendChild: Node.prototype.appendChild,
  195.  
  196. JSON: {
  197. parse: JSON.parse,
  198. stringify: JSON.stringify
  199. },
  200.  
  201. alert,
  202. confirm,
  203. prompt
  204. };
  205.  
  206. /**
  207. * 媒体标签检测,可以检测出viode、audio、以及其它标签名经过改造后的媒体Element
  208. * @param {Function} handler -必选 检出后要执行的回调函数
  209. * @returns mediaElementList
  210. */
  211. const mediaCore = (function () {
  212. let hasMediaCoreInit = false;
  213. let hasProxyHTMLMediaElement = false;
  214. let originDescriptors = {};
  215. const originMethods = {};
  216. const mediaElementList = [];
  217. const mediaElementHandler = [];
  218. const mediaMap = new original.Map();
  219.  
  220. const firstUpperCase = str => str.replace(/^\S/, s => s.toUpperCase());
  221. function isHTMLMediaElement (el) {
  222. return el instanceof original.HTMLMediaElement
  223. }
  224.  
  225. /**
  226. * 根据HTMLMediaElement的实例对象创建增强控制的相关API函数,从而实现锁定播放倍速,锁定暂停和播放等增强功能
  227. * @param {*} mediaElement - 必选,HTMLMediaElement的具体实例,例如网页上的video标签或new Audio()等
  228. * @returns mediaPlusApi
  229. */
  230. function createMediaPlusApi (mediaElement) {
  231. if (!isHTMLMediaElement(mediaElement)) { return false }
  232.  
  233. let mediaPlusApi = original.map.get.call(mediaMap, mediaElement);
  234. if (mediaPlusApi) {
  235. return mediaPlusApi
  236. }
  237.  
  238. /* 创建MediaPlusApi对象 */
  239. mediaPlusApi = {};
  240. const mediaPlusBaseApi = {
  241. /**
  242. * 创建锁,阻止外部逻辑操作mediaElement相关的属性或函数
  243. * 这里的锁逻辑只是数据状态标注和切换,具体的锁功能需在
  244. * proxyPrototypeMethod和hijackPrototypeProperty里实现
  245. */
  246. lock (keyName, duration) {
  247. const infoKey = `__${keyName}_info__`;
  248. mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {};
  249. mediaPlusApi[infoKey].lock = true;
  250.  
  251. /* 解锁时间信息 */
  252. duration = Number(duration);
  253. if (!Number.isNaN(duration) && duration > 0) {
  254. mediaPlusApi[infoKey].unLockTime = Date.now() + duration;
  255. }
  256.  
  257. // original.console.log(`[mediaPlusApi][lock][${keyName}] ${duration}`)
  258. },
  259. unLock (keyName) {
  260. const infoKey = `__${keyName}_info__`;
  261. mediaPlusApi[infoKey] = mediaPlusApi[infoKey] || {};
  262. mediaPlusApi[infoKey].lock = false;
  263. mediaPlusApi[infoKey].unLockTime = Date.now() - 100;
  264.  
  265. // original.console.log(`[mediaPlusApi][unLock][${keyName}]`)
  266. },
  267. isLock (keyName) {
  268. const info = mediaPlusApi[`__${keyName}_info__`] || {};
  269.  
  270. if (info.unLockTime) {
  271. /* 延时锁根据当前时间计算是否还处于锁状态 */
  272. return Date.now() < info.unLockTime
  273. } else {
  274. return info.lock || false
  275. }
  276. },
  277.  
  278. /* 注意:调用此处的get和set和apply不受锁的限制 */
  279. get (keyName) {
  280. if (originDescriptors[keyName] && originDescriptors[keyName].get && !originMethods[keyName]) {
  281. return originDescriptors[keyName].get.apply(mediaElement)
  282. }
  283. },
  284. set (keyName, val) {
  285. if (originDescriptors[keyName] && originDescriptors[keyName].set && !originMethods[keyName] && typeof val !== 'undefined') {
  286. // original.console.log(`[mediaPlusApi][${keyName}] 执行原生set操作`)
  287. return originDescriptors[keyName].set.apply(mediaElement, [val])
  288. }
  289. },
  290. apply (keyName) {
  291. if (originMethods[keyName] instanceof Function) {
  292. const args = Array.from(arguments);
  293. args.shift();
  294. // original.console.log(`[mediaPlusApi][${keyName}] 执行原生apply操作`)
  295. return originMethods[keyName].apply(mediaElement, args)
  296. }
  297. }
  298. };
  299.  
  300. mediaPlusApi = { ...mediaPlusApi, ...mediaPlusBaseApi };
  301.  
  302. /**
  303. * 扩展api列表。实现'playbackRate', 'volume', 'currentTime', 'play', 'pause'的纯api调用效果,具体可用API如下:
  304. * mediaPlusApi.lockPlaybackRate()
  305. * mediaPlusApi.unLockPlaybackRate()
  306. * mediaPlusApi.isLockPlaybackRate()
  307. * mediaPlusApi.getPlaybackRate()
  308. * mediaPlusApi.setPlaybackRate(val)
  309. *
  310. * mediaPlusApi.lockVolume()
  311. * mediaPlusApi.unLockVolume()
  312. * mediaPlusApi.isLockVolume()
  313. * mediaPlusApi.getVolume()
  314. * mediaPlusApi.setVolume(val)
  315. *
  316. * mediaPlusApi.lockCurrentTime()
  317. * mediaPlusApi.unLockCurrentTime()
  318. * mediaPlusApi.isLockCurrentTime()
  319. * mediaPlusApi.getCurrentTime()
  320. * mediaPlusApi.setCurrentTime(val)
  321. *
  322. * mediaPlusApi.lockPlay()
  323. * mediaPlusApi.unLockPlay()
  324. * mediaPlusApi.isLockPlay()
  325. * mediaPlusApi.applyPlay()
  326. *
  327. * mediaPlusApi.lockPause()
  328. * mediaPlusApi.unLockPause()
  329. * mediaPlusApi.isLockPause()
  330. * mediaPlusApi.applyPause()
  331. */
  332. const extApiKeys = ['playbackRate', 'volume', 'currentTime', 'play', 'pause'];
  333. const baseApiKeys = Object.keys(mediaPlusBaseApi);
  334. extApiKeys.forEach(key => {
  335. baseApiKeys.forEach(baseKey => {
  336. /* 当key对应的是函数时,不应该有get、set的api,而应该有apply的api */
  337. if (originMethods[key] instanceof Function) {
  338. if (baseKey === 'get' || baseKey === 'set') {
  339. return true
  340. }
  341. } else if (baseKey === 'apply') {
  342. return true
  343. }
  344.  
  345. mediaPlusApi[`${baseKey}${firstUpperCase(key)}`] = function () {
  346. return mediaPlusBaseApi[baseKey].apply(null, [key, ...arguments])
  347. };
  348. });
  349. });
  350.  
  351. original.map.set.call(mediaMap, mediaElement, mediaPlusApi);
  352.  
  353. return mediaPlusApi
  354. }
  355.  
  356. /* 检测到media对象的处理逻辑,依赖Proxy对media函数的代理 */
  357. function mediaDetectHandler (ctx) {
  358. if (isHTMLMediaElement(ctx) && !mediaElementList.includes(ctx)) {
  359. // console.log(`[mediaDetectHandler]`, ctx)
  360. mediaElementList.push(ctx);
  361. createMediaPlusApi(ctx);
  362.  
  363. try {
  364. mediaElementHandler.forEach(handler => {
  365. (handler instanceof Function) && handler(ctx);
  366. });
  367. } catch (e) {}
  368. }
  369. }
  370.  
  371. /* 代理方法play和pause方法,确保能正确暂停和播放 */
  372. function proxyPrototypeMethod (element, methodName) {
  373. const originFunc = element && element.prototype[methodName];
  374. if (!originFunc) return
  375.  
  376. element.prototype[methodName] = new original.Proxy(originFunc, {
  377. apply (target, ctx, args) {
  378. mediaDetectHandler(ctx);
  379. // original.console.log(`[mediaElementMethodProxy] 执行代理后的${methodName}函数`)
  380.  
  381. /* 对播放暂停逻辑进行增强处理,例如允许通过mediaPlusApi进行锁定 */
  382. if (['play', 'pause'].includes(methodName)) {
  383. const mediaPlusApi = createMediaPlusApi(ctx);
  384. if (mediaPlusApi && mediaPlusApi.isLock(methodName)) {
  385. // original.console.log(`[mediaElementMethodProxy] ${methodName}已被锁定,无法执行相关操作`)
  386. return
  387. }
  388. }
  389.  
  390. const result = target.apply(ctx, args);
  391.  
  392. // TODO 对函数执行结果进行观察判断
  393.  
  394. return result
  395. }
  396. });
  397.  
  398. // 不建议对HTMLMediaElement的原型链进行扩展,这样容易让网页检测到mediaCore增强逻辑的存在
  399. // if (originMethods[methodName]) {
  400. // element.prototype[`__${methodName}__`] = originMethods[methodName]
  401. // }
  402. }
  403.  
  404. /**
  405. * 劫持 playbackRate、volume、currentTime 属性,并增加锁定的逻辑,从而实现更强的抗干扰能力
  406. */
  407. function hijackPrototypeProperty (element, property) {
  408. if (!element || !element.prototype || !originDescriptors[property]) {
  409. return false
  410. }
  411.  
  412. original.Object.defineProperty.call(Object, element.prototype, property, {
  413. configurable: true,
  414. enumerable: true,
  415. get: function () {
  416. const val = originDescriptors[property].get.apply(this, arguments);
  417. // original.console.log(`[mediaElementPropertyHijack][${property}][get]`, val)
  418.  
  419. const mediaPlusApi = createMediaPlusApi(this);
  420. if (mediaPlusApi && mediaPlusApi.isLock(property)) {
  421. if (property === 'playbackRate') {
  422. return +!+[]
  423. }
  424. }
  425.  
  426. return val
  427. },
  428. set: function (value) {
  429. // original.console.log(`[mediaElementPropertyHijack][${property}][set]`, value)
  430.  
  431. if (property === 'src') {
  432. mediaDetectHandler(this);
  433. }
  434.  
  435. /* 对调速、调音和进度控制逻辑进行增强处理,例如允许通过mediaPlusApi这些功能进行锁定 */
  436. if (['playbackRate', 'volume', 'currentTime'].includes(property)) {
  437. const mediaPlusApi = createMediaPlusApi(this);
  438. if (mediaPlusApi && mediaPlusApi.isLock(property)) {
  439. // original.console.log(`[mediaElementPropertyHijack] ${property}已被锁定,无法执行相关操作`)
  440. return
  441. }
  442. }
  443.  
  444. return originDescriptors[property].set.apply(this, arguments)
  445. }
  446. });
  447. }
  448.  
  449. function mediaPlus (mediaElement) {
  450. return createMediaPlusApi(mediaElement)
  451. }
  452.  
  453. function mediaProxy () {
  454. if (!hasProxyHTMLMediaElement) {
  455. const proxyMethods = ['play', 'pause', 'load', 'addEventListener'];
  456. proxyMethods.forEach(methodName => { proxyPrototypeMethod(HTMLMediaElement, methodName); });
  457.  
  458. const hijackProperty = ['playbackRate', 'volume', 'currentTime', 'src'];
  459. hijackProperty.forEach(property => { hijackPrototypeProperty(HTMLMediaElement, property); });
  460.  
  461. hasProxyHTMLMediaElement = true;
  462. }
  463.  
  464. return hasProxyHTMLMediaElement
  465. }
  466.  
  467. /**
  468. * 媒体标签检测,可以检测出viode、audio、以及其它标签名经过改造后的媒体Element
  469. * @param {Function} handler -必选 检出后要执行的回调函数
  470. * @returns mediaElementList
  471. */
  472. function mediaChecker (handler) {
  473. if (!(handler instanceof Function) || mediaElementHandler.includes(handler)) {
  474. return mediaElementList
  475. } else {
  476. mediaElementHandler.push(handler);
  477. }
  478.  
  479. if (!hasProxyHTMLMediaElement) {
  480. mediaProxy();
  481. }
  482.  
  483. return mediaElementList
  484. }
  485.  
  486. /**
  487. * 初始化mediaCore相关功能
  488. */
  489. function init (mediaCheckerHandler) {
  490. if (hasMediaCoreInit) { return false }
  491.  
  492. originDescriptors = Object.getOwnPropertyDescriptors(HTMLMediaElement.prototype);
  493.  
  494. Object.keys(HTMLMediaElement.prototype).forEach(key => {
  495. try {
  496. if (HTMLMediaElement.prototype[key] instanceof Function) {
  497. originMethods[key] = HTMLMediaElement.prototype[key];
  498. }
  499. } catch (e) {}
  500. });
  501.  
  502. mediaCheckerHandler = mediaCheckerHandler instanceof Function ? mediaCheckerHandler : function () {};
  503. mediaChecker(mediaCheckerHandler);
  504.  
  505. hasMediaCoreInit = true;
  506. return true
  507. }
  508.  
  509. return {
  510. init,
  511. mediaPlus,
  512. mediaChecker,
  513. originDescriptors,
  514. originMethods,
  515. mediaElementList
  516. }
  517. })();
  518.  
  519. const mediaSource = (function () {
  520. let hasMediaSourceInit = false;
  521. const originMethods = {};
  522. const originURLMethods = {};
  523. const mediaSourceMap = new original.Map();
  524. const objectURLMap = new original.Map();
  525.  
  526. function proxyMediaSourceMethod () {
  527. if (!originMethods.addSourceBuffer || !originMethods.endOfStream) {
  528. return false
  529. }
  530.  
  531. // TODO 该代理在上层调用生效可能存在延迟,原因待研究
  532. originURLMethods.createObjectURL = originURLMethods.createObjectURL || URL.prototype.constructor.createObjectURL;
  533. URL.prototype.constructor.createObjectURL = new original.Proxy(originURLMethods.createObjectURL, {
  534. apply (target, ctx, args) {
  535. const objectURL = target.apply(ctx, args);
  536.  
  537. original.map.set.call(objectURLMap, args[0], objectURL);
  538.  
  539. return objectURL
  540. }
  541. });
  542.  
  543. MediaSource.prototype.addSourceBuffer = new original.Proxy(originMethods.addSourceBuffer, {
  544. apply (target, ctx, args) {
  545. if (!original.map.has.call(mediaSourceMap, ctx)) {
  546. original.map.set.call(mediaSourceMap, ctx, {
  547. mediaSource: ctx,
  548. createTime: Date.now(),
  549. sourceBuffer: [],
  550. endOfStream: false
  551. });
  552. }
  553.  
  554. original.console.log('[addSourceBuffer]', ctx, args);
  555.  
  556. const mediaSourceInfo = original.map.get.call(mediaSourceMap, ctx);
  557. const mimeCodecs = args[0] || '';
  558. const sourceBuffer = target.apply(ctx, args);
  559.  
  560. const sourceBufferItem = {
  561. mimeCodecs,
  562. originAppendBuffer: sourceBuffer.appendBuffer,
  563. bufferData: [],
  564. mediaInfo: {}
  565. };
  566.  
  567. try {
  568. // mimeCodecs字符串示例:'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'
  569. const mediaInfo = sourceBufferItem.mediaInfo;
  570. const tmpArr = sourceBufferItem.mimeCodecs.split(';');
  571.  
  572. mediaInfo.type = tmpArr[0].split('/')[0];
  573. mediaInfo.format = tmpArr[0].split('/')[1];
  574. mediaInfo.codecs = tmpArr[1].trim().replace('codecs=', '').replace(/["']/g, '');
  575. } catch (e) {
  576. original.console.error('[addSourceBuffer][mediaInfo] 媒体信息解析出错', sourceBufferItem, e);
  577. }
  578.  
  579. mediaSourceInfo.sourceBuffer.push(sourceBufferItem);
  580.  
  581. /* 代理sourceBuffer.appendBuffer函数,并将buffer存一份到mediaSourceInfo里 */
  582. sourceBuffer.appendBuffer = new original.Proxy(sourceBufferItem.originAppendBuffer, {
  583. apply (bufTarget, bufCtx, bufArgs) {
  584. const buffer = bufArgs[0];
  585. sourceBufferItem.bufferData.push(buffer);
  586.  
  587. /* 确保mediaUrl的存在和对应 */
  588. if (original.map.get.call(objectURLMap, ctx)) {
  589. mediaSourceInfo.mediaUrl = original.map.get.call(objectURLMap, ctx);
  590. }
  591.  
  592. return bufTarget.apply(bufCtx, bufArgs)
  593. }
  594. });
  595.  
  596. return sourceBuffer
  597. }
  598. });
  599.  
  600. MediaSource.prototype.endOfStream = new original.Proxy(originMethods.endOfStream, {
  601. apply (target, ctx, args) {
  602. /* 标识当前媒体流已加载完成 */
  603. const mediaSourceInfo = original.map.get.call(mediaSourceMap, ctx);
  604. if (mediaSourceInfo) {
  605. mediaSourceInfo.endOfStream = true;
  606. }
  607.  
  608. return target.apply(ctx, args)
  609. }
  610. });
  611. }
  612.  
  613. /**
  614. * 下载媒体资源,下载代码参考:https://juejin.cn/post/6873267073674379277
  615. */
  616. function downloadMediaSource () {
  617. mediaSourceMap.forEach(mediaSourceInfo => {
  618. if (mediaSourceInfo.hasDownload) {
  619. const confirm = original.confirm('该媒体文件已经下载过了,确定需要再次下载?');
  620. if (!confirm) {
  621. return false
  622. }
  623. }
  624.  
  625. if (!mediaSourceInfo.hasDownload && !mediaSourceInfo.endOfStream) {
  626. const confirm = original.confirm('媒体数据还没完全就绪,确定要执行下载操作?');
  627. if (!confirm) {
  628. return false
  629. }
  630.  
  631. original.console.log('[downloadMediaSource] 媒体数据还没完全就绪', mediaSourceInfo);
  632. }
  633.  
  634. mediaSourceInfo.hasDownload = true;
  635. mediaSourceInfo.sourceBuffer.forEach(sourceBufferItem => {
  636. if (!sourceBufferItem.mimeCodecs || sourceBufferItem.mimeCodecs.toString().indexOf(';') === -1) {
  637. const msg = '[downloadMediaSource][mimeCodecs][error] mimeCodecs不存在或信息异常,无法下载';
  638. original.console.error(msg, sourceBufferItem);
  639. original.alert(msg);
  640. return false
  641. }
  642.  
  643. try {
  644. let mediaTitle = sourceBufferItem.mediaInfo.title || `${document.title || Date.now()}_${sourceBufferItem.mediaInfo.type}.${sourceBufferItem.mediaInfo.format}`;
  645.  
  646. if (!sourceBufferItem.mediaInfo.title) {
  647. mediaTitle = original.prompt('请确认文件标题:', mediaTitle) || mediaTitle;
  648. sourceBufferItem.mediaInfo.title = mediaTitle;
  649. }
  650.  
  651. if (!mediaTitle.endsWith(sourceBufferItem.mediaInfo.format)) {
  652. mediaTitle = mediaTitle + '.' + sourceBufferItem.mediaInfo.format;
  653. }
  654.  
  655. const a = document.createElement('a');
  656. a.href = URL.createObjectURL(new Blob(sourceBufferItem.bufferData));
  657. a.download = mediaTitle;
  658. a.click();
  659. URL.revokeObjectURL(a.href);
  660. } catch (e) {
  661. mediaSourceInfo.hasDownload = false;
  662. const msg = '[downloadMediaSource][error]';
  663. original.console.error(msg, e);
  664. original.alert(msg);
  665. }
  666. });
  667. });
  668. }
  669.  
  670. function hasInit () {
  671. return hasMediaSourceInit
  672. }
  673.  
  674. function init () {
  675. if (hasMediaSourceInit) {
  676. return false
  677. }
  678.  
  679. if (!window.MediaSource) {
  680. return false
  681. }
  682.  
  683. Object.keys(MediaSource.prototype).forEach(key => {
  684. try {
  685. if (MediaSource.prototype[key] instanceof Function) {
  686. originMethods[key] = MediaSource.prototype[key];
  687. }
  688. } catch (e) {}
  689. });
  690.  
  691. proxyMediaSourceMethod();
  692.  
  693. hasMediaSourceInit = true;
  694. }
  695.  
  696. return {
  697. init,
  698. hasInit,
  699. originMethods,
  700. originURLMethods,
  701. mediaSourceMap,
  702. objectURLMap,
  703. downloadMediaSource
  704. }
  705. })();
  706.  
  707. /*!
  708. * @name utils.js
  709. * @description 数据类型相关的方法
  710. * @version 0.0.1
  711. * @author Blaze
  712. * @date 22/03/2019 22:46
  713. * @github https://github.com/xxxily
  714. */
  715.  
  716. /**
  717. * 准确地获取对象的具体类型 参见:https://www.talkingcoder.com/article/6333557442705696719
  718. * @param obj { all } -必选 要判断的对象
  719. * @returns {*} 返回判断的具体类型
  720. */
  721. function getType (obj) {
  722. if (obj == null) {
  723. return String(obj)
  724. }
  725. return typeof obj === 'object' || typeof obj === 'function'
  726. ? (obj.constructor && obj.constructor.name && obj.constructor.name.toLowerCase()) ||
  727. /function\s(.+?)\(/.exec(obj.constructor)[1].toLowerCase()
  728. : typeof obj
  729. }
  730.  
  731. const isType = (obj, typeName) => getType(obj) === typeName;
  732. const isObj = obj => isType(obj, 'object');
  733.  
  734. /*!
  735. * @name object.js
  736. * @description 对象操作的相关方法
  737. * @version 0.0.1
  738. * @author Blaze
  739. * @date 21/03/2019 23:10
  740. * @github https://github.com/xxxily
  741. */
  742.  
  743. /**
  744. * 对一个对象进行深度拷贝
  745. * @source -必选(Object|Array)需拷贝的对象或数组
  746. */
  747. function clone (source) {
  748. var result = {};
  749.  
  750. if (typeof source !== 'object') {
  751. return source
  752. }
  753. if (Object.prototype.toString.call(source) === '[object Array]') {
  754. result = [];
  755. }
  756. if (Object.prototype.toString.call(source) === '[object Null]') {
  757. result = null;
  758. }
  759. for (var key in source) {
  760. result[key] = (typeof source[key] === 'object') ? clone(source[key]) : source[key];
  761. }
  762. return result
  763. }
  764.  
  765. /* 遍历对象,但不包含其原型链上的属性 */
  766. function forIn (obj, fn) {
  767. fn = fn || function () {};
  768. for (var key in obj) {
  769. if (Object.hasOwnProperty.call(obj, key)) {
  770. fn(key, obj[key]);
  771. }
  772. }
  773. }
  774.  
  775. /**
  776. * 深度合并两个可枚举的对象
  777. * @param objA {object} -必选 对象A
  778. * @param objB {object} -必选 对象B
  779. * @param concatArr {boolean} -可选 合并数组,默认遇到数组的时候,直接以另外一个数组替换当前数组,将此设置true则,遇到数组的时候一律合并,而不是直接替换
  780. * @returns {*|void}
  781. */
  782. function mergeObj (objA, objB, concatArr) {
  783. function isObj (obj) {
  784. return Object.prototype.toString.call(obj) === '[object Object]'
  785. }
  786. function isArr (arr) {
  787. return Object.prototype.toString.call(arr) === '[object Array]'
  788. }
  789. if (!isObj(objA) || !isObj(objB)) return objA
  790. function deepMerge (objA, objB) {
  791. forIn(objB, function (key) {
  792. const subItemA = objA[key];
  793. const subItemB = objB[key];
  794. if (typeof subItemA === 'undefined') {
  795. objA[key] = subItemB;
  796. } else {
  797. if (isObj(subItemA) && isObj(subItemB)) {
  798. /* 进行深层合并 */
  799. objA[key] = deepMerge(subItemA, subItemB);
  800. } else {
  801. if (concatArr && isArr(subItemA) && isArr(subItemB)) {
  802. objA[key] = subItemA.concat(subItemB);
  803. } else {
  804. objA[key] = subItemB;
  805. }
  806. }
  807. }
  808. });
  809. return objA
  810. }
  811. return deepMerge(objA, objB)
  812. }
  813.  
  814. /**
  815. * 根据文本路径获取对象里面的值,如需支持数组请使用lodash的get方法
  816. * @param obj {Object} -必选 要操作的对象
  817. * @param path {String} -必选 路径信息
  818. * @returns {*}
  819. */
  820. function getValByPath (obj, path) {
  821. path = path || '';
  822. const pathArr = path.split('.');
  823. let result = obj;
  824.  
  825. /* 递归提取结果值 */
  826. for (let i = 0; i < pathArr.length; i++) {
  827. if (!result) break
  828. result = result[pathArr[i]];
  829. }
  830.  
  831. return result
  832. }
  833.  
  834. /**
  835. * 根据文本路径设置对象里面的值,如需支持数组请使用lodash的set方法
  836. * @param obj {Object} -必选 要操作的对象
  837. * @param path {String} -必选 路径信息
  838. * @param val {Any} -必选 如果不传该参,最终结果会被设置为undefined
  839. * @returns {Boolean} 返回true表示设置成功,否则设置失败
  840. */
  841. function setValByPath (obj, path, val) {
  842. if (!obj || !path || typeof path !== 'string') {
  843. return false
  844. }
  845.  
  846. let result = obj;
  847. const pathArr = path.split('.');
  848.  
  849. for (let i = 0; i < pathArr.length; i++) {
  850. if (!result) break
  851.  
  852. if (i === pathArr.length - 1) {
  853. result[pathArr[i]] = val;
  854. return Number.isNaN(val) ? Number.isNaN(result[pathArr[i]]) : result[pathArr[i]] === val
  855. }
  856.  
  857. result = result[pathArr[i]];
  858. }
  859.  
  860. return false
  861. }
  862.  
  863. const quickSort = function (arr) {
  864. if (arr.length <= 1) { return arr }
  865. var pivotIndex = Math.floor(arr.length / 2);
  866. var pivot = arr.splice(pivotIndex, 1)[0];
  867. var left = [];
  868. var right = [];
  869. for (var i = 0; i < arr.length; i++) {
  870. if (arr[i] < pivot) {
  871. left.push(arr[i]);
  872. } else {
  873. right.push(arr[i]);
  874. }
  875. }
  876. return quickSort(left).concat([pivot], quickSort(right))
  877. };
  878.  
  879. function hideDom (selector, delay) {
  880. setTimeout(function () {
  881. const dom = document.querySelector(selector);
  882. if (dom) {
  883. dom.style.opacity = 0;
  884. }
  885. }, delay || 1000 * 5);
  886. }
  887.  
  888. /**
  889. * 向上查找操作
  890. * @param dom {Element} -必选 初始dom元素
  891. * @param fn {function} -必选 每一级ParentNode的回调操作
  892. * 如果函数返回true则表示停止向上查找动作
  893. */
  894. function eachParentNode (dom, fn) {
  895. let parent = dom.parentNode;
  896. while (parent) {
  897. const isEnd = fn(parent, dom);
  898. parent = parent.parentNode;
  899. if (isEnd) {
  900. break
  901. }
  902. }
  903. }
  904.  
  905. /**
  906. * 动态加载css内容
  907. * @param cssText {String} -必选 样式的文本内容
  908. * @param id {String} -可选 指定样式文本的id号,如果已存在对应id号则不会再次插入
  909. * @param insetTo {Dom} -可选 指定插入到哪
  910. * @returns {HTMLStyleElement}
  911. */
  912. function loadCSSText (cssText, id, insetTo) {
  913. if (id && document.getElementById(id)) {
  914. return false
  915. }
  916.  
  917. const style = document.createElement('style');
  918. const head = insetTo || document.head || document.getElementsByTagName('head')[0];
  919. style.appendChild(document.createTextNode(cssText));
  920. head.appendChild(style);
  921.  
  922. if (id) {
  923. style.setAttribute('id', id);
  924. }
  925.  
  926. return style
  927. }
  928.  
  929. /**
  930. * 判断当前元素是否为可编辑元素
  931. * @param target
  932. * @returns Boolean
  933. */
  934. function isEditableTarget (target) {
  935. const isEditable = target.getAttribute && target.getAttribute('contenteditable') === 'true';
  936. const isInputDom = /INPUT|TEXTAREA|SELECT|LABEL/.test(target.nodeName);
  937. return isEditable || isInputDom
  938. }
  939.  
  940. /**
  941. * 判断某个元素是否处于shadowDom里面
  942. * 参考:https://www.coder.work/article/299700
  943. * @param node
  944. * @returns {boolean}
  945. */
  946. function isInShadow (node, returnShadowRoot) {
  947. for (; node; node = node.parentNode) {
  948. if (node.toString() === '[object ShadowRoot]') {
  949. if (returnShadowRoot) {
  950. return node
  951. } else {
  952. return true
  953. }
  954. }
  955. }
  956. return false
  957. }
  958.  
  959. /**
  960. * 判断某个元素是否处于可视区域,适用于被动调用情况,需要高性能,请使用IntersectionObserver
  961. * 参考:https://github.com/febobo/web-interview/issues/84
  962. * @param element
  963. * @returns {boolean}
  964. */
  965. function isInViewPort (element) {
  966. const viewWidth = window.innerWidth || document.documentElement.clientWidth;
  967. const viewHeight = window.innerHeight || document.documentElement.clientHeight;
  968. const {
  969. top,
  970. right,
  971. bottom,
  972. left
  973. } = element.getBoundingClientRect();
  974.  
  975. return (
  976. top >= 0 &&
  977. left >= 0 &&
  978. right <= viewWidth &&
  979. bottom <= viewHeight
  980. )
  981. }
  982.  
  983. /**
  984. * 将行内样式转换成对象的形式
  985. * @param {string} inlineStyle -必选,例如: position: relative; opacity: 1; visibility: hidden; transform: scale(0.1) rotate(180deg);
  986. * @returns {Object}
  987. */
  988.  
  989. function inlineStyleToObj (inlineStyle) {
  990. if (typeof inlineStyle !== 'string') {
  991. return {}
  992. }
  993.  
  994. const result = {};
  995. const styArr = inlineStyle.split(';');
  996. styArr.forEach(item => {
  997. const tmpArr = item.split(':');
  998. if (tmpArr.length === 2) {
  999. result[tmpArr[0].trim()] = tmpArr[1].trim();
  1000. }
  1001. });
  1002.  
  1003. return result
  1004. }
  1005.  
  1006. function objToInlineStyle (obj) {
  1007. if (Object.prototype.toString.call(obj) !== '[object Object]') {
  1008. return ''
  1009. }
  1010.  
  1011. const styleArr = [];
  1012. Object.keys(obj).forEach(key => {
  1013. styleArr.push(`${key}: ${obj[key]}`);
  1014. });
  1015.  
  1016. return styleArr.join('; ')
  1017. }
  1018.  
  1019. /* ua信息伪装 */
  1020. function fakeUA (ua) {
  1021. // Object.defineProperty(navigator, 'userAgent', {
  1022. // value: ua,
  1023. // writable: false,
  1024. // configurable: false,
  1025. // enumerable: true
  1026. // })
  1027.  
  1028. const desc = Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent');
  1029. Object.defineProperty(Navigator.prototype, 'userAgent', { ...desc, get: function () { return ua } });
  1030. }
  1031.  
  1032. /* ua信息来源:https://developers.whatismybrowser.com */
  1033. const userAgentMap = {
  1034. android: {
  1035. 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',
  1036. firefox: 'Mozilla/5.0 (Android 7.0; Mobile; rv:57.0) Gecko/57.0 Firefox/57.0'
  1037. },
  1038. iPhone: {
  1039. safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/111.0.0.0 Mobile/15E148 Safari/604.1',
  1040. 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'
  1041. },
  1042. iPad: {
  1043. 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',
  1044. 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'
  1045. },
  1046. mac: {
  1047. safari: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Safari/605.1.15',
  1048. chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Firefox) Chrome/74.0.3729.157 Safari/537.36'
  1049. }
  1050. };
  1051.  
  1052. /**
  1053. * 判断是否处于Iframe中
  1054. * @returns {boolean}
  1055. */
  1056. function isInIframe () {
  1057. return window !== window.top
  1058. }
  1059.  
  1060. /**
  1061. * 判断是否处于跨域限制的Iframe中
  1062. * @returns {boolean}
  1063. */
  1064. function isInCrossOriginFrame () {
  1065. let result = true;
  1066. try {
  1067. if (window.top.localStorage || window.top.location.href) {
  1068. result = false;
  1069. }
  1070. } catch (e) {
  1071. result = true;
  1072. }
  1073. return result
  1074. }
  1075.  
  1076. /**
  1077. * 简单的节流函数
  1078. * @param fn
  1079. * @param interval
  1080. * @returns {Function}
  1081. */
  1082. function throttle (fn, interval = 80) {
  1083. let timeout = null;
  1084. return function () {
  1085. if (timeout) return false
  1086. timeout = setTimeout(() => {
  1087. timeout = null;
  1088. }, interval);
  1089. fn.apply(this, arguments);
  1090. }
  1091. }
  1092.  
  1093. /*!
  1094. * @name url.js
  1095. * @description 用于对url进行解析的相关方法
  1096. * @version 0.0.1
  1097. * @author Blaze
  1098. * @date 27/03/2019 15:52
  1099. * @github https://github.com/xxxily
  1100. */
  1101.  
  1102. /**
  1103. * 参考示例:
  1104. * https://segmentfault.com/a/1190000006215495
  1105. * 注意:该方法必须依赖浏览器的DOM对象
  1106. */
  1107.  
  1108. function parseURL (url) {
  1109. var a = document.createElement('a');
  1110. a.href = url || window.location.href;
  1111. return {
  1112. source: url,
  1113. protocol: a.protocol.replace(':', ''),
  1114. host: a.hostname,
  1115. port: a.port,
  1116. origin: a.origin,
  1117. search: a.search,
  1118. query: a.search,
  1119. file: (a.pathname.match(/\/([^/?#]+)$/i) || ['', ''])[1],
  1120. hash: a.hash.replace('#', ''),
  1121. path: a.pathname.replace(/^([^/])/, '/$1'),
  1122. relative: (a.href.match(/tps?:\/\/[^/]+(.+)/) || ['', ''])[1],
  1123. params: (function () {
  1124. var ret = {};
  1125. var seg = [];
  1126. var paramArr = a.search.replace(/^\?/, '').split('&');
  1127.  
  1128. for (var i = 0; i < paramArr.length; i++) {
  1129. var item = paramArr[i];
  1130. if (item !== '' && item.indexOf('=')) {
  1131. seg.push(item);
  1132. }
  1133. }
  1134.  
  1135. for (var j = 0; j < seg.length; j++) {
  1136. var param = seg[j];
  1137. var idx = param.indexOf('=');
  1138. var key = param.substring(0, idx);
  1139. var val = param.substring(idx + 1);
  1140. if (!key) {
  1141. ret[val] = null;
  1142. } else {
  1143. ret[key] = val;
  1144. }
  1145. }
  1146. return ret
  1147. })()
  1148. }
  1149. }
  1150.  
  1151. /**
  1152. * 将params对象转换成字符串模式
  1153. * @param params {Object} - 必选 params对象
  1154. * @returns {string}
  1155. */
  1156. function stringifyParams (params) {
  1157. var strArr = [];
  1158.  
  1159. if (!Object.prototype.toString.call(params) === '[object Object]') {
  1160. return ''
  1161. }
  1162.  
  1163. for (var key in params) {
  1164. if (Object.hasOwnProperty.call(params, key)) {
  1165. var val = params[key];
  1166. var valType = Object.prototype.toString.call(val);
  1167.  
  1168. if (val === '' || valType === '[object Undefined]') continue
  1169.  
  1170. if (val === null) {
  1171. strArr.push(key);
  1172. } else if (valType === '[object Array]') {
  1173. strArr.push(key + '=' + val.join(','));
  1174. } else {
  1175. val = (JSON.stringify(val) || '' + val).replace(/(^"|"$)/g, '');
  1176. strArr.push(key + '=' + val);
  1177. }
  1178. }
  1179. }
  1180. return strArr.join('&')
  1181. }
  1182.  
  1183. /**
  1184. * 将通过parseURL解析出来url对象重新还原成url地址
  1185. * 主要用于查询参数被动态修改后,再重组url链接
  1186. * @param obj {Object} -必选 parseURL解析出来url对象
  1187. */
  1188. function stringifyToUrl (urlObj) {
  1189. var query = stringifyParams(urlObj.params) || '';
  1190. if (query) { query = '?' + query; }
  1191. var hash = urlObj.hash ? '#' + urlObj.hash : '';
  1192. return urlObj.origin + urlObj.path + query + hash
  1193. }
  1194.  
  1195. /* 当前用到的快捷键 */
  1196. const hasUseKey = {
  1197. keyCodeList: [13, 16, 17, 18, 27, 32, 37, 38, 39, 40, 49, 50, 51, 52, 67, 68, 69, 70, 73, 74, 75, 77, 78, 79, 80, 81, 82, 83, 84, 85, 87, 88, 89, 90, 97, 98, 99, 100, 220],
  1198. 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', '\\', '|'],
  1199. keyMap: {
  1200. enter: 13,
  1201. shift: 16,
  1202. ctrl: 17,
  1203. alt: 18,
  1204. esc: 27,
  1205. space: 32,
  1206. '←': 37,
  1207. '↑': 38,
  1208. '→': 39,
  1209. '↓': 40,
  1210. 1: 49,
  1211. 2: 50,
  1212. 3: 51,
  1213. 4: 52,
  1214. c: 67,
  1215. d: 68,
  1216. e: 69,
  1217. f: 70,
  1218. i: 73,
  1219. j: 74,
  1220. k: 75,
  1221. m: 77,
  1222. n: 78,
  1223. o: 79,
  1224. p: 80,
  1225. q: 81,
  1226. r: 82,
  1227. s: 83,
  1228. t: 84,
  1229. u: 85,
  1230. w: 87,
  1231. x: 88,
  1232. y: 89,
  1233. z: 90,
  1234. pad1: 97,
  1235. pad2: 98,
  1236. pad3: 99,
  1237. pad4: 100,
  1238. '\\': 220
  1239. }
  1240. };
  1241.  
  1242. /**
  1243. * 判断当前按键是否注册为需要用的按键
  1244. * 用于减少对其它键位的干扰
  1245. */
  1246. function isRegisterKey (event) {
  1247. const keyCode = event.keyCode;
  1248. const key = event.key.toLowerCase();
  1249. return hasUseKey.keyCodeList.includes(keyCode) ||
  1250. hasUseKey.keyList.includes(key)
  1251. }
  1252.  
  1253. /**
  1254. * 由于tampermonkey对window对象进行了封装,我们实际访问到的window并非页面真实的window
  1255. * 这就导致了如果我们需要将某些对象挂载到页面的window进行调试的时候就无法挂载了
  1256. * 所以必须使用特殊手段才能访问到页面真实的window对象,于是就有了下面这个函数
  1257. * @returns {Promise<void>}
  1258. */
  1259. async function getPageWindow () {
  1260. return new Promise(function (resolve, reject) {
  1261. if (window._pageWindow) {
  1262. return resolve(window._pageWindow)
  1263. }
  1264.  
  1265. /* 尝试通过同步的方式获取pageWindow */
  1266. try {
  1267. const pageWin = getPageWindowSync();
  1268. if (pageWin && pageWin.document && pageWin.XMLHttpRequest) {
  1269. window._pageWindow = pageWin;
  1270. resolve(pageWin);
  1271. return pageWin
  1272. }
  1273. } catch (e) {}
  1274.  
  1275. /* 下面异步获取pagewindow的方法在最新的chrome浏览器里已失效 */
  1276.  
  1277. const listenEventList = ['load', 'mousemove', 'scroll', 'get-page-window-event'];
  1278.  
  1279. function getWin (event) {
  1280. window._pageWindow = this;
  1281. // debug.log('getPageWindow succeed', event)
  1282. listenEventList.forEach(eventType => {
  1283. window.removeEventListener(eventType, getWin, true);
  1284. });
  1285. resolve(window._pageWindow);
  1286. }
  1287.  
  1288. listenEventList.forEach(eventType => {
  1289. window.addEventListener(eventType, getWin, true);
  1290. });
  1291.  
  1292. /* 自行派发事件以便用最短的时间获得pageWindow对象 */
  1293. window.dispatchEvent(new window.Event('get-page-window-event'));
  1294. })
  1295. }
  1296. getPageWindow();
  1297.  
  1298. /**
  1299. * 通过同步的方式获取pageWindow
  1300. * 注意同步获取的方式需要将脚本写入head,部分网站由于安全策略会导致写入失败,而无法正常获取
  1301. * @returns {*}
  1302. */
  1303. function getPageWindowSync (rawFunction) {
  1304. if (window.unsafeWindow) return window.unsafeWindow
  1305. if (document._win_) return document._win_
  1306.  
  1307. try {
  1308. rawFunction = rawFunction || window.__rawFunction__ || Function.prototype.constructor;
  1309. // return rawFunction('return window')()
  1310. // Function('return (function(){}.constructor("return this")());')
  1311. return rawFunction('return (function(){}.constructor("var getPageWindowSync=1; return this")());')()
  1312. } catch (e) {
  1313. console.error('getPageWindowSync error', e);
  1314.  
  1315. const head = document.head || document.querySelector('head');
  1316. const script = document.createElement('script');
  1317. script.appendChild(document.createTextNode('document._win_ = window'));
  1318. head.appendChild(script);
  1319.  
  1320. return document._win_
  1321. }
  1322. }
  1323.  
  1324. function openInTab (url, opts, referer) {
  1325. if (referer) {
  1326. const urlObj = parseURL(url);
  1327. if (!urlObj.params.referer) {
  1328. urlObj.params.referer = encodeURIComponent(window.location.href);
  1329. url = stringifyToUrl(urlObj);
  1330. }
  1331. }
  1332.  
  1333. if (window.GM_openInTab) {
  1334. window.GM_openInTab(url, opts || {
  1335. active: true,
  1336. insert: true,
  1337. setParent: true
  1338. });
  1339. }
  1340. }
  1341.  
  1342. /* 确保数字为正数 */
  1343. function numUp (num) {
  1344. if (typeof num === 'number' && num < 0) {
  1345. num = Math.abs(num);
  1346. }
  1347. return num
  1348. }
  1349.  
  1350. /* 确保数字为负数 */
  1351. function numDown (num) {
  1352. if (typeof num === 'number' && num > 0) {
  1353. num = -num;
  1354. }
  1355. return num
  1356. }
  1357.  
  1358. function isMediaElement (element) {
  1359. return element && (element instanceof HTMLMediaElement || element.HTMLMediaElement || element.HTMLVideoElement || element.HTMLAudioElement)
  1360. }
  1361.  
  1362. function isVideoElement (element) {
  1363. return element && (element instanceof HTMLVideoElement || element.HTMLVideoElement)
  1364. }
  1365.  
  1366. function isAudioElement (element) {
  1367. return element && (element instanceof HTMLAudioElement || element.HTMLAudioElement)
  1368. }
  1369.  
  1370. /*!
  1371. * configManager parse localStorage error * @name configManager.ts
  1372. * @description 配置统一管理脚本
  1373. * @version 0.0.1
  1374. * @author xxxily
  1375. * @date 2023/03/06 14:29
  1376. * @github https://github.com/xxxily
  1377. */
  1378.  
  1379. /**
  1380. * 判断localStorage是否可用
  1381. * localStorage并不能保证100%可用,所以使用前必须进行判断,否则会导致部分网站下脚本出现异常
  1382. * https://stackoverflow.com/questions/30481516/iframe-in-chrome-error-failed-to-read-localstorage-from-window-access-deni
  1383. * https://cloud.tencent.com/developer/article/1803097 (当localStorage不能用时,window.localStorage为null,而不是文中的undefined)
  1384. */
  1385. function isLocalStorageUsable () {
  1386. return window.localStorage && window.localStorage.getItem instanceof Function && window.localStorage.setItem instanceof Function
  1387. }
  1388.  
  1389. /**
  1390. * 判断GlobalStorage是否可用,目前使用的GlobalStorage是基于tampermonkey提供的相关api
  1391. * https://www.tampermonkey.net/documentation.php?ext=dhdg#GM_setValue
  1392. */
  1393. function isGlobalStorageUsable () {
  1394. return window.GM_setValue && window.GM_getValue && window.GM_deleteValue && window.GM_listValues
  1395. }
  1396.  
  1397. /**
  1398. * 存储干净的localStorage相关方法
  1399. * 防止localStorage对象下的方法被改写而导致读取和写入规则不一样的问题
  1400. */
  1401. const rawLocalStorage = (function getRawLocalStorage () {
  1402. const localStorageApis = ['getItem', 'setItem', 'removeItem', 'clear', 'key'];
  1403.  
  1404. const rawLocalStorage = {};
  1405.  
  1406. localStorageApis.forEach((apiKey) => {
  1407. if (isLocalStorageUsable()) {
  1408. rawLocalStorage[`_${apiKey}_`] = localStorage[apiKey];
  1409. rawLocalStorage[apiKey] = function () {
  1410. return rawLocalStorage[`_${apiKey}_`].apply(localStorage, arguments)
  1411. };
  1412. } else {
  1413. rawLocalStorage[apiKey] = function () {
  1414. console.error('localStorage unavailable');
  1415. };
  1416. }
  1417. });
  1418.  
  1419. return rawLocalStorage
  1420. })();
  1421.  
  1422. class ConfigManager {
  1423. constructor (opts) {
  1424. this.opts = opts;
  1425. }
  1426.  
  1427. /**
  1428. * 将confPath转换称最终存储到localStorage或globalStorage里的键名
  1429. * @param {String} confPath -必选,配置路径信息:例如:'enhance.blockSetPlaybackRate'
  1430. * @returns {keyName}
  1431. */
  1432. getConfKeyName (confPath = '') {
  1433. return this.opts.prefix + confPath.replace(/\./g, '_')
  1434. }
  1435.  
  1436. /**
  1437. * 将存储到localStorage或globalStorage里的键名转换成实际调用时候的confPath
  1438. * @param {String} keyName -必选 存储到localStorage或globalStorage里的键名,例如:'_h5player_enhance_blockSetPlaybackRate'
  1439. * @returns {confPath}
  1440. */
  1441. getConfPath (keyName = '') {
  1442. return keyName.replace(this.opts.prefix, '').replace(/_/g, '.')
  1443. }
  1444.  
  1445. getConfPathList (config) {
  1446. const confPathList = [];
  1447.  
  1448. /* 递归获取所有配置项的路径 */
  1449. function getConfPathList (config, path = '') {
  1450. Object.keys(config).forEach((key) => {
  1451. const pathKey = path ? `${path}.${key}` : key;
  1452. if (Object.prototype.toString.call(config[key]) === '[object Object]') {
  1453. getConfPathList(config[key], pathKey);
  1454. } else {
  1455. confPathList.push(pathKey);
  1456. }
  1457. });
  1458. }
  1459. getConfPathList(config);
  1460.  
  1461. return confPathList
  1462. }
  1463.  
  1464. /**
  1465. * 根据给定的配置路径,获取相关配置信息
  1466. * 获取顺序:LocalStorage > GlobalStorage > defConfig > null
  1467. * @param {String} confPath -必选,配置路径信息:例如:'enhance.blockSetPlaybackRate'
  1468. * @returns {*} 如果返回null,则表示没获取到相关配置信息
  1469. */
  1470. get (confPath) {
  1471. if (typeof confPath !== 'string') {
  1472. return null
  1473. }
  1474.  
  1475. /* 默认优先使用本地的localStorage配置 */
  1476. const localConf = this.getLocalStorage(confPath);
  1477. if (localConf !== null && localConf !== undefined) {
  1478. return localConf
  1479. }
  1480.  
  1481. /* 如果localStorage没相关配置,则尝试使用GlobalStorage的配置 */
  1482. const globalConf = this.getGlobalStorage(confPath);
  1483. if (globalConf !== null && globalConf !== undefined) {
  1484. return globalConf
  1485. }
  1486.  
  1487. /* 如果localStorage和GlobalStorage配置都没找到,则尝试在默认配置表里拿相关配置信息 */
  1488. const config = this.getConfObj();
  1489. const defConfVal = getValByPath(config, confPath);
  1490. if (typeof defConfVal !== 'undefined' && defConfVal !== null) {
  1491. return defConfVal
  1492. }
  1493.  
  1494. return null
  1495. }
  1496.  
  1497. /**
  1498. * 将配置结果写入到localStorage或GlobalStorage
  1499. * 写入顺序:LocalStorage > GlobalStorage
  1500. * 无论是否写入成功都会将结果更新到defConfig里对应的配置项上
  1501. * @param {String} confPath
  1502. * @param {*} val
  1503. * @returns {Boolean}
  1504. */
  1505. set (confPath, val) {
  1506. if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) {
  1507. return false
  1508. }
  1509.  
  1510. setValByPath(this.opts.config, confPath, val);
  1511.  
  1512. let sucStatus = false;
  1513.  
  1514. sucStatus = this.setLocalStorage(confPath, val);
  1515.  
  1516. if (!sucStatus) {
  1517. sucStatus = this.setGlobalStorage(confPath, val);
  1518. }
  1519.  
  1520. return sucStatus
  1521. }
  1522.  
  1523. /* 获取并列出当前所有已设定的配置项 */
  1524. list () {
  1525. const result = {
  1526. localConf: this.listLocalStorage(),
  1527. globalConf: this.listGlobalStorage(),
  1528. defConfig: this.opts.config
  1529. };
  1530. return result
  1531. }
  1532.  
  1533. /* 清除已经写入到本地存储里的配置项 */
  1534. clear () {
  1535. this.clearLocalStorage();
  1536. this.clearGlobalStorage();
  1537. }
  1538.  
  1539. /**
  1540. * 根据给定的配置路径,获取LocalStorage下定义的配置信息
  1541. * @param {String} confPath -必选,配置路径信息
  1542. * @returns
  1543. */
  1544. getLocalStorage (confPath) {
  1545. if (typeof confPath !== 'string') {
  1546. return null
  1547. }
  1548.  
  1549. const key = this.getConfKeyName(confPath);
  1550.  
  1551. if (isLocalStorageUsable()) {
  1552. let localConf = rawLocalStorage.getItem(key);
  1553. if (localConf !== null && localConf !== undefined) {
  1554. try {
  1555. localConf = JSON.parse(localConf);
  1556. } catch (e) {
  1557. console.error('configManager parse localStorage error:', key, localConf);
  1558. }
  1559.  
  1560. return localConf
  1561. }
  1562. }
  1563.  
  1564. return null
  1565. }
  1566.  
  1567. /**
  1568. * 根据给定的配置路径,获取GlobalStorage下定义的配置信息
  1569. * @param {String} confPath -必选,配置路径信息
  1570. * @returns
  1571. */
  1572. getGlobalStorage (confPath) {
  1573. if (typeof confPath !== 'string') {
  1574. return null
  1575. }
  1576.  
  1577. const key = this.getConfKeyName(confPath);
  1578.  
  1579. if (isGlobalStorageUsable()) {
  1580. const globalConf = window.GM_getValue(key);
  1581. if (globalConf !== null && globalConf !== undefined) {
  1582. return globalConf
  1583. }
  1584. }
  1585.  
  1586. return null
  1587. }
  1588.  
  1589. /**
  1590. * 将配置结果写入到localStorage里
  1591. * @param {String} confPath
  1592. * @param {*} val
  1593. * @returns {Boolean}
  1594. */
  1595. setLocalStorage (confPath, val) {
  1596. if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) {
  1597. return false
  1598. }
  1599.  
  1600. setValByPath(this.opts.config, confPath, val);
  1601.  
  1602. const key = this.getConfKeyName(confPath);
  1603.  
  1604. if (isLocalStorageUsable()) {
  1605. try {
  1606. if (Object.prototype.toString.call(val) === '[object Object]' || Array.isArray(val)) {
  1607. val = JSON.stringify(val);
  1608. }
  1609.  
  1610. rawLocalStorage.setItem(key, val);
  1611.  
  1612. return true
  1613. } catch (e) {
  1614. console.error('configManager set localStorage error:', key, val, e);
  1615. return false
  1616. }
  1617. } else {
  1618. return false
  1619. }
  1620. }
  1621.  
  1622. /**
  1623. * 将配置结果写入到globalStorage里
  1624. * @param {String} confPath
  1625. * @param {*} val
  1626. * @returns {Boolean}
  1627. */
  1628. setGlobalStorage (confPath, val) {
  1629. if (typeof confPath !== 'string' || typeof val === 'undefined' || val === null) {
  1630. return false
  1631. }
  1632.  
  1633. setValByPath(this.opts.config, confPath, val);
  1634.  
  1635. const key = this.getConfKeyName(confPath);
  1636.  
  1637. if (isGlobalStorageUsable()) {
  1638. try {
  1639. window.GM_setValue(key, val);
  1640. return true
  1641. } catch (e) {
  1642. console.error('configManager set globalStorage error:', key, val, e);
  1643. return false
  1644. }
  1645. } else {
  1646. return false
  1647. }
  1648. }
  1649.  
  1650. listLocalStorage () {
  1651. if (isLocalStorageUsable()) {
  1652. const result = {};
  1653. Object.keys(localStorage).forEach((key) => {
  1654. if (key.startsWith(this.opts.prefix)) {
  1655. const confPath = this.getConfPath(key);
  1656. result[confPath] = this.getLocalStorage(confPath);
  1657. }
  1658. });
  1659. return result
  1660. } else {
  1661. return {}
  1662. }
  1663. }
  1664.  
  1665. listGlobalStorage () {
  1666. if (isGlobalStorageUsable()) {
  1667. const result = {};
  1668. const globalStorage = window.GM_listValues();
  1669. globalStorage.forEach((key) => {
  1670. if (key.startsWith(this.opts.prefix)) {
  1671. const confPath = this.getConfPath(key);
  1672. result[confPath] = this.getGlobalStorage(confPath);
  1673. }
  1674. });
  1675. return result
  1676. } else {
  1677. return {}
  1678. }
  1679. }
  1680.  
  1681. getConfObj () {
  1682. const confList = this.list();
  1683.  
  1684. /* 同步全局配置到this.opts.config */
  1685. Object.keys(confList.globalConf).forEach((confPath) => {
  1686. setValByPath(this.opts.config, confPath, confList.globalConf[confPath]);
  1687. });
  1688.  
  1689. /* 同步本地配置到this.opts.config */
  1690. Object.keys(confList.localConf).forEach((confPath) => {
  1691. setValByPath(this.opts.config, confPath, confList.localConf[confPath]);
  1692. });
  1693.  
  1694. return this.opts.config
  1695. }
  1696.  
  1697. setLocalStorageByObj (config) {
  1698. const oldConfig = this.getConfObj();
  1699. const confPathList = this.getConfPathList(config);
  1700. confPathList.forEach((confPath) => {
  1701. const oldVal = getValByPath(oldConfig, confPath);
  1702. const val = getValByPath(config, confPath);
  1703.  
  1704. /* 跳过一样的值或在旧配置中不存在的值 */
  1705. if (oldVal === val || oldVal === undefined) {
  1706. return
  1707. }
  1708.  
  1709. this.setLocalStorage(confPath, val);
  1710. });
  1711. }
  1712.  
  1713. setGlobalStorageByObj (config) {
  1714. const oldConfig = this.getConfObj();
  1715. const confPathList = this.getConfPathList(config);
  1716. confPathList.forEach((confPath) => {
  1717. const oldVal = getValByPath(oldConfig, confPath);
  1718. const val = getValByPath(config, confPath);
  1719.  
  1720. /* 跳过一样的值或在旧配置中不存在的值 */
  1721.  
  1722. if (oldVal === val || oldVal === undefined) {
  1723. return
  1724. }
  1725.  
  1726. console.log('setGlobalStorageByObj', confPath, val);
  1727.  
  1728. this.setGlobalStorage(confPath, val);
  1729. });
  1730. }
  1731.  
  1732. clearLocalStorage () {
  1733. if (isLocalStorageUsable()) {
  1734. Object.keys(localStorage).forEach((key) => {
  1735. if (key.startsWith(this.opts.prefix)) {
  1736. rawLocalStorage.removeItem(key);
  1737. }
  1738. });
  1739. }
  1740. }
  1741.  
  1742. clearGlobalStorage () {
  1743. if (isGlobalStorageUsable()) {
  1744. const globalStorage = window.GM_listValues();
  1745. globalStorage.forEach((key) => {
  1746. if (key.startsWith(this.opts.prefix)) {
  1747. window.GM_deleteValue(key);
  1748. }
  1749. });
  1750. }
  1751. }
  1752.  
  1753. mergeDefConf (conf) {
  1754. return mergeObj(this.opts.config, conf)
  1755. }
  1756. }
  1757.  
  1758. /* 使用示例: */
  1759. // const myConfig = new ConfigManager({
  1760. // prefix: '_myConfig_',
  1761. // config: {
  1762. // hotkeys: [
  1763. // {
  1764. // desc: '测试',
  1765. // key: 'v',
  1766. // command: 'toogleVisible',
  1767. // /* 如需禁用快捷键,将disabled设为true */
  1768. // disabled: false,
  1769. // },
  1770. // ],
  1771. // enable: true,
  1772. // debug: false,
  1773. // },
  1774. // })
  1775. // myConfig.set('enable', false)
  1776. // /* 对于数组,暂不支持直接修改数组元素,需要先获取数组,再修改数组元素,再重新写入 */
  1777. // const hotkeys = myConfig.get('hotkeys')
  1778. // hotkeys[0].disabled = true
  1779. // myConfig.set('hotkeys', hotkeys)
  1780.  
  1781. const configManager = new ConfigManager({
  1782. prefix: '_h5player_',
  1783. config: {
  1784. enable: true,
  1785. media: {
  1786. autoPlay: false,
  1787. playbackRate: 1,
  1788. volume: 1,
  1789.  
  1790. /* 是否允许存储播放进度 */
  1791. allowRestorePlayProgress: {
  1792.  
  1793. },
  1794. /* 视频播放进度映射表 */
  1795. progress: {}
  1796. },
  1797. hotkeys: [
  1798. {
  1799. desc: '网页全屏',
  1800. key: 'shift+enter',
  1801. command: 'setWebFullScreen',
  1802. /* 如需禁用快捷键,将disabled设为true */
  1803. disabled: false
  1804. },
  1805. {
  1806. desc: '全屏',
  1807. key: 'enter',
  1808. command: 'setFullScreen'
  1809. },
  1810. {
  1811. desc: '切换画中画模式',
  1812. key: 'shift+p',
  1813. command: 'togglePictureInPicture'
  1814. },
  1815. {
  1816. desc: '视频截图',
  1817. key: 'shift+s',
  1818. command: 'capture'
  1819. },
  1820. {
  1821. desc: '启用或禁止自动恢复播放进度功能',
  1822. key: 'shift+r',
  1823. command: 'capture'
  1824. },
  1825. {
  1826. desc: '垂直镜像翻转',
  1827. key: 'shift+m',
  1828. command: 'setMirror',
  1829. args: [true]
  1830. },
  1831. {
  1832. desc: '水平镜像翻转',
  1833. key: 'm',
  1834. command: 'setMirror'
  1835. },
  1836. {
  1837. desc: '下载音视频文件(实验性功能)',
  1838. key: 'shift+d',
  1839. command: 'mediaDownload'
  1840. },
  1841. {
  1842. desc: '缩小视频画面 -0.05',
  1843. key: 'shift+x',
  1844. command: 'setScaleDown'
  1845. },
  1846. {
  1847. desc: '放大视频画面 +0.05',
  1848. key: 'shift+c',
  1849. command: 'setScaleUp'
  1850. },
  1851. {
  1852. desc: '恢复视频画面',
  1853. key: 'shift+z',
  1854. command: 'resetTransform'
  1855. },
  1856. {
  1857. desc: '画面向右移动10px',
  1858. key: 'shift+arrowright',
  1859. command: 'setTranslateRight'
  1860. },
  1861. {
  1862. desc: '画面向左移动10px',
  1863. key: 'shift+arrowleft',
  1864. command: 'setTranslateLeft'
  1865. },
  1866. {
  1867. desc: '画面向上移动10px',
  1868. key: 'shift+arrowup',
  1869. command: 'setTranslateUp'
  1870. },
  1871. {
  1872. desc: '画面向下移动10px',
  1873. key: 'shift+arrowdown',
  1874. command: 'setTranslateDown'
  1875. },
  1876. {
  1877. desc: '前进5秒',
  1878. key: 'arrowright',
  1879. command: 'setCurrentTimeUp'
  1880. },
  1881. {
  1882. desc: '后退5秒',
  1883. key: 'arrowleft',
  1884. command: 'setCurrentTimeDown'
  1885. },
  1886. {
  1887. desc: '前进30秒',
  1888. key: 'ctrl+arrowright',
  1889. command: 'setCurrentTimeUp',
  1890. args: [30]
  1891. },
  1892. {
  1893. desc: '后退30秒',
  1894. key: 'ctrl+arrowleft',
  1895. command: 'setCurrentTimeDown',
  1896. args: [-30]
  1897. },
  1898. {
  1899. desc: '音量升高 5%',
  1900. key: 'arrowup',
  1901. command: 'setVolumeUp',
  1902. args: [0.05]
  1903. },
  1904. {
  1905. desc: '音量降低 5%',
  1906. key: 'arrowdown',
  1907. command: 'setVolumeDown',
  1908. args: [-0.05]
  1909. },
  1910. {
  1911. desc: '音量升高 20%',
  1912. key: 'ctrl+arrowup',
  1913. command: 'setVolumeUp',
  1914. args: [0.2]
  1915. },
  1916. {
  1917. desc: '音量降低 20%',
  1918. key: 'ctrl+arrowdown',
  1919. command: 'setVolumeDown',
  1920. args: [-0.2]
  1921. },
  1922. {
  1923. desc: '切换暂停/播放',
  1924. key: 'space',
  1925. command: 'switchPlayStatus'
  1926. },
  1927. {
  1928. desc: '减速播放 -0.1',
  1929. key: 'x',
  1930. command: 'setPlaybackRateDown'
  1931. },
  1932. {
  1933. desc: '加速播放 +0.1',
  1934. key: 'c',
  1935. command: 'setPlaybackRateUp'
  1936. },
  1937. {
  1938. desc: '正常速度播放',
  1939. key: 'z',
  1940. command: 'resetPlaybackRate'
  1941. },
  1942. {
  1943. desc: '设置1x的播放速度',
  1944. key: 'Digit1',
  1945. command: 'setPlaybackRatePlus',
  1946. args: 1
  1947. },
  1948. {
  1949. desc: '设置1x的播放速度',
  1950. key: 'Numpad1',
  1951. command: 'setPlaybackRatePlus',
  1952. args: 1
  1953. },
  1954. {
  1955. desc: '设置2x的播放速度',
  1956. key: 'Digit2',
  1957. command: 'setPlaybackRatePlus',
  1958. args: 2
  1959. },
  1960. {
  1961. desc: '设置2x的播放速度',
  1962. key: 'Numpad2',
  1963. command: 'setPlaybackRatePlus',
  1964. args: 2
  1965. },
  1966. {
  1967. desc: '设置3x的播放速度',
  1968. key: 'Digit3',
  1969. command: 'setPlaybackRatePlus',
  1970. args: 3
  1971. },
  1972. {
  1973. desc: '设置3x的播放速度',
  1974. key: 'Numpad3',
  1975. command: 'setPlaybackRatePlus',
  1976. args: 3
  1977. },
  1978. {
  1979. desc: '设置4x的播放速度',
  1980. key: 'Digit4',
  1981. command: 'setPlaybackRatePlus',
  1982. args: 4
  1983. },
  1984. {
  1985. desc: '设置4x的播放速度',
  1986. key: 'Numpad4',
  1987. command: 'setPlaybackRatePlus',
  1988. args: 4
  1989. },
  1990. {
  1991. desc: '下一帧',
  1992. key: 'F',
  1993. command: 'freezeFrame',
  1994. args: 1
  1995. },
  1996. {
  1997. desc: '上一帧',
  1998. key: 'D',
  1999. command: 'freezeFrame',
  2000. args: -1
  2001. },
  2002. {
  2003. desc: '增加亮度',
  2004. key: 'E',
  2005. command: 'setBrightnessUp'
  2006. },
  2007. {
  2008. desc: '减少亮度',
  2009. key: 'W',
  2010. command: 'setBrightnessDown'
  2011. },
  2012. {
  2013. desc: '增加对比度',
  2014. key: 'T',
  2015. command: 'setContrastUp'
  2016. },
  2017. {
  2018. desc: '减少对比度',
  2019. key: 'R',
  2020. command: 'setContrastDown'
  2021. },
  2022. {
  2023. desc: '增加饱和度',
  2024. key: 'U',
  2025. command: 'setSaturationUp'
  2026. },
  2027. {
  2028. desc: '减少饱和度',
  2029. key: 'Y',
  2030. command: 'setSaturationDown'
  2031. },
  2032. {
  2033. desc: '增加色相',
  2034. key: 'O',
  2035. command: 'setHueUp'
  2036. },
  2037. {
  2038. desc: '减少色相',
  2039. key: 'I',
  2040. command: 'setHueDown'
  2041. },
  2042. {
  2043. desc: '模糊增加 1 px',
  2044. key: 'K',
  2045. command: 'setBlurUp'
  2046. },
  2047. {
  2048. desc: '模糊减少 1 px',
  2049. key: 'J',
  2050. command: 'setBlurDown'
  2051. },
  2052. {
  2053. desc: '图像复位',
  2054. key: 'Q',
  2055. command: 'resetFilterAndTransform'
  2056. },
  2057. {
  2058. desc: '画面旋转 90 度',
  2059. key: 'S',
  2060. command: 'setRotate'
  2061. },
  2062. {
  2063. desc: '播放下一集',
  2064. key: 'N',
  2065. command: 'setNextVideo'
  2066. },
  2067. {
  2068. desc: '执行JS脚本',
  2069. key: 'ctrl+j ctrl+s',
  2070. command: () => {
  2071. alert('自定义JS脚本');
  2072. },
  2073. when: ''
  2074. }
  2075. ],
  2076. enhance: {
  2077. /* 不禁用默认的调速逻辑,则在多个视频切换时,速度很容易被重置,所以该选项默认开启 */
  2078. blockSetPlaybackRate: true,
  2079.  
  2080. blockSetCurrentTime: false,
  2081. blockSetVolume: false,
  2082. allowExperimentFeatures: false,
  2083. allowExternalCustomConfiguration: false,
  2084. /* 是否开启音量增益功能 */
  2085. allowAcousticGain: false,
  2086. /* 是否开启跨域控制 */
  2087. allowCrossOriginControl: true,
  2088. unfoldMenu: false
  2089. },
  2090. debug: false
  2091. }
  2092. });
  2093.  
  2094. async function initUiConfigManager () {
  2095. const isUiConfigPage = location.href.indexOf('h5player.anzz.top/tools/json-editor') > -1;
  2096. const isUiConfigMode = location.href.indexOf('saveHandlerName=saveH5PlayerConfig') > -1;
  2097. if (!isUiConfigPage || !isUiConfigMode) return
  2098.  
  2099. function init (pageWindow) {
  2100. const config = JSON.parse(JSON.stringify(configManager.getConfObj()));
  2101. if (Array.isArray(config.hotkeys)) {
  2102. /* 给hotkeys的各自项添加disabled选项,以便在界面侧可以快速禁用或启用某个项 */
  2103. config.hotkeys.forEach(item => {
  2104. if (item.disabled === undefined) {
  2105. item.disabled = false;
  2106. }
  2107. });
  2108. }
  2109.  
  2110. pageWindow.jsonEditor.set(config);
  2111.  
  2112. // pageWindow.jsonEditor.collapseAll()
  2113. pageWindow.jsonEditor.expandAll();
  2114.  
  2115. pageWindow.saveH5PlayerConfig = function (editor) {
  2116. try {
  2117. const newConfig = editor.get();
  2118. configManager.setGlobalStorageByObj(newConfig);
  2119. alert('配置已更新');
  2120. } catch (e) {
  2121. alert(`配置格式异常,保存失败:${e}`);
  2122. }
  2123. };
  2124. }
  2125.  
  2126. let checkCount = 0;
  2127. function checkJSONEditor (pageWindow) {
  2128. if (!pageWindow.JSONEditor) {
  2129. if (checkCount < 30) {
  2130. setTimeout(() => {
  2131. checkCount++;
  2132. checkJSONEditor(pageWindow);
  2133. }, 200);
  2134. }
  2135.  
  2136. return
  2137. }
  2138.  
  2139. init(pageWindow);
  2140. }
  2141.  
  2142. const pageWindow = await getPageWindow();
  2143.  
  2144. if (!pageWindow) {
  2145. return
  2146. }
  2147.  
  2148. checkJSONEditor(pageWindow);
  2149. }
  2150. initUiConfigManager();
  2151.  
  2152. /* 保存重要的原始函数,防止被外部脚本污染 */
  2153. const originalMethods = {
  2154. Object: {
  2155. defineProperty: Object.defineProperty,
  2156. defineProperties: Object.defineProperties
  2157. },
  2158. setInterval: window.setInterval,
  2159. setTimeout: window.setTimeout
  2160. };
  2161.  
  2162. /**
  2163. * 任务配置中心 Task Control Center
  2164. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  2165. **/
  2166.  
  2167. class TCC {
  2168. constructor (taskConf, doTaskFunc) {
  2169. this.conf = taskConf || {
  2170. /**
  2171. * 配置示例
  2172. * 父级键名对应的是一级域名,
  2173. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  2174. * 所有子级的键值都支持使用选择器触发或函数调用
  2175. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  2176. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  2177. * */
  2178. 'demo.demo': {
  2179. fullScreen: '.fullscreen-btn',
  2180. exitFullScreen: '.exit-fullscreen-btn',
  2181. webFullScreen: function () {},
  2182. exitWebFullScreen: '.exit-fullscreen-btn',
  2183. autoPlay: '.player-start-btn',
  2184. pause: '.player-pause',
  2185. play: '.player-play',
  2186. switchPlayStatus: '.player-play',
  2187. playbackRate: function () {},
  2188. currentTime: function () {},
  2189. addCurrentTime: '.add-currenttime',
  2190. subtractCurrentTime: '.subtract-currenttime',
  2191. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  2192. shortcuts: {
  2193. /* 注册要执行自定义回调操作的快捷键 */
  2194. register: [
  2195. 'ctrl+shift+alt+c',
  2196. 'ctrl+shift+c',
  2197. 'ctrl+alt+c',
  2198. 'ctrl+c',
  2199. 'c'
  2200. ],
  2201. /* 自定义快捷键的回调操作 */
  2202. callback: function (h5Player, taskConf, data) {
  2203. const { event, player } = data;
  2204. console.log(event, player);
  2205. }
  2206. },
  2207. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  2208. include: /^.*/,
  2209. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  2210. exclude: /\t/
  2211. }
  2212. };
  2213.  
  2214. // 通过doTaskFunc回调定义配置该如何执行任务
  2215. this.doTaskFunc = doTaskFunc instanceof Function ? doTaskFunc : function () {};
  2216. }
  2217.  
  2218. setTaskConf (taskConf) { this.conf = taskConf; }
  2219.  
  2220. /**
  2221. * 获取域名 , 目前实现方式不好,需改造,对地区性域名(如com.cn)、三级及以上域名支持不好
  2222. * */
  2223. getDomain () {
  2224. const host = window.location.host;
  2225. let domain = host;
  2226. const tmpArr = host.split('.');
  2227. if (tmpArr.length > 2) {
  2228. tmpArr.shift();
  2229. domain = tmpArr.join('.');
  2230. }
  2231. return domain
  2232. }
  2233.  
  2234. /**
  2235. * 格式化配置任务
  2236. * @param isAll { boolean } -可选 默认只格式当前域名或host下的配置任务,传入true则将所有域名下的任务配置都进行格式化
  2237. */
  2238. formatTCC (isAll) {
  2239. const t = this;
  2240. const keys = Object.keys(t.conf);
  2241. const domain = t.getDomain();
  2242. const host = window.location.host;
  2243.  
  2244. function formatter (item) {
  2245. const defObj = {
  2246. include: /^.*/,
  2247. exclude: /\t/
  2248. };
  2249. item.include = item.include || defObj.include;
  2250. item.exclude = item.exclude || defObj.exclude;
  2251. return item
  2252. }
  2253.  
  2254. const result = {};
  2255. keys.forEach(function (key) {
  2256. let item = t[key];
  2257. if (isObj(item)) {
  2258. if (isAll) {
  2259. item = formatter(item);
  2260. result[key] = item;
  2261. } else {
  2262. if (key === host || key === domain) {
  2263. item = formatter(item);
  2264. result[key] = item;
  2265. }
  2266. }
  2267. }
  2268. });
  2269. return result
  2270. }
  2271.  
  2272. /* 判断所提供的配置任务是否适用于当前URL */
  2273. isMatch (taskConf) {
  2274. const url = window.location.href;
  2275. let isMatch = false;
  2276. if (!taskConf.include && !taskConf.exclude) {
  2277. isMatch = true;
  2278. } else {
  2279. if (taskConf.include && taskConf.include.test(url)) {
  2280. isMatch = true;
  2281. }
  2282. if (taskConf.exclude && taskConf.exclude.test(url)) {
  2283. isMatch = false;
  2284. }
  2285. }
  2286. return isMatch
  2287. }
  2288.  
  2289. /**
  2290. * 获取任务配置,只能获取到当前域名下的任务配置信息
  2291. * @param taskName {string} -可选 指定具体任务,默认返回所有类型的任务配置
  2292. */
  2293. getTaskConfig () {
  2294. const t = this;
  2295. if (!t._hasFormatTCC_) {
  2296. t.formatTCC();
  2297. t._hasFormatTCC_ = true;
  2298. }
  2299. const domain = t.getDomain();
  2300. const taskConf = t.conf[window.location.host] || t.conf[domain];
  2301.  
  2302. if (taskConf && t.isMatch(taskConf)) {
  2303. return taskConf
  2304. }
  2305.  
  2306. return {}
  2307. }
  2308.  
  2309. /**
  2310. * 执行当前页面下的相应任务
  2311. * @param taskName {object|string} -必选,可直接传入任务配置对象,也可用是任务名称的字符串信息,自己去查找是否有任务需要执行
  2312. * @param data {object} -可选,传给回调函数的数据
  2313. */
  2314. doTask (taskName, data) {
  2315. const t = this;
  2316. let isDo = false;
  2317. if (!taskName) return isDo
  2318. const taskConf = isObj(taskName) ? taskName : t.getTaskConfig();
  2319.  
  2320. if (!isObj(taskConf) || !taskConf[taskName]) return isDo
  2321.  
  2322. const task = taskConf[taskName];
  2323.  
  2324. if (task) {
  2325. isDo = t.doTaskFunc(taskName, taskConf, data);
  2326. }
  2327.  
  2328. return isDo
  2329. }
  2330. }
  2331.  
  2332. class Debug {
  2333. constructor (msg, printTime = false) {
  2334. const t = this;
  2335. msg = msg || 'debug message:';
  2336. t.log = t.createDebugMethod('log', null, msg);
  2337. t.error = t.createDebugMethod('error', null, msg);
  2338. t.info = t.createDebugMethod('info', null, msg);
  2339. t.warn = t.createDebugMethod('warn', null, msg);
  2340. }
  2341.  
  2342. create (msg) {
  2343. return new Debug(msg)
  2344. }
  2345.  
  2346. createDebugMethod (name, color, tipsMsg) {
  2347. name = name || 'info';
  2348.  
  2349. const bgColorMap = {
  2350. info: '#2274A5',
  2351. log: '#95B46A',
  2352. warn: '#F5A623',
  2353. error: '#D33F49'
  2354. };
  2355.  
  2356. const printTime = this.printTime;
  2357.  
  2358. return function () {
  2359. if (!window._debugMode_) {
  2360. return false
  2361. }
  2362.  
  2363. const msg = tipsMsg || 'debug message:';
  2364.  
  2365. const arg = Array.from(arguments);
  2366. arg.unshift(`color: white; background-color: ${color || bgColorMap[name] || '#95B46A'}`);
  2367.  
  2368. if (printTime) {
  2369. const curTime = new Date();
  2370. const H = curTime.getHours();
  2371. const M = curTime.getMinutes();
  2372. const S = curTime.getSeconds();
  2373. arg.unshift(`%c [${H}:${M}:${S}] ${msg} `);
  2374. } else {
  2375. arg.unshift(`%c ${msg} `);
  2376. }
  2377.  
  2378. window.console[name].apply(window.console, arg);
  2379. }
  2380. }
  2381.  
  2382. isDebugMode () {
  2383. return Boolean(window._debugMode_)
  2384. }
  2385. }
  2386.  
  2387. var Debug$1 = new Debug();
  2388.  
  2389. var debug = Debug$1.create('h5player message:');
  2390.  
  2391. const $q = function (str) { return document.querySelector(str) };
  2392.  
  2393. /**
  2394. * 任务配置中心 Task Control Center
  2395. * 用于配置所有无法进行通用处理的任务,如不同网站的全屏方式不一样,必须调用网站本身的全屏逻辑,才能确保字幕、弹幕等正常工作
  2396. * */
  2397.  
  2398. const taskConf = {
  2399. /**
  2400. * 配置示例
  2401. * 父级键名对应的是一级域名,
  2402. * 子级键名对应的相关功能名称,键值对应的该功能要触发的点击选择器或者要调用的相关函数
  2403. * 所有子级的键值都支持使用选择器触发或函数调用
  2404. * 配置了子级的则使用子级配置逻辑进行操作,否则使用默认逻辑
  2405. * 注意:include,exclude这两个子级键名除外,这两个是用来进行url范围匹配的
  2406. * */
  2407. 'demo.demo': {
  2408. // disable: true, // 在该域名下禁止插件的所有功能
  2409. fullScreen: '.fullscreen-btn',
  2410. exitFullScreen: '.exit-fullscreen-btn',
  2411. webFullScreen: function () {},
  2412. exitWebFullScreen: '.exit-fullscreen-btn',
  2413. autoPlay: '.player-start-btn',
  2414. // pause: ['.player-pause', '.player-pause02'], //多种情况对应不同的选择器时,可使用数组,插件会对选择器进行遍历,知道找到可用的为止
  2415. pause: '.player-pause',
  2416. play: '.player-play',
  2417. afterPlay: function (h5Player, taskConf) {},
  2418. afterPause: function (h5Player, taskConf) {},
  2419. switchPlayStatus: '.player-play',
  2420. playbackRate: function () {},
  2421. // playbackRate: true, // 当给某个功能设置true时,表示使用网站自身的能力控制视频,而忽略插件的能力
  2422. currentTime: function () {},
  2423. addCurrentTime: '.add-currenttime',
  2424. subtractCurrentTime: '.subtract-currenttime',
  2425. // 自定义快捷键的执行方式,如果是组合键,必须是 ctrl-->shift-->alt 这样的顺序,没有可以忽略,键名必须全小写
  2426. shortcuts: {
  2427. /* 注册要执行自定义回调操作的快捷键 */
  2428. register: [
  2429. 'ctrl+shift+alt+c',
  2430. 'ctrl+shift+c',
  2431. 'ctrl+alt+c',
  2432. 'ctrl+c',
  2433. 'c'
  2434. ],
  2435. /* 自定义快捷键的回调操作 */
  2436. callback: function (h5Player, taskConf, data) {
  2437. const { event, player } = data;
  2438. console.log(event, player);
  2439. }
  2440. },
  2441.  
  2442. /* 阻止网站自身的调速行为,增强突破调速限制的能力 */
  2443. blockSetPlaybackRate: true,
  2444. /* 阻止网站自身的播放进度控制逻辑,增强突破进度调控限制的能力 */
  2445. blockSetCurrentTime: true,
  2446. /* 阻止网站自身的音量控制逻辑,排除网站自身的调音干扰 */
  2447. blockSetVolume: true,
  2448.  
  2449. /* 当前域名下需包含的路径信息,默认整个域名下所有路径可用 必须是正则 */
  2450. include: /^.*/,
  2451. /* 当前域名下需排除的路径信息,默认不排除任何路径 必须是正则 */
  2452. exclude: /\t/
  2453. },
  2454. 'youtube.com': {
  2455. webFullScreen: 'button.ytp-size-button',
  2456. fullScreen: 'button.ytp-fullscreen-button',
  2457. next: '.ytp-next-button',
  2458. afterPlay: function (h5Player, taskConf) {
  2459. /* 解决快捷键暂停、播放后一直有loading图标滞留的问题 */
  2460. const player = h5Player.player();
  2461. const playerwWrap = player.closest('.html5-video-player');
  2462.  
  2463. if (!playerwWrap) {
  2464. return
  2465. }
  2466.  
  2467. playerwWrap.classList.add('ytp-autohide', 'playing-mode');
  2468.  
  2469. if (!playerwWrap.hasBindCustomEvents) {
  2470. const mousemoveHander = (event) => {
  2471. playerwWrap.classList.remove('ytp-autohide', 'ytp-hide-info-bar');
  2472.  
  2473. clearTimeout(playerwWrap.mousemoveTimer);
  2474. playerwWrap.mousemoveTimer = setTimeout(() => {
  2475. playerwWrap.classList.add('ytp-autohide', 'ytp-hide-info-bar');
  2476. }, 1000 * 2);
  2477. };
  2478.  
  2479. const clickHander = (event) => {
  2480. h5Player.switchPlayStatus();
  2481. mousemoveHander();
  2482. };
  2483.  
  2484. player.addEventListener('mousemove', mousemoveHander);
  2485. player.addEventListener('click', clickHander);
  2486.  
  2487. playerwWrap.hasBindCustomEvents = true;
  2488. }
  2489.  
  2490. const spinner = playerwWrap.querySelector('.ytp-spinner');
  2491.  
  2492. if (spinner) {
  2493. const hiddenSpinner = () => { spinner && (spinner.style.visibility = 'hidden'); };
  2494. const visibleSpinner = () => { spinner && (spinner.style.visibility = 'visible'); };
  2495.  
  2496. /* 点击播放时立即隐藏spinner */
  2497. hiddenSpinner();
  2498.  
  2499. clearTimeout(playerwWrap.spinnerTimer);
  2500. playerwWrap.spinnerTimer = setTimeout(() => {
  2501. /* 1秒后将spinner设置为none,并且恢复Spinner的可见状态,以便其它逻辑仍能正确控制spinner的显隐状态 */
  2502. spinner.style.display = 'none';
  2503. visibleSpinner();
  2504. }, 1000);
  2505. }
  2506. },
  2507. afterPause: function (h5Player, taskConf) {
  2508. const player = h5Player.player();
  2509. const playerwWrap = player.closest('.html5-video-player');
  2510.  
  2511. if (!playerwWrap) return
  2512.  
  2513. playerwWrap.classList.remove('ytp-autohide', 'playing-mode');
  2514. playerwWrap.classList.add('paused-mode');
  2515. },
  2516. shortcuts: {
  2517. register: [
  2518. 'escape'
  2519. ],
  2520. callback: function (h5Player, taskConf, data) {
  2521. const { event } = data;
  2522. if (event.keyCode === 27) {
  2523. /* 取消播放下一个推荐的视频 */
  2524. if (document.querySelector('.ytp-upnext').style.display !== 'none') {
  2525. document.querySelector('.ytp-upnext-cancel-button').click();
  2526. }
  2527. }
  2528. }
  2529. }
  2530. },
  2531. 'netflix.com': {
  2532. // 停止在netflix下使用插件的所有功能
  2533. // disable: true,
  2534. fullScreen: 'button.button-nfplayerFullscreen',
  2535. addCurrentTime: 'button.button-nfplayerFastForward',
  2536. subtractCurrentTime: 'button.button-nfplayerBackTen',
  2537. /**
  2538. * 使用netflix自身的调速,因为目前插件没法解决调速导致的服务中断问题
  2539. * https://github.com/xxxily/h5player/issues/234
  2540. * https://github.com/xxxily/h5player/issues/317
  2541. * https://github.com/xxxily/h5player/issues/381
  2542. * https://github.com/xxxily/h5player/issues/179
  2543. * https://github.com/xxxily/h5player/issues/147
  2544. */
  2545. playbackRate: true,
  2546. shortcuts: {
  2547. /**
  2548. * TODO
  2549. * netflix 一些用户习惯使用F键进行全屏,所以此处屏蔽掉f键的下一帧功能
  2550. * 后续开放自定义配置能力后,让用户自行决定是否屏蔽
  2551. */
  2552. register: [
  2553. 'f'
  2554. ],
  2555. callback: function (h5Player, taskConf, data) {
  2556. return true
  2557. }
  2558. }
  2559. },
  2560. 'bilibili.com': {
  2561. fullScreen: function () {
  2562. const fullScreen = $q('.bpx-player-ctrl-full') || $q('.squirtle-video-fullscreen') || $q('.bilibili-player-video-btn-fullscreen');
  2563. if (fullScreen) {
  2564. fullScreen.click();
  2565. return true
  2566. }
  2567. },
  2568. webFullScreen: function () {
  2569. const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen');
  2570. const webFullscreenEnter = $q('.bpx-player-ctrl-web-enter') || $q('.squirtle-pagefullscreen-inactive');
  2571. const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active');
  2572. if (oldWebFullscreen || (webFullscreenEnter && webFullscreenLeave)) {
  2573. const webFullscreen = oldWebFullscreen || (getComputedStyle(webFullscreenLeave).display === 'none' ? webFullscreenEnter : webFullscreenLeave);
  2574. webFullscreen.click();
  2575.  
  2576. /* 取消弹幕框聚焦,干扰了快捷键的操作 */
  2577. setTimeout(function () {
  2578. const danmaku = $q('.bpx-player-dm-input') || $q('.bilibili-player-video-danmaku-input');
  2579. danmaku && danmaku.blur();
  2580. }, 1000 * 0.1);
  2581.  
  2582. return true
  2583. }
  2584. },
  2585. autoPlay: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'],
  2586. switchPlayStatus: ['.bpx-player-ctrl-play', '.squirtle-video-start', '.bilibili-player-video-btn-start'],
  2587. next: ['.bpx-player-ctrl-next', '.squirtle-video-next', '.bilibili-player-video-btn-next', '.bpx-player-ctrl-btn[aria-label="下一个"]'],
  2588. init: function (h5Player, taskConf) {},
  2589. shortcuts: {
  2590. register: [
  2591. 'escape'
  2592. ],
  2593. callback: function (h5Player, taskConf, data) {
  2594. const { event } = data;
  2595. if (event.keyCode === 27) {
  2596. /* 退出网页全屏 */
  2597. const oldWebFullscreen = $q('.bilibili-player-video-web-fullscreen');
  2598. if (oldWebFullscreen && oldWebFullscreen.classList.contains('closed')) {
  2599. oldWebFullscreen.click();
  2600. } else {
  2601. const webFullscreenLeave = $q('.bpx-player-ctrl-web-leave') || $q('.squirtle-pagefullscreen-active');
  2602. if (getComputedStyle(webFullscreenLeave).display !== 'none') {
  2603. webFullscreenLeave.click();
  2604. }
  2605. }
  2606. }
  2607. }
  2608. }
  2609. },
  2610. 't.bilibili.com': {
  2611. fullScreen: 'button[name="fullscreen-button"]'
  2612. },
  2613. 'live.bilibili.com': {
  2614. init: function () {
  2615. if (!JSON._stringifySource_) {
  2616. JSON._stringifySource_ = JSON.stringify;
  2617.  
  2618. JSON.stringify = function (arg1) {
  2619. try {
  2620. return JSON._stringifySource_.apply(this, arguments)
  2621. } catch (e) {
  2622. console.error('JSON.stringify 解释出错:', e, arg1);
  2623. }
  2624. };
  2625. }
  2626. },
  2627. fullScreen: '.bilibili-live-player-video-controller-fullscreen-btn button',
  2628. webFullScreen: '.bilibili-live-player-video-controller-web-fullscreen-btn button',
  2629. switchPlayStatus: '.bilibili-live-player-video-controller-start-btn button'
  2630. },
  2631. 'acfun.cn': {
  2632. fullScreen: '[data-bind-key="screenTip"]',
  2633. webFullScreen: '[data-bind-key="webTip"]',
  2634. switchPlayStatus: function (h5player) {
  2635. /* 无法抢得控制权,只好延迟判断要不要干预 */
  2636. const player = h5player.player();
  2637. const status = player.paused;
  2638. setTimeout(function () {
  2639. if (status === player.paused) {
  2640. if (player.paused) {
  2641. player.play();
  2642. } else {
  2643. player.pause();
  2644. }
  2645. }
  2646. }, 200);
  2647. }
  2648. },
  2649. 'ixigua.com': {
  2650. fullScreen: ['xg-fullscreen.xgplayer-fullscreen', '.xgplayer-control-item__entry[aria-label="全屏"]', '.xgplayer-control-item__entry[aria-label="退出全屏"]'],
  2651. webFullScreen: ['xg-cssfullscreen.xgplayer-cssfullscreen', '.xgplayer-control-item__entry[aria-label="剧场模式"]', '.xgplayer-control-item__entry[aria-label="退出剧场模式"]']
  2652. },
  2653. 'tv.sohu.com': {
  2654. fullScreen: 'button[data-title="网页全屏"]',
  2655. webFullScreen: 'button[data-title="全屏"]'
  2656. },
  2657. 'iqiyi.com': {
  2658. fullScreen: '.iqp-btn-fullscreen',
  2659. webFullScreen: '.iqp-btn-webscreen',
  2660. next: '.iqp-btn-next',
  2661. init: function (h5Player, taskConf) {
  2662. // 隐藏水印
  2663. hideDom('.iqp-logo-box');
  2664. // 移除暂停广告
  2665. window.GM_addStyle(`
  2666. div[templatetype="common_pause"]{ display:none }
  2667. .iqp-logo-box{ display:none !important }
  2668. `);
  2669. }
  2670. },
  2671. 'youku.com': {
  2672. fullScreen: '.control-fullscreen-icon',
  2673. next: '.control-next-video',
  2674. init: function (h5Player, taskConf) {
  2675. // 隐藏水印
  2676. hideDom('.youku-layer-logo');
  2677. }
  2678. },
  2679. 'ted.com': {
  2680. fullScreen: 'button.Fullscreen'
  2681. },
  2682. 'qq.com': {
  2683. pause: '.container_inner .txp-shadow-mod',
  2684. play: '.container_inner .txp-shadow-mod',
  2685. shortcuts: {
  2686. register: ['c', 'x', 'z', '1', '2', '3', '4'],
  2687. callback: function (h5Player, taskConf, data) {
  2688. const { event } = data;
  2689. const key = event.key.toLowerCase();
  2690. const keyName = 'customShortcuts_' + key;
  2691.  
  2692. if (!h5Player[keyName]) {
  2693. /* 第一次按下快捷键使用默认逻辑进行调速 */
  2694. h5Player[keyName] = {
  2695. time: Date.now(),
  2696. playbackRate: h5Player.playbackRate
  2697. };
  2698. return false
  2699. } else {
  2700. /* 第一次操作后的200ms内的操作都是由默认逻辑进行调速 */
  2701. if (Date.now() - h5Player[keyName].time < 200) {
  2702. return false
  2703. }
  2704.  
  2705. /* 判断是否需进行降级处理,利用sessionStorage进行调速 */
  2706. if (h5Player[keyName] === h5Player.playbackRate || h5Player[keyName] === true) {
  2707. if (window.sessionStorage.playbackRate && /(c|x|z|1|2|3|4)/.test(key)) {
  2708. const curSpeed = Number(window.sessionStorage.playbackRate);
  2709. const perSpeed = curSpeed - 0.1 >= 0 ? curSpeed - 0.1 : 0.1;
  2710. const nextSpeed = curSpeed + 0.1 <= 4 ? curSpeed + 0.1 : 4;
  2711. let targetSpeed = curSpeed;
  2712. switch (key) {
  2713. case 'z' :
  2714. targetSpeed = 1;
  2715. break
  2716. case 'c' :
  2717. targetSpeed = nextSpeed;
  2718. break
  2719. case 'x' :
  2720. targetSpeed = perSpeed;
  2721. break
  2722. default :
  2723. targetSpeed = Number(key);
  2724. break
  2725. }
  2726.  
  2727. window.sessionStorage.playbackRate = targetSpeed;
  2728. h5Player.setCurrentTime(0.01, true);
  2729. h5Player.setPlaybackRate(targetSpeed, true);
  2730. return true
  2731. }
  2732.  
  2733. /* 标识默认调速方案失效,需启用sessionStorage调速方案 */
  2734. h5Player[keyName] = true;
  2735. } else {
  2736. /* 标识默认调速方案生效 */
  2737. h5Player[keyName] = false;
  2738. }
  2739. }
  2740. }
  2741. },
  2742. fullScreen: 'txpdiv[data-report="window-fullscreen"]',
  2743. webFullScreen: 'txpdiv[data-report="browser-fullscreen"]',
  2744. next: 'txpdiv[data-report="play-next"]',
  2745. init: function (h5Player, taskConf) {
  2746. // 隐藏水印
  2747. hideDom('.txp-watermark');
  2748. hideDom('.txp-watermark-action');
  2749. },
  2750. include: /(v.qq|sports.qq)/
  2751. },
  2752. 'pan.baidu.com': {
  2753. fullScreen: function (h5Player, taskConf) {
  2754. h5Player.player().parentNode.querySelector('.vjs-fullscreen-control').click();
  2755. }
  2756. },
  2757. // 'pornhub.com': {
  2758. // fullScreen: 'div[class*="icon-fullscreen"]',
  2759. // webFullScreen: 'div[class*="icon-size-large"]'
  2760. // },
  2761. 'facebook.com': {
  2762. fullScreen: function (h5Player, taskConf) {
  2763. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  2764. if (actionBtn && actionBtn.length > 3) {
  2765. /* 模拟点击倒数第二个按钮 */
  2766. actionBtn[actionBtn.length - 2].click();
  2767. return true
  2768. }
  2769. },
  2770. webFullScreen: function (h5Player, taskConf) {
  2771. const actionBtn = h5Player.player().parentNode.querySelectorAll('button');
  2772. if (actionBtn && actionBtn.length > 3) {
  2773. /* 模拟点击倒数第二个按钮 */
  2774. actionBtn[actionBtn.length - 2].click();
  2775. return true
  2776. }
  2777. },
  2778. shortcuts: {
  2779. /* 在视频模式下按esc键,自动返回上一层界面 */
  2780. register: [
  2781. 'escape'
  2782. ],
  2783. /* 自定义快捷键的回调操作 */
  2784. callback: function (h5Player, taskConf, data) {
  2785. eachParentNode(h5Player.player(), function (parentNode) {
  2786. if (parentNode.getAttribute('data-fullscreen-container') === 'true') {
  2787. const goBackBtn = parentNode.parentNode.querySelector('div>a>i>u');
  2788. if (goBackBtn) {
  2789. goBackBtn.parentNode.parentNode.click();
  2790. }
  2791. return true
  2792. }
  2793. });
  2794. }
  2795. }
  2796. },
  2797. 'douyu.com': {
  2798. fullScreen: function (h5Player, taskConf) {
  2799. const player = h5Player.player();
  2800. const container = player._fullScreen_.getContainer();
  2801. if (player._isFullScreen_) {
  2802. container.querySelector('div[title="退出窗口全屏"]').click();
  2803. } else {
  2804. container.querySelector('div[title="窗口全屏"]').click();
  2805. }
  2806. player._isFullScreen_ = !player._isFullScreen_;
  2807. return true
  2808. },
  2809. webFullScreen: function (h5Player, taskConf) {
  2810. const player = h5Player.player();
  2811. const container = player._fullScreen_.getContainer();
  2812. if (player._isWebFullScreen_) {
  2813. container.querySelector('div[title="退出网页全屏"]').click();
  2814. } else {
  2815. container.querySelector('div[title="网页全屏"]').click();
  2816. }
  2817. player._isWebFullScreen_ = !player._isWebFullScreen_;
  2818. return true
  2819. }
  2820. },
  2821. 'open.163.com': {
  2822. init: function (h5Player, taskConf) {
  2823. const player = h5Player.player();
  2824. /**
  2825. * 不设置CORS标识,这样才能跨域截图
  2826. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
  2827. * https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_settings_attributes
  2828. */
  2829. player.setAttribute('crossOrigin', 'anonymous');
  2830. }
  2831. },
  2832. 'agefans.tv': {
  2833. init: function (h5Player, taskConf) {
  2834. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  2835. }
  2836. },
  2837. 'chaoxing.com': {
  2838. fullScreen: '.vjs-fullscreen-control'
  2839. },
  2840. 'yixi.tv': {
  2841. init: function (h5Player, taskConf) {
  2842. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  2843. }
  2844. },
  2845. 'douyin.com': {
  2846. fullScreen: '.xgplayer-fullscreen',
  2847. webFullScreen: '.xgplayer-page-full-screen',
  2848. next: ['.xgplayer-playswitch-next'],
  2849. init: function (h5Player, taskConf) {
  2850. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  2851. }
  2852. },
  2853. 'live.douyin.com': {
  2854. fullScreen: '.xgplayer-fullscreen',
  2855. webFullScreen: '.xgplayer-page-full-screen',
  2856. next: ['.xgplayer-playswitch-next'],
  2857. init: function (h5Player, taskConf) {
  2858. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  2859. }
  2860. },
  2861. 'zhihu.com': {
  2862. fullScreen: ['button[aria-label="全屏"]', 'button[aria-label="退出全屏"]'],
  2863. play: function (h5Player, taskConf, data) {
  2864. const player = h5Player.player();
  2865. if (player && player.parentNode && player.parentNode.parentNode) {
  2866. const maskWrap = player.parentNode.parentNode.querySelector('div~div:nth-child(3)');
  2867. if (maskWrap) {
  2868. const mask = maskWrap.querySelector('div');
  2869. if (mask && mask.innerText === '') {
  2870. mask.click();
  2871. }
  2872. }
  2873. }
  2874. },
  2875. init: function (h5Player, taskConf) {
  2876. h5Player.player().setAttribute('crossOrigin', 'anonymous');
  2877. }
  2878. },
  2879. 'weibo.com': {
  2880. fullScreen: ['button.wbpv-fullscreen-control'],
  2881. // webFullScreen: ['div[title="关闭弹层"]', 'div.wbpv-open-layer-button']
  2882. webFullScreen: ['div.wbpv-open-layer-button']
  2883. }
  2884. };
  2885.  
  2886. function h5PlayerTccInit (h5Player) {
  2887. return new TCC(taskConf, function (taskName, taskConf, data) {
  2888. try {
  2889. const task = taskConf[taskName];
  2890. const wrapDom = h5Player.getPlayerWrapDom();
  2891.  
  2892. if (!task) { return }
  2893.  
  2894. if (taskName === 'shortcuts') {
  2895. if (isObj(task) && task.callback instanceof Function) {
  2896. return task.callback(h5Player, taskConf, data)
  2897. }
  2898. } else if (task instanceof Function) {
  2899. try {
  2900. return task(h5Player, taskConf, data)
  2901. } catch (e) {
  2902. debug.error('任务配置中心的自定义函数执行失败:', taskName, taskConf, data, e);
  2903. return false
  2904. }
  2905. } else if (typeof task === 'boolean') {
  2906. return task
  2907. } else {
  2908. const selectorList = Array.isArray(task) ? task : [task];
  2909. for (let i = 0; i < selectorList.length; i++) {
  2910. const selector = selectorList[i];
  2911.  
  2912. /* 触发选择器上的点击事件 */
  2913. if (wrapDom && wrapDom.querySelector(selector)) {
  2914. // 在video的父元素里查找,是为了尽可能兼容多实例下的逻辑
  2915. wrapDom.querySelector(selector).click();
  2916. return true
  2917. } else if (document.querySelector(selector)) {
  2918. document.querySelector(selector).click();
  2919. return true
  2920. }
  2921. }
  2922. }
  2923. } catch (e) {
  2924. debug.error('任务配置中心的自定义任务执行失败:', taskName, taskConf, data, e);
  2925. return false
  2926. }
  2927. })
  2928. }
  2929.  
  2930. function mergeTaskConf (config) {
  2931. return mergeObj(taskConf, config)
  2932. }
  2933.  
  2934. /* ua伪装配置 */
  2935. const fakeConfig = {
  2936. // 'tv.cctv.com': userAgentMap.iPhone.chrome,
  2937. // 'v.qq.com': userAgentMap.iPad.chrome,
  2938. 'open.163.com': userAgentMap.iPhone.chrome,
  2939. 'm.open.163.com': userAgentMap.iPhone.chrome,
  2940. /* 百度盘的非会员会使用自身的专用播放器,导致没法使用h5player,所以需要通过伪装ua来解决该问题 */
  2941. 'pan.baidu.com': userAgentMap.iPhone.safari
  2942. };
  2943.  
  2944. function setFakeUA (ua) {
  2945. const host = window.location.host;
  2946. ua = ua || fakeConfig[host];
  2947.  
  2948. /**
  2949. * 动态判断是否需要进行ua伪装
  2950. * 下面方案暂时不可用
  2951. * 由于部分网站跳转至移动端后域名不一致,形成跨域问题
  2952. * 导致无法同步伪装配置而不断死循环跳转
  2953. * eg. open.163.com
  2954. * */
  2955. // let customUA = window.localStorage.getItem('_h5_player_user_agent_')
  2956. // debug.log(customUA, window.location.href, window.navigator.userAgent, document.referrer)
  2957. // if (customUA) {
  2958. // fakeUA(customUA)
  2959. // alert(customUA)
  2960. // } else {
  2961. // alert('ua false')
  2962. // }
  2963.  
  2964. ua && fakeUA(ua);
  2965. }
  2966.  
  2967. /**
  2968. * 元素全屏API,同时兼容网页全屏
  2969. */
  2970.  
  2971. hackAttachShadow();
  2972. class FullScreen {
  2973. constructor (dom, pageMode) {
  2974. this.dom = dom;
  2975. this.shadowRoot = null;
  2976. this.fullStatus = false;
  2977. // 默认全屏模式,如果传入pageMode则表示进行的是页面全屏操作
  2978. this.pageMode = pageMode || false;
  2979. const fullPageStyle = `
  2980. ._webfullscreen_box_size_ {
  2981. width: 100% !important;
  2982. height: 100% !important;
  2983. }
  2984. ._webfullscreen_ {
  2985. display: block !important;
  2986. position: fixed !important;
  2987. width: 100% !important;
  2988. height: 100% !important;
  2989. top: 0 !important;
  2990. left: 0 !important;
  2991. background: #000 !important;
  2992. z-index: 999999 !important;
  2993. }
  2994. ._webfullscreen_zindex_ {
  2995. z-index: 999999 !important;
  2996. }
  2997. `;
  2998. /* 将样式插入到全局页面中 */
  2999. if (!window._hasInitFullPageStyle_ && window.GM_addStyle) {
  3000. window.GM_addStyle(fullPageStyle);
  3001. window._hasInitFullPageStyle_ = true;
  3002. }
  3003.  
  3004. /* 将样式插入到shadowRoot中 */
  3005. const shadowRoot = isInShadow(dom, true);
  3006. if (shadowRoot) {
  3007. this.shadowRoot = shadowRoot;
  3008. loadCSSText(fullPageStyle, 'fullPageStyle', shadowRoot);
  3009. }
  3010.  
  3011. const t = this;
  3012. window.addEventListener('keyup', (event) => {
  3013. const key = event.key.toLowerCase();
  3014. if (key === 'escape') {
  3015. if (t.isFull()) {
  3016. t.exit();
  3017. } else if (t.isFullScreen()) {
  3018. t.exitFullScreen();
  3019. }
  3020. }
  3021. }, true);
  3022.  
  3023. this.getContainer();
  3024. }
  3025.  
  3026. eachParentNode (dom, fn) {
  3027. let parent = dom.parentNode;
  3028. while (parent && parent.classList) {
  3029. const isEnd = fn(parent, dom);
  3030. parent = parent.parentNode;
  3031. if (isEnd) {
  3032. break
  3033. }
  3034. }
  3035. }
  3036.  
  3037. getContainer () {
  3038. const t = this;
  3039. if (t._container_) return t._container_
  3040.  
  3041. const d = t.dom;
  3042. const domBox = d.getBoundingClientRect();
  3043. let container = d;
  3044. t.eachParentNode(d, function (parentNode) {
  3045. const noParentNode = !parentNode || !parentNode.getBoundingClientRect;
  3046. if (noParentNode || parentNode.getAttribute('data-fullscreen-container')) {
  3047. container = parentNode;
  3048. return true
  3049. }
  3050.  
  3051. const parentBox = parentNode.getBoundingClientRect();
  3052. const isInsideTheBox = parentBox.width <= domBox.width && parentBox.height <= domBox.height;
  3053. if (isInsideTheBox) {
  3054. container = parentNode;
  3055. } else {
  3056. return true
  3057. }
  3058. });
  3059.  
  3060. container.setAttribute('data-fullscreen-container', 'true');
  3061. t._container_ = container;
  3062. return container
  3063. }
  3064.  
  3065. isFull () {
  3066. return this.dom.classList.contains('_webfullscreen_') || this.fullStatus
  3067. }
  3068.  
  3069. isFullScreen () {
  3070. const d = document;
  3071. return !!(d.fullscreen || d.webkitIsFullScreen || d.mozFullScreen ||
  3072. d.fullscreenElement || d.webkitFullscreenElement || d.mozFullScreenElement)
  3073. }
  3074.  
  3075. enterFullScreen () {
  3076. const c = this.getContainer();
  3077. const enterFn = c.requestFullscreen || c.webkitRequestFullScreen || c.mozRequestFullScreen || c.msRequestFullScreen;
  3078. enterFn && enterFn.call(c);
  3079. }
  3080.  
  3081. enter () {
  3082. const t = this;
  3083. if (t.isFull()) return
  3084. const container = t.getContainer();
  3085. let needSetIndex = false;
  3086. if (t.dom === container) {
  3087. needSetIndex = true;
  3088. }
  3089.  
  3090. function addFullscreenStyleToParentNode (node) {
  3091. t.eachParentNode(node, function (parentNode) {
  3092. parentNode.classList.add('_webfullscreen_');
  3093. if (container === parentNode || needSetIndex) {
  3094. needSetIndex = true;
  3095. parentNode.classList.add('_webfullscreen_zindex_');
  3096. }
  3097. });
  3098. }
  3099. addFullscreenStyleToParentNode(t.dom);
  3100.  
  3101. /* 判断dom自身是否需要加上webfullscreen样式 */
  3102. if (t.dom.parentNode) {
  3103. const domBox = t.dom.getBoundingClientRect();
  3104. const domParentBox = t.dom.parentNode.getBoundingClientRect();
  3105. if (domParentBox.width - domBox.width >= 5) {
  3106. t.dom.classList.add('_webfullscreen_');
  3107. }
  3108.  
  3109. if (t.shadowRoot && t.shadowRoot._shadowHost) {
  3110. const shadowHost = t.shadowRoot._shadowHost;
  3111. const shadowHostBox = shadowHost.getBoundingClientRect();
  3112. if (shadowHostBox.width <= domBox.width) {
  3113. shadowHost.classList.add('_webfullscreen_');
  3114. addFullscreenStyleToParentNode(shadowHost);
  3115. }
  3116. }
  3117. }
  3118.  
  3119. const fullScreenMode = !t.pageMode;
  3120. if (fullScreenMode) {
  3121. t.enterFullScreen();
  3122. }
  3123.  
  3124. this.fullStatus = true;
  3125. }
  3126.  
  3127. exitFullScreen () {
  3128. const d = document;
  3129. const exitFn = d.exitFullscreen || d.webkitExitFullscreen || d.mozCancelFullScreen || d.msExitFullscreen;
  3130. exitFn && exitFn.call(d);
  3131. }
  3132.  
  3133. exit () {
  3134. const t = this;
  3135.  
  3136. function removeFullscreenStyleToParentNode (node) {
  3137. t.eachParentNode(node, function (parentNode) {
  3138. parentNode.classList.remove('_webfullscreen_');
  3139. parentNode.classList.remove('_webfullscreen_zindex_');
  3140. });
  3141. }
  3142. removeFullscreenStyleToParentNode(t.dom);
  3143.  
  3144. t.dom.classList.remove('_webfullscreen_');
  3145.  
  3146. if (t.shadowRoot && t.shadowRoot._shadowHost) {
  3147. const shadowHost = t.shadowRoot._shadowHost;
  3148. shadowHost.classList.remove('_webfullscreen_');
  3149. removeFullscreenStyleToParentNode(shadowHost);
  3150. }
  3151.  
  3152. const fullScreenMode = !t.pageMode;
  3153. if (fullScreenMode || t.isFullScreen()) {
  3154. t.exitFullScreen();
  3155. }
  3156. this.fullStatus = false;
  3157. }
  3158.  
  3159. toggle () {
  3160. this.isFull() ? this.exit() : this.enter();
  3161. }
  3162. }
  3163.  
  3164. /*!
  3165. * @name videoCapturer.js
  3166. * @version 0.0.1
  3167. * @author Blaze
  3168. * @date 2019/9/21 12:03
  3169. * @github https://github.com/xxxily
  3170. */
  3171.  
  3172. async function setClipboard (blob) {
  3173. if (navigator.clipboard) {
  3174. navigator.clipboard.write([
  3175. // eslint-disable-next-line no-undef
  3176. new ClipboardItem({
  3177. [blob.type]: blob
  3178. })
  3179. ]).then(() => {
  3180. console.info('[setClipboard] clipboard suc');
  3181. }).catch((e) => {
  3182. console.error('[setClipboard] clipboard err', e);
  3183. });
  3184. } else {
  3185. console.error('当前网站不支持将数据写入到剪贴板里,见:\n https://developer.mozilla.org/en-US/docs/Web/API/Clipboard');
  3186. }
  3187. }
  3188.  
  3189. var videoCapturer = {
  3190. /**
  3191. * 进行截图操作
  3192. * @param video {dom} -必选 video dom 标签
  3193. * @returns {boolean}
  3194. */
  3195. capture (video, download, title) {
  3196. if (!video) return false
  3197. const t = this;
  3198. const currentTime = `${Math.floor(video.currentTime / 60)}'${(video.currentTime % 60).toFixed(3)}''`;
  3199. const captureTitle = title || `${document.title}_${currentTime}`;
  3200.  
  3201. /* 截图核心逻辑 */
  3202. video.setAttribute('crossorigin', 'anonymous');
  3203. const canvas = document.createElement('canvas');
  3204. canvas.width = video.videoWidth;
  3205. canvas.height = video.videoHeight;
  3206. const context = canvas.getContext('2d');
  3207. context.drawImage(video, 0, 0, canvas.width, canvas.height);
  3208.  
  3209. if (download) {
  3210. t.download(canvas, captureTitle, video);
  3211. } else {
  3212. t.previe(canvas, captureTitle);
  3213. }
  3214.  
  3215. return canvas
  3216. },
  3217. /**
  3218. * 预览截取到的画面内容
  3219. * @param canvas
  3220. */
  3221. previe (canvas, title) {
  3222. canvas.style = 'max-width:100%';
  3223. const previewPage = window.open('', '_blank');
  3224. previewPage.document.title = `capture previe - ${title || 'Untitled'}`;
  3225. previewPage.document.body.style.textAlign = 'center';
  3226. previewPage.document.body.style.background = '#000';
  3227. previewPage.document.body.appendChild(canvas);
  3228. },
  3229. /**
  3230. * canvas 下载截取到的内容
  3231. * @param canvas
  3232. */
  3233. download (canvas, title, video) {
  3234. title = title || 'videoCapturer_' + Date.now();
  3235.  
  3236. try {
  3237. canvas.toBlob(function (blob) {
  3238. const el = document.createElement('a');
  3239. el.download = `${title}.jpg`;
  3240. el.href = URL.createObjectURL(blob);
  3241. el.click();
  3242.  
  3243. /* 尝试复制到剪贴板 */
  3244. setClipboard(blob);
  3245. }, 'image/jpg', 0.99);
  3246. } catch (e) {
  3247. videoCapturer.previe(canvas, title);
  3248. console.error('视频源受CORS标识限制,无法直接下载截图,见:\n https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS');
  3249. console.error(video, e);
  3250. }
  3251. }
  3252. };
  3253.  
  3254. /**
  3255. * 鼠标事件观测对象
  3256. * 用于实现鼠标事件的穿透响应,有别于pointer-events:none
  3257. * pointer-events:none是设置当前层允许穿透
  3258. * 而MouseObserver是:即使不知道target上面存在多少层遮挡,一样可以响应鼠标事件
  3259. */
  3260.  
  3261. class MouseObserver {
  3262. constructor (observeOpt) {
  3263. // eslint-disable-next-line no-undef
  3264. this.observer = new IntersectionObserver((infoList) => {
  3265. infoList.forEach((info) => {
  3266. info.target.IntersectionObserverEntry = info;
  3267. });
  3268. }, observeOpt || {});
  3269.  
  3270. this.observeList = [];
  3271. }
  3272.  
  3273. _observe (target) {
  3274. let hasObserve = false;
  3275. for (let i = 0; i < this.observeList.length; i++) {
  3276. const el = this.observeList[i];
  3277. if (target === el) {
  3278. hasObserve = true;
  3279. break
  3280. }
  3281. }
  3282.  
  3283. if (!hasObserve) {
  3284. this.observer.observe(target);
  3285. this.observeList.push(target);
  3286. }
  3287. }
  3288.  
  3289. _unobserve (target) {
  3290. this.observer.unobserve(target);
  3291. const newObserveList = [];
  3292. this.observeList.forEach((el) => {
  3293. if (el !== target) {
  3294. newObserveList.push(el);
  3295. }
  3296. });
  3297. this.observeList = newObserveList;
  3298. }
  3299.  
  3300. /**
  3301. * 增加事件绑定
  3302. * @param target {element} -必选 要绑定事件的dom对象
  3303. * @param type {string} -必选 要绑定的事件,只支持鼠标事件
  3304. * @param listener {function} -必选 符合触发条件时的响应函数
  3305. */
  3306. on (target, type, listener, options) {
  3307. const t = this;
  3308. t._observe(target);
  3309.  
  3310. if (!target.MouseObserverEvent) {
  3311. target.MouseObserverEvent = {};
  3312. }
  3313. target.MouseObserverEvent[type] = true;
  3314.  
  3315. if (!t._mouseObserver_) {
  3316. t._mouseObserver_ = {};
  3317. }
  3318.  
  3319. if (!t._mouseObserver_[type]) {
  3320. t._mouseObserver_[type] = [];
  3321.  
  3322. window.addEventListener(type, (event) => {
  3323. t.observeList.forEach((target) => {
  3324. const isVisibility = target.IntersectionObserverEntry && target.IntersectionObserverEntry.intersectionRatio > 0;
  3325. const isReg = target.MouseObserverEvent[event.type] === true;
  3326. if (isVisibility && isReg) {
  3327. /* 判断是否符合触发侦听器事件条件 */
  3328. const bound = target.getBoundingClientRect();
  3329. const offsetX = event.x - bound.x;
  3330. const offsetY = event.y - bound.y;
  3331. const isNeedTap = offsetX <= bound.width && offsetX >= 0 && offsetY <= bound.height && offsetY >= 0;
  3332.  
  3333. if (isNeedTap) {
  3334. /* 执行监听回调 */
  3335. const listenerList = t._mouseObserver_[type];
  3336. listenerList.forEach((listener) => {
  3337. if (listener instanceof Function) {
  3338. listener.call(t, event, {
  3339. x: offsetX,
  3340. y: offsetY
  3341. }, target);
  3342. }
  3343. });
  3344. }
  3345. }
  3346. });
  3347. }, options);
  3348. }
  3349.  
  3350. /* 将监听回调加入到事件队列 */
  3351. if (listener instanceof Function) {
  3352. t._mouseObserver_[type].push(listener);
  3353. }
  3354. }
  3355.  
  3356. /**
  3357. * 解除事件绑定
  3358. * @param target {element} -必选 要解除事件的dom对象
  3359. * @param type {string} -必选 要解除的事件,只支持鼠标事件
  3360. * @param listener {function} -必选 绑定事件时的响应函数
  3361. * @returns {boolean}
  3362. */
  3363. off (target, type, listener) {
  3364. const t = this;
  3365. if (!target || !type || !listener || !t._mouseObserver_ || !t._mouseObserver_[type] || !target.MouseObserverEvent || !target.MouseObserverEvent[type]) return false
  3366.  
  3367. const newListenerList = [];
  3368. const listenerList = t._mouseObserver_[type];
  3369. let isMatch = false;
  3370. listenerList.forEach((listenerItem) => {
  3371. if (listenerItem === listener) {
  3372. isMatch = true;
  3373. } else {
  3374. newListenerList.push(listenerItem);
  3375. }
  3376. });
  3377.  
  3378. if (isMatch) {
  3379. t._mouseObserver_[type] = newListenerList;
  3380.  
  3381. /* 侦听器已被完全移除 */
  3382. if (newListenerList.length === 0) {
  3383. delete target.MouseObserverEvent[type];
  3384. }
  3385.  
  3386. /* 当MouseObserverEvent为空对象时移除观测对象 */
  3387. if (JSON.stringify(target.MouseObserverEvent[type]) === '{}') {
  3388. t._unobserve(target);
  3389. }
  3390. }
  3391. }
  3392. }
  3393.  
  3394. /**
  3395. * 简单的i18n库
  3396. */
  3397.  
  3398. class I18n {
  3399. constructor (config) {
  3400. this._languages = {};
  3401. this._locale = this.getClientLang();
  3402. this._defaultLanguage = '';
  3403. this.init(config);
  3404. }
  3405.  
  3406. init (config) {
  3407. if (!config) return false
  3408.  
  3409. const t = this;
  3410. t._locale = config.locale || t._locale;
  3411. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  3412. t._languages = config.languages || t._languages;
  3413. t._defaultLanguage = config.defaultLanguage || t._defaultLanguage;
  3414. }
  3415.  
  3416. use () {}
  3417.  
  3418. t (path) {
  3419. const t = this;
  3420. let result = t.getValByPath(t._languages[t._locale] || {}, path);
  3421.  
  3422. /* 版本回退 */
  3423. if (!result && t._locale !== t._defaultLanguage) {
  3424. result = t.getValByPath(t._languages[t._defaultLanguage] || {}, path);
  3425. }
  3426.  
  3427. return result || ''
  3428. }
  3429.  
  3430. /* 当前语言值 */
  3431. language () {
  3432. return this._locale
  3433. }
  3434.  
  3435. languages () {
  3436. return this._languages
  3437. }
  3438.  
  3439. changeLanguage (locale) {
  3440. if (this._languages[locale]) {
  3441. this._locale = locale;
  3442. return locale
  3443. } else {
  3444. return false
  3445. }
  3446. }
  3447.  
  3448. /**
  3449. * 根据文本路径获取对象里面的值
  3450. * @param obj {Object} -必选 要操作的对象
  3451. * @param path {String} -必选 路径信息
  3452. * @returns {*}
  3453. */
  3454. getValByPath (obj, path) {
  3455. path = path || '';
  3456. const pathArr = path.split('.');
  3457. let result = obj;
  3458.  
  3459. /* 递归提取结果值 */
  3460. for (let i = 0; i < pathArr.length; i++) {
  3461. if (!result) break
  3462. result = result[pathArr[i]];
  3463. }
  3464.  
  3465. return result
  3466. }
  3467.  
  3468. /* 获取客户端当前的语言环境 */
  3469. getClientLang () {
  3470. return navigator.languages ? navigator.languages[0] : navigator.language
  3471. }
  3472. }
  3473.  
  3474. var zhCN = {
  3475. website: '脚本官网',
  3476. about: '关于',
  3477. issues: '问题反馈',
  3478. setting: '设置',
  3479. hotkeys: '快捷键',
  3480. donate: '请作者喝杯咖啡👍',
  3481. recommend: '❤️ 免费ChatGPT ❤️',
  3482. enableScript: '启用脚本',
  3483. disableScript: '禁用脚本',
  3484. openCrossOriginFramePage: '单独打开跨域的页面',
  3485. disableInitAutoPlay: '禁止在此网站自动播放视频',
  3486. enableInitAutoPlay: '允许在此网站自动播放视频',
  3487. restoreConfiguration: '还原全局的默认配置',
  3488. blockSetPlaybackRate: '禁用默认速度调节逻辑',
  3489. blockSetCurrentTime: '禁用默认播放进度控制逻辑',
  3490. blockSetVolume: '禁用默认音量控制逻辑',
  3491. unblockSetPlaybackRate: '允许默认速度调节逻辑',
  3492. unblockSetCurrentTime: '允许默认播放进度控制逻辑',
  3493. unblockSetVolume: '允许默认音量控制逻辑',
  3494. allowAcousticGain: '开启音量增益能力',
  3495. notAllowAcousticGain: '禁用音量增益能力',
  3496. allowCrossOriginControl: '开启跨域控制能力',
  3497. notAllowCrossOriginControl: '禁用跨域控制能力',
  3498. allowExperimentFeatures: '开启实验性功能',
  3499. notAllowExperimentFeatures: '禁用实验性功能',
  3500. experimentFeaturesWarning: '实验性功能容易造成一些不确定的问题,请谨慎开启',
  3501. allowExternalCustomConfiguration: '开启外部自定义能力',
  3502. notAllowExternalCustomConfiguration: '关闭外部自定义能力',
  3503. configFail: '配置失败',
  3504. globalSetting: '全局设置',
  3505. localSetting: '仅用于此网站',
  3506. openDebugMode: '开启调试模式',
  3507. closeDebugMode: '关闭调试模式',
  3508. unfoldMenu: '展开菜单',
  3509. foldMenu: '折叠菜单',
  3510. tipsMsg: {
  3511. playspeed: '播放速度:',
  3512. forward: '前进:',
  3513. backward: '后退:',
  3514. seconds: '秒',
  3515. volume: '音量:',
  3516. nextframe: '定位:下一帧',
  3517. previousframe: '定位:上一帧',
  3518. stopframe: '定格帧画面:',
  3519. play: '播放',
  3520. pause: '暂停',
  3521. arpl: '允许自动恢复播放进度',
  3522. drpl: '禁止自动恢复播放进度',
  3523. brightness: '图像亮度:',
  3524. contrast: '图像对比度:',
  3525. saturation: '图像饱和度:',
  3526. hue: '图像色相:',
  3527. blur: '图像模糊度:',
  3528. imgattrreset: '图像属性:复位',
  3529. imgrotate: '画面旋转:',
  3530. onplugin: '启用h5Player插件',
  3531. offplugin: '禁用h5Player插件',
  3532. globalmode: '全局模式:',
  3533. playbackrestored: '为你恢复上次播放进度',
  3534. playbackrestoreoff: '恢复播放进度功能已禁用,按 SHIFT+R 可开启该功能',
  3535. horizontal: '水平位移:',
  3536. vertical: '垂直位移:',
  3537. horizontalMirror: '水平镜像',
  3538. verticalMirror: '垂直镜像',
  3539. videozoom: '视频缩放率:'
  3540. }
  3541. };
  3542.  
  3543. var enUS = {
  3544. website: 'Script website',
  3545. about: 'About',
  3546. issues: 'Issues',
  3547. setting: 'Setting',
  3548. hotkeys: 'Hotkeys',
  3549. donate: 'Donate',
  3550. enableScript: 'enable script',
  3551. disableScript: 'disable script',
  3552. openCrossOriginFramePage: 'Open cross-domain pages alone',
  3553. disableInitAutoPlay: 'Prohibit autoplay of videos on this site',
  3554. enableInitAutoPlay: 'Allow autoplay videos on this site',
  3555. restoreConfiguration: 'Restore the global default configuration',
  3556. blockSetPlaybackRate: 'Disable default speed regulation logic',
  3557. blockSetCurrentTime: 'Disable default playback progress control logic',
  3558. blockSetVolume: 'Disable default volume control logic',
  3559. unblockSetPlaybackRate: 'Allow default speed adjustment logic',
  3560. unblockSetCurrentTime: 'Allow default playback progress control logic',
  3561. unblockSetVolume: 'Allow default volume control logic',
  3562. allowAcousticGain: 'Turn on volume boost',
  3563. notAllowAcousticGain: 'Disable volume boost ability',
  3564. allowCrossOriginControl: 'Enable cross-domain control capability',
  3565. notAllowCrossOriginControl: 'Disable cross-domain control capabilities',
  3566. allowExperimentFeatures: 'Turn on experimental features',
  3567. notAllowExperimentFeatures: 'Disable experimental features',
  3568. experimentFeaturesWarning: 'Experimental features are likely to cause some uncertain problems, please turn on with caution',
  3569. allowExternalCustomConfiguration: 'Enable external customization capabilities',
  3570. notAllowExternalCustomConfiguration: 'Turn off external customization capabilities',
  3571. configFail: 'Configuration failed',
  3572. globalSetting: 'Global Settings',
  3573. localSetting: 'For this site only',
  3574. openDebugMode: 'Enable debug mode',
  3575. closeDebugMode: 'Turn off debug mode',
  3576. unfoldMenu: 'Expand menu',
  3577. foldMenu: 'Collapse menu',
  3578. tipsMsg: {
  3579. playspeed: 'Speed: ',
  3580. forward: 'Forward: ',
  3581. backward: 'Backward: ',
  3582. seconds: 'sec',
  3583. volume: 'Volume: ',
  3584. nextframe: 'Next frame',
  3585. previousframe: 'Previous frame',
  3586. stopframe: 'Stopframe: ',
  3587. play: 'Play',
  3588. pause: 'Pause',
  3589. arpl: 'Allow auto resume playback progress',
  3590. drpl: 'Disable auto resume playback progress',
  3591. brightness: 'Brightness: ',
  3592. contrast: 'Contrast: ',
  3593. saturation: 'Saturation: ',
  3594. hue: 'HUE: ',
  3595. blur: 'Blur: ',
  3596. imgattrreset: 'Attributes: reset',
  3597. imgrotate: 'Picture rotation: ',
  3598. onplugin: 'ON h5Player plugin',
  3599. offplugin: 'OFF h5Player plugin',
  3600. globalmode: 'Global mode: ',
  3601. playbackrestored: 'Restored the last playback progress for you',
  3602. playbackrestoreoff: 'The function of restoring the playback progress is disabled. Press SHIFT+R to turn on the function',
  3603. horizontal: 'Horizontal displacement: ',
  3604. vertical: 'Vertical displacement: ',
  3605. horizontalMirror: 'Horizontal mirror',
  3606. verticalMirror: 'vertical mirror',
  3607. videozoom: 'Video zoom: '
  3608. },
  3609. demo: 'demo-test'
  3610. };
  3611.  
  3612. var ru = {
  3613. website: 'официальный сайт скрипта',
  3614. about: 'около',
  3615. issues: 'обратная связь',
  3616. setting: 'установка',
  3617. hotkeys: 'горячие клавиши',
  3618. donate: 'пожертвовать',
  3619. enableScript: 'включить скрипт',
  3620. disableScript: 'отключить скрипт',
  3621. openCrossOriginFramePage: 'Открывать только междоменные страницы',
  3622. disableInitAutoPlay: 'Запретить автовоспроизведение видео на этом сайте',
  3623. enableInitAutoPlay: 'Разрешить автоматическое воспроизведение видео на этом сайте',
  3624. restoreConfiguration: 'Восстановить глобальную конфигурацию по умолчанию',
  3625. blockSetPlaybackRate: 'Отключить логику регулирования скорости по умолчанию',
  3626. blockSetCurrentTime: 'Отключить логику управления ходом воспроизведения по умолчанию',
  3627. blockSetVolume: 'Отключить логику управления громкостью по умолчанию',
  3628. unblockSetPlaybackRate: 'Разрешить логику регулировки скорости по умолчанию',
  3629. unblockSetCurrentTime: 'Разрешить логику управления ходом воспроизведения по умолчанию',
  3630. unblockSetVolume: 'Разрешить логику управления громкостью по умолчанию',
  3631. allowAcousticGain: 'Включите усиление громкости',
  3632. notAllowAcousticGain: 'Отключить возможность увеличения громкости',
  3633. allowCrossOriginControl: 'Включить возможность междоменного контроля',
  3634. notAllowCrossOriginControl: 'Отключить возможности междоменного контроля',
  3635. allowExperimentFeatures: 'Включить экспериментальные функции',
  3636. notAllowExperimentFeatures: 'Отключить экспериментальные функции',
  3637. experimentFeaturesWarning: 'Экспериментальные функции могут вызвать определенные проблемы, включайте их с осторожностью.',
  3638. allowExternalCustomConfiguration: 'Включить возможности внешней настройки',
  3639. notAllowExternalCustomConfiguration: 'Отключить возможности внешней настройки',
  3640. configFail: 'Ошибка конфигурации',
  3641. globalSetting: 'Глобальные настройки',
  3642. localSetting: 'только для этого сайта',
  3643. openDebugMode: 'Включить режим отладки',
  3644. closeDebugMode: 'отключить режим отладки',
  3645. unfoldMenu: 'развернуть меню',
  3646. foldMenu: 'свернуть меню',
  3647. tipsMsg: {
  3648. playspeed: 'Скорость: ',
  3649. forward: 'Вперёд: ',
  3650. backward: 'Назад: ',
  3651. seconds: ' сек',
  3652. volume: 'Громкость: ',
  3653. nextframe: 'Следующий кадр',
  3654. previousframe: 'Предыдущий кадр',
  3655. stopframe: 'Стоп-кадр: ',
  3656. play: 'Запуск',
  3657. pause: 'Пауза',
  3658. arpl: 'Разрешить автоматическое возобновление прогресса воспроизведения',
  3659. drpl: 'Запретить автоматическое возобновление прогресса воспроизведения',
  3660. brightness: 'Яркость: ',
  3661. contrast: 'Контраст: ',
  3662. saturation: 'Насыщенность: ',
  3663. hue: 'Оттенок: ',
  3664. blur: 'Размытие: ',
  3665. imgattrreset: 'Атрибуты: сброс',
  3666. imgrotate: 'Поворот изображения: ',
  3667. onplugin: 'ВКЛ: плагин воспроизведения',
  3668. offplugin: 'ВЫКЛ: плагин воспроизведения',
  3669. globalmode: 'Глобальный режим:',
  3670. playbackrestored: 'Восстановлен последний прогресс воспроизведения',
  3671. playbackrestoreoff: 'Функция восстановления прогресса воспроизведения отключена. Нажмите SHIFT + R, чтобы включить функцию',
  3672. horizontal: 'Горизонтальное смещение: ',
  3673. vertical: 'Вертикальное смещение: ',
  3674. horizontalMirror: 'Горизонтальное зеркало',
  3675. verticalMirror: 'вертикальное зеркало',
  3676. videozoom: 'Увеличить видео: '
  3677. }
  3678. };
  3679.  
  3680. var zhTW = {
  3681. website: '腳本官網',
  3682. about: '關於',
  3683. issues: '反饋',
  3684. setting: '設置',
  3685. hotkeys: '快捷鍵',
  3686. donate: '讚賞',
  3687. enableScript: '啟用腳本',
  3688. disableScript: '禁用腳本',
  3689. openCrossOriginFramePage: '單獨打開跨域的頁面',
  3690. disableInitAutoPlay: '禁止在此網站自動播放視頻',
  3691. enableInitAutoPlay: '允許在此網站自動播放視頻',
  3692. restoreConfiguration: '還原全局的默認配置',
  3693. blockSetPlaybackRate: '禁用默認速度調節邏輯',
  3694. blockSetCurrentTime: '禁用默認播放進度控制邏輯',
  3695. blockSetVolume: '禁用默認音量控制邏輯',
  3696. unblockSetPlaybackRate: '允許默認速度調節邏輯',
  3697. unblockSetCurrentTime: '允許默認播放進度控制邏輯',
  3698. unblockSetVolume: '允許默認音量控制邏輯',
  3699. allowAcousticGain: '開啟音量增益能力',
  3700. notAllowAcousticGain: '禁用音量增益能力',
  3701. allowCrossOriginControl: '開啟跨域控制能力',
  3702. notAllowCrossOriginControl: '禁用跨域控制能力',
  3703. allowExperimentFeatures: '開啟實驗性功能',
  3704. notAllowExperimentFeatures: '禁用實驗性功能',
  3705. experimentFeaturesWarning: '實驗性功能容易造成一些不確定的問題,請謹慎開啟',
  3706. allowExternalCustomConfiguration: '開啟外部自定義能力',
  3707. notAllowExternalCustomConfiguration: '關閉外部自定義能力',
  3708. configFail: '配置失敗',
  3709. globalSetting: '全局設置',
  3710. localSetting: '僅用於此網站',
  3711. openDebugMode: '開啟調試模式',
  3712. closeDebugMode: '關閉調試模式',
  3713. unfoldMenu: '展開菜單',
  3714. foldMenu: '折疊菜單',
  3715. tipsMsg: {
  3716. playspeed: '播放速度:',
  3717. forward: '向前:',
  3718. backward: '向後:',
  3719. seconds: '秒',
  3720. volume: '音量:',
  3721. nextframe: '定位:下一幀',
  3722. previousframe: '定位:上一幀',
  3723. stopframe: '定格幀畫面:',
  3724. play: '播放',
  3725. pause: '暫停',
  3726. arpl: '允許自動恢復播放進度',
  3727. drpl: '禁止自動恢復播放進度',
  3728. brightness: '圖像亮度:',
  3729. contrast: '圖像對比度:',
  3730. saturation: '圖像飽和度:',
  3731. hue: '圖像色相:',
  3732. blur: '圖像模糊度:',
  3733. imgattrreset: '圖像屬性:復位',
  3734. imgrotate: '畫面旋轉:',
  3735. onplugin: '啟用h5Player插件',
  3736. offplugin: '禁用h5Player插件',
  3737. globalmode: '全局模式:',
  3738. playbackrestored: '為你恢復上次播放進度',
  3739. playbackrestoreoff: '恢復播放進度功能已禁用,按 SHIFT+R 可開啟該功能',
  3740. horizontal: '水平位移:',
  3741. vertical: '垂直位移:',
  3742. horizontalMirror: '水平鏡像',
  3743. verticalMirror: '垂直鏡像',
  3744. videozoom: '視頻縮放率:'
  3745. }
  3746. };
  3747.  
  3748. const messages = {
  3749. 'zh-CN': zhCN,
  3750. zh: zhCN,
  3751. 'zh-HK': zhTW,
  3752. 'zh-TW': zhTW,
  3753. 'en-US': enUS,
  3754. en: enUS,
  3755. ru: ru
  3756. };
  3757.  
  3758. const i18n = new I18n({
  3759. defaultLanguage: 'en',
  3760. /* 指定当前要是使用的语言环境,默认无需指定,会自动读取 */
  3761. // locale: 'zh-TW',
  3762. languages: messages
  3763. });
  3764.  
  3765. /* 用于获取全局唯一的id */
  3766. let __globalId__ = 0;
  3767. function getId () {
  3768. if (window.GM_getValue && window.GM_setValue) {
  3769. let gID = window.GM_getValue('_global_id_');
  3770. if (!gID) gID = 0;
  3771. gID = Number(gID) + 1;
  3772. window.GM_setValue('_global_id_', gID);
  3773. return gID
  3774. } else {
  3775. /* 如果不处于油猴插件下,则该id为页面自己独享的id */
  3776. __globalId__ = Number(__globalId__) + 1;
  3777. return __globalId__
  3778. }
  3779. }
  3780.  
  3781. let curTabId = null;
  3782.  
  3783. /**
  3784. * 获取当前TAB标签的Id号,可用于iframe确定自己是否处于同一TAB标签下
  3785. * @returns {Promise<any>}
  3786. */
  3787. function getTabId () {
  3788. return new Promise((resolve, reject) => {
  3789. if (window.GM_getTab instanceof Function) {
  3790. window.GM_getTab(function (obj) {
  3791. if (!obj.tabId) {
  3792. obj.tabId = getId();
  3793. window.GM_saveTab(obj);
  3794. }
  3795. /* 每次获取都更新当前Tab的id号 */
  3796. curTabId = obj.tabId;
  3797. resolve(obj.tabId);
  3798. });
  3799. } else {
  3800. /* 非油猴插件下,无法确定iframe是否处于同一个tab下 */
  3801. resolve(Date.now());
  3802. }
  3803. })
  3804. }
  3805.  
  3806. /* 一开始就初始化好curTabId,这样后续就不需要异步获取Tabid,部分场景下需要用到 */
  3807. getTabId();
  3808.  
  3809. /*!
  3810. * @name monkeyMsg.js
  3811. * @version 0.0.1
  3812. * @author Blaze
  3813. * @date 2019/9/21 14:22
  3814. */
  3815. // import debug from './debug'
  3816.  
  3817. /**
  3818. * 将对象数据里面可存储到GM_setValue里面的值提取出来
  3819. * @param obj {objcet} -必选 打算要存储的对象数据
  3820. * @param deep {number} -可选 如果对象层级非常深,则须限定递归的层级,默认最高不能超过3级
  3821. * @returns {{}}
  3822. */
  3823. function extractDatafromOb (obj, deep) {
  3824. deep = deep || 1;
  3825. if (deep > 3) return {}
  3826.  
  3827. const result = {};
  3828. if (typeof obj === 'object') {
  3829. for (const key in obj) {
  3830. const val = obj[key];
  3831. const valType = typeof val;
  3832. if (valType === 'number' || valType === 'string' || valType === 'boolean') {
  3833. Object.defineProperty(result, key, {
  3834. value: val,
  3835. writable: true,
  3836. configurable: true,
  3837. enumerable: true
  3838. });
  3839. } else if (valType === 'object' && Object.prototype.propertyIsEnumerable.call(obj, key)) {
  3840. /* 进行递归提取 */
  3841. result[key] = extractDatafromOb(val, deep + 1);
  3842. } else if (valType === 'array') {
  3843. result[key] = val;
  3844. }
  3845. }
  3846. }
  3847. return result
  3848. }
  3849.  
  3850. const monkeyMsg = {
  3851. /**
  3852. * 发送消息,除了正常发送信息外,还会补充各类必要的信息
  3853. * @param name {string} -必选 要发送给那个字段,接收时要一致才能监听的正确
  3854. * @param data {Any} -必选 要发送的数据
  3855. * @param throttleInterval -可选,因为会出现莫名奇妙的重复发送情况,为了消除重复发送带来的副作用,所以引入节流限制逻辑,即限制某个时间间隔内只能发送一次,多余的次数自动抛弃掉,默认80ms
  3856. * @returns {Promise<void>}
  3857. */
  3858. send (name, data, throttleInterval = 80) {
  3859. if (!window.GM_getValue || !window.GM_setValue) {
  3860. return false
  3861. }
  3862.  
  3863. /* 阻止频繁发送修改事件 */
  3864. const oldMsg = window.GM_getValue(name);
  3865. if (oldMsg && oldMsg.updateTime) {
  3866. const interval = Math.abs(Date.now() - oldMsg.updateTime);
  3867. if (interval < throttleInterval) {
  3868. return false
  3869. }
  3870. }
  3871.  
  3872. const msg = {
  3873. /* 发送过来的数据 */
  3874. data,
  3875. /* 补充标签ID,用于判断是否同处一个tab标签下 */
  3876. tabId: curTabId || 'undefined',
  3877. /* 补充消息的页面来源的标题信息 */
  3878. title: document.title,
  3879. /* 补充消息的页面来源信息 */
  3880. referrer: extractDatafromOb(window.location),
  3881. /* 最近一次更新该数据的时间 */
  3882. updateTime: Date.now()
  3883. };
  3884. if (typeof data === 'object') {
  3885. msg.data = extractDatafromOb(data);
  3886. }
  3887. window.GM_setValue(name, msg);
  3888.  
  3889. // debug.info(`[monkeyMsg-send][${name}]`, msg)
  3890. },
  3891. set: (name, data) => monkeyMsg.send(name, data),
  3892. get: (name) => window.GM_getValue && window.GM_getValue(name),
  3893. on: (name, fn) => window.GM_addValueChangeListener && window.GM_addValueChangeListener(name, function (name, oldVal, newVal, remote) {
  3894. // debug.info(`[monkeyMsg-on][${name}]`, oldVal, newVal, remote)
  3895.  
  3896. /* 补充消息来源是否出自同一个Tab的判断字段 */
  3897. newVal.originTab = newVal.tabId === curTabId;
  3898.  
  3899. fn instanceof Function && fn.apply(null, arguments);
  3900. }),
  3901. off: (listenerId) => window.GM_removeValueChangeListener && window.GM_removeValueChangeListener(listenerId),
  3902.  
  3903. /**
  3904. * 进行monkeyMsg的消息广播,该广播每两秒钟发送一次,其它任意页面可通接收到的广播信息来更新一些变量信息
  3905. * 主要用以解决通过setInterval或setTimeout因页面可视状态和性能策略导致的不运行或执行频率异常而不能正确更新变量状态的问题
  3906. * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API
  3907. * 广播也不能100%保证不受性能策略的影响,但只要有一个网页处于前台运行,则就能正常工作
  3908. * @param handler {Function} -必选 接收到广播信息时的回调函数
  3909. * @returns
  3910. */
  3911. broadcast (handler) {
  3912. const broadcastName = '__monkeyMsgBroadcast__';
  3913. monkeyMsg._monkeyMsgBroadcastHandler_ = monkeyMsg._monkeyMsgBroadcastHandler_ || [];
  3914. handler instanceof Function && monkeyMsg._monkeyMsgBroadcastHandler_.push(handler);
  3915.  
  3916. if (monkeyMsg._hasMonkeyMsgBroadcast_) {
  3917. return broadcastName
  3918. }
  3919.  
  3920. monkeyMsg.on(broadcastName, function () {
  3921. monkeyMsg._monkeyMsgBroadcastHandler_.forEach(handler => {
  3922. handler.apply(null, arguments);
  3923. });
  3924. });
  3925.  
  3926. setInterval(function () {
  3927. /* 通过限定时间间隔来防止多个页面批量发起广播信息 */
  3928. const data = monkeyMsg.get(broadcastName);
  3929. if (data && Date.now() - data.updateTime < 1000 * 2) {
  3930. return false
  3931. }
  3932.  
  3933. monkeyMsg.send(broadcastName, {});
  3934. }, 1000 * 2);
  3935.  
  3936. return broadcastName
  3937. }
  3938. };
  3939.  
  3940. /*!
  3941. * @name crossTabCtl.js
  3942. * @description 跨Tab控制脚本逻辑
  3943. * @version 0.0.1
  3944. * @author Blaze
  3945. * @date 2019/11/21 上午11:56
  3946. * @github https://github.com/xxxily
  3947. */
  3948.  
  3949. const crossTabCtl = {
  3950. /* 在进行跨Tab控制时,排除转发的快捷键,以减少对重要快捷键的干扰 */
  3951. excludeShortcuts (event) {
  3952. if (!event || typeof event.keyCode === 'undefined') {
  3953. return false
  3954. }
  3955.  
  3956. const excludeKeyCode = ['c', 'v', 'f', 'd'];
  3957.  
  3958. if (event.ctrlKey || event.metaKey) {
  3959. const key = event.key.toLowerCase();
  3960. if (excludeKeyCode.includes(key)) {
  3961. return true
  3962. } else {
  3963. return false
  3964. }
  3965. } else {
  3966. return false
  3967. }
  3968. },
  3969. /* 意外退出的时候leavepictureinpicture事件并不会被调用,所以只能通过轮询来更新画中画信息 */
  3970. updatePictureInPictureInfo () {
  3971. setInterval(function () {
  3972. if (document.pictureInPictureElement) {
  3973. monkeyMsg.send('globalPictureInPictureInfo', {
  3974. usePictureInPicture: true
  3975. });
  3976. }
  3977. }, 1000 * 1.5);
  3978.  
  3979. /**
  3980. * 通过setInterval来更新globalPictureInPictureInfo会受页面可见性和性能策略影响而得不到更新
  3981. * 见: https://developer.mozilla.org/zh-CN/docs/Web/API/Page_Visibility_API
  3982. * 所以通过增加monkeyMsg广播机制来校准globalPictureInPictureInfo状态
  3983. */
  3984. monkeyMsg.broadcast(function () {
  3985. // console.log('[monkeyMsg][broadcast]', ...arguments)
  3986. if (document.pictureInPictureElement) {
  3987. monkeyMsg.send('globalPictureInPictureInfo', {
  3988. usePictureInPicture: true
  3989. });
  3990. }
  3991. });
  3992. },
  3993. /* 判断当前是否开启了画中画功能 */
  3994. hasOpenPictureInPicture () {
  3995. const data = monkeyMsg.get('globalPictureInPictureInfo');
  3996.  
  3997. /* 画中画的全局信息更新时间差在3s内,才认为当前开启了画中画模式,否则有可能意外退出,而没修改usePictureInPicture的值,造成误判 */
  3998. if (data && data.data) {
  3999. if (data.data.usePictureInPicture) {
  4000. return Math.abs(Date.now() - data.updateTime) < 1000 * 3
  4001. } else {
  4002. /**
  4003. * 检测到画中画已经被关闭,但还没关闭太久的话,允许有个短暂的时间段内让用户跨TAB控制视频
  4004. * 例如:暂停视频播放
  4005. */
  4006. return Math.abs(Date.now() - data.updateTime) < 1000 * 15
  4007. }
  4008. }
  4009.  
  4010. return false
  4011. },
  4012. /**
  4013. * 判断是否需要发送跨Tab控制按键信息
  4014. */
  4015. isNeedSendCrossTabCtlEvent () {
  4016. const t = crossTabCtl;
  4017.  
  4018. /* 画中画开启后,判断不在同一个Tab才发送事件 */
  4019. const data = monkeyMsg.get('globalPictureInPictureInfo');
  4020. if (t.hasOpenPictureInPicture() && data.tabId !== curTabId) {
  4021. return true
  4022. } else {
  4023. return false
  4024. }
  4025. },
  4026. crossTabKeydownEvent (event) {
  4027. const t = crossTabCtl;
  4028. /* 处于可编辑元素中不执行任何快捷键 */
  4029. const target = event.composedPath ? event.composedPath()[0] || event.target : event.target;
  4030. if (isEditableTarget(target)) return
  4031. if (t.isNeedSendCrossTabCtlEvent() && isRegisterKey(event) && !t.excludeShortcuts(event)) {
  4032. // 阻止事件冒泡和默认事件
  4033. event.stopPropagation();
  4034. event.preventDefault();
  4035.  
  4036. /* 广播按键消息,进行跨Tab控制 */
  4037. // keydownEvent里已经包含了globalKeydownEvent事件
  4038. // monkeyMsg.send('globalKeydownEvent', event)
  4039.  
  4040. return true
  4041. }
  4042. },
  4043. bindCrossTabEvent () {
  4044. const t = crossTabCtl;
  4045. if (t._hasBindEvent_) return
  4046. document.removeEventListener('keydown', t.crossTabKeydownEvent);
  4047. document.addEventListener('keydown', t.crossTabKeydownEvent, true);
  4048. t._hasBindEvent_ = true;
  4049. },
  4050. init () {
  4051. const t = crossTabCtl;
  4052. t.updatePictureInPictureInfo();
  4053. t.bindCrossTabEvent();
  4054. }
  4055. };
  4056.  
  4057. /*!
  4058. * @name index.js
  4059. * @description hookJs JS AOP切面编程辅助库
  4060. * @version 0.0.1
  4061. * @author Blaze
  4062. * @date 2020/10/22 17:40
  4063. * @github https://github.com/xxxily
  4064. */
  4065.  
  4066. const win = typeof window === 'undefined' ? global : window;
  4067. const toStr = Function.prototype.call.bind(Object.prototype.toString);
  4068. /* 特殊场景,如果把Boolean也hook了,很容易导致调用溢出,所以是需要使用原生Boolean */
  4069. const toBoolean = Boolean.originMethod ? Boolean.originMethod : Boolean;
  4070. const util = {
  4071. toStr,
  4072. isObj: obj => toStr(obj) === '[object Object]',
  4073. /* 判断是否为引用类型,用于更宽泛的场景 */
  4074. isRef: obj => typeof obj === 'object',
  4075. isReg: obj => toStr(obj) === '[object RegExp]',
  4076. isFn: obj => obj instanceof Function,
  4077. isAsyncFn: fn => toStr(fn) === '[object AsyncFunction]',
  4078. isPromise: obj => toStr(obj) === '[object Promise]',
  4079. firstUpperCase: str => str.replace(/^\S/, s => s.toUpperCase()),
  4080. toArr: arg => Array.from(Array.isArray(arg) ? arg : [arg]),
  4081.  
  4082. debug: {
  4083. log () {
  4084. let log = win.console.log;
  4085. /* 如果log也被hook了,则使用未被hook前的log函数 */
  4086. if (log.originMethod) { log = log.originMethod; }
  4087. if (win._debugMode_) {
  4088. log.apply(win.console, arguments);
  4089. }
  4090. }
  4091. },
  4092. /* 获取包含自身、继承、可枚举、不可枚举的键名 */
  4093. getAllKeys (obj) {
  4094. const tmpArr = [];
  4095. for (const key in obj) { tmpArr.push(key); }
  4096. const allKeys = Array.from(new Set(tmpArr.concat(Reflect.ownKeys(obj))));
  4097. return allKeys
  4098. }
  4099. };
  4100.  
  4101. class HookJs {
  4102. constructor (useProxy) {
  4103. this.useProxy = useProxy || false;
  4104. this.hookPropertiesKeyName = '_hookProperties' + Date.now();
  4105. }
  4106.  
  4107. hookJsPro () {
  4108. return new HookJs(true)
  4109. }
  4110.  
  4111. _addHook (hookMethod, fn, type, classHook) {
  4112. const hookKeyName = type + 'Hooks';
  4113. const hookMethodProperties = hookMethod[this.hookPropertiesKeyName];
  4114. if (!hookMethodProperties[hookKeyName]) {
  4115. hookMethodProperties[hookKeyName] = [];
  4116. }
  4117.  
  4118. /* 注册(储存)要被调用的hook函数,同时防止重复注册 */
  4119. let hasSameHook = false;
  4120. for (let i = 0; i < hookMethodProperties[hookKeyName].length; i++) {
  4121. if (fn === hookMethodProperties[hookKeyName][i]) {
  4122. hasSameHook = true;
  4123. break
  4124. }
  4125. }
  4126.  
  4127. if (!hasSameHook) {
  4128. fn.classHook = classHook || false;
  4129. hookMethodProperties[hookKeyName].push(fn);
  4130. }
  4131. }
  4132.  
  4133. _runHooks (parentObj, methodName, originMethod, hookMethod, target, ctx, args, classHook, hookPropertiesKeyName) {
  4134. const hookMethodProperties = hookMethod[hookPropertiesKeyName];
  4135. const beforeHooks = hookMethodProperties.beforeHooks || [];
  4136. const afterHooks = hookMethodProperties.afterHooks || [];
  4137. const errorHooks = hookMethodProperties.errorHooks || [];
  4138. const hangUpHooks = hookMethodProperties.hangUpHooks || [];
  4139. const replaceHooks = hookMethodProperties.replaceHooks || [];
  4140. const execInfo = {
  4141. result: null,
  4142. error: null,
  4143. args: args,
  4144. type: ''
  4145. };
  4146.  
  4147. function runHooks (hooks, type) {
  4148. let hookResult = null;
  4149. execInfo.type = type || '';
  4150. if (Array.isArray(hooks)) {
  4151. hooks.forEach(fn => {
  4152. if (util.isFn(fn) && classHook === fn.classHook) {
  4153. hookResult = fn(args, parentObj, methodName, originMethod, execInfo, ctx);
  4154. }
  4155. });
  4156. }
  4157. return hookResult
  4158. }
  4159.  
  4160. const runTarget = (function () {
  4161. if (classHook) {
  4162. return function () {
  4163. // eslint-disable-next-line new-cap
  4164. return new target(...args)
  4165. }
  4166. } else {
  4167. return function () {
  4168. return target.apply(ctx, args)
  4169. }
  4170. }
  4171. })();
  4172.  
  4173. const beforeHooksResult = runHooks(beforeHooks, 'before');
  4174. /* 支持终止后续调用的指令 */
  4175. if (beforeHooksResult && beforeHooksResult === 'STOP-INVOKE') {
  4176. return beforeHooksResult
  4177. }
  4178.  
  4179. if (hangUpHooks.length || replaceHooks.length) {
  4180. /**
  4181. * 当存在hangUpHooks或replaceHooks的时候是不会触发原来函数的
  4182. * 本质上来说hangUpHooks和replaceHooks是一样的,只是外部的定义描述不一致和分类不一致而已
  4183. */
  4184. runHooks(hangUpHooks, 'hangUp');
  4185. runHooks(replaceHooks, 'replace');
  4186. } else {
  4187. if (errorHooks.length) {
  4188. try {
  4189. execInfo.result = runTarget();
  4190. } catch (err) {
  4191. execInfo.error = err;
  4192. const errorHooksResult = runHooks(errorHooks, 'error');
  4193. /* 支持执行错误后不抛出异常的指令 */
  4194. if (errorHooksResult && errorHooksResult === 'SKIP-ERROR') ; else {
  4195. throw err
  4196. }
  4197. }
  4198. } else {
  4199. execInfo.result = runTarget();
  4200. }
  4201. }
  4202.  
  4203. /**
  4204. * 执行afterHooks,如果返回的是Promise,理论上应该进行进一步的细分处理
  4205. * 但添加细分处理逻辑后发现性能下降得比较厉害,且容易出现各种异常,所以决定不在hook里处理Promise情况
  4206. * 下面是原Promise处理逻辑,添加后会导致以下网站卡死或无法访问:
  4207. * wenku.baidu.com
  4208. * https://pubs.rsc.org/en/content/articlelanding/2021/sc/d1sc01881g#!divAbstract
  4209. * https://www.elsevier.com/connect/coronavirus-information-center
  4210. */
  4211. // if (execInfo.result && execInfo.result.then && util.isPromise(execInfo.result)) {
  4212. // execInfo.result.then(function (data) {
  4213. // execInfo.result = data
  4214. // runHooks(afterHooks, 'after')
  4215. // return Promise.resolve.apply(ctx, arguments)
  4216. // }).catch(function (err) {
  4217. // execInfo.error = err
  4218. // runHooks(errorHooks, 'error')
  4219. // return Promise.reject.apply(ctx, arguments)
  4220. // })
  4221. // }
  4222.  
  4223. runHooks(afterHooks, 'after');
  4224.  
  4225. return execInfo.result
  4226. }
  4227.  
  4228. _proxyMethodcGenerator (parentObj, methodName, originMethod, classHook, context, proxyHandler) {
  4229. const t = this;
  4230. const useProxy = t.useProxy;
  4231. let hookMethod = null;
  4232.  
  4233. /* 存在缓存则使用缓存的hookMethod */
  4234. if (t.isHook(originMethod)) {
  4235. hookMethod = originMethod;
  4236. } else if (originMethod[t.hookPropertiesKeyName] && t.isHook(originMethod[t.hookPropertiesKeyName].hookMethod)) {
  4237. hookMethod = originMethod[t.hookPropertiesKeyName].hookMethod;
  4238. }
  4239.  
  4240. if (hookMethod) {
  4241. if (!hookMethod[t.hookPropertiesKeyName].isHook) {
  4242. /* 重新标注被hook状态 */
  4243. hookMethod[t.hookPropertiesKeyName].isHook = true;
  4244. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  4245. }
  4246. return hookMethod
  4247. }
  4248.  
  4249. /* 使用Proxy模式进行hook可以获得更多特性,但性能也会稍差一些 */
  4250. if (useProxy && Proxy) {
  4251. /* 注意:使用Proxy代理,hookMethod和originMethod将共用同一对象 */
  4252. const handler = { ...proxyHandler };
  4253.  
  4254. /* 下面的写法确定了proxyHandler是无法覆盖construct和apply操作的 */
  4255. if (classHook) {
  4256. handler.construct = function (target, args, newTarget) {
  4257. context = context || this;
  4258. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, context, args, true, t.hookPropertiesKeyName)
  4259. };
  4260. } else {
  4261. handler.apply = function (target, ctx, args) {
  4262. ctx = context || ctx;
  4263. return t._runHooks(parentObj, methodName, originMethod, hookMethod, target, ctx, args, false, t.hookPropertiesKeyName)
  4264. };
  4265. }
  4266.  
  4267. hookMethod = new Proxy(originMethod, handler);
  4268. } else {
  4269. hookMethod = function () {
  4270. /**
  4271. * 注意此处不能通过 context = context || this
  4272. * 然后通过把context当ctx传递过去
  4273. * 这将导致ctx引用错误
  4274. */
  4275. const ctx = context || this;
  4276. return t._runHooks(parentObj, methodName, originMethod, hookMethod, originMethod, ctx, arguments, classHook, t.hookPropertiesKeyName)
  4277. };
  4278.  
  4279. /* 确保子对象和原型链跟originMethod保持一致 */
  4280. const keys = Reflect.ownKeys(originMethod);
  4281. keys.forEach(keyName => {
  4282. try {
  4283. Object.defineProperty(hookMethod, keyName, {
  4284. get: function () {
  4285. return originMethod[keyName]
  4286. },
  4287. set: function (val) {
  4288. originMethod[keyName] = val;
  4289. }
  4290. });
  4291. } catch (err) {
  4292. // 设置defineProperty的时候出现异常,可能导致hookMethod部分功能缺失,也可能不受影响
  4293. util.debug.log(`[proxyMethodcGenerator] hookMethod defineProperty abnormal. hookMethod:${methodName}, definePropertyName:${keyName}`, err);
  4294. }
  4295. });
  4296. hookMethod.prototype = originMethod.prototype;
  4297. }
  4298.  
  4299. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName] = {};
  4300.  
  4301. hookMethodProperties.originMethod = originMethod;
  4302. hookMethodProperties.hookMethod = hookMethod;
  4303. hookMethodProperties.isHook = true;
  4304. hookMethodProperties.classHook = classHook;
  4305.  
  4306. util.debug.log(`[hook method] ${util.toStr(parentObj)} ${methodName}`);
  4307.  
  4308. return hookMethod
  4309. }
  4310.  
  4311. _getObjKeysByRule (obj, rule) {
  4312. let excludeRule = null;
  4313. let result = rule;
  4314.  
  4315. if (util.isObj(rule) && rule.include) {
  4316. excludeRule = rule.exclude;
  4317. rule = rule.include;
  4318. result = rule;
  4319. }
  4320.  
  4321. /**
  4322. * for in、Object.keys与Reflect.ownKeys的区别见:
  4323. * https://es6.ruanyifeng.com/#docs/object#%E5%B1%9E%E6%80%A7%E7%9A%84%E9%81%8D%E5%8E%86
  4324. */
  4325. if (rule === '*') {
  4326. result = Object.keys(obj);
  4327. } else if (rule === '**') {
  4328. result = Reflect.ownKeys(obj);
  4329. } else if (rule === '***') {
  4330. result = util.getAllKeys(obj);
  4331. } else if (util.isReg(rule)) {
  4332. result = util.getAllKeys(obj).filter(keyName => rule.test(keyName));
  4333. }
  4334.  
  4335. /* 如果存在排除规则,则需要进行排除 */
  4336. if (excludeRule) {
  4337. result = Array.isArray(result) ? result : [result];
  4338. if (util.isReg(excludeRule)) {
  4339. result = result.filter(keyName => !excludeRule.test(keyName));
  4340. } else if (Array.isArray(excludeRule)) {
  4341. result = result.filter(keyName => !excludeRule.includes(keyName));
  4342. } else {
  4343. result = result.filter(keyName => excludeRule !== keyName);
  4344. }
  4345. }
  4346.  
  4347. return util.toArr(result)
  4348. }
  4349.  
  4350. /**
  4351. * 判断某个函数是否已经被hook
  4352. * @param fn {Function} -必选 要判断的函数
  4353. * @returns {boolean}
  4354. */
  4355. isHook (fn) {
  4356. if (!fn || !fn[this.hookPropertiesKeyName]) {
  4357. return false
  4358. }
  4359. const hookMethodProperties = fn[this.hookPropertiesKeyName];
  4360. return util.isFn(hookMethodProperties.originMethod) && fn !== hookMethodProperties.originMethod
  4361. }
  4362.  
  4363. /**
  4364. * 判断对象下的某个值是否具备hook的条件
  4365. * 注意:具备hook条件和能否直接修改值是两回事,
  4366. * 在进行hook的时候还要检查descriptor.writable是否为false
  4367. * 如果为false则要修改成true才能hook成功
  4368. * @param parentObj
  4369. * @param keyName
  4370. * @returns {boolean}
  4371. */
  4372. isAllowHook (parentObj, keyName) {
  4373. /* 有些对象会设置getter,让读取值的时候就抛错,所以需要try catch 判断能否正常读取属性 */
  4374. try { if (!parentObj[keyName]) return false } catch (e) { return false }
  4375. const descriptor = Object.getOwnPropertyDescriptor(parentObj, keyName);
  4376. return !(descriptor && descriptor.configurable === false)
  4377. }
  4378.  
  4379. /**
  4380. * hook 核心函数
  4381. * @param parentObj {Object} -必选 被hook函数依赖的父对象
  4382. * @param hookMethods {Object|Array|RegExp|string} -必选 被hook函数的函数名或函数名的匹配规则
  4383. * @param fn {Function} -必选 hook之后的回调方法
  4384. * @param type {String} -可选 默认before,指定运行hook函数回调的时机,可选字符串:before、after、replace、error、hangUp
  4385. * @param classHook {Boolean} -可选 默认false,指定是否为针对new(class)操作的hook
  4386. * @param context {Object} -可选 指定运行被hook函数时的上下文对象
  4387. * @param proxyHandler {Object} -可选 仅当用Proxy进行hook时有效,默认使用的是Proxy的apply handler进行hook,如果你有特殊需求也可以配置自己的handler以实现更复杂的功能
  4388. * 附注:不使用Proxy进行hook,可以获得更高性能,但也意味着通用性更差些,对于要hook HTMLElement.prototype、EventTarget.prototype这些对象里面的非实例的函数往往会失败而导致被hook函数执行出错
  4389. * @returns {boolean}
  4390. */
  4391. hook (parentObj, hookMethods, fn, type, classHook, context, proxyHandler) {
  4392. /* 支持对象形式的传参 */
  4393. const opts = arguments[0];
  4394. if (util.isObj(opts) && opts.parentObj && opts.hookMethods) {
  4395. parentObj = opts.parentObj;
  4396. hookMethods = opts.hookMethods;
  4397. fn = opts.fn;
  4398. type = opts.type;
  4399. classHook = opts.classHook;
  4400. context = opts.context;
  4401. proxyHandler = opts.proxyHandler;
  4402. }
  4403.  
  4404. classHook = toBoolean(classHook);
  4405. type = type || 'before';
  4406.  
  4407. if ((!util.isRef(parentObj) && !util.isFn(parentObj)) || !util.isFn(fn) || !hookMethods) {
  4408. return false
  4409. }
  4410.  
  4411. const t = this;
  4412.  
  4413. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  4414. hookMethods.forEach(methodName => {
  4415. if (!t.isAllowHook(parentObj, methodName)) {
  4416. util.debug.log(`${util.toStr(parentObj)} [${methodName}] does not support modification`);
  4417. return false
  4418. }
  4419.  
  4420. const descriptor = Object.getOwnPropertyDescriptor(parentObj, methodName);
  4421. if (descriptor && descriptor.writable === false) {
  4422. Object.defineProperty(parentObj, methodName, { writable: true });
  4423. }
  4424.  
  4425. const originMethod = parentObj[methodName];
  4426. let hookMethod = null;
  4427.  
  4428. /* 非函数无法进行hook操作 */
  4429. if (!util.isFn(originMethod)) {
  4430. return false
  4431. }
  4432.  
  4433. hookMethod = t._proxyMethodcGenerator(parentObj, methodName, originMethod, classHook, context, proxyHandler);
  4434.  
  4435. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  4436. if (hookMethodProperties.classHook !== classHook) {
  4437. util.debug.log(`${util.toStr(parentObj)} [${methodName}] Cannot support functions hook and classes hook at the same time `);
  4438. return false
  4439. }
  4440.  
  4441. /* 使用hookMethod接管需要被hook的方法 */
  4442. if (parentObj[methodName] !== hookMethod) {
  4443. parentObj[methodName] = hookMethod;
  4444. }
  4445.  
  4446. t._addHook(hookMethod, fn, type, classHook);
  4447. });
  4448. }
  4449.  
  4450. /* 专门针对new操作的hook,本质上是hook函数的别名,可以少传classHook这个参数,并且明确语义 */
  4451. hookClass (parentObj, hookMethods, fn, type, context, proxyHandler) {
  4452. return this.hook(parentObj, hookMethods, fn, type, true, context, proxyHandler)
  4453. }
  4454.  
  4455. /**
  4456. * 取消对某个函数的hook
  4457. * @param parentObj {Object} -必选 要取消被hook函数依赖的父对象
  4458. * @param hookMethods {Object|Array|RegExp|string} -必选 要取消被hook函数的函数名或函数名的匹配规则
  4459. * @param type {String} -可选 默认before,指定要取消的hook类型,可选字符串:before、after、replace、error、hangUp,如果不指定该选项则取消所有类型下的所有回调
  4460. * @param fn {Function} -必选 取消指定的hook回调函数,如果不指定该选项则取消对应type类型下的所有回调
  4461. * @returns {boolean}
  4462. */
  4463. unHook (parentObj, hookMethods, type, fn) {
  4464. if (!util.isRef(parentObj) || !hookMethods) {
  4465. return false
  4466. }
  4467.  
  4468. const t = this;
  4469. hookMethods = t._getObjKeysByRule(parentObj, hookMethods);
  4470. hookMethods.forEach(methodName => {
  4471. if (!t.isAllowHook(parentObj, methodName)) {
  4472. return false
  4473. }
  4474.  
  4475. const hookMethod = parentObj[methodName];
  4476.  
  4477. if (!t.isHook(hookMethod)) {
  4478. return false
  4479. }
  4480.  
  4481. const hookMethodProperties = hookMethod[t.hookPropertiesKeyName];
  4482. const originMethod = hookMethodProperties.originMethod;
  4483.  
  4484. if (type) {
  4485. const hookKeyName = type + 'Hooks';
  4486. const hooks = hookMethodProperties[hookKeyName] || [];
  4487.  
  4488. if (fn) {
  4489. /* 删除指定类型下的指定hook函数 */
  4490. for (let i = 0; i < hooks.length; i++) {
  4491. if (fn === hooks[i]) {
  4492. hookMethodProperties[hookKeyName].splice(i, 1);
  4493. util.debug.log(`[unHook ${hookKeyName} func] ${util.toStr(parentObj)} ${methodName}`, fn);
  4494. break
  4495. }
  4496. }
  4497. } else {
  4498. /* 删除指定类型下的所有hook函数 */
  4499. if (Array.isArray(hookMethodProperties[hookKeyName])) {
  4500. hookMethodProperties[hookKeyName] = [];
  4501. util.debug.log(`[unHook all ${hookKeyName}] ${util.toStr(parentObj)} ${methodName}`);
  4502. }
  4503. }
  4504. } else {
  4505. /* 彻底还原被hook的函数 */
  4506. if (util.isFn(originMethod)) {
  4507. parentObj[methodName] = originMethod;
  4508. delete parentObj[methodName][t.hookPropertiesKeyName];
  4509.  
  4510. // Object.keys(hookMethod).forEach(keyName => {
  4511. // if (/Hooks$/.test(keyName) && Array.isArray(hookMethod[keyName])) {
  4512. // hookMethod[keyName] = []
  4513. // }
  4514. // })
  4515. //
  4516. // hookMethod.isHook = false
  4517. // parentObj[methodName] = originMethod
  4518. // delete parentObj[methodName].originMethod
  4519. // delete parentObj[methodName].hookMethod
  4520. // delete parentObj[methodName].isHook
  4521. // delete parentObj[methodName].isClassHook
  4522.  
  4523. util.debug.log(`[unHook method] ${util.toStr(parentObj)} ${methodName}`);
  4524. }
  4525. }
  4526. });
  4527. }
  4528.  
  4529. _hook (args, type) {
  4530. const t = this;
  4531. return function (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4532. const opts = args[0];
  4533. if (util.isObj(opts) && opts.parentObj && opts.hookMethods) {
  4534. opts.type = type;
  4535. }
  4536. return t.hook.apply(t, args)
  4537. }
  4538. }
  4539.  
  4540. /* 源函数运行前的hook */
  4541. before (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4542. return this.hook(obj, hookMethods, fn, 'before', classHook, context, proxyHandler)
  4543. }
  4544.  
  4545. /* 源函数运行后的hook */
  4546. after (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4547. return this.hook(obj, hookMethods, fn, 'after', classHook, context, proxyHandler)
  4548. }
  4549.  
  4550. /* 替换掉要hook的函数,不再运行源函数,换成运行其他逻辑 */
  4551. replace (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4552. return this.hook(obj, hookMethods, fn, 'replace', classHook, context, proxyHandler)
  4553. }
  4554.  
  4555. /* 源函数运行出错时的hook */
  4556. error (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4557. return this.hook(obj, hookMethods, fn, 'error', classHook, context, proxyHandler)
  4558. }
  4559.  
  4560. /* 底层实现逻辑与replace一样,都是替换掉要hook的函数,不再运行源函数,只不过是为了明确语义,将源函数挂起不再执行,原则上也不再执行其他逻辑,如果要执行其他逻辑请使用replace hook */
  4561. hangUp (obj, hookMethods, fn, classHook, context, proxyHandler) {
  4562. return this.hook(obj, hookMethods, fn, 'hangUp', classHook, context, proxyHandler)
  4563. }
  4564. }
  4565.  
  4566. const hookJs = new HookJs(true);
  4567.  
  4568. /**
  4569. * 禁止对playbackRate进行锁定
  4570. * 部分播放器会阻止修改playbackRate
  4571. * 通过hackDefineProperty来反阻止playbackRate的修改
  4572. * 参考: https://greasyfork.org/zh-CN/scripts/372673
  4573. */
  4574.  
  4575. function hackDefineProperCore (target, key, option) {
  4576. if (option && target && target instanceof Element && typeof key === 'string' && key.indexOf('on') >= 0) {
  4577. option.configurable = true;
  4578. }
  4579.  
  4580. if (target instanceof HTMLVideoElement) {
  4581. const unLockProperties = ['playbackRate', 'currentTime', 'volume', 'muted'];
  4582. if (unLockProperties.includes(key)) {
  4583. try {
  4584. debug.log(`禁止对${key}进行锁定`);
  4585. option.configurable = true;
  4586. key = key + '_hack';
  4587. } catch (e) {
  4588. debug.error(`禁止锁定${key}失败!`, e);
  4589. }
  4590. }
  4591. }
  4592.  
  4593. return [target, key, option]
  4594. }
  4595.  
  4596. function hackDefineProperOnError (args, parentObj, methodName, originMethod, execInfo, ctx) {
  4597. debug.error(`${methodName} error:`, execInfo.error);
  4598.  
  4599. /* 忽略执行异常 */
  4600. return 'SKIP-ERROR'
  4601. }
  4602.  
  4603. function hackDefineProperty () {
  4604. hookJs.before(Object, 'defineProperty', function (args, parentObj, methodName, originMethod, execInfo, ctx) {
  4605. const option = args[2];
  4606. const ele = args[0];
  4607. const key = args[1];
  4608. const afterArgs = hackDefineProperCore(ele, key, option);
  4609. afterArgs.forEach((arg, i) => {
  4610. args[i] = arg;
  4611. });
  4612. });
  4613.  
  4614. hookJs.before(Object, 'defineProperties', function (args, parentObj, methodName, originMethod, execInfo, ctx) {
  4615. const properties = args[1];
  4616. const ele = args[0];
  4617. if (ele && ele instanceof Element) {
  4618. Object.keys(properties).forEach(key => {
  4619. const option = properties[key];
  4620. const afterArgs = hackDefineProperCore(ele, key, option);
  4621. args[0] = afterArgs[0];
  4622. delete properties[key];
  4623. properties[afterArgs[1]] = afterArgs[2];
  4624. });
  4625. }
  4626. });
  4627.  
  4628. hookJs.error(Object, 'defineProperty', hackDefineProperOnError);
  4629. hookJs.error(Object, 'defineProperties', hackDefineProperOnError);
  4630. }
  4631.  
  4632. /*!
  4633. * @name menuCommand.js
  4634. * @version 0.0.1
  4635. * @author Blaze
  4636. * @date 2019/9/21 14:22
  4637. */
  4638.  
  4639. const monkeyMenu = {
  4640. menuIds: {},
  4641. on (title, fn, accessKey) {
  4642. if (title instanceof Function) {
  4643. title = title();
  4644. }
  4645.  
  4646. if (window.GM_registerMenuCommand) {
  4647. const menuId = window.GM_registerMenuCommand(title, fn, accessKey);
  4648.  
  4649. this.menuIds[menuId] = {
  4650. title,
  4651. fn,
  4652. accessKey
  4653. };
  4654.  
  4655. return menuId
  4656. }
  4657. },
  4658.  
  4659. off (id) {
  4660. if (window.GM_unregisterMenuCommand) {
  4661. delete this.menuIds[id];
  4662.  
  4663. /**
  4664. * 批量移除已注册的按钮时,在某些性能较差的机子上会留下数字title的菜单残留
  4665. * 应该属于插件自身导致的BUG,暂时无法解决
  4666. * 所以此处暂时不进行菜单移除,tampermonkey会自动对同名菜单进行合并
  4667. */
  4668. // return window.GM_unregisterMenuCommand(id)
  4669. }
  4670. },
  4671.  
  4672. clear () {
  4673. Object.keys(this.menuIds).forEach(id => {
  4674. this.off(id);
  4675. });
  4676. },
  4677.  
  4678. /**
  4679. * 通过菜单配置进行批量注册,注册前会清空之前注册过的所有菜单
  4680. * @param {array|function} menuOpts 菜单配置,如果是函数则会调用该函数获取菜单配置,并且当菜单被点击后会重新创建菜单,实现菜单的动态更新
  4681. */
  4682. build (menuOpts) {
  4683. this.clear();
  4684.  
  4685. if (Array.isArray(menuOpts)) {
  4686. menuOpts.forEach(menu => {
  4687. if (menu.disable === true) { return }
  4688. this.on(menu.title, menu.fn, menu.accessKey);
  4689. });
  4690. } else if (menuOpts instanceof Function) {
  4691. const menuList = menuOpts();
  4692. if (Array.isArray(menuList)) {
  4693. this._menuBuilder_ = menuOpts;
  4694.  
  4695. menuList.forEach(menu => {
  4696. if (menu.disable === true) { return }
  4697.  
  4698. const menuFn = () => {
  4699. try {
  4700. menu.fn.apply(menu, arguments);
  4701. } catch (e) {
  4702. console.error('[monkeyMenu]', menu.title, e);
  4703. }
  4704.  
  4705. // 每次菜单点击后,重新注册菜单,这样可以确保菜单的状态是最新的
  4706. setTimeout(() => {
  4707. // console.log('[monkeyMenu rebuild]', menu.title)
  4708. this.build(this._menuBuilder_);
  4709. }, 100);
  4710. };
  4711.  
  4712. this.on(menu.title, menuFn, menu.accessKey);
  4713. });
  4714. } else {
  4715. console.error('monkeyMenu build error, no menuList return', menuOpts);
  4716. }
  4717. }
  4718. }
  4719. };
  4720.  
  4721. /*!
  4722. * @name menuManager.js
  4723. * @description 菜单管理器
  4724. * @version 0.0.1
  4725. * @author xxxily
  4726. * @date 2022/08/11 10:05
  4727. * @github https://github.com/xxxily
  4728. */
  4729.  
  4730. function refreshPage (msg) {
  4731. msg = msg || '配置已更改,马上刷新页面让配置生效?';
  4732. const status = confirm(msg);
  4733. if (status) {
  4734. window.location.reload();
  4735. }
  4736. }
  4737.  
  4738. let monkeyMenuList = [
  4739. {
  4740. title: i18n.t('website'),
  4741. fn: () => {
  4742. openInTab('https://h5player.anzz.top/');
  4743. }
  4744. },
  4745. {
  4746. title: i18n.t('hotkeys'),
  4747. fn: () => {
  4748. openInTab('https://h5player.anzz.top/home/Introduction.html#%E5%BF%AB%E6%8D%B7%E9%94%AE%E5%88%97%E8%A1%A8');
  4749. }
  4750. },
  4751. {
  4752. title: i18n.t('issues'),
  4753. disable: !configManager.get('enhance.unfoldMenu'),
  4754. fn: () => {
  4755. openInTab('https://github.com/xxxily/h5player/issues');
  4756. }
  4757. },
  4758. {
  4759. title: i18n.t('donate'),
  4760. fn: () => {
  4761. openInTab('https://h5player.anzz.top/#%E8%B5%9E');
  4762. }
  4763. },
  4764. /* 推广位,只允许推荐有用的东西 */
  4765. {
  4766. title: i18n.t('recommend'),
  4767. // disable: !i18n.language().includes('zh'),
  4768. disable: true,
  4769. fn: () => {
  4770. function randomZeroOrOne () {
  4771. return Math.floor(Math.random() * 2)
  4772. }
  4773.  
  4774. if (randomZeroOrOne()) {
  4775. openInTab('https://hello-ai.anzz.top/home/');
  4776. } else {
  4777. openInTab('https://github.com/xxxily/hello-ai');
  4778. }
  4779. }
  4780. },
  4781. {
  4782. title: i18n.t('globalSetting'),
  4783. disable: !i18n.language().includes('zh'),
  4784. fn: () => {
  4785. // openInTab(`https://h5player.anzz.top/tools/json-editor/index.html?mode=code&referrer=${encodeURIComponent(window.location.href)}`)
  4786. openInTab('https://h5player.anzz.top/tools/json-editor/index.html?mode=tree&saveHandlerName=saveH5PlayerConfig&expandAll=true&json={}');
  4787. }
  4788. },
  4789. {
  4790. title: `${configManager.get('enhance.unfoldMenu') ? i18n.t('foldMenu') : i18n.t('unfoldMenu')} ${i18n.t('globalSetting')}」`,
  4791. fn: () => {
  4792. const confirm = window.confirm(configManager.get('enhance.unfoldMenu') ? i18n.t('foldMenu') : i18n.t('unfoldMenu'));
  4793. if (confirm) {
  4794. configManager.setGlobalStorage('enhance.unfoldMenu', !configManager.get('enhance.unfoldMenu'));
  4795. window.location.reload();
  4796. }
  4797. }
  4798. },
  4799. {
  4800. title: i18n.t('setting'),
  4801. disable: true,
  4802. fn: () => {
  4803. openInTab('https://h5player.anzz.top/configure/', null, true);
  4804. window.alert('功能开发中,敬请期待...');
  4805. }
  4806. },
  4807. {
  4808. title: `${configManager.get('enable') ? i18n.t('disableScript') : i18n.t('enableScript')} ${i18n.t('localSetting')}」`,
  4809. disable: !configManager.get('enhance.unfoldMenu'),
  4810. fn: () => {
  4811. const confirm = window.confirm(configManager.get('enable') ? i18n.t('disableScript') : i18n.t('enableScript'));
  4812. if (confirm) {
  4813. configManager.setLocalStorage('enable', !configManager.get('enable'));
  4814. window.location.reload();
  4815. }
  4816. }
  4817. },
  4818. {
  4819. title: i18n.t('restoreConfiguration'),
  4820. disable: !configManager.get('enhance.unfoldMenu'),
  4821. fn: () => {
  4822. configManager.clear();
  4823. refreshPage();
  4824. }
  4825. }
  4826. ];
  4827.  
  4828. /* 菜单构造函数(必须是函数才能在点击后动态更新菜单状态) */
  4829. function menuBuilder () {
  4830. return monkeyMenuList
  4831. }
  4832.  
  4833. /* 注册动态菜单 */
  4834. function menuRegister () {
  4835. monkeyMenu.build(menuBuilder);
  4836. }
  4837.  
  4838. /**
  4839. * 增加菜单项
  4840. * @param {Object|Array} menuOpts 菜单的配置项目,多个配置项目用数组表示
  4841. */
  4842. function addMenu (menuOpts, before) {
  4843. menuOpts = Array.isArray(menuOpts) ? menuOpts : [menuOpts];
  4844. menuOpts = menuOpts.filter(item => item.title && !item.disabled);
  4845.  
  4846. if (before) {
  4847. /* 将菜单追加到其它菜单的前面 */
  4848. monkeyMenuList = menuOpts.concat(monkeyMenuList);
  4849. } else {
  4850. monkeyMenuList = monkeyMenuList.concat(menuOpts);
  4851. }
  4852.  
  4853. /* 重新注册菜单 */
  4854. menuRegister();
  4855. }
  4856.  
  4857. /**
  4858. * 注册跟h5player相关的菜单,只有检测到存在媒体标签了才会注册
  4859. */
  4860. function registerH5playerMenus (h5player) {
  4861. const t = h5player;
  4862. const player = t.player();
  4863. const foldMenu = !configManager.get('enhance.unfoldMenu');
  4864.  
  4865. if (player && !t._hasRegisterH5playerMenus_) {
  4866. const menus = [
  4867. {
  4868. title: () => i18n.t('openCrossOriginFramePage'),
  4869. disable: foldMenu || !isInCrossOriginFrame(),
  4870. fn: () => {
  4871. openInTab(location.href);
  4872. }
  4873. },
  4874. {
  4875. title: () => `${configManager.get('enhance.blockSetCurrentTime') ? i18n.t('unblockSetCurrentTime') : i18n.t('blockSetCurrentTime')} ${i18n.t('localSetting')}」`,
  4876. type: 'local',
  4877. disable: foldMenu,
  4878. fn: () => {
  4879. const confirm = window.confirm(configManager.get('enhance.blockSetCurrentTime') ? i18n.t('unblockSetCurrentTime') : i18n.t('blockSetCurrentTime'));
  4880. if (confirm) {
  4881. configManager.setLocalStorage('enhance.blockSetCurrentTime', !configManager.get('enhance.blockSetCurrentTime'));
  4882. window.location.reload();
  4883. }
  4884. }
  4885. },
  4886. {
  4887. title: () => `${configManager.get('enhance.blockSetVolume') ? i18n.t('unblockSetVolume') : i18n.t('blockSetVolume')} ${i18n.t('localSetting')}」`,
  4888. type: 'local',
  4889. disable: foldMenu,
  4890. fn: () => {
  4891. const confirm = window.confirm(configManager.get('enhance.blockSetVolume') ? i18n.t('unblockSetVolume') : i18n.t('blockSetVolume'));
  4892. if (confirm) {
  4893. configManager.setLocalStorage('enhance.blockSetVolume', !configManager.get('enhance.blockSetVolume'));
  4894. window.location.reload();
  4895. }
  4896. }
  4897. },
  4898. {
  4899. title: () => `${configManager.get('enhance.blockSetPlaybackRate') ? i18n.t('unblockSetPlaybackRate') : i18n.t('blockSetPlaybackRate')} ${i18n.t('globalSetting')}」`,
  4900. type: 'global',
  4901. disable: foldMenu,
  4902. fn: () => {
  4903. const confirm = window.confirm(configManager.get('enhance.blockSetPlaybackRate') ? i18n.t('unblockSetPlaybackRate') : i18n.t('blockSetPlaybackRate'));
  4904. if (confirm) {
  4905. /* 倍速参数,只能全局设置 */
  4906. configManager.setGlobalStorage('enhance.blockSetPlaybackRate', !configManager.get('enhance.blockSetPlaybackRate'));
  4907. window.location.reload();
  4908. }
  4909. }
  4910. },
  4911. {
  4912. title: () => `${configManager.get('enhance.allowAcousticGain') ? i18n.t('notAllowAcousticGain') : i18n.t('allowAcousticGain')} ${i18n.t('globalSetting')}」`,
  4913. type: 'global',
  4914. disable: foldMenu,
  4915. fn: () => {
  4916. const confirm = window.confirm(configManager.get('enhance.allowAcousticGain') ? i18n.t('notAllowAcousticGain') : i18n.t('allowAcousticGain'));
  4917. if (confirm) {
  4918. configManager.setGlobalStorage('enhance.allowAcousticGain', !configManager.getGlobalStorage('enhance.allowAcousticGain'));
  4919. window.location.reload();
  4920. }
  4921. }
  4922. },
  4923. {
  4924. title: () => `${configManager.get('enhance.allowCrossOriginControl') ? i18n.t('notAllowCrossOriginControl') : i18n.t('allowCrossOriginControl')} ${i18n.t('globalSetting')}」`,
  4925. type: 'global',
  4926. disable: foldMenu,
  4927. fn: () => {
  4928. const confirm = window.confirm(configManager.get('enhance.allowCrossOriginControl') ? i18n.t('notAllowCrossOriginControl') : i18n.t('allowCrossOriginControl'));
  4929. if (confirm) {
  4930. configManager.setGlobalStorage('enhance.allowCrossOriginControl', !configManager.getGlobalStorage('enhance.allowCrossOriginControl'));
  4931. window.location.reload();
  4932. }
  4933. }
  4934. },
  4935. {
  4936. title: () => `${configManager.get('enhance.allowExperimentFeatures') ? i18n.t('notAllowExperimentFeatures') : i18n.t('allowExperimentFeatures')} ${i18n.t('globalSetting')}」`,
  4937. type: 'global',
  4938. disable: foldMenu,
  4939. fn: () => {
  4940. const confirm = window.confirm(configManager.get('enhance.allowExperimentFeatures') ? i18n.t('notAllowExperimentFeatures') : i18n.t('experimentFeaturesWarning'));
  4941. if (confirm) {
  4942. configManager.setGlobalStorage('enhance.allowExperimentFeatures', !configManager.get('enhance.allowExperimentFeatures'));
  4943. window.location.reload();
  4944. }
  4945. }
  4946. },
  4947. {
  4948. title: () => `${configManager.get('enhance.allowExternalCustomConfiguration') ? i18n.t('notAllowExternalCustomConfiguration') : i18n.t('allowExternalCustomConfiguration')} ${i18n.t('globalSetting')}」`,
  4949. type: 'global',
  4950. disable: foldMenu,
  4951. fn: () => {
  4952. const confirm = window.confirm(configManager.get('enhance.allowExternalCustomConfiguration') ? i18n.t('notAllowExternalCustomConfiguration') : i18n.t('allowExternalCustomConfiguration'));
  4953. if (confirm) {
  4954. configManager.setGlobalStorage('enhance.allowExternalCustomConfiguration', !configManager.getGlobalStorage('enhance.allowExternalCustomConfiguration'));
  4955. window.location.reload();
  4956. }
  4957. }
  4958. },
  4959. {
  4960. title: () => `${configManager.getGlobalStorage('debug') ? i18n.t('closeDebugMode') : i18n.t('openDebugMode')} ${i18n.t('globalSetting')}」`,
  4961. disable: foldMenu,
  4962. fn: () => {
  4963. const confirm = window.confirm(configManager.getGlobalStorage('debug') ? i18n.t('closeDebugMode') : i18n.t('openDebugMode'));
  4964. if (confirm) {
  4965. configManager.setGlobalStorage('debug', !configManager.getGlobalStorage('debug'));
  4966. window.location.reload();
  4967. }
  4968. }
  4969. }
  4970. ];
  4971.  
  4972. let titlePrefix = '';
  4973. if (isInIframe()) {
  4974. titlePrefix = `[${location.hostname}]`;
  4975.  
  4976. /* 补充title前缀 */
  4977. menus.forEach(menu => {
  4978. const titleFn = menu.title;
  4979. if (titleFn instanceof Function && menu.type === 'local') {
  4980. menu.title = () => titlePrefix + titleFn();
  4981. }
  4982. });
  4983. }
  4984.  
  4985. addMenu(menus);
  4986.  
  4987. t._hasRegisterH5playerMenus_ = true;
  4988. }
  4989. }
  4990.  
  4991. /**
  4992. * 代理视频播放器的事件注册和取消注册的函数,以对注册事件进行调试或阻断
  4993. * @param {*} player
  4994. * @returns
  4995. */
  4996. function proxyHTMLMediaElementEvent () {
  4997. if (HTMLMediaElement.prototype._rawAddEventListener_) {
  4998. return false
  4999. }
  5000.  
  5001. HTMLMediaElement.prototype._rawAddEventListener_ = HTMLMediaElement.prototype.addEventListener;
  5002. HTMLMediaElement.prototype._rawRemoveEventListener_ = HTMLMediaElement.prototype.removeEventListener;
  5003.  
  5004. HTMLMediaElement.prototype.addEventListener = new Proxy(HTMLMediaElement.prototype.addEventListener, {
  5005. apply (target, ctx, args) {
  5006. const eventName = args[0];
  5007. const listener = args[1];
  5008. if (listener instanceof Function && eventName === 'ratechange') {
  5009. /* 对注册了ratechange事件进行检测,如果存在异常行为,则尝试挂起事件 */
  5010.  
  5011. args[1] = new Proxy(listener, {
  5012. apply (target, ctx, args) {
  5013. if (ctx) {
  5014. /* 阻止调速检测,并进行反阻止 */
  5015. if (ctx.playbackRate && eventName === 'ratechange') {
  5016. if (ctx._hasBlockRatechangeEvent_) {
  5017. return true
  5018. }
  5019.  
  5020. const oldRate = ctx.playbackRate;
  5021. const startTime = Date.now();
  5022.  
  5023. const result = target.apply(ctx, args);
  5024.  
  5025. /**
  5026. * 通过判断执行ratechange前后的速率是否被改变,
  5027. * 以及是否出现了超长的执行时间(可能出现了alert弹窗)来检测是否可能存在阻止调速的行为
  5028. * 其他检测手段待补充
  5029. */
  5030. const blockRatechangeBehave1 = oldRate !== ctx.playbackRate || Date.now() - startTime > 1000;
  5031. const blockRatechangeBehave2 = ctx._setPlaybackRate_ && ctx._setPlaybackRate_.value !== ctx.playbackRate;
  5032. if (blockRatechangeBehave1 || blockRatechangeBehave2) {
  5033. debug.info(`[execVideoEvent][${eventName}]检测到可能存在阻止调速的行为,已禁止${eventName}事件的执行`, listener);
  5034. ctx._hasBlockRatechangeEvent_ = true;
  5035. return true
  5036. } else {
  5037. return result
  5038. }
  5039. }
  5040. }
  5041.  
  5042. try {
  5043. return target.apply(ctx, args)
  5044. } catch (e) {
  5045. debug.error(`[proxyPlayerEvent][${eventName}]`, listener, e);
  5046. }
  5047. }
  5048. });
  5049. }
  5050.  
  5051. return target.apply(ctx, args)
  5052. }
  5053. });
  5054. }
  5055.  
  5056. /*!
  5057. * @name hotkeysRunner.js
  5058. * @description 热键运行器,实现类似vscode的热键配置方式
  5059. * @version 0.0.1
  5060. * @author xxxily
  5061. * @date 2022/11/23 18:22
  5062. * @github https://github.com/xxxily
  5063. */
  5064.  
  5065. const Map$1 = window.Map;
  5066. const WeakMap = window.WeakMap;
  5067. function isObj$1 (obj) { return Object.prototype.toString.call(obj) === '[object Object]' }
  5068.  
  5069. function getValByPath$1 (obj, path) {
  5070. path = path || '';
  5071. const pathArr = path.split('.');
  5072. let result = obj;
  5073.  
  5074. /* 递归提取结果值 */
  5075. for (let i = 0; i < pathArr.length; i++) {
  5076. if (!result) break
  5077. result = result[pathArr[i]];
  5078. }
  5079.  
  5080. return result
  5081. }
  5082.  
  5083. function toArrArgs (args) {
  5084. return Array.isArray(args) ? args : (typeof args === 'undefined' ? [] : [args])
  5085. }
  5086.  
  5087. function isModifierKey (key) {
  5088. return [
  5089. 'ctrl', 'controlleft', 'controlright',
  5090. 'shift', 'shiftleft', 'shiftright',
  5091. 'alt', 'altleft', 'altright',
  5092. 'meta', 'metaleft', 'metaright',
  5093. 'capsLock'].includes(key.toLowerCase())
  5094. }
  5095.  
  5096. const keyAlias = {
  5097. ControlLeft: 'ctrl',
  5098. ControlRight: 'ctrl',
  5099. ShiftLeft: 'shift',
  5100. ShiftRight: 'shift',
  5101. AltLeft: 'alt',
  5102. AltRight: 'alt',
  5103. MetaLeft: 'meta',
  5104. MetaRight: 'meta'
  5105. };
  5106.  
  5107. const combinationKeysMonitor = (function () {
  5108. const combinationKeysState = new Map$1();
  5109.  
  5110. const hasInit = new WeakMap();
  5111.  
  5112. function init (win = window) {
  5113. if (!win || win !== win.self || !win.addEventListener || hasInit.get(win)) {
  5114. return false
  5115. }
  5116.  
  5117. const timers = {};
  5118.  
  5119. function activeCombinationKeysState (event) {
  5120. isModifierKey(event.code) && combinationKeysState.set(event.code, true);
  5121. }
  5122.  
  5123. function inactivateCombinationKeysState (event) {
  5124. if (!(event instanceof KeyboardEvent)) {
  5125. combinationKeysState.forEach((val, key) => {
  5126. combinationKeysState.set(key, false);
  5127. });
  5128. return true
  5129. }
  5130.  
  5131. /**
  5132. * combinationKeysState状态必须保留一段时间,否则当外部定义的是keyup事件时候,由于这个先注册也先执行,
  5133. * 马上更改combinationKeysState状态,会导致后面定义的事件拿到的是未激活组合键的状态
  5134. */
  5135. if (isModifierKey(event.code)) {
  5136. clearTimeout(timers[event.code]);
  5137. timers[event.code] = setTimeout(() => { combinationKeysState.set(event.code, false); }, 50);
  5138. }
  5139. }
  5140.  
  5141. win.addEventListener('keydown', activeCombinationKeysState, true);
  5142. win.addEventListener('keypress', activeCombinationKeysState, true);
  5143. win.addEventListener('keyup', inactivateCombinationKeysState, true);
  5144. win.addEventListener('blur', inactivateCombinationKeysState, true);
  5145.  
  5146. hasInit.set(win, true);
  5147. }
  5148.  
  5149. function getCombinationKeys () {
  5150. const result = new Map$1();
  5151. combinationKeysState.forEach((val, key) => {
  5152. if (val === true) {
  5153. result.set(key, val);
  5154. }
  5155. });
  5156. return result
  5157. }
  5158.  
  5159. return {
  5160. combinationKeysState,
  5161. getCombinationKeys,
  5162. init
  5163. }
  5164. })();
  5165.  
  5166. class HotkeysRunner {
  5167. constructor (hotkeys, win = window) {
  5168. this.window = win;
  5169. /* Mac和window使用的修饰符是不一样的 */
  5170. this.MOD = typeof navigator === 'object' && /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
  5171. // 'Control', 'Shift', 'Alt', 'Meta'
  5172.  
  5173. this.prevPress = null;
  5174. this._prevTimer_ = null;
  5175.  
  5176. this.setHotkeys(hotkeys);
  5177. combinationKeysMonitor.init(win);
  5178. }
  5179.  
  5180. /* 设置其它window对象的组合键监控逻辑 */
  5181. setCombinationKeysMonitor (win) {
  5182. this.window = win;
  5183. combinationKeysMonitor.init(win);
  5184. }
  5185.  
  5186. /* 数据预处理 */
  5187. hotkeysPreprocess (hotkeys) {
  5188. if (!Array.isArray(hotkeys)) {
  5189. return false
  5190. }
  5191.  
  5192. hotkeys.forEach((config) => {
  5193. if (!isObj$1(config) || !config.key || typeof config.key !== 'string') {
  5194. return false
  5195. }
  5196.  
  5197. const keyName = config.key.trim().toLowerCase();
  5198. const mod = this.MOD.toLowerCase();
  5199.  
  5200. /* 增加格式化后的hotkeys数组 */
  5201. config.keyBindings = keyName.split(' ').map(press => {
  5202. const keys = press.split(/\b\+/);
  5203. const mods = [];
  5204. let key = '';
  5205.  
  5206. keys.forEach((k) => {
  5207. k = k === '$mod' ? mod : k;
  5208.  
  5209. if (isModifierKey(k)) {
  5210. mods.push(k);
  5211. } else {
  5212. key = k;
  5213. }
  5214. });
  5215.  
  5216. return [mods, key]
  5217. });
  5218. });
  5219.  
  5220. return hotkeys
  5221. }
  5222.  
  5223. setHotkeys (hotkeys) {
  5224. this.hotkeys = this.hotkeysPreprocess(hotkeys) || [];
  5225. }
  5226.  
  5227. /**
  5228. * 判断当前提供的键盘事件和预期的热键配置是否匹配
  5229. * @param {KeyboardEvent} event
  5230. * @param {Array} press 例如:[['alt', 'shift'], 's']
  5231. * @param {Object} prevCombinationKeys
  5232. * @returns
  5233. */
  5234. isMatch (event, press) {
  5235. if (!event || !Array.isArray(press)) { return false }
  5236.  
  5237. const combinationKeys = event.combinationKeys || combinationKeysMonitor.getCombinationKeys();
  5238. const mods = press[0];
  5239. const key = press[1];
  5240.  
  5241. /* 修饰符个数不匹配 */
  5242. if (mods.length !== combinationKeys.size) {
  5243. return false
  5244. }
  5245.  
  5246. /* 当前按下的键位和预期的键位不匹配 */
  5247. if (key && event.key.toLowerCase() !== key && event.code.toLowerCase() !== key) {
  5248. return false
  5249. }
  5250.  
  5251. /* 当前按下的修饰符和预期的修饰符不匹配 */
  5252. let result = true;
  5253. const modsKey = new Map$1();
  5254. combinationKeys.forEach((val, key) => {
  5255. /* 补充各种可能情况的标识 */
  5256. modsKey.set(key, val);
  5257. modsKey.set(key.toLowerCase(), val);
  5258. keyAlias[key] && modsKey.set(keyAlias[key], val);
  5259. });
  5260.  
  5261. mods.forEach((key) => {
  5262. if (!modsKey.has(key)) {
  5263. result = false;
  5264. }
  5265. });
  5266.  
  5267. return result
  5268. }
  5269.  
  5270. isMatchPrevPress (press) { return this.isMatch(this.prevPress, press) }
  5271.  
  5272. run (opts = {}) {
  5273. const KeyboardEvent = this.window.KeyboardEvent;
  5274. if (!(opts.event instanceof KeyboardEvent)) { return false }
  5275.  
  5276. const event = opts.event;
  5277. const target = opts.target || null;
  5278. const conditionHandler = opts.conditionHandler || opts.whenHandler;
  5279.  
  5280. let matchResult = null;
  5281.  
  5282. this.hotkeys.forEach(hotkeyConf => {
  5283. if (hotkeyConf.disabled || !hotkeyConf.keyBindings) {
  5284. return false
  5285. }
  5286.  
  5287. let press = hotkeyConf.keyBindings[0];
  5288.  
  5289. /* 当存在prevPress,则不再响应与prevPress不匹配的其它快捷键 */
  5290. if (this.prevPress && (hotkeyConf.keyBindings.length <= 1 || !this.isMatchPrevPress(press))) {
  5291. return false
  5292. }
  5293.  
  5294. /* 如果存在上一轮的操作快捷键记录,且之前的快捷键与第一个keyBindings定义的快捷键匹配,则去匹配第二个keyBindings */
  5295. if (this.prevPress && hotkeyConf.keyBindings.length > 1 && this.isMatchPrevPress(press)) {
  5296. press = hotkeyConf.keyBindings[1];
  5297. }
  5298.  
  5299. const isMatch = this.isMatch(event, press);
  5300. if (!isMatch) { return false }
  5301.  
  5302. matchResult = hotkeyConf;
  5303.  
  5304. /* 是否阻止事件冒泡和阻止默认事件 */
  5305. const stopPropagation = opts.stopPropagation || hotkeyConf.stopPropagation;
  5306. const preventDefault = opts.preventDefault || hotkeyConf.preventDefault;
  5307. stopPropagation && event.stopPropagation();
  5308. preventDefault && event.preventDefault();
  5309.  
  5310. /* 记录上一次操作的快捷键,且一段时间后清空该操作的记录 */
  5311. if (press === hotkeyConf.keyBindings[0] && hotkeyConf.keyBindings.length > 1) {
  5312. /* 将prevPress变成一个具有event相关字段的对象 */
  5313. this.prevPress = {
  5314. combinationKeys: combinationKeysMonitor.getCombinationKeys(),
  5315. code: event.code,
  5316. key: event.key,
  5317. keyCode: event.keyCode,
  5318. altKey: event.altKey,
  5319. shiftKey: event.shiftKey,
  5320. ctrlKey: event.ctrlKey,
  5321. metaKey: event.metaKey
  5322. };
  5323.  
  5324. clearTimeout(this._prevTimer_);
  5325. this._prevTimer_ = setTimeout(() => { this.prevPress = null; }, 1000);
  5326.  
  5327. return true
  5328. }
  5329.  
  5330. /* 如果当前匹配到了第二个快捷键,则当forEach循环结束后,马上注销prevPress,给其它快捷键让行 */
  5331. if (hotkeyConf.keyBindings.length > 1 && press !== hotkeyConf.keyBindings[0]) {
  5332. setTimeout(() => { this.prevPress = null; }, 0);
  5333. }
  5334.  
  5335. /* 执行hotkeyConf.command对应的函数或命令 */
  5336. const args = toArrArgs(hotkeyConf.args);
  5337. let commandFunc = hotkeyConf.command;
  5338. if (target && typeof hotkeyConf.command === 'string') {
  5339. commandFunc = getValByPath$1(target, hotkeyConf.command);
  5340. }
  5341.  
  5342. if (!(commandFunc instanceof Function) && target) {
  5343. throw new Error(`[hotkeysRunner] 未找到command: ${hotkeyConf.command} 对应的函数`)
  5344. }
  5345.  
  5346. if (hotkeyConf.when && conditionHandler instanceof Function) {
  5347. const isMatchCondition = conditionHandler.apply(target, toArrArgs(hotkeyConf.when));
  5348. if (isMatchCondition === true) {
  5349. commandFunc.apply(target, args);
  5350. }
  5351. } else {
  5352. commandFunc.apply(target, args);
  5353. }
  5354. });
  5355.  
  5356. return matchResult
  5357. }
  5358.  
  5359. binding (opts = {}) {
  5360. if (!isObj$1(opts) || !Array.isArray(opts.hotkeys)) {
  5361. throw new Error('[hotkeysRunner] 提供给binding的参数不正确')
  5362. }
  5363.  
  5364. opts.el = opts.el || this.window;
  5365. opts.type = opts.type || 'keydown';
  5366. opts.debug && (this.debug = true);
  5367.  
  5368. this.setHotkeys(opts.hotkeys);
  5369.  
  5370. if (typeof opts.el === 'string') {
  5371. opts.el = document.querySelector(opts.el);
  5372. }
  5373.  
  5374. opts.el.addEventListener(opts.type, (event) => {
  5375. opts.event = event;
  5376. this.run(opts);
  5377. }, true);
  5378. }
  5379. }
  5380.  
  5381. /* eslint-disable camelcase */
  5382.  
  5383. /**
  5384. * @license Copyright 2017 - Chris West - MIT Licensed
  5385. * Prototype to easily set the volume (actual and perceived), loudness,
  5386. * decibels, and gain value.
  5387. * https://cwestblog.com/2017/08/22/web-audio-api-controlling-audio-video-loudness/
  5388. */
  5389. function MediaElementAmplifier (mediaElem) {
  5390. this._context = new (window.AudioContext || window.webkitAudioContext)();
  5391. this._source = this._context.createMediaElementSource(this._element = mediaElem);
  5392. this._source.connect(this._gain = this._context.createGain());
  5393. this._gain.connect(this._context.destination);
  5394. }
  5395. [
  5396. 'getContext',
  5397. 'getSource',
  5398. 'getGain',
  5399. 'getElement',
  5400. [
  5401. 'getVolume',
  5402. function (opt_getPerceived) {
  5403. return (opt_getPerceived ? this.getLoudness() : 1) * this._element.volume
  5404. }
  5405. ],
  5406. [
  5407. 'setVolume',
  5408. function (value, opt_setPerceived) {
  5409. var volume = value / (opt_setPerceived ? this.getLoudness() : 1);
  5410. if (volume > 1) {
  5411. this.setLoudness(this.getLoudness() * volume);
  5412. volume = 1;
  5413. }
  5414. this._element.volume = volume;
  5415. }
  5416. ],
  5417. ['getGainValue', function () { return this._gain.gain.value }],
  5418. ['setGainValue', function (value) { this._gain.gain.value = value; }],
  5419. ['getDecibels', function () { return 20 * Math.log10(this.getGainValue()) }],
  5420. ['setDecibels', function (value) { this.setGainValue(Math.pow(10, value / 20)); }],
  5421. ['getLoudness', function () { return Math.pow(2, this.getDecibels() / 10) }],
  5422. ['setLoudness', function (value) { this.setDecibels(10 * Math.log2(value)); }]
  5423. ].forEach(function (name, fn) {
  5424. if (typeof name === 'string') {
  5425. fn = function () { return this[name.replace('get', '').toLowerCase()] };
  5426. } else {
  5427. fn = name[1];
  5428. name = name[0];
  5429. }
  5430. MediaElementAmplifier.prototype[name] = fn;
  5431. });
  5432.  
  5433. function download (url, title) {
  5434. const downloadEl = document.createElement('a');
  5435. downloadEl.href = url;
  5436. downloadEl.target = '_blank';
  5437. downloadEl.download = title;
  5438. downloadEl.click();
  5439. }
  5440.  
  5441. function mediaDownload (mediaEl, title, downloadType) {
  5442. if (mediaEl && (mediaEl.src || mediaEl.currentSrc) && !mediaEl.src.startsWith('blob:')) {
  5443. const mediaInfo = {
  5444. type: mediaEl instanceof HTMLVideoElement ? 'video' : 'audio',
  5445. format: mediaEl instanceof HTMLVideoElement ? 'mp4' : 'mp3'
  5446. };
  5447. let mediaTitle = `${title || mediaEl.title || document.title || Date.now()}_${mediaInfo.type}.${mediaInfo.format}`;
  5448.  
  5449. /**
  5450. * 当媒体包含source标签时,媒体标签的真实地址将会是currentSrc
  5451. * https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/currentSrc
  5452. */
  5453. const mediaUrl = mediaEl.src || mediaEl.currentSrc;
  5454.  
  5455. /* 小于5分钟的媒体文件,尝试通过fetch下载 */
  5456. if (downloadType === 'blob' || mediaEl.duration < 60 * 5) {
  5457. if (mediaEl.downloading) {
  5458. /* 距上次点下载小于1s的情况直接不响应任何操作 */
  5459. if (Date.now() - mediaEl.downloading < 1000 * 1) {
  5460. return false
  5461. } else {
  5462. const confirm = original.confirm('文件正在下载中,确定重复执行此操作?');
  5463. if (!confirm) {
  5464. return false
  5465. }
  5466. }
  5467. }
  5468.  
  5469. if (mediaEl.hasDownload) {
  5470. const confirm = original.confirm('该媒体文件已经下载过了,确定需要再次下载?');
  5471. if (!confirm) {
  5472. return false
  5473. }
  5474. }
  5475.  
  5476. mediaTitle = original.prompt('请确认文件标题:', mediaTitle) || mediaTitle;
  5477.  
  5478. if (!mediaTitle.endsWith(mediaInfo.format)) {
  5479. mediaTitle = mediaTitle + '.' + mediaInfo.format;
  5480. }
  5481.  
  5482. let fetchUrl = mediaUrl;
  5483. if (mediaUrl.startsWith('http://') && location.href.startsWith('https://')) {
  5484. /* 在https里fetch http资源会导致 block:mixed-content 错误,所以尝试将地址统一成https开头 */
  5485. fetchUrl = mediaUrl.replace('http://', 'https://');
  5486. }
  5487.  
  5488. mediaEl.downloading = Date.now();
  5489. fetch(fetchUrl).then(res => {
  5490. res.blob().then(blob => {
  5491. const blobUrl = window.URL.createObjectURL(blob);
  5492. download(blobUrl, mediaTitle);
  5493.  
  5494. mediaEl.hasDownload = true;
  5495. delete mediaEl.downloading;
  5496. window.URL.revokeObjectURL(blobUrl);
  5497. });
  5498. }).catch(err => {
  5499. original.console.error('fetch下载操作失败:', err);
  5500.  
  5501. /* 下载兜底 */
  5502. download(mediaUrl, mediaTitle);
  5503. });
  5504. } else {
  5505. download(mediaUrl, mediaTitle);
  5506. }
  5507. } else if (mediaSource.hasInit()) {
  5508. /* 下载通过MediaSource管理的媒体文件 */
  5509. mediaSource.downloadMediaSource();
  5510. } else {
  5511. original.alert('当前媒体文件无法下载,下载功能待优化完善');
  5512. }
  5513. }
  5514.  
  5515. /* 定义支持哪些媒体标签 */
  5516. // const supportMediaTags = ['video', 'bwp-video', 'audio']
  5517. const supportMediaTags = ['video', 'bwp-video'];
  5518.  
  5519. let TCC$1 = null;
  5520. const h5Player = {
  5521. mediaCore,
  5522. mediaPlusApi: null,
  5523. mediaSource,
  5524. configManager,
  5525. /* 提示文本的字号 */
  5526. fontSize: 12,
  5527. enable: true,
  5528. globalMode: true,
  5529. playerInstance: null,
  5530. scale: 1,
  5531. translate: {
  5532. x: 0,
  5533. y: 0
  5534. },
  5535. rotate: 0,
  5536.  
  5537. /* 水平镜像翻转, 0 或 180 */
  5538. rotateY: 0,
  5539. /* 垂直镜像翻转, 0 或 180 */
  5540. rotateX: 0,
  5541.  
  5542. defaultTransform: {
  5543. scale: 1,
  5544. translate: {
  5545. x: 0,
  5546. y: 0
  5547. },
  5548. rotate: 0,
  5549. rotateY: 0,
  5550. rotateX: 0
  5551. },
  5552.  
  5553. /* 存储旧的Transform值 */
  5554. historyTransform: {},
  5555.  
  5556. playbackRate: configManager.get('media.playbackRate'),
  5557. volume: configManager.get('media.volume'),
  5558. lastPlaybackRate: 1,
  5559. /* 快进快退步长 */
  5560. skipStep: 5,
  5561.  
  5562. /* 监听鼠标活动的观察对象 */
  5563. mouseObserver: new MouseObserver(),
  5564.  
  5565. /* 获取当前播放器的实例 */
  5566. player: function () {
  5567. const t = this;
  5568. let playerInstance = t.playerInstance;
  5569.  
  5570. if (!playerInstance) {
  5571. const mediaList = t.getPlayerList();
  5572. if (mediaList.length) {
  5573. playerInstance = mediaList[mediaList.length - 1];
  5574. t.playerInstance = playerInstance;
  5575. t.initPlayerInstance(mediaList.length === 1);
  5576. }
  5577. }
  5578.  
  5579. if (playerInstance && !t.mediaPlusApi) {
  5580. t.mediaPlusApi = mediaCore.mediaPlus(playerInstance);
  5581. }
  5582.  
  5583. return playerInstance
  5584. },
  5585.  
  5586. isAudioInstance () {
  5587. return isAudioElement(this.player())
  5588. },
  5589.  
  5590. /* 每个网页可能存在的多个video播放器 */
  5591. getPlayerList: function () {
  5592. const list = mediaCore.mediaElementList || [];
  5593.  
  5594. function findPlayer (context) {
  5595. supportMediaTags.forEach(tagName => {
  5596. context.querySelectorAll(tagName).forEach(function (player) {
  5597. if (player.tagName.toLowerCase() === 'bwp-video') {
  5598. /* 将B站的BWP-VIDEO标识为HTMLVideoElement */
  5599. player.HTMLVideoElement = true;
  5600. }
  5601.  
  5602. if (isMediaElement(player) && !list.includes(player)) {
  5603. list.push(player);
  5604. }
  5605. });
  5606. });
  5607. }
  5608.  
  5609. findPlayer(document);
  5610.  
  5611. // 被封装在 shadow dom 里面的video
  5612. if (window._shadowDomList_) {
  5613. window._shadowDomList_.forEach(function (shadowRoot) {
  5614. findPlayer(shadowRoot);
  5615. });
  5616. }
  5617.  
  5618. return list
  5619. },
  5620.  
  5621. getPlayerWrapDom: function () {
  5622. const t = this;
  5623. const player = t.player();
  5624. if (!player) return
  5625.  
  5626. let wrapDom = null;
  5627. const playerBox = player.getBoundingClientRect();
  5628. eachParentNode(player, function (parent) {
  5629. if (parent === document || !parent.getBoundingClientRect) return
  5630. const parentBox = parent.getBoundingClientRect();
  5631. if (parentBox.width && parentBox.height) {
  5632. if (parentBox.width === playerBox.width && parentBox.height === playerBox.height) {
  5633. wrapDom = parent;
  5634. }
  5635. }
  5636. });
  5637. return wrapDom
  5638. },
  5639.  
  5640. /* 挂载到页面上的window对象,用于调试 */
  5641. async mountToGlobal () {
  5642. try {
  5643. const pageWindow = await getPageWindow();
  5644. if (pageWindow) {
  5645. pageWindow._h5Player = h5Player || 'null';
  5646. if (window.top !== window) {
  5647. pageWindow._h5PlayerInFrame = h5Player || 'null';
  5648. }
  5649. pageWindow._window = window || '';
  5650. debug.log('h5Player对象已成功挂载到全局');
  5651. }
  5652. } catch (e) {
  5653. debug.error(e);
  5654. }
  5655. },
  5656.  
  5657. /**
  5658. * 初始化播放器实例
  5659. * @param isSingle 是否为单实例video标签
  5660. */
  5661. initPlayerInstance (isSingle) {
  5662. const t = this;
  5663. if (!t.playerInstance) return
  5664.  
  5665. const player = t.playerInstance;
  5666.  
  5667. t.mediaPlusApi = mediaCore.mediaPlus(player);
  5668.  
  5669. t.initPlaybackRate();
  5670. t.isFoucs();
  5671. t.proxyPlayerInstance(player);
  5672.  
  5673. t.unLockPlaybackRate();
  5674. t.setPlaybackRate();
  5675. t.lockPlaybackRate(1000);
  5676.  
  5677. /* 增加通用全屏,网页全屏api */
  5678. player._fullScreen_ = new FullScreen(player);
  5679. player._fullPageScreen_ = new FullScreen(player, true);
  5680.  
  5681. /* 注册热键运行器 */
  5682. t.registerHotkeysRunner();
  5683.  
  5684. if (!player._hasCanplayEvent_) {
  5685. player.addEventListener('canplay', function (event) {
  5686. t.initAutoPlay(player);
  5687. });
  5688. player._hasCanplayEvent_ = true;
  5689. }
  5690.  
  5691. /* 播放的时候进行相关同步操作 */
  5692. if (!player._hasPlayerInitEvent_) {
  5693. let setPlaybackRateOnPlayingCount = 0;
  5694. player.addEventListener('playing', function (event) {
  5695. t.unLockPlaybackRate();
  5696. t.setPlaybackRate(null, true);
  5697. t.lockPlaybackRate(1000);
  5698.  
  5699. /* 同步播放音量 */
  5700. if (configManager.get('enhance.blockSetVolume') === true && event.target.muted === false) {
  5701. t.setVolume(configManager.getGlobalStorage('media.volume'), true);
  5702. }
  5703.  
  5704. /* 禁止默认的进度控制 */
  5705. if (configManager.get('enhance.blockSetCurrentTime') === true) {
  5706. t.lockCurrentTime();
  5707. }
  5708.  
  5709. /* 恢复播放进度 */
  5710. t.setPlayProgress(player);
  5711.  
  5712. if (setPlaybackRateOnPlayingCount === 0) {
  5713. /* 同步之前设定的播放速度,音量等 */
  5714. t.unLockPlaybackRate();
  5715. t.setPlaybackRate();
  5716. t.lockPlaybackRate(1000);
  5717.  
  5718. /* 启动播放进度记录 */
  5719. setTimeout(() => {
  5720. t.playProgressRecorder(player);
  5721. }, 2000);
  5722. } else {
  5723. t.unLockPlaybackRate();
  5724. t.setPlaybackRate(null, true);
  5725. t.lockPlaybackRate(1000);
  5726. }
  5727. setPlaybackRateOnPlayingCount += 1;
  5728. });
  5729.  
  5730. player._hasPlayerInitEvent_ = true;
  5731. }
  5732.  
  5733. /* 进行自定义初始化操作 */
  5734. const taskConf = TCC$1.getTaskConfig();
  5735. if (taskConf.init) {
  5736. TCC$1.doTask('init', player);
  5737. }
  5738.  
  5739. /* 注册鼠标响应事件 */
  5740. t.mouseObserver.on(player, 'click', function (event, offset, target) {
  5741. // debug.log('捕捉到鼠标点击事件:', event, offset, target)
  5742. });
  5743.  
  5744. /* 画中画事件监听 */
  5745. player.addEventListener('enterpictureinpicture', () => {
  5746. monkeyMsg.send('globalPictureInPictureInfo', {
  5747. usePictureInPicture: true
  5748. });
  5749. debug.log('enterpictureinpicture', player);
  5750. });
  5751. player.addEventListener('leavepictureinpicture', () => {
  5752. t.leavepictureinpictureTime = Date.now();
  5753.  
  5754. monkeyMsg.send('globalPictureInPictureInfo', {
  5755. usePictureInPicture: false
  5756. });
  5757. debug.log('leavepictureinpicture', player);
  5758. });
  5759.  
  5760. if (debug.isDebugMode()) {
  5761. player.addEventListener('loadeddata', function () {
  5762. debug.log(`video url: ${player.src} video duration: ${player.duration} video dom:`, player);
  5763. });
  5764.  
  5765. player.addEventListener('durationchange', function () {
  5766. debug.log(`video durationchange: ${player.duration}`);
  5767. });
  5768. }
  5769. },
  5770.  
  5771. registerHotkeysRunner () {
  5772. if (!this.hotkeysRunner) {
  5773. this.hotkeysRunner = new HotkeysRunner(configManager.get('hotkeys'));
  5774.  
  5775. if (isInIframe() && !isInCrossOriginFrame()) {
  5776. /* 让顶层页面也可以监听组合键的触发 */
  5777. this.hotkeysRunner.setCombinationKeysMonitor(window.top);
  5778. }
  5779. }
  5780. },
  5781.  
  5782. /* 刚关闭画中画不久,此段时间内允许跨TAB控制 */
  5783. isLeavepictureinpictureAwhile () {
  5784. const t = this;
  5785. return t.leavepictureinpictureTime && (Date.now() - t.leavepictureinpictureTime < 1000 * 10)
  5786. },
  5787.  
  5788. /**
  5789. * 对播放器实例的方法或属性进行代理
  5790. * @param player
  5791. */
  5792. proxyPlayerInstance (player) {
  5793. if (!player) return
  5794.  
  5795. /* 要代理的方法或属性列表 */
  5796. const proxyList = [
  5797. 'play',
  5798. 'pause'
  5799. ];
  5800.  
  5801. proxyList.forEach(key => {
  5802. const originKey = 'origin_' + key;
  5803. if (Reflect.has(player, key) && !Reflect.has(player, originKey)) {
  5804. player[originKey] = player[key];
  5805. const proxy = new Proxy(player[key], {
  5806. apply (target, ctx, args) {
  5807. // debug.log(key + '被调用')
  5808.  
  5809. /* 处理挂起逻辑 */
  5810. const hangUpInfo = player._hangUpInfo_ || {};
  5811. const hangUpDetail = hangUpInfo[key] || hangUpInfo['hangUp_' + key];
  5812. const needHangUp = hangUpDetail && hangUpDetail.timeout >= Date.now();
  5813. if (needHangUp) {
  5814. debug.log(key + '已被挂起,本次调用将被忽略');
  5815. return false
  5816. }
  5817.  
  5818. return target.apply(ctx || player, args)
  5819. }
  5820. });
  5821.  
  5822. player[key] = proxy;
  5823. }
  5824. });
  5825.  
  5826. if (!player._hangUp_) {
  5827. player._hangUpInfo_ = {};
  5828. /**
  5829. * 挂起player某个函数的调用
  5830. * @param name {String} -必选 player方法或属性名,名字写对外,还须要该方法或属性被代理了才能进行挂起,否则这将是个无效的调用
  5831. * @param timeout {Number} -可选 挂起多长时间,默认200ms
  5832. * @private
  5833. */
  5834. player._hangUp_ = function (name, timeout) {
  5835. timeout = Number(timeout) || 200;
  5836. // debug.log('_hangUp_', name, timeout)
  5837. player._hangUpInfo_[name] = {
  5838. timeout: Date.now() + timeout
  5839. };
  5840. };
  5841.  
  5842. /* 取消挂起 */
  5843. player._unHangUp_ = function (name) {
  5844. if (player._hangUpInfo_ && player._hangUpInfo_[name]) {
  5845. player._hangUpInfo_[name].timeout = Date.now() - 1;
  5846. }
  5847. };
  5848. }
  5849. },
  5850.  
  5851. /**
  5852. * 初始化自动播放逻辑
  5853. * 必须是配置了自动播放按钮选择器得的才会进行自动播放
  5854. */
  5855. initAutoPlay: function (p) {
  5856. const t = this;
  5857. const player = p || t.player();
  5858. const taskConf = TCC$1.getTaskConfig();
  5859.  
  5860. /* 注册开启禁止自动播放的控制菜单 */
  5861. if (taskConf.autoPlay) {
  5862. if (configManager.getLocalStorage('media.autoPlay') === null) {
  5863. configManager.setLocalStorage('media.autoPlay', true);
  5864. }
  5865.  
  5866. addMenu({
  5867. title: () => configManager.getLocalStorage('media.autoPlay') ? i18n.t('disableInitAutoPlay') : i18n.t('enableInitAutoPlay'),
  5868. fn: () => {
  5869. const confirm = window.confirm(configManager.getLocalStorage('media.autoPlay') ? i18n.t('disableInitAutoPlay') : i18n.t('enableInitAutoPlay'));
  5870. if (confirm) {
  5871. const autoPlay = configManager.getLocalStorage('media.autoPlay');
  5872. if (autoPlay === null) {
  5873. alert(i18n.t('configFail'));
  5874. } else {
  5875. configManager.setLocalStorage('media.autoPlay', !autoPlay);
  5876. }
  5877. }
  5878. }
  5879. });
  5880. }
  5881.  
  5882. // 在轮询重试的时候,如果实例变了,或处于隐藏页面中则不进行自动播放操作
  5883. if (!configManager.get('media.autoPlay') || (!p && t.hasInitAutoPlay) || !player || (p && p !== t.player()) || document.hidden) {
  5884. return false
  5885. }
  5886.  
  5887. /**
  5888. * 元素不在可视范围,不允许进行初始化自动播放逻辑
  5889. * 由于iframe下元素的可视范围判断不准确,所以iframe下也禁止初始化自动播放逻辑
  5890. * TODO 待优化
  5891. */
  5892. if (!isInViewPort(player) || isInIframe()) {
  5893. return false
  5894. }
  5895.  
  5896. if (!taskConf.autoPlay) {
  5897. return false
  5898. }
  5899.  
  5900. t.hasInitAutoPlay = true;
  5901.  
  5902. if (player && taskConf.autoPlay && player.paused) {
  5903. TCC$1.doTask('autoPlay');
  5904. if (player.paused) {
  5905. // 轮询重试
  5906. if (!player._initAutoPlayCount_) {
  5907. player._initAutoPlayCount_ = 1;
  5908. }
  5909. player._initAutoPlayCount_ += 1;
  5910. if (player._initAutoPlayCount_ >= 10) {
  5911. return false
  5912. }
  5913. setTimeout(function () {
  5914. t.initAutoPlay(player);
  5915. }, 200);
  5916. }
  5917. }
  5918. },
  5919.  
  5920. /* 设置视频全屏 */
  5921. setFullScreen () {
  5922. const player = this.player();
  5923. const isDo = TCC$1.doTask('fullScreen');
  5924. if (!isDo && player && player._fullScreen_) {
  5925. player._fullScreen_.toggle();
  5926. }
  5927. },
  5928.  
  5929. /* 设置页面全屏 */
  5930. setWebFullScreen: function () {
  5931. const t = this;
  5932. const player = t.player();
  5933. const isDo = TCC$1.doTask('webFullScreen');
  5934. if (!isDo && player && player._fullPageScreen_) {
  5935. player._fullPageScreen_.toggle();
  5936. }
  5937. },
  5938.  
  5939. initPlaybackRate () {
  5940. const t = this;
  5941. t.playbackRate = t.getPlaybackRate();
  5942. },
  5943.  
  5944. playbackRateInfo: {
  5945. lockTimeout: Date.now() - 1,
  5946. time: Date.now(),
  5947. /* 未初始化播放实列前,不知道倍速是多少,所以设置为-1 */
  5948. value: -1
  5949. },
  5950.  
  5951. getPlaybackRate () {
  5952. let playbackRate = configManager.get('media.playbackRate') || this.playbackRate;
  5953. if (isInIframe()) {
  5954. const globalPlaybackRate = configManager.getGlobalStorage('media.playbackRate');
  5955. if (globalPlaybackRate) {
  5956. playbackRate = globalPlaybackRate;
  5957. }
  5958. }
  5959. return Number(Number(playbackRate).toFixed(1))
  5960. },
  5961.  
  5962. /* 锁定playbackRate,禁止调速 */
  5963. lockPlaybackRate: function (timeout = 200) {
  5964. if (this.mediaPlusApi) {
  5965. if (configManager.get('enhance.blockSetPlaybackRate') === true) {
  5966. // 如果配置了要锁死外部对playbackRate的操作,则直接给一个超大的值
  5967. timeout = 1000 * 60 * 60 * 24 * 365;
  5968. }
  5969.  
  5970. this.mediaPlusApi.lockPlaybackRate(timeout);
  5971. return true
  5972. }
  5973.  
  5974. this.playbackRateInfo.lockTimeout = Date.now() + timeout;
  5975. },
  5976.  
  5977. unLockPlaybackRate: function () {
  5978. if (this.mediaPlusApi) {
  5979. this.mediaPlusApi.unLockPlaybackRate();
  5980. return true
  5981. }
  5982.  
  5983. this.playbackRateInfo.lockTimeout = Date.now() - 1;
  5984. },
  5985.  
  5986. isLockPlaybackRate: function () {
  5987. if (this.mediaPlusApi) {
  5988. return this.mediaPlusApi.isLockPlaybackRate()
  5989. }
  5990.  
  5991. return Date.now() - this.playbackRateInfo.lockTimeout < 0
  5992. },
  5993.  
  5994. /* 解决高低倍速频繁切换后,音画不同步的问题 */
  5995. fixPlaybackRate: function (oldPlaybackRate) {
  5996. const t = this;
  5997. const curPlaybackRate = t.getPlaybackRate();
  5998.  
  5999. if (Math.abs(curPlaybackRate - oldPlaybackRate) > 1) {
  6000. t.setCurrentTimeUp(0.1, true);
  6001. }
  6002. },
  6003.  
  6004. /* 设置播放速度 */
  6005. setPlaybackRate: function (num, notips, duplicate) {
  6006. const t = this;
  6007. const player = t.player();
  6008.  
  6009. if (t.isLockPlaybackRate()) {
  6010. debug.info('调速能力已被锁定');
  6011. return false
  6012. }
  6013.  
  6014. if (TCC$1.doTask('playbackRate')) {
  6015. // debug.log('[TCC][playbackRate]', 'suc')
  6016. return
  6017. }
  6018.  
  6019. if (!player) return
  6020.  
  6021. const oldPlaybackRate = t.getPlaybackRate();
  6022.  
  6023. let curPlaybackRate;
  6024. if (num) {
  6025. num = Number(num);
  6026. if (Number.isNaN(num)) {
  6027. debug.error('h5player: 播放速度转换出错');
  6028. return false
  6029. }
  6030.  
  6031. if (num <= 0) {
  6032. num = 0.1;
  6033. } else if (num > 16) {
  6034. num = 16;
  6035. }
  6036.  
  6037. num = Number(num.toFixed(1));
  6038. curPlaybackRate = num;
  6039. } else {
  6040. curPlaybackRate = t.getPlaybackRate();
  6041. }
  6042.  
  6043. /* 记录播放速度的信息 */
  6044. t.playbackRate = curPlaybackRate;
  6045. if (isInIframe()) {
  6046. configManager.setGlobalStorage('media.playbackRate', curPlaybackRate);
  6047. } else {
  6048. configManager.set('media.playbackRate', curPlaybackRate);
  6049. }
  6050.  
  6051. if (t.mediaPlusApi) {
  6052. t.mediaPlusApi.setPlaybackRate(curPlaybackRate);
  6053.  
  6054. if (!(!num && curPlaybackRate === 1) && !notips) {
  6055. t.tips(i18n.t('tipsMsg.playspeed') + player.playbackRate);
  6056. }
  6057.  
  6058. /* 将播放倍速同步到全部媒体元素 */
  6059. const mediaList = t.getPlayerList();
  6060. mediaList.forEach(media => {
  6061. if (media !== player) {
  6062. const mediaPlusApi = mediaCore.mediaPlus(media);
  6063. mediaPlusApi && mediaPlusApi.setPlaybackRate(curPlaybackRate);
  6064. }
  6065. });
  6066.  
  6067. t.fixPlaybackRate(oldPlaybackRate);
  6068. return true
  6069. }
  6070.  
  6071. delete player.playbackRate;
  6072. player.playbackRate = curPlaybackRate;
  6073.  
  6074. t.playbackRateInfo.time = Date.now();
  6075. t.playbackRateInfo.value = curPlaybackRate;
  6076. player._setPlaybackRate_ = {
  6077. time: Date.now(),
  6078. value: curPlaybackRate
  6079. };
  6080.  
  6081. try {
  6082. const playbackRateDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'playbackRate');
  6083. originalMethods.Object.defineProperty.call(Object, player, 'playbackRate', {
  6084. configurable: true,
  6085. get: function () {
  6086. /**
  6087. * 在油管,如果返回的是playbackRateDescriptor.get.apply(player, arguments),调速会出现波动和异常
  6088. * 暂时不知是什么原因,所以还是先返回curPlaybackRate
  6089. */
  6090. return curPlaybackRate || playbackRateDescriptor.get.apply(player, arguments)
  6091. },
  6092. set: function (val) {
  6093. if (typeof val !== 'number') {
  6094. return false
  6095. }
  6096.  
  6097. /* 有些网站是通过定时器不断刷playbackRate的,所以通过计时器减少不必要的信息输出 */
  6098. !Number.isInteger(player._blockSetPlaybackRateTips_) && (player._blockSetPlaybackRateTips_ = 0);
  6099.  
  6100. if (TCC$1.doTask('blockSetPlaybackRate')) {
  6101. player._blockSetPlaybackRateTips_++;
  6102. player._blockSetPlaybackRateTips_ < 3 && debug.info('调速能力已被自定义的调速任务进行处理');
  6103. return false
  6104. }
  6105.  
  6106. if (configManager.get('enhance.blockSetPlaybackRate') === true) {
  6107. player._blockSetPlaybackRateTips_++;
  6108. player._blockSetPlaybackRateTips_ < 3 && debug.info('调速能力已被blockSetPlaybackRate锁定');
  6109. return false
  6110. } else {
  6111. t.setPlaybackRate(val);
  6112. }
  6113. }
  6114. });
  6115. } catch (e) {
  6116. debug.error('解锁playbackRate失败', e);
  6117. }
  6118.  
  6119. /* 本身处于1倍播放速度的时候不再提示 */
  6120. if (!num && curPlaybackRate === 1) {
  6121. return true
  6122. } else {
  6123. !notips && t.tips(i18n.t('tipsMsg.playspeed') + player.playbackRate);
  6124. }
  6125.  
  6126. /**
  6127. * 重复触发最后一次倍速的设定
  6128. * 解决YouTube快速调速时并不生效,要停顿下来再调节一下才能生效的问题
  6129. */
  6130. if (!duplicate && configManager.get('enhance.blockSetPlaybackRate') === true) {
  6131. clearTimeout(t._setPlaybackRateDuplicate_);
  6132. clearTimeout(t._setPlaybackRateDuplicate2_);
  6133. const duplicatePlaybackRate = () => {
  6134. t.unLockPlaybackRate();
  6135. t.setPlaybackRate(curPlaybackRate, true, true);
  6136. t.lockPlaybackRate(1000);
  6137. };
  6138. t._setPlaybackRateDuplicate_ = setTimeout(duplicatePlaybackRate, 600);
  6139. /* 600ms时重新触发无效的话,再来个1200ms后触发,如果是1200ms才生效,则调速生效的延迟已经非常明显了 */
  6140. t._setPlaybackRateDuplicate2_ = setTimeout(duplicatePlaybackRate, 1200);
  6141. }
  6142.  
  6143. t.fixPlaybackRate(oldPlaybackRate);
  6144. },
  6145.  
  6146. /**
  6147. * 加强版的倍速调节,当短时间内设置同一个值时,会认为需更快的跳速能力
  6148. * 则会对调速的数值进行叠加放大,从而达到快速跳跃地进行倍速调节的目的
  6149. * 可用于视频广告的高速快进,片头片尾的速看等场景
  6150. * @param {*} num
  6151. */
  6152. setPlaybackRatePlus: function (num) {
  6153. num = Number(num);
  6154. if (!num || Number.isNaN(num)) {
  6155. return false
  6156. }
  6157.  
  6158. const t = this;
  6159. t.playbackRatePlusInfo = t.playbackRatePlusInfo || {};
  6160. t.playbackRatePlusInfo[num] = t.playbackRatePlusInfo[num] || {
  6161. time: Date.now() - 1000,
  6162. value: num
  6163. };
  6164.  
  6165. if (Date.now() - t.playbackRatePlusInfo[num].time < 300) {
  6166. t.playbackRatePlusInfo[num].value = t.playbackRatePlusInfo[num].value + num;
  6167. } else {
  6168. t.playbackRatePlusInfo[num].value = num;
  6169. }
  6170.  
  6171. t.playbackRatePlusInfo[num].time = Date.now();
  6172.  
  6173. t.unLockPlaybackRate();
  6174. t.setPlaybackRate(t.playbackRatePlusInfo[num].value);
  6175. t.lockPlaybackRate(1000);
  6176. },
  6177.  
  6178. /* 恢复播放速度,还原到1倍速度、或恢复到上次的倍速 */
  6179. resetPlaybackRate: function (player) {
  6180. const t = this;
  6181. player = player || t.player();
  6182.  
  6183. t.unLockPlaybackRate();
  6184.  
  6185. const oldPlaybackRate = Number(player.playbackRate);
  6186. const playbackRate = oldPlaybackRate === 1 ? t.lastPlaybackRate : 1;
  6187. if (oldPlaybackRate !== 1) {
  6188. t.lastPlaybackRate = oldPlaybackRate;
  6189. }
  6190.  
  6191. t.setPlaybackRate(playbackRate);
  6192.  
  6193. /* 防止外部调速逻辑的干扰,所以锁定一段时间 */
  6194. t.lockPlaybackRate(1000);
  6195. },
  6196.  
  6197. /* 提升播放速率 */
  6198. setPlaybackRateUp (num) {
  6199. num = numUp(num) || 0.1;
  6200. if (this.player()) {
  6201. this.unLockPlaybackRate();
  6202. this.setPlaybackRate(this.player().playbackRate + num);
  6203.  
  6204. /* 防止外部调速逻辑的干扰,所以锁定一段时间 */
  6205. this.lockPlaybackRate(1000);
  6206. }
  6207. },
  6208.  
  6209. /* 降低播放速率 */
  6210. setPlaybackRateDown (num) {
  6211. num = numDown(num) || -0.1;
  6212. if (this.player()) {
  6213. this.unLockPlaybackRate();
  6214. this.setPlaybackRate(this.player().playbackRate + num);
  6215.  
  6216. /* 防止外部调速逻辑的干扰,所以锁定一段时间 */
  6217. this.lockPlaybackRate(1000);
  6218. }
  6219. },
  6220.  
  6221. /**
  6222. * 锁定播放进度的控制逻辑
  6223. * 跟锁定音量和倍速不一样,播放进度是跟视频实例有密切相关的,所以其锁定信息必须依附于播放实例
  6224. */
  6225. lockCurrentTime: function (timeout = 200) {
  6226. if (this.mediaPlusApi) {
  6227. if (configManager.get('enhance.blockSetCurrentTime') === true) {
  6228. // 如果配置了要锁死外部对currentTime的操作,则直接给一个超大的值
  6229. timeout = 1000 * 60 * 60 * 24 * 365;
  6230. }
  6231.  
  6232. this.mediaPlusApi.lockCurrentTime(timeout);
  6233. return true
  6234. }
  6235.  
  6236. const player = this.player();
  6237. if (player) {
  6238. player.currentTimeInfo = player.currentTimeInfo || {};
  6239. player.currentTimeInfo.lockTimeout = Date.now() + timeout;
  6240. }
  6241. },
  6242.  
  6243. unLockCurrentTime: function () {
  6244. if (this.mediaPlusApi) {
  6245. this.mediaPlusApi.unLockCurrentTime();
  6246. return true
  6247. }
  6248.  
  6249. const player = this.player();
  6250. if (player) {
  6251. player.currentTimeInfo = player.currentTimeInfo || {};
  6252. player.currentTimeInfo.lockTimeout = Date.now() - 1;
  6253. }
  6254. },
  6255.  
  6256. isLockCurrentTime: function () {
  6257. if (this.mediaPlusApi) {
  6258. return this.mediaPlusApi.isLockCurrentTime()
  6259. }
  6260.  
  6261. const player = this.player();
  6262. if (player && player.currentTimeInfo && player.currentTimeInfo.lockTimeout) {
  6263. return Date.now() - player.currentTimeInfo.lockTimeout < 0
  6264. } else {
  6265. return false
  6266. }
  6267. },
  6268.  
  6269. /* 设置播放进度 */
  6270. setCurrentTime: function (num) {
  6271. if (!num && num !== 0) return
  6272. num = Number(num);
  6273. const _num = Math.abs(Number(num.toFixed(1)));
  6274.  
  6275. const t = this;
  6276. const player = t.player();
  6277.  
  6278. if (t.isLockCurrentTime()) {
  6279. return false
  6280. }
  6281.  
  6282. if (TCC$1.doTask('currentTime')) {
  6283. // debug.log('[TCC][currentTime]', 'suc')
  6284. return
  6285. }
  6286.  
  6287. if (this.mediaPlusApi) {
  6288. this.mediaPlusApi.setCurrentTime(_num);
  6289. return true
  6290. }
  6291.  
  6292. delete player.currentTime;
  6293. player.currentTime = _num;
  6294. player.currentTimeInfo = player.currentTimeInfo || {};
  6295. player.currentTimeInfo.time = Date.now();
  6296. player.currentTimeInfo.value = _num;
  6297.  
  6298. try {
  6299. const currentTimeDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'currentTime');
  6300. originalMethods.Object.defineProperty.call(Object, player, 'currentTime', {
  6301. configurable: true,
  6302. enumerable: true,
  6303. get: function () {
  6304. return currentTimeDescriptor.get.apply(player, arguments)
  6305. },
  6306. set: function (val) {
  6307. if (typeof val !== 'number' || TCC$1.doTask('blockSetCurrentTime') || configManager.get('enhance.blockSetCurrentTime') === true) {
  6308. return false
  6309. }
  6310.  
  6311. if (t.isLockCurrentTime()) {
  6312. return false
  6313. }
  6314.  
  6315. player.currentTimeInfo.time = Date.now();
  6316. player.currentTimeInfo.value = val;
  6317.  
  6318. return currentTimeDescriptor.set.apply(player, arguments)
  6319. }
  6320. });
  6321. } catch (e) {
  6322. debug.error('解锁currentTime失败', e);
  6323. }
  6324. },
  6325.  
  6326. setCurrentTimeUp (num, hideTips) {
  6327. num = Number(numUp(num) || this.skipStep);
  6328.  
  6329. if (TCC$1.doTask('addCurrentTime')) ; else {
  6330. if (this.player()) {
  6331. this.unLockCurrentTime();
  6332. this.setCurrentTime(this.player().currentTime + num);
  6333.  
  6334. /* 防止外部进度控制逻辑的干扰,所以锁定一段时间 */
  6335. this.lockCurrentTime(500);
  6336.  
  6337. if (!hideTips) {
  6338. this.tips(i18n.t('tipsMsg.forward') + num + i18n.t('tipsMsg.seconds'));
  6339. }
  6340. }
  6341. }
  6342. },
  6343.  
  6344. setCurrentTimeDown (num) {
  6345. num = Number(numDown(num) || -this.skipStep);
  6346.  
  6347. if (TCC$1.doTask('subtractCurrentTime')) ; else {
  6348. if (this.player()) {
  6349. let currentTime = this.player().currentTime + num;
  6350. if (currentTime < 1) {
  6351. currentTime = 0;
  6352. }
  6353.  
  6354. this.unLockCurrentTime();
  6355. this.setCurrentTime(currentTime);
  6356.  
  6357. /* 防止外部进度控制逻辑的干扰,所以锁定一段时间 */
  6358. this.lockCurrentTime(500);
  6359.  
  6360. this.tips(i18n.t('tipsMsg.backward') + Math.abs(num) + i18n.t('tipsMsg.seconds'));
  6361. }
  6362. }
  6363. },
  6364.  
  6365. volumeInfo: {
  6366. lockTimeout: Date.now() - 1,
  6367. time: Date.now(),
  6368. /* 未初始化播放实列前,不知道音量是多少,所以设置为-1 */
  6369. value: -1
  6370. },
  6371.  
  6372. getVolume: function () {
  6373. let volume = configManager.get('media.volume');
  6374. if (isInIframe() || configManager.get('enhance.blockSetVolume') === true) {
  6375. const globalVolume = configManager.getGlobalStorage('media.volume');
  6376. if (globalVolume !== null) {
  6377. volume = globalVolume;
  6378. }
  6379. }
  6380. return Number(Number(volume).toFixed(2))
  6381. },
  6382.  
  6383. /* 锁定音量,禁止调音 */
  6384. lockVolume: function (timeout = 200) {
  6385. if (this.mediaPlusApi) {
  6386. if (configManager.get('enhance.blockSetVolume') === true) {
  6387. // 如果配置了要锁死外部对voluem的操作,则直接给一个超大的值
  6388. timeout = 1000 * 60 * 60 * 24 * 365;
  6389. }
  6390.  
  6391. this.mediaPlusApi.lockVolume(timeout);
  6392. return true
  6393. }
  6394.  
  6395. this.volumeInfo.lockTimeout = Date.now() + timeout;
  6396. },
  6397.  
  6398. unLockVolume: function () {
  6399. if (this.mediaPlusApi) {
  6400. this.mediaPlusApi.unLockVolume();
  6401. return true
  6402. }
  6403.  
  6404. this.volumeInfo.lockTimeout = Date.now() - 1;
  6405. },
  6406.  
  6407. isLockVolume: function () {
  6408. if (this.mediaPlusApi) {
  6409. return this.mediaPlusApi.isLockVolume()
  6410. }
  6411.  
  6412. return Date.now() - this.volumeInfo.lockTimeout < 0
  6413. },
  6414.  
  6415. /* 设置声音大小 */
  6416. setVolume: function (num, notips, outerCall) {
  6417. const t = this;
  6418. const player = t.player();
  6419.  
  6420. if (t.isLockVolume()) {
  6421. return false
  6422. }
  6423.  
  6424. if (!num && num !== 0) {
  6425. num = t.getVolume();
  6426. }
  6427.  
  6428. num = Number(num).toFixed(2);
  6429. if (num < 0) {
  6430. num = 0;
  6431. }
  6432.  
  6433. if (num > 1 && configManager.get('enhance.allowAcousticGain')) {
  6434. num = Math.ceil(num);
  6435.  
  6436. try {
  6437. player._amp_ = player._amp_ || new MediaElementAmplifier(player);
  6438. } catch (e) {
  6439. num = 1;
  6440. debug.error('媒体声音响度增益逻辑异常', e);
  6441. }
  6442.  
  6443. /* 限定增益的最大值 */
  6444. if (num > 6) {
  6445. num = 6;
  6446. }
  6447.  
  6448. if (!player._amp_ || !player._amp_.setLoudness) {
  6449. num = 1;
  6450. }
  6451. } else if (num > 1) {
  6452. num = 1;
  6453. }
  6454.  
  6455. /* 记录播放音量信息 */
  6456. t.volume = num;
  6457.  
  6458. /* 使用音量增益逻辑,增益音量不进行本地存储记录 */
  6459. if (num > 1 && player._amp_ && player._amp_.setLoudness) {
  6460. player._amp_.setLoudness(num);
  6461.  
  6462. if (!outerCall) { player.muted = false; }
  6463.  
  6464. !notips && t.tips(i18n.t('tipsMsg.volume') + parseInt(num * 100) + '%');
  6465. return true
  6466. }
  6467.  
  6468. if (isInIframe() || configManager.get('enhance.blockSetVolume') === true) {
  6469. configManager.setGlobalStorage('media.volume', num);
  6470. } else {
  6471. configManager.setLocalStorage('media.volume', num);
  6472. }
  6473.  
  6474. if (t.mediaPlusApi) {
  6475. t.mediaPlusApi.setVolume(num);
  6476.  
  6477. /* 将播放音量同步到全部媒体元素 */
  6478. const mediaList = t.getPlayerList();
  6479. mediaList.forEach(media => {
  6480. if (media !== player) {
  6481. const mediaPlusApi = mediaCore.mediaPlus(media);
  6482. mediaPlusApi && mediaPlusApi.setVolume(num);
  6483. }
  6484. });
  6485. } else {
  6486. delete player.volume;
  6487. player.volume = num;
  6488. t.volumeInfo.time = Date.now();
  6489. t.volumeInfo.value = num;
  6490.  
  6491. try {
  6492. const volumeDescriptor = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume');
  6493. originalMethods.Object.defineProperty.call(Object, player, 'volume', {
  6494. configurable: true,
  6495. get: function () {
  6496. return volumeDescriptor.get.apply(player, arguments)
  6497. },
  6498. set: function (val) {
  6499. if (typeof val !== 'number' || val < 0) {
  6500. return false
  6501. }
  6502.  
  6503. if (TCC$1.doTask('blockSetVolume') || configManager.get('enhance.blockSetVolume') === true) {
  6504. return false
  6505. } else {
  6506. t.setVolume(val, false, true);
  6507. }
  6508. }
  6509. });
  6510. } catch (e) {
  6511. debug.error('解锁volume失败', e);
  6512. }
  6513. }
  6514.  
  6515. /* 调节音量的时候顺便把静音模式关闭 */
  6516. if (!outerCall) { player.muted = false; }
  6517.  
  6518. !notips && t.tips(i18n.t('tipsMsg.volume') + parseInt(player.volume * 100) + '%');
  6519. },
  6520.  
  6521. setVolumeUp (num) {
  6522. num = numUp(num) || 0.2;
  6523. const player = this.player();
  6524. if (player) {
  6525. this.unLockVolume();
  6526.  
  6527. if (this.volume > 1 && player._amp_) {
  6528. this.setVolume(this.volume + num);
  6529. } else {
  6530. this.setVolume(player.volume + num);
  6531. }
  6532.  
  6533. /* 防止外部调音逻辑的干扰,所以锁定一段时间 */
  6534. this.lockVolume(500);
  6535. }
  6536. },
  6537.  
  6538. setVolumeDown (num) {
  6539. num = numDown(num) || -0.2;
  6540. const player = this.player();
  6541. if (player) {
  6542. this.unLockVolume();
  6543.  
  6544. if (this.volume > 1 && player._amp_) {
  6545. this.setVolume(Math.floor(this.volume + num));
  6546. } else {
  6547. this.setVolume(player.volume + num);
  6548. }
  6549.  
  6550. /* 防止外部调音逻辑的干扰,所以锁定一段时间 */
  6551. this.lockVolume(500);
  6552. }
  6553. },
  6554.  
  6555. /* 采集Transform值的历史变更记录,以便后续进行还原 */
  6556. collectTransformHistoryInfo () {
  6557. const t = this;
  6558. Object.keys(t.defaultTransform).forEach(key => {
  6559. if (key === 'translate') {
  6560. const translate = t.defaultTransform.translate;
  6561. t.historyTransform.translate = t.historyTransform.translate || {};
  6562. Object.keys(translate).forEach(subKey => {
  6563. if (Number(t.translate[subKey]) !== t.defaultTransform.translate[subKey]) {
  6564. t.historyTransform.translate[subKey] = t.translate[subKey];
  6565. }
  6566. });
  6567. } else {
  6568. if (Number(t[key]) !== t.defaultTransform[key]) {
  6569. t.historyTransform[key] = t[key];
  6570. }
  6571. }
  6572. });
  6573. },
  6574.  
  6575. /* 判断h5Player下的Transform值是否跟默认的Transform值一致 */
  6576. isSameAsDefaultTransform () {
  6577. let result = true;
  6578. const t = this;
  6579. Object.keys(t.defaultTransform).forEach(key => {
  6580. if (isObj(t.defaultTransform[key])) {
  6581. Object.keys(t.defaultTransform[key]).forEach(subKey => {
  6582. if (Number(t[key][subKey]) !== t.defaultTransform[key][subKey]) {
  6583. result = false;
  6584. }
  6585. });
  6586. } else {
  6587. if (Number(t[key]) !== t.defaultTransform[key]) {
  6588. result = false;
  6589. }
  6590. }
  6591. });
  6592. return result
  6593. },
  6594.  
  6595. /* 设置视频画面的缩放与位移 */
  6596. setTransform (notTips) {
  6597. const t = this;
  6598. const player = t.player();
  6599. const scale = t.scale = Number(t.scale).toFixed(2);
  6600. const translate = t.translate;
  6601.  
  6602. const mirror = t.rotateX === 180 ? `rotateX(${t.rotateX}deg)` : (t.rotateY === 180 ? `rotateY(${t.rotateY}deg)` : '');
  6603. player.style.transform = `scale(${scale}) translate(${translate.x}px, ${translate.y}px) rotate(${t.rotate}deg) ${mirror}`;
  6604.  
  6605. let tipsMsg = i18n.t('tipsMsg.videozoom') + `${(scale * 100).toFixed(0)}%`;
  6606. if (translate.x) {
  6607. tipsMsg += ` ${i18n.t('tipsMsg.horizontal')}${t.translate.x}px`;
  6608. }
  6609. if (translate.y) {
  6610. tipsMsg += ` ${i18n.t('tipsMsg.vertical')}${t.translate.y}px`;
  6611. }
  6612.  
  6613. if (notTips === true) ; else {
  6614. t.collectTransformHistoryInfo();
  6615. t.tips(tipsMsg);
  6616. }
  6617.  
  6618. /* 始终保持transform样式的正常 */
  6619. if (!t._transformStateGuard_) {
  6620. t._transformStateGuard_ = setInterval(() => {
  6621. t.setTransform(true);
  6622. }, 300);
  6623. }
  6624. },
  6625.  
  6626. /* 视频画面旋转 90 度 */
  6627. setRotate () {
  6628. const t = this;
  6629. t.rotate += 90;
  6630. if (t.rotate % 360 === 0) t.rotate = 0;
  6631. t.setTransform(true);
  6632. t.tips(i18n.t('tipsMsg.imgrotate') + t.rotate + '°');
  6633. },
  6634.  
  6635. /* 设置镜像翻转 */
  6636. setMirror (vertical = false) {
  6637. const t = this;
  6638. let tipsMsg = '';
  6639. if (vertical) {
  6640. t.rotateX = t.rotateX === 0 ? 180 : 0;
  6641. tipsMsg += ` ${i18n.t('tipsMsg.verticalMirror')} ${t.rotateX}deg`;
  6642. } else {
  6643. t.rotateY = t.rotateY === 0 ? 180 : 0;
  6644. tipsMsg += ` ${i18n.t('tipsMsg.horizontalMirror')} ${t.rotateY}deg`;
  6645. }
  6646.  
  6647. t.setTransform(true);
  6648. t.tips(tipsMsg);
  6649. },
  6650.  
  6651. /* 缩放视频画面 */
  6652. setScale (num) {
  6653. if (Number.isNaN(this.scale) || Number.isNaN(num)) {
  6654. this.scale = 1;
  6655. } else {
  6656. this.scale = num;
  6657. }
  6658.  
  6659. this.setTransform();
  6660. },
  6661.  
  6662. /* 视频放大 +0.1 */
  6663. setScaleUp (num) {
  6664. num = numUp(num) || 0.05;
  6665. this.setScale(Number(this.scale) + num);
  6666. },
  6667.  
  6668. /* 视频缩小 -0.1 */
  6669. setScaleDown (num) {
  6670. num = numDown(num) || -0.05;
  6671. this.setScale(Number(this.scale) + num);
  6672. },
  6673.  
  6674. /* 设置视频画面的位移属性 */
  6675. setTranslate (x, y) {
  6676. if (typeof x === 'number') {
  6677. this.translate.x = x;
  6678. }
  6679.  
  6680. if (typeof y === 'number') {
  6681. this.translate.y = y;
  6682. }
  6683.  
  6684. this.setTransform();
  6685. },
  6686.  
  6687. /* 视频画面向右平移 */
  6688. setTranslateRight (num) {
  6689. num = numUp(num) || 10;
  6690. this.setTranslate(this.translate.x + num);
  6691. },
  6692.  
  6693. /* 视频画面向左平移 */
  6694. setTranslateLeft (num) {
  6695. num = numDown(num) || -10;
  6696. this.setTranslate(this.translate.x + num);
  6697. },
  6698.  
  6699. /* 视频画面向上平移 */
  6700. setTranslateUp (num) {
  6701. num = numUp(num) || 10;
  6702. this.setTranslate(null, this.translate.y - num);
  6703. },
  6704.  
  6705. /* 视频画面向下平移 */
  6706. setTranslateDown (num) {
  6707. num = numDown(num) || -10;
  6708. this.setTranslate(null, this.translate.y - num);
  6709. },
  6710.  
  6711. resetTransform (notTips) {
  6712. const t = this;
  6713.  
  6714. if (t.isSameAsDefaultTransform() && Object.keys(t.historyTransform).length) {
  6715. /* 还原成历史记录中的Transform值 */
  6716. Object.keys(t.historyTransform).forEach(key => {
  6717. if (isObj(t.historyTransform[key])) {
  6718. Object.keys(t.historyTransform[key]).forEach(subKey => {
  6719. t[key][subKey] = t.historyTransform[key][subKey];
  6720. });
  6721. } else {
  6722. t[key] = t.historyTransform[key];
  6723. }
  6724. });
  6725. } else {
  6726. /* 还原成默认的Transform值 */
  6727. const defaultTransform = clone(t.defaultTransform);
  6728. Object.keys(defaultTransform).forEach(key => {
  6729. t[key] = defaultTransform[key];
  6730. });
  6731. }
  6732.  
  6733. t.setTransform(notTips);
  6734. },
  6735.  
  6736. /**
  6737. * 定格帧画面
  6738. * @param perFps {Number} -可选 默认 1,即定格到下一帧,如果是-1则为定格到上一帧
  6739. */
  6740. freezeFrame (perFps) {
  6741. perFps = perFps || 1;
  6742. const t = this;
  6743. const player = t.player();
  6744.  
  6745. /* 跳帧 */
  6746. player.currentTime += Number(perFps / t.fps);
  6747.  
  6748. /* 定格画面 */
  6749. if (!player.paused) player.pause();
  6750.  
  6751. /* 有些播放器发现画面所在位置变了会自动进行播放,所以此时需要对播放操作进行挂起 */
  6752. player._hangUp_ && player._hangUp_('play', 400);
  6753.  
  6754. if (perFps === 1) {
  6755. t.tips(i18n.t('tipsMsg.nextframe'));
  6756. } else if (perFps === -1) {
  6757. t.tips(i18n.t('tipsMsg.previousframe'));
  6758. } else {
  6759. t.tips(i18n.t('tipsMsg.stopframe') + perFps);
  6760. }
  6761. },
  6762.  
  6763. /**
  6764. * 切换画中画功能
  6765. */
  6766. togglePictureInPicture () {
  6767. const player = this.player();
  6768. if (window._isPictureInPicture_ && document.pictureInPictureElement) {
  6769. document.exitPictureInPicture().then(() => {
  6770. window._isPictureInPicture_ = null;
  6771. }).catch((e) => {
  6772. window._isPictureInPicture_ = null;
  6773. debug.error('[togglePictureInPicture]', e);
  6774. });
  6775. } else {
  6776. player.requestPictureInPicture && player.requestPictureInPicture().then(() => {
  6777. window._isPictureInPicture_ = true;
  6778. }).catch((e) => {
  6779. window._isPictureInPicture_ = null;
  6780. debug.error('[togglePictureInPicture]', e);
  6781. });
  6782. }
  6783. },
  6784.  
  6785. /* 播放下一个视频,默认是没有这个功能的,只有在TCC里配置了next字段才会有该功能 */
  6786. setNextVideo () {
  6787. const isDo = TCC$1.doTask('next');
  6788. if (!isDo) {
  6789. debug.log('当前网页不支持一键播放下个视频功能~');
  6790. }
  6791. },
  6792.  
  6793. /* 切换播放状态 */
  6794. switchPlayStatus () {
  6795. const t = this;
  6796. const player = t.player();
  6797. if (TCC$1.doTask('switchPlayStatus')) {
  6798. // debug.log('[TCC][switchPlayStatus]', 'suc')
  6799. return
  6800. }
  6801.  
  6802. if (player.paused) {
  6803. if (TCC$1.doTask('play')) ; else {
  6804. if (t.mediaPlusApi) {
  6805. t.mediaPlusApi.lockPause(400);
  6806. t.mediaPlusApi.applyPlay();
  6807. } else {
  6808. /* 挂起其它逻辑的暂停操作,确保播放状态生效 */
  6809. if (player._hangUp_ instanceof Function) {
  6810. player._hangUp_('pause', 400);
  6811. player._unHangUp_('play');
  6812. }
  6813.  
  6814. player.play();
  6815. }
  6816.  
  6817. t.tips(i18n.t('tipsMsg.play'));
  6818. }
  6819.  
  6820. TCC$1.doTask('afterPlay');
  6821. } else {
  6822. if (TCC$1.doTask('pause')) ; else {
  6823. if (t.mediaPlusApi) {
  6824. t.mediaPlusApi.lockPlay(400);
  6825. t.mediaPlusApi.applyPause();
  6826. } else {
  6827. /* 挂起其它逻辑的播放操作,确保暂停状态生效 */
  6828. if (player._hangUp_ instanceof Function) {
  6829. player._hangUp_('play', 400);
  6830. player._unHangUp_('pause');
  6831. }
  6832.  
  6833. player.pause();
  6834. }
  6835.  
  6836. t.tips(i18n.t('tipsMsg.pause'));
  6837. }
  6838.  
  6839. TCC$1.doTask('afterPause');
  6840. }
  6841. },
  6842.  
  6843. isAllowRestorePlayProgress: function () {
  6844. const allowRestoreVal = configManager.get(`media.allowRestorePlayProgress.${window.location.host}`);
  6845. return allowRestoreVal === null || allowRestoreVal
  6846. },
  6847. /* 切换自动恢复播放进度的状态 */
  6848. switchRestorePlayProgressStatus: function () {
  6849. const t = h5Player;
  6850. let isAllowRestorePlayProgress = t.isAllowRestorePlayProgress();
  6851.  
  6852. if (isInCrossOriginFrame()) {
  6853. isAllowRestorePlayProgress = false;
  6854. } else {
  6855. /* 进行值反转 */
  6856. isAllowRestorePlayProgress = !isAllowRestorePlayProgress;
  6857. }
  6858.  
  6859. configManager.set(`media.allowRestorePlayProgress.${window.location.host}`, isAllowRestorePlayProgress);
  6860.  
  6861. /* 操作提示 */
  6862. if (isAllowRestorePlayProgress) {
  6863. t.tips(i18n.t('tipsMsg.arpl'));
  6864. t.setPlayProgress(t.player());
  6865. } else {
  6866. t.tips(i18n.t('tipsMsg.drpl'));
  6867. }
  6868. },
  6869. tipsClassName: 'html_player_enhance_tips',
  6870.  
  6871. getTipsContainer: function (videoEl) {
  6872. const t = h5Player;
  6873. const player = videoEl || t.player();
  6874. // 使用getContainer获取到的父节点弊端太多,暂时弃用
  6875. // const _tispContainer_ = player._tispContainer_ || getContainer(player);
  6876.  
  6877. let tispContainer = player.parentNode || player;
  6878.  
  6879. /* 如果父节点为无长宽的元素,则再往上查找一级 */
  6880. const containerBox = tispContainer.getBoundingClientRect();
  6881. if ((!containerBox.width || !containerBox.height) && tispContainer.parentNode) {
  6882. tispContainer = tispContainer.parentNode;
  6883. }
  6884.  
  6885. return tispContainer
  6886. },
  6887. tips: function (str) {
  6888. const t = h5Player;
  6889. const player = t.player();
  6890. if (!player) {
  6891. debug.log('h5Player Tips:', str);
  6892. return true
  6893. }
  6894.  
  6895. const isAudio = t.isAudioInstance();
  6896. const parentNode = isAudio ? document.body : t.getTipsContainer();
  6897.  
  6898. if (parentNode === player) {
  6899. debug.info('获取tips的包裹容器异常:', player, str);
  6900. return false
  6901. }
  6902.  
  6903. let backupStyle = '';
  6904. if (!isAudio) {
  6905. // 修复部分提示按钮位置异常问题
  6906. const defStyle = parentNode.getAttribute('style') || '';
  6907.  
  6908. backupStyle = parentNode.getAttribute('style-backup') || '';
  6909. if (!backupStyle) {
  6910. let backupSty = defStyle || 'style-backup: none';
  6911. const backupStyObj = inlineStyleToObj(backupSty);
  6912.  
  6913. /**
  6914. * 修复因为缓存时机获取到错误样式的问题
  6915. * 例如在:https://www.xuetangx.com/
  6916. */
  6917. if (backupStyObj.opacity === '0') {
  6918. backupStyObj.opacity = '1';
  6919. }
  6920. if (backupStyObj.visibility === 'hidden') {
  6921. backupStyObj.visibility = 'visible';
  6922. }
  6923.  
  6924. backupSty = objToInlineStyle(backupStyObj);
  6925.  
  6926. parentNode.setAttribute('style-backup', backupSty);
  6927. backupStyle = defStyle;
  6928. } else {
  6929. /* 如果defStyle被外部修改了,则需要更新备份样式 */
  6930. if (defStyle && !defStyle.includes('style-backup')) {
  6931. backupStyle = defStyle;
  6932. }
  6933. }
  6934.  
  6935. const newStyleArr = backupStyle.split(';');
  6936.  
  6937. const oldPosition = parentNode.getAttribute('def-position') || window.getComputedStyle(parentNode).position;
  6938. if (parentNode.getAttribute('def-position') === null) {
  6939. parentNode.setAttribute('def-position', oldPosition || '');
  6940. }
  6941. if (['static', 'inherit', 'initial', 'unset', ''].includes(oldPosition)) {
  6942. newStyleArr.push('position: relative');
  6943. }
  6944.  
  6945. const playerBox = player.getBoundingClientRect();
  6946. const parentNodeBox = parentNode.getBoundingClientRect();
  6947. /* 不存在高宽时,给包裹节点一个最小高宽,才能保证提示能正常显示 */
  6948. if (!parentNodeBox.width || !parentNodeBox.height) {
  6949. newStyleArr.push('min-width:' + playerBox.width + 'px');
  6950. newStyleArr.push('min-height:' + playerBox.height + 'px');
  6951. }
  6952.  
  6953. parentNode.setAttribute('style', newStyleArr.join(';'));
  6954.  
  6955. const newPlayerBox = player.getBoundingClientRect();
  6956. if (Math.abs(newPlayerBox.height - playerBox.height) > 50) {
  6957. parentNode.setAttribute('style', backupStyle);
  6958. // debug.info('应用新样式后给播放器高宽造成了严重的偏差,样式已被还原:', player, playerBox, newPlayerBox)
  6959. }
  6960. }
  6961.  
  6962. const tipsSelector = '.' + t.tipsClassName;
  6963.  
  6964. /* 当出现多个tips元素时,将这些tips元素全部移除 */
  6965. const tipsList = document.querySelectorAll(tipsSelector);
  6966. if (tipsList.length > 1) {
  6967. tipsList.forEach(tipsItem => {
  6968. tipsItem.remove();
  6969. });
  6970. }
  6971.  
  6972. let tipsDom = parentNode.querySelector(tipsSelector);
  6973.  
  6974. /* 提示dom未初始化的,则进行初始化 */
  6975. if (!tipsDom) {
  6976. t.initTips();
  6977. tipsDom = parentNode.querySelector(tipsSelector);
  6978. if (!tipsDom) {
  6979. debug.log('init h5player tips dom error...');
  6980. return false
  6981. }
  6982. }
  6983.  
  6984. const style = tipsDom.style;
  6985. tipsDom.innerText = str;
  6986.  
  6987. for (var i = 0; i < 3; i++) {
  6988. if (this.on_off[i]) clearTimeout(this.on_off[i]);
  6989. }
  6990.  
  6991. function showTips () {
  6992. style.display = 'block';
  6993. t.on_off[0] = setTimeout(function () {
  6994. style.opacity = 1;
  6995. }, 50);
  6996. t.on_off[1] = setTimeout(function () {
  6997. // 隐藏提示框和还原样式
  6998. style.opacity = 0;
  6999. style.display = 'none';
  7000. if (backupStyle) {
  7001. parentNode.setAttribute('style', backupStyle);
  7002. }
  7003. }, 2000);
  7004. }
  7005.  
  7006. if (style.display === 'block') {
  7007. style.display = 'none';
  7008. clearTimeout(this.on_off[3]);
  7009. t.on_off[2] = setTimeout(function () {
  7010. showTips();
  7011. }, 100);
  7012. } else {
  7013. showTips();
  7014. }
  7015. },
  7016.  
  7017. /* 设置提示DOM的样式 */
  7018. initTips: function () {
  7019. const t = h5Player;
  7020. const isAudio = t.isAudioInstance();
  7021. const parentNode = isAudio ? document.body : t.getTipsContainer();
  7022. if (parentNode.querySelector('.' + t.tipsClassName)) return
  7023.  
  7024. // top: 50%;
  7025. // left: 50%;
  7026. // transform: translate(-50%,-50%);
  7027. let tipsStyle = `
  7028. position: absolute;
  7029. z-index: 999999;
  7030. font-size: ${t.fontSize || 16}px;
  7031. padding: 5px 10px;
  7032. background: rgba(0,0,0,0.4);
  7033. color:white;
  7034. top: 0;
  7035. left: 0;
  7036. transition: all 500ms ease;
  7037. opacity: 0;
  7038. border-bottom-right-radius: 5px;
  7039. display: none;
  7040. -webkit-font-smoothing: subpixel-antialiased;
  7041. font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
  7042. -webkit-user-select: none;
  7043. `;
  7044.  
  7045. if (isAudio) {
  7046. tipsStyle = `
  7047. position: fixed;
  7048. z-index: 999999;
  7049. font-size: ${t.fontSize || 16}px;
  7050. padding: 5px 10px;
  7051. background: rgba(0,0,0,0.4);
  7052. color:white;
  7053. bottom: 0;
  7054. right: 0;
  7055. transition: all 500ms ease;
  7056. opacity: 0;
  7057. border-top-left-radius: 5px;
  7058. display: none;
  7059. -webkit-font-smoothing: subpixel-antialiased;
  7060. font-family: 'microsoft yahei', Verdana, Geneva, sans-serif;
  7061. -webkit-user-select: none;
  7062. `;
  7063. }
  7064.  
  7065. const tips = document.createElement('div');
  7066. tips.setAttribute('style', tipsStyle);
  7067. tips.setAttribute('class', t.tipsClassName);
  7068. parentNode.appendChild(tips);
  7069. },
  7070. on_off: new Array(3),
  7071. fps: 30,
  7072. /* 滤镜效果 */
  7073. filter: {
  7074. key: [1, 1, 1, 0, 0],
  7075. setup: function () {
  7076. var view = 'brightness({0}) contrast({1}) saturate({2}) hue-rotate({3}deg) blur({4}px)';
  7077. for (var i = 0; i < 5; i++) {
  7078. view = view.replace('{' + i + '}', String(this.key[i]));
  7079. this.key[i] = Number(this.key[i]);
  7080. }
  7081. h5Player.player().style.filter = view;
  7082. },
  7083. reset: function () {
  7084. this.key[0] = 1;
  7085. this.key[1] = 1;
  7086. this.key[2] = 1;
  7087. this.key[3] = 0;
  7088. this.key[4] = 0;
  7089. this.setup();
  7090. }
  7091. },
  7092.  
  7093. setFilter (item, num, isDown) {
  7094. if (![0, 1, 2, 3, 4].includes(item) || typeof num !== 'number') {
  7095. debug.error('[setFilter]', '参数有误', item, num);
  7096. return false
  7097. }
  7098.  
  7099. /* 如果标识为down,则自动取负数值 */
  7100. if (isDown === true) {
  7101. if (num && num > 0) { num = -num; }
  7102. }
  7103.  
  7104. const nameMap = {
  7105. 0: 'brightness',
  7106. 1: 'contrast',
  7107. 2: 'saturation',
  7108. 3: 'hue',
  7109. 4: 'blur'
  7110. };
  7111.  
  7112. const t = this;
  7113. t.filter.key[item] += num || 0.1;
  7114. t.filter.key[item] = t.filter.key[item].toFixed(2);
  7115.  
  7116. if (t.filter.key[item] < 0 && nameMap[item] !== 'hue') {
  7117. t.filter.key[item] = 0;
  7118. }
  7119.  
  7120. t.filter.setup();
  7121. t.tips(i18n.t(`tipsMsg.${nameMap[item]}`) + parseInt(t.filter.key[item] * 100) + '%');
  7122. },
  7123.  
  7124. /* 设置视频的亮度 */
  7125. setBrightness (num) {
  7126. this.setFilter(0, num);
  7127. },
  7128.  
  7129. /* 提升视频的亮度 */
  7130. setBrightnessUp (num) {
  7131. this.setFilter(0, num || 0.1);
  7132. },
  7133.  
  7134. /* 降低视频的亮度 */
  7135. setBrightnessDown (num) {
  7136. this.setFilter(0, num || -0.1, true);
  7137. },
  7138.  
  7139. /* 设置视频的对比度 */
  7140. setContrast (num) {
  7141. this.setFilter(1, num);
  7142. },
  7143.  
  7144. /* 提升视频的对比度 */
  7145. setContrastUp (num) {
  7146. this.setFilter(1, num || 0.1);
  7147. },
  7148.  
  7149. /* 降低视频的对比度 */
  7150. setContrastDown (num) {
  7151. this.setFilter(1, num || -0.1, true);
  7152. },
  7153.  
  7154. /* 设置饱和度 */
  7155. setSaturation (num) {
  7156. this.setFilter(2, num);
  7157. },
  7158.  
  7159. /* 提升饱和度 */
  7160. setSaturationUp (num) {
  7161. this.setFilter(2, num || 0.1);
  7162. },
  7163.  
  7164. /* 降低饱和度 */
  7165. setSaturationDown (num) {
  7166. this.setFilter(2, num || -0.1, true);
  7167. },
  7168.  
  7169. /* 设置色相 */
  7170. setHue (num) {
  7171. this.setFilter(3, num);
  7172. },
  7173.  
  7174. /* 增加色相 */
  7175. setHueUp (num) {
  7176. this.setFilter(3, num || 1);
  7177. },
  7178.  
  7179. /* 降低色相 */
  7180. setHueDown (num) {
  7181. this.setFilter(3, num || -1, true);
  7182. },
  7183.  
  7184. /* 设置模糊度 */
  7185. setBlur (num) {
  7186. this.setFilter(4, num);
  7187. },
  7188.  
  7189. /* 增加模糊度 */
  7190. setBlurUp (num) {
  7191. this.setFilter(4, num || 1);
  7192. },
  7193.  
  7194. /* 降低模糊度 */
  7195. setBlurDown (num) {
  7196. this.setFilter(4, num || -1, true);
  7197. },
  7198.  
  7199. resetFilterAndTransform () {
  7200. const t = this;
  7201.  
  7202. t.resetTransform(true);
  7203. t.filter.reset();
  7204. t.tips(i18n.t('tipsMsg.imgattrreset'));
  7205. },
  7206.  
  7207. mediaDownload () {
  7208. if (configManager.get('enhance.allowExperimentFeatures')) {
  7209. debug.warn('[experimentFeatures][mediaDownload]');
  7210. mediaDownload(this.player());
  7211. }
  7212. },
  7213.  
  7214. capture () {
  7215. const player = this.player();
  7216. videoCapturer.capture(player, true);
  7217.  
  7218. /* 暂停画面 */
  7219. if (!player.paused && !document.pictureInPictureElement && document.visibilityState !== 'visible') {
  7220. this.freezeFrame();
  7221. }
  7222. },
  7223.  
  7224. _isFoucs: false,
  7225.  
  7226. /* 播放器的聚焦事件 */
  7227. isFoucs: function () {
  7228. const t = h5Player;
  7229. const player = t.player();
  7230. if (!player) return
  7231.  
  7232. player.onmouseenter = function (e) {
  7233. h5Player._isFoucs = true;
  7234. };
  7235. player.onmouseleave = function (e) {
  7236. h5Player._isFoucs = false;
  7237. };
  7238. },
  7239. /* 播放器事件响应器 */
  7240. palyerTrigger: function (player, event) {
  7241. if (!player || !event) return
  7242. const t = h5Player;
  7243. const keyCode = event.keyCode;
  7244. const key = event.key.toLowerCase();
  7245.  
  7246. if (event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey) {
  7247. // 网页全屏
  7248. if (key === 'enter') {
  7249. t.setWebFullScreen();
  7250. }
  7251.  
  7252. // 进入或退出画中画模式
  7253. if (key === 'p') {
  7254. t.togglePictureInPicture();
  7255. }
  7256.  
  7257. // 截图并下载保存
  7258. if (key === 's') {
  7259. t.capture();
  7260. }
  7261.  
  7262. if (key === 'r') {
  7263. t.switchRestorePlayProgressStatus();
  7264. }
  7265.  
  7266. if (key === 'm') {
  7267. /* 垂直镜像翻转 */
  7268. t.setMirror(true);
  7269. }
  7270.  
  7271. if (key === 'd') {
  7272. t.mediaDownload();
  7273. }
  7274.  
  7275. // 视频画面缩放相关事件
  7276. const allowKeys = ['x', 'c', 'z', 'arrowright', 'arrowleft', 'arrowup', 'arrowdown'];
  7277. if (!allowKeys.includes(key)) return
  7278.  
  7279. t.scale = Number(t.scale);
  7280. switch (key) {
  7281. // shift+X:视频缩小 -0.1
  7282. case 'x':
  7283. t.setScaleDown();
  7284. break
  7285. // shift+C:视频放大 +0.1
  7286. case 'c':
  7287. t.setScaleUp();
  7288. break
  7289. // shift+Z:视频恢复正常大小
  7290. case 'z':
  7291. t.resetTransform();
  7292. break
  7293. case 'arrowright':
  7294. t.setTranslateRight();
  7295. break
  7296. case 'arrowleft':
  7297. t.setTranslateLeft();
  7298. break
  7299. case 'arrowup':
  7300. t.setTranslateUp();
  7301. break
  7302. case 'arrowdown':
  7303. t.setTranslateDown();
  7304. break
  7305. }
  7306.  
  7307. // 阻止事件冒泡
  7308. event.stopPropagation();
  7309. event.preventDefault();
  7310. return true
  7311. }
  7312.  
  7313. // ctrl+方向键右→:快进30秒
  7314. if (event.ctrlKey && keyCode === 39) {
  7315. t.setCurrentTimeUp(t.skipStep * 6);
  7316. }
  7317. // ctrl+方向键左←:后退30秒
  7318. if (event.ctrlKey && keyCode === 37) {
  7319. t.setCurrentTimeDown(-t.skipStep * 6);
  7320. }
  7321.  
  7322. // ctrl+方向键上↑:音量升高 20%
  7323. if (event.ctrlKey && keyCode === 38) {
  7324. t.setVolumeUp(0.2);
  7325. }
  7326. // 方向键下↓:音量降低 20%
  7327. if (event.ctrlKey && keyCode === 40) {
  7328. t.setVolumeDown(-0.2);
  7329. }
  7330.  
  7331. // 防止其它无关组合键冲突
  7332. if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey) return
  7333.  
  7334. // 方向键右→:快进5秒
  7335. if (keyCode === 39) {
  7336. t.setCurrentTimeUp();
  7337. }
  7338. // 方向键左←:后退5秒
  7339. if (keyCode === 37) {
  7340. t.setCurrentTimeDown();
  7341. }
  7342.  
  7343. // 方向键上↑:音量升高 10%
  7344. if (keyCode === 38) {
  7345. t.setVolumeUp(0.05);
  7346. }
  7347. // 方向键下↓:音量降低 10%
  7348. if (keyCode === 40) {
  7349. t.setVolumeDown(-0.05);
  7350. }
  7351.  
  7352. // 空格键:暂停/播放
  7353. if (keyCode === 32) {
  7354. t.switchPlayStatus();
  7355. }
  7356.  
  7357. // 按键X:减速播放 -0.1
  7358. if (keyCode === 88) {
  7359. t.setPlaybackRateDown();
  7360. }
  7361. // 按键C:加速播放 +0.1
  7362. if (keyCode === 67) {
  7363. t.setPlaybackRateUp();
  7364. }
  7365. // 按键Z:正常速度播放
  7366. if (keyCode === 90) {
  7367. t.resetPlaybackRate();
  7368. }
  7369.  
  7370. // 按1-4设置播放速度 49-52;97-100
  7371. if ((keyCode >= 49 && keyCode <= 52) || (keyCode >= 97 && keyCode <= 100)) {
  7372. t.setPlaybackRatePlus(event.key);
  7373. }
  7374.  
  7375. // 按键F:下一帧
  7376. if (keyCode === 70) {
  7377. t.freezeFrame(1);
  7378. }
  7379. // 按键D:上一帧
  7380. if (keyCode === 68) {
  7381. t.freezeFrame(-1);
  7382. }
  7383.  
  7384. // 按键E:亮度增加%
  7385. if (keyCode === 69) {
  7386. t.setBrightnessUp();
  7387. }
  7388. // 按键W:亮度减少%
  7389. if (keyCode === 87) {
  7390. t.setBrightnessDown();
  7391. }
  7392.  
  7393. // 按键T:对比度增加%
  7394. if (keyCode === 84) {
  7395. t.setContrastUp();
  7396. }
  7397. // 按键R:对比度减少%
  7398. if (keyCode === 82) {
  7399. t.setContrastDown();
  7400. }
  7401.  
  7402. // 按键U:饱和度增加%
  7403. if (keyCode === 85) {
  7404. t.setSaturationUp();
  7405. }
  7406. // 按键Y:饱和度减少%
  7407. if (keyCode === 89) {
  7408. t.setSaturationDown();
  7409. }
  7410.  
  7411. // 按键O:色相增加 1 度
  7412. if (keyCode === 79) {
  7413. t.setHueUp();
  7414. }
  7415. // 按键I:色相减少 1 度
  7416. if (keyCode === 73) {
  7417. t.setHueDown();
  7418. }
  7419.  
  7420. // 按键K:模糊增加 1 px
  7421. if (keyCode === 75) {
  7422. t.setBlurUp();
  7423. }
  7424. // 按键J:模糊减少 1 px
  7425. if (keyCode === 74) {
  7426. t.setBlurDown();
  7427. }
  7428.  
  7429. // 按键Q:图像复位
  7430. if (keyCode === 81) {
  7431. t.resetFilterAndTransform();
  7432. }
  7433.  
  7434. // 按键S:画面旋转 90 度
  7435. if (keyCode === 83) {
  7436. t.setRotate();
  7437. }
  7438.  
  7439. /* 水平镜像翻转 */
  7440. if (keyCode === 77) {
  7441. t.setMirror();
  7442. }
  7443.  
  7444. // 按键回车,进入全屏
  7445. if (keyCode === 13) {
  7446. t.setFullScreen();
  7447. }
  7448.  
  7449. if (key === 'n') {
  7450. t.setNextVideo();
  7451. }
  7452.  
  7453. // 阻止事件冒泡
  7454. event.stopPropagation();
  7455. event.preventDefault();
  7456. return true
  7457. },
  7458.  
  7459. /* 运行自定义的快捷键操作,如果运行了会返回true */
  7460. runCustomShortcuts: function (player, event) {
  7461. if (!player || !event) return
  7462. const key = event.key.toLowerCase();
  7463. const taskConf = TCC$1.getTaskConfig();
  7464. const confIsCorrect = isObj(taskConf.shortcuts) &&
  7465. Array.isArray(taskConf.shortcuts.register) &&
  7466. taskConf.shortcuts.callback instanceof Function;
  7467.  
  7468. /* 判断当前触发的快捷键是否已被注册 */
  7469. function isRegister () {
  7470. const list = taskConf.shortcuts.register;
  7471.  
  7472. /* 当前触发的组合键 */
  7473. const combineKey = [];
  7474. if (event.ctrlKey) {
  7475. combineKey.push('ctrl');
  7476. }
  7477. if (event.shiftKey) {
  7478. combineKey.push('shift');
  7479. }
  7480. if (event.altKey) {
  7481. combineKey.push('alt');
  7482. }
  7483. if (event.metaKey) {
  7484. combineKey.push('command');
  7485. }
  7486.  
  7487. combineKey.push(key);
  7488.  
  7489. /* 通过循环判断当前触发的组合键和已注册的组合键是否完全一致 */
  7490. let hasReg = false;
  7491. list.forEach((shortcut) => {
  7492. const regKey = shortcut.split('+');
  7493. if (combineKey.length === regKey.length) {
  7494. let allMatch = true;
  7495. regKey.forEach((key) => {
  7496. if (!combineKey.includes(key)) {
  7497. allMatch = false;
  7498. }
  7499. });
  7500. if (allMatch) {
  7501. hasReg = true;
  7502. }
  7503. }
  7504. });
  7505.  
  7506. return hasReg
  7507. }
  7508.  
  7509. if (confIsCorrect && isRegister()) {
  7510. // 执行自定义快捷键操作
  7511. const isDo = TCC$1.doTask('shortcuts', {
  7512. event,
  7513. player,
  7514. h5Player
  7515. });
  7516.  
  7517. if (isDo) {
  7518. event.stopPropagation();
  7519. event.preventDefault();
  7520. }
  7521.  
  7522. return isDo
  7523. } else {
  7524. return false
  7525. }
  7526. },
  7527.  
  7528. /* 按键响应方法 */
  7529. keydownEvent: function (event) {
  7530. const t = h5Player;
  7531. const keyCode = event.keyCode;
  7532. // const key = event.key.toLowerCase()
  7533. const player = t.player();
  7534.  
  7535. /* 处于可编辑元素中不执行任何快捷键 */
  7536. const target = event.composedPath ? event.composedPath()[0] || event.target : event.target;
  7537. if (isEditableTarget(target)) return
  7538.  
  7539. /* 广播按键消息,进行跨域控制 */
  7540. monkeyMsg.send('globalKeydownEvent', event, 0);
  7541.  
  7542. if (!player) {
  7543. if (t.hasCrossOriginVideoDetected) {
  7544. if (!configManager.get('enhance.allowCrossOriginControl')) {
  7545. return false
  7546. }
  7547.  
  7548. /**
  7549. * 利用热键运行器的匹配能力来决定要不要禁止事件冒泡和阻止默认事件
  7550. * 解决处于跨TAB、跨域控制时造成其它默认快捷键响应异常的问题
  7551. */
  7552. if (t.hotkeysRunner && t.hotkeysRunner.run) {
  7553. t.hotkeysRunner.run({
  7554. event,
  7555. stopPropagation: true,
  7556. preventDefault: true
  7557. });
  7558. } else {
  7559. t.registerHotkeysRunner();
  7560. event.stopPropagation();
  7561. event.preventDefault();
  7562. }
  7563.  
  7564. // debug.log('当前页面检出了跨域受限的视频,仍需阻止默认事件和事件冒泡')
  7565. }
  7566.  
  7567. // debug.log('无可用的媒体元素,不执行相关操作')
  7568. return false
  7569. }
  7570.  
  7571. /* 切换插件的可用状态 */
  7572. if (event.ctrlKey && keyCode === 32) {
  7573. t.enable = !t.enable;
  7574. if (t.enable) {
  7575. t.tips(i18n.t('tipsMsg.onplugin'));
  7576. } else {
  7577. t.tips(i18n.t('tipsMsg.offplugin'));
  7578. }
  7579. }
  7580.  
  7581. if (!t.enable) {
  7582. debug.log('h5Player 已禁用~');
  7583. return false
  7584. }
  7585.  
  7586. // 按ctrl+\ 键进入聚焦或取消聚焦状态,用于视频标签被遮挡的场景
  7587. if (event.ctrlKey && keyCode === 220) {
  7588. t.globalMode = !t.globalMode;
  7589. if (t.globalMode) {
  7590. t.tips(i18n.t('tipsMsg.globalmode') + ' ON');
  7591. } else {
  7592. t.tips(i18n.t('tipsMsg.globalmode') + ' OFF');
  7593. }
  7594. }
  7595.  
  7596. /* 非全局模式下,不聚焦则不执行快捷键的操作 */
  7597. if (!t.globalMode && !t._isFoucs) return
  7598.  
  7599. /* 判断是否执行了自定义快捷键操作,如果是则不再响应后面默认定义操作 */
  7600. if (t.runCustomShortcuts(player, event) === true) return
  7601.  
  7602. /* 热键运行器匹配到相关执行任务便不在执行后续的palyerTrigger */
  7603. if (t.hotkeysRunner && t.hotkeysRunner.run) {
  7604. const matchResult = t.hotkeysRunner.run({
  7605. event,
  7606. target: t,
  7607. stopPropagation: true,
  7608. preventDefault: true,
  7609. conditionHandler (condition) {
  7610. // TODO 完善条件限定回调逻辑
  7611. if (condition) {
  7612. return true
  7613. }
  7614. }
  7615. });
  7616.  
  7617. if (matchResult) {
  7618. debug.info('[hotkeysRunner][matchResult]', matchResult);
  7619. return true
  7620. }
  7621. } else {
  7622. /* 未用到的按键不进行任何事件监听 */
  7623. if (!isRegisterKey(event)) { return false }
  7624.  
  7625. /* 响应播放器相关操作 */
  7626. t.palyerTrigger(player, event);
  7627. }
  7628. },
  7629.  
  7630. /**
  7631. * 获取播放进度
  7632. * @param player -可选 对应的h5 播放器对象, 如果不传,则获取到的是整个播放进度表,传则获取当前播放器的播放进度
  7633. */
  7634. getPlayProgress: function (player) {
  7635. const progressMap = configManager.get('media.progress') || {};
  7636.  
  7637. if (!player) {
  7638. return progressMap
  7639. } else {
  7640. const keyName = window.location.href + player.duration;
  7641. if (progressMap[keyName]) {
  7642. /* 对于直播的视频流,会出现记录的duration和当前视频duration不一致的情况,这时候通过返回currentTime来忽略恢复播放进度 */
  7643. if (Number.isNaN(Number(player.duration)) || Number(progressMap[keyName].duration) !== Number(player.duration)) {
  7644. return player.currentTime
  7645. } else {
  7646. return progressMap[keyName].progress
  7647. }
  7648. } else {
  7649. return player.currentTime
  7650. }
  7651. }
  7652. },
  7653. /* 播放进度记录器 */
  7654. playProgressRecorder: function (player) {
  7655. const t = h5Player;
  7656. clearTimeout(player._playProgressTimer_);
  7657. function recorder (player) {
  7658. player._playProgressTimer_ = setTimeout(function () {
  7659. /* 时长小于两分钟的视频不记录播放进度 */
  7660. const isToShort = !player.duration || Number.isNaN(Number(player.duration)) || player.duration < 120;
  7661. const isLeave = document.visibilityState !== 'visible' && player.paused;
  7662.  
  7663. if (!t.isAllowRestorePlayProgress() || isToShort || isLeave) {
  7664. recorder(player);
  7665. return true
  7666. }
  7667.  
  7668. const progressMap = t.getPlayProgress() || {};
  7669. const list = Object.keys(progressMap);
  7670. const keyName = window.location.href + player.duration;
  7671.  
  7672. /**
  7673. * 对首次记录到progressMap的值进行标记
  7674. * 用于防止手动切换播放进度时,执行到错误的恢复逻辑
  7675. */
  7676. if (!progressMap[keyName]) {
  7677. t._firstProgressRecord_ = keyName;
  7678. t._hasRestorePlayProgress_ = keyName;
  7679. }
  7680.  
  7681. /* 只保存最近10个视频的播放进度 */
  7682. if (list.length > 10) {
  7683. /* 根据更新的时间戳,取出最早添加播放进度的记录项 */
  7684. let timeList = [];
  7685. list.forEach(function (keyName) {
  7686. progressMap[keyName] && progressMap[keyName].t && timeList.push(progressMap[keyName].t);
  7687. });
  7688. timeList = quickSort(timeList);
  7689. const timestamp = timeList[0];
  7690.  
  7691. /* 删除最早添加的记录项 */
  7692. list.forEach(function (keyName) {
  7693. if (progressMap[keyName].t === timestamp) {
  7694. delete progressMap[keyName];
  7695. }
  7696. });
  7697. }
  7698.  
  7699. /* 记录当前播放进度 */
  7700. progressMap[keyName] = {
  7701. progress: player.currentTime,
  7702. duration: player.duration,
  7703. t: new Date().getTime()
  7704. };
  7705.  
  7706. /* 存储播放进度表 */
  7707. configManager.setLocalStorage('media.progress', progressMap);
  7708.  
  7709. /* 循环侦听 */
  7710. recorder(player);
  7711. }, 1000 * 2);
  7712. }
  7713. recorder(player);
  7714. },
  7715.  
  7716. /* 设置播放进度 */
  7717. setPlayProgress: function (player) {
  7718. const t = h5Player;
  7719. if (!player || !player.duration || Number.isNaN(player.duration)) return
  7720.  
  7721. const curTime = Number(t.getPlayProgress(player));
  7722.  
  7723. /* 要恢复进度的时间过小或大于player.duration都不符合规范,不进行进度恢复操作 */
  7724. if (!curTime || Number.isNaN(curTime) || curTime < 10 || curTime >= player.duration) return
  7725.  
  7726. /* 忽略恢复进度时间与当前播放进度时间相差不大的情况 */
  7727. if (Math.abs(curTime - player.currentTime) < 2) {
  7728. return false
  7729. }
  7730.  
  7731. const progressKey = window.location.href + player.duration;
  7732. t._hasRestorePlayProgress_ = t._hasRestorePlayProgress_ || '';
  7733.  
  7734. if (t._hasRestorePlayProgress_ === progressKey || t._firstProgressRecord_ === progressKey) {
  7735. if (t._hasRestorePlayProgress_ === progressKey) {
  7736. t._firstProgressRecord_ = '';
  7737. }
  7738. return false
  7739. }
  7740.  
  7741. if (t.isAllowRestorePlayProgress()) {
  7742. // 比curTime少1.5s可以让用户知道是前面的画面,从而有个衔接上了的感觉
  7743. player.currentTime = curTime - 1.5;
  7744. t._hasRestorePlayProgress_ = progressKey;
  7745. t.tips(i18n.t('tipsMsg.playbackrestored'));
  7746. } else {
  7747. t.tips(i18n.t('tipsMsg.playbackrestoreoff'));
  7748. }
  7749. },
  7750.  
  7751. setPlayerInstance (el) {
  7752. if (!el && !el.getBoundingClientRect) {
  7753. return false
  7754. }
  7755.  
  7756. const t = h5Player;
  7757.  
  7758. if (t.player() === el) {
  7759. return false
  7760. }
  7761.  
  7762. if (!t.playerInstance && isMediaElement(el)) {
  7763. t.playerInstance = el;
  7764. t.initPlayerInstance(false);
  7765. return true
  7766. }
  7767.  
  7768. if (isVideoElement(el)) {
  7769. const elParentNode = t.getTipsContainer(el);
  7770. const elInfo = el.getBoundingClientRect();
  7771. const parentElInfo = elParentNode && elParentNode.getBoundingClientRect();
  7772. if (elInfo && elInfo.width > 200 && parentElInfo && parentElInfo.width > 200) {
  7773. t.playerInstance = el;
  7774. t.initPlayerInstance(false);
  7775. }
  7776. } else if (isAudioElement(el)) {
  7777. if (isAudioElement(t.playerInstance) || (isVideoElement(t.playerInstance) && !t.playerInstance.isConnected)) {
  7778. t.playerInstance = el;
  7779. t.initPlayerInstance(false);
  7780. }
  7781. }
  7782. },
  7783.  
  7784. /**
  7785. * 视频元素是否出现在视口里的观察对象,用于优化多视频实例的实例切换
  7786. * https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API
  7787. */
  7788. intersectionObserver: new IntersectionObserver(function (entries, observer) {
  7789. const t = h5Player;
  7790. // debug.log('[intersectionObserver]', entries)
  7791.  
  7792. let tmpIntersectionRatio = 0;
  7793. entries.forEach(entrie => {
  7794. entrie.target._intersectionInfo_ = entrie;
  7795.  
  7796. if (entrie.intersectionRatio > tmpIntersectionRatio && entrie.intersectionRatio > 0.4) {
  7797. tmpIntersectionRatio = entrie.intersectionRatio;
  7798.  
  7799. const oldPlayer = t.player();
  7800. if (oldPlayer && oldPlayer._intersectionInfo_ && tmpIntersectionRatio < oldPlayer._intersectionInfo_.intersectionRatio) {
  7801. /* 新实例的视图范围比旧的小,则不切换实例 */
  7802. return
  7803. }
  7804.  
  7805. /* 切换视频实例 */
  7806. const toggleResult = t.setPlayerInstance(entrie.target);
  7807. toggleResult && debug.log('[intersectionObserver] 切换视频实例', entrie);
  7808. }
  7809. });
  7810. }, {
  7811. threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
  7812. }),
  7813.  
  7814. /**
  7815. * 检测h5播放器是否存在
  7816. * @param callback
  7817. */
  7818. detecH5Player: function () {
  7819. const t = this;
  7820. const playerList = t.getPlayerList();
  7821.  
  7822. if (playerList.length) {
  7823. // debug.log('检测到HTML5视频!', location.href, h5Player, playerList)
  7824.  
  7825. /* 单video实例标签的情况 */
  7826. if (playerList.length === 1) {
  7827. t.playerInstance = playerList[0];
  7828. t.initPlayerInstance(true);
  7829. }
  7830.  
  7831. /* 多video实例标签的情况 */
  7832. playerList.forEach(function (player) {
  7833. /* 鼠标移到其上面的时候重新指定实例 */
  7834. if (!player._hasMouseRedirectEvent_) {
  7835. player.addEventListener('mouseenter', function (event) {
  7836. t.setPlayerInstance(event.target);
  7837. });
  7838. player._hasMouseRedirectEvent_ = true;
  7839. }
  7840.  
  7841. /* 播放器开始播放的时候重新指向实例 */
  7842. if (!player._hasPlayingRedirectEvent_) {
  7843. player.addEventListener('playing', function (event) {
  7844. const media = event.target;
  7845.  
  7846. /* 对于超短的音视频可能是某些操作反馈的特效,可忽略对其进行播放实例切换 */
  7847. if (media.duration && media.duration < 8) {
  7848. return false
  7849. }
  7850.  
  7851. t.setPlayerInstance(media);
  7852. });
  7853. player._hasPlayingRedirectEvent_ = true;
  7854. }
  7855.  
  7856. /* 当被观察到出现在浏览器视口里时,切换视频实例 */
  7857. if (!player._hasIntersectionObserver_) {
  7858. t.intersectionObserver.observe(player);
  7859. player._hasIntersectionObserver_ = true;
  7860. }
  7861. });
  7862.  
  7863. if (isInCrossOriginFrame()) {
  7864. /* 广播检测到H5Player的消息 */
  7865. monkeyMsg.send('videoDetected', {
  7866. src: t.playerInstance.src
  7867. });
  7868. }
  7869.  
  7870. registerH5playerMenus(h5Player);
  7871. }
  7872. },
  7873.  
  7874. /* 响应来自按键消息的广播 */
  7875. bindFakeEvent () {
  7876. const t = this;
  7877. if (t._hasBindFakeEvent_) return
  7878.  
  7879. /* 触发来自消息广播的模拟事件,实现跨域、跨Tab控制视频播放 */
  7880. let triggerFakeEvent = function (name, oldVal, newVal, remote) {
  7881. const player = t.player();
  7882. if (player) {
  7883. const fakeEvent = newVal.data;
  7884. fakeEvent.stopPropagation = () => { };
  7885. fakeEvent.preventDefault = () => { };
  7886. t.palyerTrigger(player, fakeEvent);
  7887.  
  7888. debug.log('已响应跨Tab/跨域按键控制信息:', newVal);
  7889. }
  7890. };
  7891.  
  7892. /**
  7893. * 操作节流控制,减少按键消息频率,
  7894. * 注意,开启节流控制后导致复合按键(如:shift+s)没法生效
  7895. */
  7896. if (!crossTabCtl.hasOpenPictureInPicture() && !t.hasCrossOriginVideoDetected) {
  7897. triggerFakeEvent = throttle(triggerFakeEvent, 80);
  7898. }
  7899.  
  7900. /* 注册响应来自按键消息的广播的事件 */
  7901. monkeyMsg.on('globalKeydownEvent', async (name, oldVal, newVal, remote) => {
  7902. if (remote) {
  7903. if (isInCrossOriginFrame()) {
  7904. /**
  7905. * 同处跨域受限页面,且都处于可见状态,大概率处于同一个Tab标签里,但不是100%
  7906. * tabId一致则100%为同一标签下
  7907. */
  7908. if (document.visibilityState === 'visible' && newVal.originTab) {
  7909. triggerFakeEvent(name, oldVal, newVal, remote);
  7910. }
  7911. } else if (crossTabCtl.hasOpenPictureInPicture()) {
  7912. /* 跨Tab控制画中画里面的视频播放 */
  7913. if (!newVal.originTab && (document.pictureInPictureElement || t.isLeavepictureinpictureAwhile())) {
  7914. triggerFakeEvent(name, oldVal, newVal, remote);
  7915. }
  7916. }
  7917. }
  7918. });
  7919.  
  7920. t._hasBindFakeEvent_ = true;
  7921. },
  7922.  
  7923. /* 绑定相关事件 */
  7924. bindEvent: function () {
  7925. const t = this;
  7926. if (t._hasBindEvent_) return
  7927.  
  7928. document.removeEventListener('keydown', t.keydownEvent);
  7929. document.addEventListener('keydown', t.keydownEvent, true);
  7930.  
  7931. /* 兼容iframe操作 */
  7932. if (isInIframe() && !isInCrossOriginFrame()) {
  7933. window.top.document.removeEventListener('keydown', t.keydownEvent);
  7934. window.top.document.addEventListener('keydown', t.keydownEvent, true);
  7935. }
  7936.  
  7937. t._hasBindEvent_ = true;
  7938. },
  7939.  
  7940. setCustomConfiguration (config, tag = 'Default') {
  7941. if (!config) return false
  7942.  
  7943. const configuration = configManager.mergeDefConf(config.customConfiguration);
  7944. const taskConf = mergeTaskConf(config.customTaskControlCenter);
  7945. if (TCC$1 && TCC$1.setTaskConf) {
  7946. TCC$1.setTaskConf(taskConf);
  7947. }
  7948.  
  7949. h5Player.hasSetCustomConfiguration = tag;
  7950. debug.info(`[CustomConfiguration][${tag}]`, configuration, taskConf);
  7951. },
  7952.  
  7953. mergeExternalConfiguration (config, tag = 'Default') {
  7954. if (!config || !configManager.getGlobalStorage('enhance.allowExternalCustomConfiguration')) return false
  7955. h5Player.setCustomConfiguration(config, 'External');
  7956. h5Player.hasExternalCustomConfiguration = tag;
  7957. },
  7958.  
  7959. init: function (global) {
  7960. var t = this;
  7961.  
  7962. if (window.unsafeWindow && window.unsafeWindow.__h5PlayerCustomConfiguration__) {
  7963. !t.hasExternalCustomConfiguration && t.mergeExternalConfiguration(window.unsafeWindow.__h5PlayerCustomConfiguration__);
  7964. }
  7965.  
  7966. if (TCC$1 && TCC$1.doTask('disable') === true) {
  7967. debug.info(`[TCC][disable][${location.host}] 已禁止在该网站运行视频检测逻辑,您可查看任务配置中心的相关配置了解详情`);
  7968. return true
  7969. }
  7970.  
  7971. if (!configManager.get('enable')) {
  7972. debug.info(`[config][disable][${location.host}] 当前网站已禁用脚本,如要启用脚本,请在菜单里开启`);
  7973. return true
  7974. }
  7975.  
  7976. if (!global) {
  7977. /* 检测是否存在H5播放器 */
  7978. t.detecH5Player();
  7979. return true
  7980. }
  7981.  
  7982. if (configManager.get('debug') === true) {
  7983. window._debugMode_ = true;
  7984. t.mountToGlobal();
  7985. }
  7986.  
  7987. setFakeUA();
  7988.  
  7989. /* 初始化任务配置中心 */
  7990. TCC$1 = h5PlayerTccInit(t);
  7991.  
  7992. /* 绑定键盘事件 */
  7993. t.bindEvent();
  7994. t.bindFakeEvent();
  7995.  
  7996. /* 响应来自跨域受限的视频检出事件 */
  7997. monkeyMsg.on('videoDetected', async (name, oldVal, newVal, remote) => {
  7998. if (newVal.originTab) {
  7999. t.hasCrossOriginVideoDetected = true;
  8000. }
  8001.  
  8002. debug.log('[hasCrossOriginVideoDetected]', t, name, oldVal, newVal, remote);
  8003. });
  8004.  
  8005. /* 当页面处于可视化状态时,初始化自定义播放逻辑 */
  8006. document.addEventListener('visibilitychange', function () {
  8007. h5Player.initAutoPlay();
  8008. });
  8009.  
  8010. if (window.unsafeWindow && configManager.getGlobalStorage('enhance.allowExternalCustomConfiguration')) {
  8011. window.unsafeWindow.__setH5PlayerCustomConfiguration__ = t.mergeExternalConfiguration;
  8012. }
  8013. }
  8014. };
  8015.  
  8016. async function h5PlayerInit () {
  8017. try {
  8018. mediaCore.init(function (mediaElement) {
  8019. // debug.log('[mediaCore][mediaChecker]', mediaElement)
  8020. h5Player.init();
  8021. });
  8022.  
  8023. if (configManager.get('enhance.allowExperimentFeatures')) {
  8024. mediaSource.init();
  8025. debug.warn(`[experimentFeatures][warning] ${i18n.t('experimentFeaturesWarning')}`);
  8026. debug.warn('[experimentFeatures][mediaSource][activated]');
  8027. }
  8028.  
  8029. /* 禁止对playbackRate等属性进行锁定 */
  8030. hackDefineProperty();
  8031. // if (!location.host.includes('bilibili')) {}
  8032.  
  8033. /* 禁止对shadowdom使用close模式 */
  8034. hackAttachShadow();
  8035.  
  8036. /* 对所有事件进行接管 */
  8037. proxyHTMLMediaElementEvent();
  8038. // hackEventListener()
  8039. } catch (e) {
  8040. console.error('h5player hack error', e);
  8041. }
  8042.  
  8043. menuRegister();
  8044.  
  8045. try {
  8046. /* 初始化全局所需的相关方法 */
  8047. h5Player.init(true);
  8048.  
  8049. /* 检测到有视频标签就进行初始化 */
  8050. supportMediaTags.forEach(tagName => {
  8051. ready(tagName, function () {
  8052. h5Player.init();
  8053. });
  8054. });
  8055.  
  8056. /* 检测shadow dom 下面的video */
  8057. document.addEventListener('addShadowRoot', function (e) {
  8058. const shadowRoot = e.detail.shadowRoot;
  8059. supportMediaTags.forEach(tagName => {
  8060. ready(tagName, function (element) {
  8061. h5Player.init();
  8062. }, shadowRoot);
  8063. });
  8064. });
  8065.  
  8066. /* 初始化跨Tab控制逻辑 */
  8067. crossTabCtl.init();
  8068.  
  8069. if (isInIframe()) {
  8070. debug.log('h5Player init suc, in iframe:', window, window.location.href);
  8071. } else {
  8072. debug.log('h5Player init suc', window, h5Player);
  8073. }
  8074.  
  8075. if (isInCrossOriginFrame()) {
  8076. debug.log('当前处于跨域受限的iframe中,h5Player部分功能可能无法正常开启', window.location.href);
  8077. }
  8078. } catch (e) {
  8079. debug.error('h5Player init fail', e);
  8080. }
  8081. }
  8082.  
  8083. function init (retryCount = 0) {
  8084. if (!window.document || !window.document.documentElement) {
  8085. setTimeout(() => {
  8086. if (retryCount < 200) {
  8087. init(retryCount + 1);
  8088. } else {
  8089. console.error('[h5player message:]', 'not documentElement detected!', window);
  8090. }
  8091. }, 10);
  8092.  
  8093. return false
  8094. } else if (retryCount > 0) {
  8095. console.warn('[h5player message:]', 'documentElement detected!', retryCount, window);
  8096. }
  8097.  
  8098. h5PlayerInit();
  8099. }
  8100.  
  8101. /**
  8102. * 某些极端情况下,直接访问window对象都会导致报错,所以整个init都try起来
  8103. * 例如:www.icourse163.org 就有一定的机率异常
  8104. */
  8105. let initTryCount = 0;
  8106. try {
  8107. init(0);
  8108. } catch (e) {
  8109. setTimeout(() => {
  8110. if (initTryCount < 200) {
  8111. initTryCount++;
  8112. init(0);
  8113. console.error('[h5player message:]', 'init error', initTryCount, e);
  8114. }
  8115. }, 10);
  8116. }