bilibili merged flv+mp4+ass+enhance

bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,MKV打包,播放体验增强,原生appsecret,不借助其他网站

目前为 2018-11-03 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name bilibili merged flv+mp4+ass+enhance
  3. // @namespace http://qli5.tk/
  4. // @homepageURL https://github.com/Xmader/bilitwin/
  5. // @supportURL https://github.com/Xmader/bilitwin/issues
  6. // @description bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,MKV打包,播放体验增强,原生appsecret,不借助其他网站
  7. // @match *://www.bilibili.com/video/av*
  8. // @match *://bangumi.bilibili.com/anime/*/play*
  9. // @match *://www.bilibili.com/bangumi/play/ep*
  10. // @match *://www.bilibili.com/bangumi/play/ss*
  11. // @match *://www.bilibili.com/bangumi/media/md*
  12. // @match *://www.biligame.com/detail/*
  13. // @match *://vc.bilibili.com/video/*
  14. // @match *://www.bilibili.com/watchlater/
  15. // @version 1.19.3
  16. // @author qli5
  17. // @copyright qli5, 2014+, 田生, grepmusic, zheng qian, ryiwamoto, xmader
  18. // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
  19. // @grant none
  20. // @run-at document-start
  21. // ==/UserScript==
  22.  
  23. /***
  24. *
  25. * @author qli5 <goodlq11[at](163|gmail).com>
  26. *
  27. * BiliTwin consists of two parts - BiliMonkey and BiliPolyfill.
  28. * They are bundled because I am too lazy to write two user interfaces.
  29. *
  30. * So what is the difference between BiliMonkey and BiliPolyfill?
  31. *
  32. * BiliMonkey deals with network. It is a (naIve) Service Worker.
  33. * This is also why it uses IndexedDB instead of localStorage.
  34. * BiliPolyfill deals with experience. It is more a "user script".
  35. * Everything it can do can be done by hand.
  36. *
  37. * BiliPolyfill will be pointless in the long run - I believe bilibili
  38. * will finally provide these functions themselves.
  39. *
  40. * This Source Code Form is subject to the terms of the Mozilla Public
  41. * License, v. 2.0. If a copy of the MPL was not distributed with this
  42. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  43. *
  44. * Covered Software is provided under this License on an “as is” basis,
  45. * without warranty of any kind, either expressed, implied, or statutory,
  46. * including, without limitation, warranties that the Covered Software
  47. * is free of defects, merchantable, fit for a particular purpose or
  48. * non-infringing. The entire risk as to the quality and performance of
  49. * the Covered Software is with You. Should any Covered Software prove
  50. * defective in any respect, You (not any Contributor) assume the cost
  51. * of any necessary servicing, repair, or correction. This disclaimer
  52. * of warranty constitutes an essential part of this License. No use of
  53. * any Covered Software is authorized under this License except under
  54. * this disclaimer.
  55. *
  56. * Under no circumstances and under no legal theory, whether tort
  57. * (including negligence), contract, or otherwise, shall any Contributor,
  58. * or anyone who distributes Covered Software as permitted above, be
  59. * liable to You for any direct, indirect, special, incidental, or
  60. * consequential damages of any character including, without limitation,
  61. * damages for lost profits, loss of goodwill, work stoppage, computer
  62. * failure or malfunction, or any and all other commercial damages or
  63. * losses, even if such party shall have been informed of the possibility
  64. * of such damages. This limitation of liability shall not apply to
  65. * liability for death or personal injury resulting from such party’s
  66. * negligence to the extent applicable law prohibits such limitation.
  67. * Some jurisdictions do not allow the exclusion or limitation of
  68. * incidental or consequential damages, so this exclusion and limitation
  69. * may not apply to You.
  70. */
  71.  
  72. /***
  73. * This is a bundled code. While it is not uglified, it may still be too
  74. * complex for reviewing. Please refer to
  75. * https://github.com/Xmader/bilitwin/
  76. * for source code.
  77. */
  78.  
  79. /***
  80. * Copyright (C) 2018 Qli5. All Rights Reserved.
  81. *
  82. * @author qli5 <goodlq11[at](163|gmail).com>
  83. *
  84. * This Source Code Form is subject to the terms of the Mozilla Public
  85. * License, v. 2.0. If a copy of the MPL was not distributed with this
  86. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  87. */
  88.  
  89. /**
  90. * Basically a Promise that exposes its resolve and reject callbacks
  91. */
  92. class AsyncContainer {
  93. /***
  94. * The thing is, if we cannot cancel a promise, we should at least be able to
  95. * explicitly mark a promise as garbage collectible.
  96. *
  97. * Yes, this is something like cancelable Promise. But I insist they are different.
  98. */
  99. constructor(callback) {
  100. // 1. primary promise
  101. this.primaryPromise = new Promise((s, j) => {
  102. this.resolve = arg => { s(arg); return arg; };
  103. this.reject = arg => { j(arg); return arg; };
  104. });
  105.  
  106. // 2. hang promise
  107. this.hangReturn = Symbol();
  108. this.hangPromise = new Promise(s => this.hang = () => s(this.hangReturn));
  109. this.destroiedThen = this.hangPromise.then.bind(this.hangPromise);
  110. this.primaryPromise.then(() => this.state = 'fulfilled');
  111. this.primaryPromise.catch(() => this.state = 'rejected');
  112. this.hangPromise.then(() => this.state = 'hanged');
  113.  
  114. // 4. race
  115. this.promise = Promise
  116. .race([this.primaryPromise, this.hangPromise])
  117. .then(s => s == this.hangReturn ? new Promise(() => { }) : s);
  118.  
  119. // 5. inherit
  120. this.then = this.promise.then.bind(this.promise);
  121. this.catch = this.promise.catch.bind(this.promise);
  122. this.finally = this.promise.finally.bind(this.promise);
  123.  
  124. // 6. optional callback
  125. if (typeof callback == 'function') callback(this.resolve, this.reject);
  126. }
  127.  
  128. /***
  129. * Memory leak notice:
  130. *
  131. * The V8 implementation of Promise requires
  132. * 1. the resolve handler of a Promise
  133. * 2. the reject handler of a Promise
  134. * 3. !! the Promise object itself !!
  135. * to be garbage collectible to correctly free Promise runtime contextes
  136. *
  137. * This piece of code will work
  138. * void (async () => {
  139. * const buf = new Uint8Array(1024 * 1024 * 1024);
  140. * for (let i = 0; i < buf.length; i++) buf[i] = i;
  141. * await new Promise(() => { });
  142. * return buf;
  143. * })();
  144. * if (typeof gc == 'function') gc();
  145. *
  146. * This piece of code will cause a Promise context mem leak
  147. * const deadPromise = new Promise(() => { });
  148. * void (async () => {
  149. * const buf = new Uint8Array(1024 * 1024 * 1024);
  150. * for (let i = 0; i < buf.length; i++) buf[i] = i;
  151. * await deadPromise;
  152. * return buf;
  153. * })();
  154. * if (typeof gc == 'function') gc();
  155. *
  156. * In other words, do NOT directly inherit from promise. You will need to
  157. * dereference it on destroying.
  158. */
  159. destroy() {
  160. this.hang();
  161. this.resolve = () => { };
  162. this.reject = this.resolve;
  163. this.hang = this.resolve;
  164. this.primaryPromise = null;
  165. this.hangPromise = null;
  166. this.promise = null;
  167. this.then = this.resolve;
  168. this.catch = this.resolve;
  169. this.finally = this.resolve;
  170. this.destroiedThen = f => f();
  171. /***
  172. * For ease of debug, do not dereference hangReturn
  173. *
  174. * If run from console, mysteriously this tiny symbol will help correct gc
  175. * before a console.clear
  176. */
  177. //this.hangReturn = null;
  178. }
  179.  
  180. static _UNIT_TEST() {
  181. const containers = [];
  182. async function foo() {
  183. const buf = new Uint8Array(600 * 1024 * 1024);
  184. for (let i = 0; i < buf.length; i++) buf[i] = i;
  185. const ac = new AsyncContainer();
  186. ac.destroiedThen(() => console.log('asyncContainer destroied'));
  187. containers.push(ac);
  188. await ac;
  189. return buf;
  190. }
  191. const foos = [foo(), foo(), foo()];
  192. containers.forEach(e => e.destroy());
  193. console.warn('Check your RAM usage. I allocated 1.8GB in three dead-end promises.');
  194. return [foos, containers];
  195. }
  196. }
  197.  
  198. /***
  199. * Copyright (C) 2018 Qli5. All Rights Reserved.
  200. *
  201. * @author qli5 <goodlq11[at](163|gmail).com>
  202. *
  203. * This Source Code Form is subject to the terms of the Mozilla Public
  204. * License, v. 2.0. If a copy of the MPL was not distributed with this
  205. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  206. */
  207.  
  208. /**
  209. * Provides common util for all bilibili user scripts
  210. */
  211. class BiliUserJS {
  212. static async getIframeWin() {
  213. if (document.querySelector('#bofqi > iframe').contentDocument.getElementById('bilibiliPlayer')) {
  214. return document.querySelector('#bofqi > iframe').contentWindow;
  215. }
  216. else {
  217. return new Promise(resolve => {
  218. document.querySelector('#bofqi > iframe').addEventListener('load', () => {
  219. resolve(document.querySelector('#bofqi > iframe').contentWindow);
  220. }, { once: true });
  221. });
  222. }
  223. }
  224.  
  225. static async getPlayerWin() {
  226. if (location.href.includes('/watchlater/#/list')) {
  227. await new Promise(resolve => {
  228. window.addEventListener('hashchange', () => resolve(location.href), { once: true });
  229. });
  230. }
  231. if (location.href.includes('/watchlater/#/')) {
  232. if (!document.getElementById('bofqi')) {
  233. await new Promise(resolve => {
  234. const observer = new MutationObserver(() => {
  235. if (document.getElementById('bofqi')) {
  236. resolve(document.getElementById('bofqi'));
  237. observer.disconnect();
  238. }
  239. });
  240. observer.observe(document, { childList: true, subtree: true });
  241. });
  242. }
  243. }
  244. if (document.getElementById('bilibiliPlayer')) {
  245. return window;
  246. }
  247. else if (document.querySelector('#bofqi > iframe')) {
  248. return BiliUserJS.getIframeWin();
  249. }
  250. else if (document.querySelector('#bofqi > object')) {
  251. throw 'Need H5 Player';
  252. }
  253. else if (!(document.getElementById('bofqi') instanceof Node) && document.querySelector("video")) {
  254. top.location.reload(); // 刷新
  255. }
  256. else {
  257. return new Promise(resolve => {
  258. const observer = new MutationObserver(() => {
  259. if (document.getElementById('bilibiliPlayer')) {
  260. observer.disconnect();
  261. resolve(window);
  262. }
  263. else if (document.querySelector('#bofqi > iframe')) {
  264. observer.disconnect();
  265. resolve(BiliUserJS.getIframeWin());
  266. }
  267. else if (document.querySelector('#bofqi > object')) {
  268. observer.disconnect();
  269. throw 'Need H5 Player';
  270. }
  271. });
  272. observer.observe(document.getElementById('bofqi'), { childList: true });
  273. });
  274. }
  275. }
  276.  
  277. static tryGetPlayerWinSync() {
  278. if (document.getElementById('bilibiliPlayer')) {
  279. return window;
  280. }
  281. else if (document.querySelector('#bofqi > object')) {
  282. throw 'Need H5 Player';
  283. }
  284. }
  285.  
  286. static getCidRefreshPromise(playerWin) {
  287. /***********
  288. * !!!Race condition!!!
  289. * We must finish everything within one microtask queue!
  290. *
  291. * bilibili script:
  292. * videoElement.remove() -> setTimeout(0) -> [[microtask]] -> load playurl
  293. * \- synchronous macrotask -/ || \- synchronous
  294. * ||
  295. * the only position to inject monkey.sniffDefaultFormat
  296. */
  297. const cidRefresh = new AsyncContainer();
  298.  
  299. // 1. no active video element in document => cid refresh
  300. const observer = new MutationObserver(() => {
  301. if (!playerWin.document.getElementsByTagName('video')[0]) {
  302. observer.disconnect();
  303. cidRefresh.resolve();
  304. }
  305. });
  306. observer.observe(playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  307.  
  308. // 2. playerWin unload => cid refresh
  309. playerWin.addEventListener('unload', () => Promise.resolve().then(() => cidRefresh.resolve()));
  310.  
  311. return cidRefresh;
  312. }
  313.  
  314. static async domContentLoadedThen(func) {
  315. if (document.readyState == 'loading') {
  316. return new Promise(resolve => {
  317. document.addEventListener('DOMContentLoaded', () => resolve(func()), { once: true });
  318. })
  319. }
  320. else {
  321. return func();
  322. }
  323. }
  324. }
  325.  
  326. /***
  327. * Copyright (C) 2018 Qli5. All Rights Reserved.
  328. *
  329. * @author qli5 <goodlq11[at](163|gmail).com>
  330. *
  331. * This Source Code Form is subject to the terms of the Mozilla Public
  332. * License, v. 2.0. If a copy of the MPL was not distributed with this
  333. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  334. */
  335.  
  336. /**
  337. * A promisified indexedDB with large file(>100MB) support
  338. */
  339. class CacheDB {
  340. constructor(dbName = 'biliMonkey', osName = 'flv', keyPath = 'name', maxItemSize = 100 * 1024 * 1024) {
  341. // Neither Chrome or Firefox can handle item size > 100M
  342. this.dbName = dbName;
  343. this.osName = osName;
  344. this.keyPath = keyPath;
  345. this.maxItemSize = maxItemSize;
  346. this.db = null;
  347. }
  348.  
  349. async getDB() {
  350. if (this.db) return this.db;
  351. this.db = new Promise((resolve, reject) => {
  352. const openRequest = indexedDB.open(this.dbName);
  353. openRequest.onupgradeneeded = e => {
  354. const db = e.target.result;
  355. if (!db.objectStoreNames.contains(this.osName)) {
  356. db.createObjectStore(this.osName, { keyPath: this.keyPath });
  357. }
  358. };
  359. openRequest.onsuccess = e => {
  360. return resolve(this.db = e.target.result);
  361. };
  362. openRequest.onerror = reject;
  363. });
  364. return this.db;
  365. }
  366.  
  367. async addData(item, name = item.name, data = item.data || item) {
  368. if (!data instanceof Blob) throw 'CacheDB: data must be a Blob';
  369. const itemChunks = [];
  370. const numChunks = Math.ceil(data.size / this.maxItemSize);
  371. for (let i = 0; i < numChunks; i++) {
  372. itemChunks.push({
  373. name: `${name}/part_${i}`,
  374. numChunks,
  375. data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
  376. });
  377. }
  378.  
  379. const reqCascade = new Promise(async (resolve, reject) => {
  380. const db = await this.getDB();
  381. const objectStore = db.transaction([this.osName], 'readwrite').objectStore(this.osName);
  382. const onsuccess = e => {
  383. const chunk = itemChunks.pop();
  384. if (!chunk) return resolve(e);
  385. const req = objectStore.add(chunk);
  386. req.onerror = reject;
  387. req.onsuccess = onsuccess;
  388. };
  389. onsuccess();
  390. });
  391.  
  392. return reqCascade;
  393. }
  394.  
  395. async putData(item, name = item.name, data = item.data || item) {
  396. if (!data instanceof Blob) throw 'CacheDB: data must be a Blob';
  397. const itemChunks = [];
  398. const numChunks = Math.ceil(data.size / this.maxItemSize);
  399. for (let i = 0; i < numChunks; i++) {
  400. itemChunks.push({
  401. name: `${name}/part_${i}`,
  402. numChunks,
  403. data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
  404. });
  405. }
  406.  
  407. const reqCascade = new Promise(async (resolve, reject) => {
  408. const db = await this.getDB();
  409. const objectStore = db.transaction([this.osName], 'readwrite').objectStore(this.osName);
  410. const onsuccess = e => {
  411. const chunk = itemChunks.pop();
  412. if (!chunk) return resolve(e);
  413. const req = objectStore.put(chunk);
  414. req.onerror = reject;
  415. req.onsuccess = onsuccess;
  416. };
  417. onsuccess();
  418. });
  419.  
  420. return reqCascade;
  421. }
  422.  
  423. async getData(name) {
  424. const reqCascade = new Promise(async (resolve, reject) => {
  425. const dataChunks = [];
  426. const db = await this.getDB();
  427. const objectStore = db.transaction([this.osName], 'readwrite').objectStore(this.osName);
  428. const probe = objectStore.get(`${name}/part_0`);
  429. probe.onerror = reject;
  430. probe.onsuccess = e => {
  431. // 1. Probe fails => key does not exist
  432. if (!probe.result) return resolve(null);
  433.  
  434. // 2. How many chunks to retrieve?
  435. const { numChunks } = probe.result;
  436.  
  437. // 3. Cascade on the remaining chunks
  438. const onsuccess = e => {
  439. dataChunks.push(e.target.result.data);
  440. if (dataChunks.length == numChunks) return resolve(dataChunks);
  441. const req = objectStore.get(`${name}/part_${dataChunks.length}`);
  442. req.onerror = reject;
  443. req.onsuccess = onsuccess;
  444. };
  445. onsuccess(e);
  446. };
  447. });
  448.  
  449. const dataChunks = await reqCascade;
  450.  
  451. return dataChunks ? { name, data: new Blob(dataChunks) } : null;
  452. }
  453.  
  454. async deleteData(name) {
  455. const reqCascade = new Promise(async (resolve, reject) => {
  456. let currentChunkNum = 0;
  457. const db = await this.getDB();
  458. const objectStore = db.transaction([this.osName], 'readwrite').objectStore(this.osName);
  459. const probe = objectStore.get(`${name}/part_0`);
  460. probe.onerror = reject;
  461. probe.onsuccess = e => {
  462. // 1. Probe fails => key does not exist
  463. if (!probe.result) return resolve(null);
  464.  
  465. // 2. How many chunks to delete?
  466. const { numChunks } = probe.result;
  467.  
  468. // 3. Cascade on the remaining chunks
  469. const onsuccess = e => {
  470. const req = objectStore.delete(`${name}/part_${currentChunkNum}`);
  471. req.onerror = reject;
  472. req.onsuccess = onsuccess;
  473. currentChunkNum++;
  474. if (currentChunkNum == numChunks) return resolve(e);
  475. };
  476. onsuccess();
  477. };
  478. });
  479.  
  480. return reqCascade;
  481. }
  482.  
  483. async deleteEntireDB() {
  484. const req = indexedDB.deleteDatabase(this.dbName);
  485. return new Promise((resolve, reject) => {
  486. req.onsuccess = () => resolve(this.db = null);
  487. req.onerror = reject;
  488. });
  489. }
  490.  
  491. static async _UNIT_TEST() {
  492. let db = new CacheDB();
  493. console.warn('Storing 201MB...');
  494. console.log(await db.putData(new Blob([new ArrayBuffer(201 * 1024 * 1024)]), 'test'));
  495. console.warn('Deleting 201MB...');
  496. console.log(await db.deleteData('test'));
  497. }
  498. }
  499.  
  500. /***
  501. * Copyright (C) 2018 Qli5. All Rights Reserved.
  502. *
  503. * @author qli5 <goodlq11[at](163|gmail).com>
  504. *
  505. * This Source Code Form is subject to the terms of the Mozilla Public
  506. * License, v. 2.0. If a copy of the MPL was not distributed with this
  507. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  508. */
  509.  
  510. /**
  511. * A more powerful fetch with
  512. * 1. onprogress handler
  513. * 2. partial response getter
  514. */
  515. class DetailedFetchBlob {
  516. constructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror, fetch = init.fetch || top.fetch) {
  517. // Fire in the Fox fix
  518. if (this.firefoxConstructor(input, init, onprogress, onabort, onerror)) return;
  519. // Now I know why standardizing cancelable Promise is that difficult
  520. // PLEASE refactor me!
  521. this.onprogress = onprogress;
  522. this.onabort = onabort;
  523. this.onerror = onerror;
  524. this.abort = null;
  525. this.loaded = init.cacheLoaded || 0;
  526. this.total = init.cacheLoaded || 0;
  527. this.lengthComputable = false;
  528. this.buffer = [];
  529. this.blob = null;
  530. this.reader = null;
  531. this.blobPromise = fetch(input, init).then(res => {
  532. if (this.reader == 'abort') return res.body.getReader().cancel().then(() => null);
  533. if (!res.ok) throw `HTTP Error ${res.status}: ${res.statusText}`;
  534. this.lengthComputable = res.headers.has('Content-Length');
  535. this.total += parseInt(res.headers.get('Content-Length')) || Infinity;
  536. if (this.lengthComputable) {
  537. this.reader = res.body.getReader();
  538. return this.blob = this.consume();
  539. }
  540. else {
  541. if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
  542. return this.blob = res.blob();
  543. }
  544. });
  545. this.blobPromise.then(() => this.abort = () => { });
  546. this.blobPromise.catch(e => this.onerror({ target: this, type: e }));
  547. this.promise = Promise.race([
  548. this.blobPromise,
  549. new Promise(resolve => this.abort = () => {
  550. this.onabort({ target: this, type: 'abort' });
  551. resolve('abort');
  552. this.buffer = [];
  553. this.blob = null;
  554. if (this.reader) this.reader.cancel();
  555. else this.reader = 'abort';
  556. })
  557. ]).then(s => s == 'abort' ? new Promise(() => { }) : s);
  558. this.then = this.promise.then.bind(this.promise);
  559. this.catch = this.promise.catch.bind(this.promise);
  560. }
  561.  
  562. getPartialBlob() {
  563. return new Blob(this.buffer);
  564. }
  565.  
  566. async getBlob() {
  567. return this.promise;
  568. }
  569.  
  570. async pump() {
  571. while (true) {
  572. let { done, value } = await this.reader.read();
  573. if (done) return this.loaded;
  574. this.loaded += value.byteLength;
  575. this.buffer.push(new Blob([value]));
  576. if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
  577. }
  578. }
  579.  
  580. async consume() {
  581. await this.pump();
  582. this.blob = new Blob(this.buffer);
  583. this.buffer = null;
  584. return this.blob;
  585. }
  586.  
  587. firefoxConstructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror) {
  588. if (!top.navigator.userAgent.includes('Firefox')) return false;
  589. this.onprogress = onprogress;
  590. this.onabort = onabort;
  591. this.onerror = onerror;
  592. this.abort = null;
  593. this.loaded = init.cacheLoaded || 0;
  594. this.total = init.cacheLoaded || 0;
  595. this.lengthComputable = false;
  596. this.buffer = [];
  597. this.blob = null;
  598. this.reader = undefined;
  599. this.blobPromise = new Promise((resolve, reject) => {
  600. let xhr = new XMLHttpRequest();
  601. xhr.responseType = 'moz-chunked-arraybuffer';
  602. xhr.onload = () => { resolve(this.blob = new Blob(this.buffer)); this.buffer = null; };
  603. let cacheLoaded = this.loaded;
  604. xhr.onprogress = e => {
  605. this.loaded = e.loaded + cacheLoaded;
  606. this.total = e.total + cacheLoaded;
  607. this.lengthComputable = e.lengthComputable;
  608. this.buffer.push(new Blob([xhr.response]));
  609. if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
  610. };
  611. xhr.onabort = e => this.onabort({ target: this, type: 'abort' });
  612. xhr.onerror = e => { this.onerror({ target: this, type: e.type }); reject(e); };
  613. this.abort = xhr.abort.bind(xhr);
  614. xhr.open('get', input);
  615. xhr.send();
  616. });
  617. this.promise = this.blobPromise;
  618. this.then = this.promise.then.bind(this.promise);
  619. this.catch = this.promise.catch.bind(this.promise);
  620. return true;
  621. }
  622. }
  623.  
  624. /***
  625. * Copyright (C) 2018 Qli5. All Rights Reserved.
  626. *
  627. * @author qli5 <goodlq11[at](163|gmail).com>
  628. *
  629. * This Source Code Form is subject to the terms of the Mozilla Public
  630. * License, v. 2.0. If a copy of the MPL was not distributed with this
  631. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  632. */
  633.  
  634. /**
  635. * A simple emulation of pthread_mutex
  636. */
  637. class Mutex {
  638. constructor() {
  639. this.queueTail = Promise.resolve();
  640. this.resolveHead = null;
  641. }
  642.  
  643. /**
  644. * await mutex.lock = pthread_mutex_lock
  645. * @returns a promise to be resolved when the mutex is available
  646. */
  647. async lock() {
  648. let myResolve;
  649. let _queueTail = this.queueTail;
  650. this.queueTail = new Promise(resolve => myResolve = resolve);
  651. await _queueTail;
  652. this.resolveHead = myResolve;
  653. return;
  654. }
  655.  
  656. /**
  657. * mutex.unlock = pthread_mutex_unlock
  658. */
  659. unlock() {
  660. this.resolveHead();
  661. return;
  662. }
  663.  
  664. /**
  665. * lock, ret = await async, unlock, return ret
  666. * @param {(Function|Promise)} promise async thing to wait for
  667. */
  668. async lockAndAwait(promise) {
  669. await this.lock();
  670. let ret;
  671. try {
  672. if (typeof promise == 'function') promise = promise();
  673. ret = await promise;
  674. }
  675. finally {
  676. this.unlock();
  677. }
  678. return ret;
  679. }
  680.  
  681. static _UNIT_TEST() {
  682. let m = new Mutex();
  683. function sleep(time) {
  684. return new Promise(r => setTimeout(r, time));
  685. }
  686. m.lockAndAwait(() => {
  687. console.warn('Check message timestamps.');
  688. console.warn('Bad:');
  689. console.warn('1 1 1 1 1:5s');
  690. console.warn(' 1 1 1 1 1:10s');
  691. console.warn('Good:');
  692. console.warn('1 1 1 1 1:5s');
  693. console.warn(' 1 1 1 1 1:10s');
  694. });
  695. m.lockAndAwait(async () => {
  696. await sleep(1000);
  697. await sleep(1000);
  698. await sleep(1000);
  699. await sleep(1000);
  700. await sleep(1000);
  701. });
  702. m.lockAndAwait(async () => console.log('5s!'));
  703. m.lockAndAwait(async () => {
  704. await sleep(1000);
  705. await sleep(1000);
  706. await sleep(1000);
  707. await sleep(1000);
  708. await sleep(1000);
  709. });
  710. m.lockAndAwait(async () => console.log('10s!'));
  711. }
  712. }
  713.  
  714. /**
  715. * @typedef DanmakuColor
  716. * @property {number} r
  717. * @property {number} g
  718. * @property {number} b
  719. */
  720. /**
  721. * @typedef Danmaku
  722. * @property {string} text
  723. * @property {number} time
  724. * @property {string} mode
  725. * @property {number} size
  726. * @property {DanmakuColor} color
  727. * @property {boolean} bottom
  728. */
  729.  
  730. const parser = {};
  731.  
  732. /**
  733. * @param {Danmaku} danmaku
  734. * @returns {boolean}
  735. */
  736. const danmakuFilter = danmaku => {
  737. if (!danmaku) return false;
  738. if (!danmaku.text) return false;
  739. if (!danmaku.mode) return false;
  740. if (!danmaku.size) return false;
  741. if (danmaku.time < 0 || danmaku.time >= 360000) return false;
  742. return true;
  743. };
  744.  
  745. const parseRgb256IntegerColor = color => {
  746. const rgb = parseInt(color, 10);
  747. const r = (rgb >>> 4) & 0xff;
  748. const g = (rgb >>> 2) & 0xff;
  749. const b = (rgb >>> 0) & 0xff;
  750. return { r, g, b };
  751. };
  752.  
  753. const parseNiconicoColor = mail => {
  754. const colorTable = {
  755. red: { r: 255, g: 0, b: 0 },
  756. pink: { r: 255, g: 128, b: 128 },
  757. orange: { r: 255, g: 184, b: 0 },
  758. yellow: { r: 255, g: 255, b: 0 },
  759. green: { r: 0, g: 255, b: 0 },
  760. cyan: { r: 0, g: 255, b: 255 },
  761. blue: { r: 0, g: 0, b: 255 },
  762. purple: { r: 184, g: 0, b: 255 },
  763. black: { r: 0, g: 0, b: 0 },
  764. };
  765. const defaultColor = { r: 255, g: 255, b: 255 };
  766. const line = mail.toLowerCase().split(/\s+/);
  767. const color = Object.keys(colorTable).find(color => line.includes(color));
  768. return color ? colorTable[color] : defaultColor;
  769. };
  770.  
  771. const parseNiconicoMode = mail => {
  772. const line = mail.toLowerCase().split(/\s+/);
  773. if (line.includes('ue')) return 'TOP';
  774. if (line.includes('shita')) return 'BOTTOM';
  775. return 'RTL';
  776. };
  777.  
  778. const parseNiconicoSize = mail => {
  779. const line = mail.toLowerCase().split(/\s+/);
  780. if (line.includes('big')) return 36;
  781. if (line.includes('small')) return 16;
  782. return 25;
  783. };
  784.  
  785. /**
  786. * @param {string|ArrayBuffer} content
  787. * @return {{ cid: number, danmaku: Array<Danmaku> }}
  788. */
  789. parser.bilibili = function (content) {
  790. const text = typeof content === 'string' ? content : new TextDecoder('utf-8').decode(content);
  791. const clean = text.replace(/(?:[\0-\x08\x0B\f\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF])/g, '').replace(/.*?\?>/,"");
  792. const data = (new DOMParser()).parseFromString(clean, 'text/xml');
  793. const cid = +data.querySelector('chatid,oid').textContent;
  794. /** @type {Array<Danmaku>} */
  795. const danmaku = Array.from(data.querySelectorAll('d')).map(d => {
  796. const p = d.getAttribute('p');
  797. const [time, mode, size, color, create, bottom, sender, id] = p.split(',');
  798. return {
  799. text: d.textContent,
  800. time: +time,
  801. // We do not support ltr mode
  802. mode: [null, 'RTL', 'RTL', 'RTL', 'BOTTOM', 'TOP'][+mode],
  803. size: +size,
  804. color: parseRgb256IntegerColor(color),
  805. bottom: bottom > 0,
  806. };
  807. }).filter(danmakuFilter);
  808. return { cid, danmaku };
  809. };
  810.  
  811. /**
  812. * @param {string|ArrayBuffer} content
  813. * @return {{ cid: number, danmaku: Array<Danmaku> }}
  814. */
  815. parser.acfun = function (content) {
  816. const text = typeof content === 'string' ? content : new TextDecoder('utf-8').decode(content);
  817. const data = JSON.parse(text);
  818. const list = data.reduce((x, y) => x.concat(y), []);
  819. const danmaku = list.map(line => {
  820. const [time, color, mode, size, sender, create, uuid] = line.c.split(','), text = line.m;
  821. return {
  822. text,
  823. time: +time,
  824. color: parseRgb256IntegerColor(+color),
  825. mode: [null, 'RTL', null, null, 'BOTTOM', 'TOP'][mode],
  826. size: +size,
  827. bottom: false,
  828. uuid,
  829. };
  830. }).filter(danmakuFilter);
  831. return { danmaku };
  832. };
  833.  
  834. /**
  835. * @param {string|ArrayBuffer} content
  836. * @return {{ cid: number, danmaku: Array<Danmaku> }}
  837. */
  838. parser.niconico = function (content) {
  839. const text = typeof content === 'string' ? content : new TextDecoder('utf-8').decode(content);
  840. const data = JSON.parse(text);
  841. const list = data.map(item => item.chat).filter(x => x);
  842. const { thread } = list.find(comment => comment.thread);
  843. const danmaku = list.map(comment => {
  844. if (!comment.content || !(comment.vpos >= 0) || !comment.no) return null;
  845. const { vpos, mail = '', content, no } = comment;
  846. return {
  847. text: content,
  848. time: vpos / 100,
  849. color: parseNiconicoColor(mail),
  850. mode: parseNiconicoMode(mail),
  851. size: parseNiconicoSize(mail),
  852. bottom: false,
  853. id: no,
  854. };
  855. }).filter(danmakuFilter);
  856. return { thread, danmaku };
  857. };
  858.  
  859. const font = {};
  860.  
  861. // Meansure using canvas
  862. font.textByCanvas = function () {
  863. const canvas = document.createElement('canvas');
  864. const context = canvas.getContext('2d');
  865. return function (fontname, text, fontsize) {
  866. context.font = `bold ${fontsize}px ${fontname}`;
  867. return Math.ceil(context.measureText(text).width);
  868. };
  869. };
  870.  
  871. // Meansure using <div>
  872. font.textByDom = function () {
  873. const container = document.createElement('div');
  874. container.setAttribute('style', 'all: initial !important');
  875. const content = document.createElement('div');
  876. content.setAttribute('style', [
  877. 'top: -10000px', 'left: -10000px',
  878. 'width: auto', 'height: auto', 'position: absolute',
  879. ].map(item => item + ' !important;').join(' '));
  880. const active = () => { document.body.parentNode.appendChild(content); };
  881. if (!document.body) document.addEventListener('DOMContentLoaded', active);
  882. else active();
  883. return (fontname, text, fontsize) => {
  884. content.textContent = text;
  885. content.style.font = `bold ${fontsize}px ${fontname}`;
  886. return content.clientWidth;
  887. };
  888. };
  889.  
  890. font.text = (function () {
  891. // https://bugzilla.mozilla.org/show_bug.cgi?id=561361
  892. if (/linux/i.test(navigator.platform)) {
  893. return font.textByDom();
  894. } else {
  895. return font.textByCanvas();
  896. }
  897. }());
  898.  
  899. font.valid = (function () {
  900. const cache = new Map();
  901. const textWidth = font.text;
  902. // Use following texts for checking
  903. const sampleText = [
  904. 'The quick brown fox jumps over the lazy dog',
  905. '7531902468', ',.!-', ',。:!',
  906. '天地玄黄', '則近道矣',
  907. 'あいうえお', 'アイウエオガパ', 'アイウエオガパ',
  908. ].join('');
  909. // Some given font family is avaliable iff we can meansure different width compared to other fonts
  910. const sampleFont = [
  911. 'monospace', 'sans-serif', 'sans',
  912. 'Symbol', 'Arial', 'Comic Sans MS', 'Fixed', 'Terminal',
  913. 'Times', 'Times New Roman',
  914. 'SimSum', 'Microsoft YaHei', 'PingFang SC', 'Heiti SC', 'WenQuanYi Micro Hei',
  915. 'Pmingliu', 'Microsoft JhengHei', 'PingFang TC', 'Heiti TC',
  916. 'MS Gothic', 'Meiryo', 'Hiragino Kaku Gothic Pro', 'Hiragino Mincho Pro',
  917. ];
  918. const diffFont = function (base, test) {
  919. const baseSize = textWidth(base, sampleText, 72);
  920. const testSize = textWidth(test + ',' + base, sampleText, 72);
  921. return baseSize !== testSize;
  922. };
  923. const validFont = function (test) {
  924. if (cache.has(test)) return cache.get(test);
  925. const result = sampleFont.some(base => diffFont(base, test));
  926. cache.set(test, result);
  927. return result;
  928. };
  929. return validFont;
  930. }());
  931.  
  932. const rtlCanvas = function (options) {
  933. const {
  934. resolutionX: wc, // width of canvas
  935. resolutionY: hc, // height of canvas
  936. bottomReserved: b, // reserved bottom height for subtitle
  937. rtlDuration: u, // duration appeared on screen
  938. maxDelay: maxr, // max allowed delay
  939. } = options;
  940.  
  941. // Initial canvas border
  942. let used = [
  943. // p: top
  944. // m: bottom
  945. // tf: time completely enter screen
  946. // td: time completely leave screen
  947. // b: allow conflict with subtitle
  948. // add a fake danmaku for describe top of screen
  949. { p: -Infinity, m: 0, tf: Infinity, td: Infinity, b: false },
  950. // add a fake danmaku for describe bottom of screen
  951. { p: hc, m: Infinity, tf: Infinity, td: Infinity, b: false },
  952. // add a fake danmaku for placeholder of subtitle
  953. { p: hc - b, m: hc, tf: Infinity, td: Infinity, b: true },
  954. ];
  955. // Find out some position is available
  956. const available = (hv, t0s, t0l, b) => {
  957. const suggestion = [];
  958. // Upper edge of candidate position should always be bottom of other danmaku (or top of screen)
  959. used.forEach(i => {
  960. if (i.m + hv >= hc) return;
  961. const p = i.m;
  962. const m = p + hv;
  963. let tas = t0s;
  964. let tal = t0l;
  965. // and left border should be right edge of others
  966. used.forEach(j => {
  967. if (j.p >= m) return;
  968. if (j.m <= p) return;
  969. if (j.b && b) return;
  970. tas = Math.max(tas, j.tf);
  971. tal = Math.max(tal, j.td);
  972. });
  973. const r = Math.max(tas - t0s, tal - t0l);
  974. if (r > maxr) return;
  975. // save a candidate position
  976. suggestion.push({ p, r });
  977. });
  978. // sorted by its vertical position
  979. suggestion.sort((x, y) => x.p - y.p);
  980. let mr = maxr;
  981. // the bottom and later choice should be ignored
  982. const filtered = suggestion.filter(i => {
  983. if (i.r >= mr) return false;
  984. mr = i.r;
  985. return true;
  986. });
  987. return filtered;
  988. };
  989. // mark some area as used
  990. let use = (p, m, tf, td) => {
  991. used.push({ p, m, tf, td, b: false });
  992. };
  993. // remove danmaku not needed anymore by its time
  994. const syn = (t0s, t0l) => {
  995. used = used.filter(i => i.tf > t0s || i.td > t0l);
  996. };
  997. // give a score in range [0, 1) for some position
  998. const score = i => {
  999. if (i.r > maxr) return -Infinity;
  1000. return 1 - Math.hypot(i.r / maxr, i.p / hc) * Math.SQRT1_2;
  1001. };
  1002. // add some danmaku
  1003. return line => {
  1004. const {
  1005. time: t0s, // time sent (start to appear if no delay)
  1006. width: wv, // width of danmaku
  1007. height: hv, // height of danmaku
  1008. bottom: b, // is subtitle
  1009. } = line;
  1010. const t0l = wc / (wv + wc) * u + t0s; // time start to leave
  1011. syn(t0s, t0l);
  1012. const al = available(hv, t0s, t0l, b);
  1013. if (!al.length) return null;
  1014. const scored = al.map(i => [score(i), i]);
  1015. const best = scored.reduce((x, y) => {
  1016. return x[0] > y[0] ? x : y;
  1017. })[1];
  1018. const ts = t0s + best.r; // time start to enter
  1019. const tf = wv / (wv + wc) * u + ts; // time complete enter
  1020. const td = u + ts; // time complete leave
  1021. use(best.p, best.p + hv, tf, td);
  1022. return {
  1023. top: best.p,
  1024. time: ts,
  1025. };
  1026. };
  1027. };
  1028.  
  1029. const fixedCanvas = function (options) {
  1030. const {
  1031. resolutionY: hc,
  1032. bottomReserved: b,
  1033. fixDuration: u,
  1034. maxDelay: maxr,
  1035. } = options;
  1036. let used = [
  1037. { p: -Infinity, m: 0, td: Infinity, b: false },
  1038. { p: hc, m: Infinity, td: Infinity, b: false },
  1039. { p: hc - b, m: hc, td: Infinity, b: true },
  1040. ];
  1041. // Find out some available position
  1042. const fr = (p, m, t0s, b) => {
  1043. let tas = t0s;
  1044. used.forEach(j => {
  1045. if (j.p >= m) return;
  1046. if (j.m <= p) return;
  1047. if (j.b && b) return;
  1048. tas = Math.max(tas, j.td);
  1049. });
  1050. const r = tas - t0s;
  1051. if (r > maxr) return null;
  1052. return { r, p, m };
  1053. };
  1054. // layout for danmaku at top
  1055. const top = (hv, t0s, b) => {
  1056. const suggestion = [];
  1057. used.forEach(i => {
  1058. if (i.m + hv >= hc) return;
  1059. suggestion.push(fr(i.m, i.m + hv, t0s, b));
  1060. });
  1061. return suggestion.filter(x => x);
  1062. };
  1063. // layout for danmaku at bottom
  1064. const bottom = (hv, t0s, b) => {
  1065. const suggestion = [];
  1066. used.forEach(i => {
  1067. if (i.p - hv <= 0) return;
  1068. suggestion.push(fr(i.p - hv, i.p, t0s, b));
  1069. });
  1070. return suggestion.filter(x => x);
  1071. };
  1072. const use = (p, m, td) => {
  1073. used.push({ p, m, td, b: false });
  1074. };
  1075. const syn = t0s => {
  1076. used = used.filter(i => i.td > t0s);
  1077. };
  1078. // Score every position
  1079. const score = (i, is_top) => {
  1080. if (i.r > maxr) return -Infinity;
  1081. const f = p => is_top ? p : (hc - p);
  1082. return 1 - (i.r / maxr * (31 / 32) + f(i.p) / hc * (1 / 32));
  1083. };
  1084. return function (line) {
  1085. const { time: t0s, height: hv, bottom: b } = line;
  1086. const is_top = line.mode === 'TOP';
  1087. syn(t0s);
  1088. const al = (is_top ? top : bottom)(hv, t0s, b);
  1089. if (!al.length) return null;
  1090. const scored = al.map(function (i) { return [score(i, is_top), i]; });
  1091. const best = scored.reduce(function (x, y) {
  1092. return x[0] > y[0] ? x : y;
  1093. }, [-Infinity, null])[1];
  1094. if (!best) return null;
  1095. use(best.p, best.m, best.r + t0s + u);
  1096. return { top: best.p, time: best.r + t0s };
  1097. };
  1098. };
  1099.  
  1100. const placeDanmaku = function (options) {
  1101. const layers = options.maxOverlap;
  1102. const normal = Array(layers).fill(null).map(x => rtlCanvas(options));
  1103. const fixed = Array(layers).fill(null).map(x => fixedCanvas(options));
  1104. return function (line) {
  1105. line.fontSize = Math.round(line.size * options.fontSize);
  1106. line.height = line.fontSize;
  1107. line.width = line.width || font.text(options.fontFamily, line.text, line.fontSize) || 1;
  1108.  
  1109. if (line.mode === 'RTL') {
  1110. const pos = normal.reduce((pos, layer) => pos || layer(line), null);
  1111. if (!pos) return null;
  1112. const { top, time } = pos;
  1113. line.layout = {
  1114. type: 'Rtl',
  1115. start: {
  1116. x: options.resolutionX + line.width / 2,
  1117. y: top + line.height,
  1118. time,
  1119. },
  1120. end: {
  1121. x: -line.width / 2,
  1122. y: top + line.height,
  1123. time: options.rtlDuration + time,
  1124. },
  1125. };
  1126. } else if (['TOP', 'BOTTOM'].includes(line.mode)) {
  1127. const pos = fixed.reduce((pos, layer) => pos || layer(line), null);
  1128. if (!pos) return null;
  1129. const { top, time } = pos;
  1130. line.layout = {
  1131. type: 'Fix',
  1132. start: {
  1133. x: Math.round(options.resolutionX / 2),
  1134. y: top + line.height,
  1135. time,
  1136. },
  1137. end: {
  1138. time: options.fixDuration + time,
  1139. },
  1140. };
  1141. }
  1142. return line;
  1143. };
  1144. };
  1145.  
  1146. // main layout algorithm
  1147. const layout = async function (danmaku, optionGetter) {
  1148. const options = JSON.parse(JSON.stringify(optionGetter));
  1149. const sorted = danmaku.slice(0).sort(({ time: x }, { time: y }) => x - y);
  1150. const place = placeDanmaku(options);
  1151. const result = Array(sorted.length);
  1152. let length = 0;
  1153. for (let i = 0, l = sorted.length; i < l; i++) {
  1154. let placed = place(sorted[i]);
  1155. if (placed) result[length++] = placed;
  1156. if ((i + 1) % 1000 === 0) {
  1157. await new Promise(resolve => setTimeout(resolve, 0));
  1158. }
  1159. }
  1160. result.length = length;
  1161. result.sort((x, y) => x.layout.start.time - y.layout.start.time);
  1162. return result;
  1163. };
  1164.  
  1165. // escape string for ass
  1166. const textEscape = s => (
  1167. // VSFilter do not support escaped "{" or "}"; we use full-width version instead
  1168. s.replace(/{/g, '{').replace(/}/g, '}').replace(/\s/g, ' ')
  1169. );
  1170.  
  1171. const formatColorChannel = v => (v & 255).toString(16).toUpperCase().padStart(2, '0');
  1172.  
  1173. // format color
  1174. const formatColor = color => '&H' + (
  1175. [color.b, color.g, color.r].map(formatColorChannel).join('')
  1176. );
  1177.  
  1178. // format timestamp
  1179. const formatTimestamp = time => {
  1180. const value = Math.round(time * 100) * 10;
  1181. const rem = value % 3600000;
  1182. const hour = (value - rem) / 3600000;
  1183. const fHour = hour.toFixed(0).padStart(2, '0');
  1184. const fRem = new Date(rem).toISOString().slice(-11, -2);
  1185. return fHour + fRem;
  1186. };
  1187.  
  1188. // test is default color
  1189. const isDefaultColor = ({ r, g, b }) => r === 255 && g === 255 && b === 255;
  1190. // test is dark color
  1191. const isDarkColor = ({ r, g, b }) => r * 0.299 + g * 0.587 + b * 0.114 < 0x30;
  1192.  
  1193. // Ass header
  1194. const header = info => [
  1195. '[Script Info]',
  1196. `Title: ${info.title}`,
  1197. `Original Script: ${info.original}`,
  1198. 'ScriptType: v4.00+',
  1199. 'Collisions: Normal',
  1200. `PlayResX: ${info.playResX}`,
  1201. `PlayResY: ${info.playResY}`,
  1202. 'Timer: 100.0000',
  1203. '',
  1204. '[V4+ Styles]',
  1205. 'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
  1206. `Style: Fix,${info.fontFamily},${info.fontSize},&H${info.alpha}FFFFFF,&H${info.alpha}FFFFFF,&H${info.alpha}000000,&H${info.alpha}000000,${info.bold},0,0,0,100,100,0,0,1,2,0,2,20,20,2,0`,
  1207. `Style: Rtl,${info.fontFamily},${info.fontSize},&H${info.alpha}FFFFFF,&H${info.alpha}FFFFFF,&H${info.alpha}000000,&H${info.alpha}000000,${info.bold},0,0,0,100,100,0,0,1,2,0,2,20,20,2,0`,
  1208. '',
  1209. '[Events]',
  1210. 'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
  1211. ];
  1212.  
  1213. // Set color of text
  1214. const lineColor = ({ color }) => {
  1215. let output = [];
  1216. if (!isDefaultColor(color)) output.push(`\\c${formatColor(color)}`);
  1217. if (isDarkColor(color)) output.push(`\\3c&HFFFFFF`);
  1218. return output.join('');
  1219. };
  1220.  
  1221. // Set fontsize
  1222. let defaultFontSize;
  1223. const lineFontSize = ({ size }) => {
  1224. if (size === defaultFontSize) return '';
  1225. return `\\fs${size}`;
  1226. };
  1227. const getCommonFontSize = list => {
  1228. const count = new Map();
  1229. let commonCount = 0, common = 1;
  1230. list.forEach(({ size }) => {
  1231. let value = 1;
  1232. if (count.has(size)) value = count.get(size) + 1;
  1233. count.set(size, value);
  1234. if (value > commonCount) {
  1235. commonCount = value;
  1236. common = size;
  1237. }
  1238. });
  1239. defaultFontSize = common;
  1240. return common;
  1241. };
  1242.  
  1243. // Add animation of danmaku
  1244. const lineMove = ({ layout: { type, start = null, end = null } }) => {
  1245. if (type === 'Rtl' && start && end) return `\\move(${start.x},${start.y},${end.x},${end.y})`;
  1246. if (type === 'Fix' && start) return `\\pos(${start.x},${start.y})`;
  1247. return '';
  1248. };
  1249.  
  1250. // format one line
  1251. const formatLine = line => {
  1252. const start = formatTimestamp(line.layout.start.time);
  1253. const end = formatTimestamp(line.layout.end.time);
  1254. const type = line.layout.type;
  1255. const color = lineColor(line);
  1256. const fontSize = lineFontSize(line);
  1257. const move = lineMove(line);
  1258. const format = `${color}${fontSize}${move}`;
  1259. const text = textEscape(line.text);
  1260. return `Dialogue: 0,${start},${end},${type},,20,20,2,,{${format}}${text}`;
  1261. };
  1262.  
  1263. const ass = (danmaku, options) => {
  1264. const info = {
  1265. title: danmaku.meta.name,
  1266. original: `Generated by tiansh/ass-danmaku (embedded in liqi0816/bilitwin) based on ${danmaku.meta.url}`,
  1267. playResX: options.resolutionX,
  1268. playResY: options.resolutionY,
  1269. fontFamily: options.fontFamily.split(",")[0],
  1270. fontSize: getCommonFontSize(danmaku.layout),
  1271. alpha: formatColorChannel(0xFF * (100 - options.textOpacity * 100) / 100),
  1272. bold: options.bold? -1 : 0,
  1273. };
  1274. return [
  1275. ...header(info),
  1276. ...danmaku.layout.map(formatLine).filter(x => x),
  1277. ].join('\r\n');
  1278. };
  1279.  
  1280. /**
  1281. * @file Common works for reading / writing optinos
  1282. */
  1283.  
  1284. /**
  1285. * @returns {string}
  1286. */
  1287. const predefFontFamily = () => {
  1288. // const sc = ['Microsoft YaHei', 'PingFang SC', 'Noto Sans CJK SC'];
  1289. // replaced with bilibili defaults
  1290. const sc = ["SimHei", "'Microsoft JhengHei'", "SimSun", "NSimSun", "FangSong", "'Microsoft YaHei'", "'Microsoft Yahei UI Light'", "'Noto Sans CJK SC Bold'", "'Noto Sans CJK SC DemiLight'", "'Noto Sans CJK SC Regular'"];
  1291. const tc = ['Microsoft JhengHei', 'PingFang TC', 'Noto Sans CJK TC'];
  1292. const ja = ['MS PGothic', 'Hiragino Kaku Gothic Pro', 'Noto Sans CJK JP'];
  1293. const lang = navigator.language;
  1294. const fonts = /^ja/.test(lang) ? ja : /^zh(?!.*Hans).*(?:TW|HK|MO)/.test(lang) ? tc : sc;
  1295. const chosed = fonts.find(font$$1 => font.valid(font$$1)) || fonts[0];
  1296. return chosed;
  1297. };
  1298.  
  1299. const attributes = [
  1300. { name: 'resolutionX', type: 'number', min: 480, predef: 560 },
  1301. { name: 'resolutionY', type: 'number', min: 360, predef: 420 },
  1302. { name: 'bottomReserved', type: 'number', min: 0, predef: 60 },
  1303. { name: 'fontFamily', type: 'string', predef: predefFontFamily(), valid: font$$1 => font.valid(font$$1) },
  1304. { name: 'fontSize', type: 'number', min: 0, predef: 1, step: 0.01 },
  1305. { name: 'textSpace', type: 'number', min: 0, predef: 0 },
  1306. { name: 'rtlDuration', type: 'number', min: 0.1, predef: 8, step: 0.1 },
  1307. { name: 'fixDuration', type: 'number', min: 0.1, predef: 4, step: 0.1 },
  1308. { name: 'maxDelay', type: 'number', min: 0, predef: 6, step: 0.1 },
  1309. { name: 'textOpacity', type: 'number', min: 0.1, max: 1, predef: 0.6 },
  1310. { name: 'maxOverlap', type: 'number', min: 1, max: 20, predef: 1 },
  1311. { name: 'bold', type: 'boolean', predef: true },
  1312. ];
  1313.  
  1314. const attrNormalize = (option, { name, type, min = -Infinity, max = Infinity, step = 1, predef, valid }) => {
  1315. let value = option;
  1316. if (type === 'number') value = +value;
  1317. else if (type === 'string') value = '' + value;
  1318. else if (type === 'boolean') value = !!value;
  1319. if (valid && !valid(value)) value = predef;
  1320. if (type === 'number') {
  1321. if (Number.isNaN(value)) value = predef;
  1322. if (value < min) value = min;
  1323. if (value > max) value = max;
  1324. if (name !='textOpacity') value = Math.round((value - min) / step) * step + min;
  1325. }
  1326. return value;
  1327. };
  1328.  
  1329. /**
  1330. * @param {ExtOption} option
  1331. * @returns {ExtOption}
  1332. */
  1333. const normalize = function (option) {
  1334. return Object.assign({},
  1335. ...attributes.map(attr => ({ [attr.name]: attrNormalize(option[attr.name], attr) }))
  1336. );
  1337. };
  1338.  
  1339. /**
  1340. * Convert file content to Blob which describe the file
  1341. * @param {string} content
  1342. * @returns {Blob}
  1343. */
  1344. const convertToBlob = content => {
  1345. const encoder = new TextEncoder();
  1346. // Add a BOM to make some ass parser library happier
  1347. const bom = '\ufeff';
  1348. const encoded = encoder.encode(bom + content);
  1349. const blob = new Blob([encoded], { type: 'application/octet-stream' });
  1350. return blob;
  1351. };
  1352.  
  1353. /***
  1354. * Copyright (C) 2018 Qli5. All Rights Reserved.
  1355. *
  1356. * @author qli5 <goodlq11[at](163|gmail).com>
  1357. *
  1358. * This Source Code Form is subject to the terms of the Mozilla Public
  1359. * License, v. 2.0. If a copy of the MPL was not distributed with this
  1360. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  1361. */
  1362.  
  1363. /**
  1364. * An API wrapper of tiansh/ass-danmaku for liqi0816/bilitwin
  1365. */
  1366. class ASSConverter {
  1367. /**
  1368. * @typedef {ExtOption}
  1369. * @property {number} resolutionX canvas width for drawing danmaku (px)
  1370. * @property {number} resolutionY canvas height for drawing danmaku (px)
  1371. * @property {number} bottomReserved reserved height at bottom for drawing danmaku (px)
  1372. * @property {string} fontFamily danmaku font family
  1373. * @property {number} fontSize danmaku font size (ratio)
  1374. * @property {number} textSpace space between danmaku (px)
  1375. * @property {number} rtlDuration duration of right to left moving danmaku appeared on screen (s)
  1376. * @property {number} fixDuration duration of keep bottom / top danmaku appeared on screen (s)
  1377. * @property {number} maxDelay // maxinum amount of allowed delay (s)
  1378. * @property {number} textOpacity // opacity of text, in range of [0, 1]
  1379. * @property {number} maxOverlap // maxinum layers of danmaku
  1380. */
  1381.  
  1382. /**
  1383. * @param {ExtOption} option tiansh/ass-danmaku compatible option
  1384. */
  1385. constructor(option = {}) {
  1386. this.option = option;
  1387. }
  1388.  
  1389. get option() {
  1390. return this.normalizedOption;
  1391. }
  1392.  
  1393. set option(e) {
  1394. return this.normalizedOption = normalize(e);
  1395. }
  1396.  
  1397. /**
  1398. * @param {Danmaku[]} danmaku use ASSConverter.parseXML
  1399. * @param {string} title
  1400. * @param {string} originalURL
  1401. */
  1402. async genASS(danmaku, title = 'danmaku', originalURL = 'anonymous xml') {
  1403. const layout$$1 = await layout(danmaku, this.option);
  1404. const ass$$1 = ass({
  1405. content: danmaku,
  1406. layout: layout$$1,
  1407. meta: {
  1408. name: title,
  1409. url: originalURL
  1410. }
  1411. }, this.option);
  1412. return ass$$1;
  1413. }
  1414.  
  1415. async genASSBlob(danmaku, title = 'danmaku', originalURL = 'anonymous xml') {
  1416. return convertToBlob(await this.genASS(danmaku, title, originalURL));
  1417. }
  1418.  
  1419. /**
  1420. * @typedef DanmakuColor
  1421. * @property {number} r
  1422. * @property {number} g
  1423. * @property {number} b
  1424. */
  1425.  
  1426. /**
  1427. * @typedef Danmaku
  1428. * @property {string} text
  1429. * @property {number} time
  1430. * @property {string} mode
  1431. * @property {number} size
  1432. * @property {DanmakuColor} color
  1433. * @property {boolean} bottom
  1434. */
  1435.  
  1436. /**
  1437. * @param {string} xml bilibili danmaku xml
  1438. * @returns {Danmaku[]}
  1439. */
  1440. static parseXML(xml) {
  1441. return parser.bilibili(xml).danmaku;
  1442. }
  1443.  
  1444.  
  1445. static _UNIT_TEST() {
  1446. const e = new ASSConverter();
  1447. const xml = `<?xml version="1.0" encoding="UTF-8"?><i><chatserver>chat.bilibili.com</chatserver><chatid>32873758</chatid><mission>0</mission><maxlimit>6000</maxlimit><state>0</state><realname>0</realname><source>k-v</source><d p="0.00000,1,25,16777215,1519733589,0,d286a97b,4349604072">真第一</d><d p="7.29900,1,25,16777215,1519733812,0,3548796c,4349615908">五分钟前</d><d p="587.05100,1,25,16777215,1519734291,0,f2ed792f,4349641325">惊呆了!</d><d p="136.82200,1,25,16777215,1519734458,0,1e5784f,4349652071">神王代表虚空</d><d p="0.00000,1,25,16777215,1519736251,0,f16cbf44,4349751461">66666666666666666</d><d p="590.60400,1,25,16777215,1519736265,0,fbb3d1b3,4349752331">这要吹多长时间</d><d p="537.15500,1,25,16777215,1519736280,0,1e5784f,4349753170">反而不是,疾病是个恶魔,别人说她伪装成了精灵</d><d p="872.08200,1,25,16777215,1519736881,0,1e5784f,4349787709">精灵都会吃</d><d p="2648.42500,1,25,16777215,1519737840,0,e9e6b2b4,4349844463">就不能大部分都是铜币么?</d><d p="2115.09400,1,25,16777215,1519738271,0,3548796c,4349870808">吓死我了。。。</d><d p="11.45400,1,25,16777215,1519739974,0,9937b428,4349974512">???</d><d p="1285.73900,1,25,16777215,1519748274,0,3bb4c9ee,4350512859">儿砸</d><d p="595.48600,1,25,16777215,1519757148,0,f3ed26b6,4350787048">怕是要吹到缺氧哦</d><d p="1206.31500,1,25,16777215,1519767204,0,62a9186a,4350882680">233333333333333</d><d p="638.68700,1,25,16777215,1519769219,0,de0a99ae,4350893310">菜鸡的借口</d><d p="655.76500,1,25,16777215,1519769236,0,de0a99ae,4350893397">竟然吹蜡烛打医生</d><d p="2235.89600,1,25,16777215,1519769418,0,de0a99ae,4350894325">这暴击率太高了</d><d p="389.88700,1,25,16777215,1519780435,0,8879732c,4351021740">医生好想进10万,血,上万甲</d><d p="2322.47900,1,25,16777215,1519780901,0,e509a801,4351032321">前一个命都没了</d><d p="2408.93600,1,25,16777215,1519801350,0,1a692eb6,4351826484">23333333333333</d><d p="1290.62000,1,25,16777215,1519809649,0,af8f12dc,4352159267">儿砸~</d><d p="917.96300,1,25,16777215,1519816770,0,fef64b6a,4352474878">应该姆西自己控制洛斯 七杀点太快了差评</d><d p="2328.03100,1,25,16777215,1519825291,0,8549205d,4352919003">现在前一个连命都没了啊喂</d><d p="1246.16700,1,25,16777215,1519827514,0,fef64b6a,4353052309">不如走到面前用扫射 基本全中 伤害爆表</d><d p="592.38100,1,25,16777215,1519912489,0,edc3f0a9,4355960085">这是这个游戏最震撼的几幕之一</d></i>`;
  1448. console.log(window.ass = e.genASSBlob(ASSConverter.parseXML(xml)));
  1449. }
  1450. }
  1451.  
  1452. /***
  1453. * Copyright (C) 2018 Qli5. All Rights Reserved.
  1454. *
  1455. * @author qli5 <goodlq11[at](163|gmail).com>
  1456. *
  1457. * This Source Code Form is subject to the terms of the Mozilla Public
  1458. * License, v. 2.0. If a copy of the MPL was not distributed with this
  1459. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  1460. */
  1461.  
  1462. /**
  1463. * A util to hook a function
  1464. */
  1465. class HookedFunction extends Function {
  1466. constructor(...init) {
  1467. // 1. init parameter
  1468. const { raw, pre, post } = HookedFunction.parseParameter(...init);
  1469.  
  1470. // 2. build bundle
  1471. const self = function (...args) {
  1472. const { raw, pre, post } = self;
  1473. const context = { args, target: raw, ret: undefined, hook: self };
  1474. pre.forEach(e => e.call(this, context));
  1475. if (context.target) context.ret = context.target.apply(this, context.args);
  1476. post.forEach(e => e.call(this, context));
  1477. return context.ret;
  1478. };
  1479. Object.setPrototypeOf(self, HookedFunction.prototype);
  1480. self.raw = raw;
  1481. self.pre = pre;
  1482. self.post = post;
  1483.  
  1484. // 3. cheat babel - it complains about missing super(), even if it is actual valid
  1485. try {
  1486. return self;
  1487. } catch (e) {
  1488. super();
  1489. return self;
  1490. }
  1491. }
  1492.  
  1493. addPre(...func) {
  1494. this.pre.push(...func);
  1495. }
  1496.  
  1497. addPost(...func) {
  1498. this.post.push(...func);
  1499. }
  1500.  
  1501. addCallback(...func) {
  1502. this.addPost(...func);
  1503. }
  1504.  
  1505. removePre(func) {
  1506. this.pre = this.pre.filter(e => e != func);
  1507. }
  1508.  
  1509. removePost(func) {
  1510. this.post = this.post.filter(e => e != func);
  1511. }
  1512.  
  1513. removeCallback(func) {
  1514. this.removePost(func);
  1515. }
  1516.  
  1517. static parseParameter(...init) {
  1518. // 1. clone init
  1519. init = init.slice();
  1520.  
  1521. // 2. default
  1522. let raw = null;
  1523. let pre = [];
  1524. let post = [];
  1525.  
  1526. // 3. (raw, ...others)
  1527. if (typeof init[0] === 'function') raw = init.shift();
  1528.  
  1529. // 4. iterate through parameters
  1530. for (const e of init) {
  1531. if (!e) {
  1532. continue;
  1533. }
  1534. else if (Array.isArray(e)) {
  1535. pre = post;
  1536. post = e;
  1537. }
  1538. else if (typeof e == 'object') {
  1539. if (typeof e.raw == 'function') raw = e.raw;
  1540. if (typeof e.pre == 'function') pre.push(e.pre);
  1541. if (typeof e.post == 'function') post.push(e.post);
  1542. if (Array.isArray(e.pre)) pre = e.pre;
  1543. if (Array.isArray(e.post)) post = e.post;
  1544. }
  1545. else if (typeof e == 'function') {
  1546. post.push(e);
  1547. }
  1548. else {
  1549. throw new TypeError(`HookedFunction: cannot recognize paramter ${e} of type ${typeof e}`);
  1550. }
  1551. }
  1552. return { raw, pre, post };
  1553. }
  1554.  
  1555. static hook(...init) {
  1556. // 1. init parameter
  1557. const { raw, pre, post } = HookedFunction.parseParameter(...init);
  1558.  
  1559. // 2 wrap
  1560. // 2.1 already wrapped => concat
  1561. if (raw instanceof HookedFunction) {
  1562. raw.pre.push(...pre);
  1563. raw.post.push(...post);
  1564. return raw;
  1565. }
  1566.  
  1567. // 2.2 otherwise => new
  1568. else {
  1569. return new HookedFunction({ raw, pre, post });
  1570. }
  1571. }
  1572.  
  1573. static hookDebugger(raw, pre = true, post = false) {
  1574. // 1. init hook
  1575. if (!HookedFunction.hookDebugger.hook) HookedFunction.hookDebugger.hook = function (ctx) { debugger };
  1576.  
  1577. // 2 wrap
  1578. // 2.1 already wrapped => concat
  1579. if (raw instanceof HookedFunction) {
  1580. if (pre && !raw.pre.includes(HookedFunction.hookDebugger.hook)) {
  1581. raw.pre.push(HookedFunction.hookDebugger.hook);
  1582. }
  1583. if (post && !raw.post.includes(HookedFunction.hookDebugger.hook)) {
  1584. raw.post.push(HookedFunction.hookDebugger.hook);
  1585. }
  1586. return raw;
  1587. }
  1588.  
  1589. // 2.2 otherwise => new
  1590. else {
  1591. return new HookedFunction({
  1592. raw,
  1593. pre: pre && HookedFunction.hookDebugger.hook || undefined,
  1594. post: post && HookedFunction.hookDebugger.hook || undefined,
  1595. });
  1596. }
  1597. }
  1598. }
  1599.  
  1600. /***
  1601. * BiliMonkey
  1602. * A bilibili user script
  1603. * Copyright (C) 2018 Qli5. All Rights Reserved.
  1604. *
  1605. * @author qli5 <goodlq11[at](163|gmail).com>
  1606. *
  1607. * This Source Code Form is subject to the terms of the Mozilla Public
  1608. * License, v. 2.0. If a copy of the MPL was not distributed with this
  1609. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  1610. *
  1611. * The FLV merge utility is a Javascript translation of
  1612. * https://github.com/grepmusic/flvmerge
  1613. * by grepmusic
  1614. *
  1615. * The ASS convert utility is a fork of
  1616. * https://github.com/tiansh/ass-danmaku
  1617. * by tiansh
  1618. *
  1619. * The FLV demuxer is from
  1620. * https://github.com/Bilibili/flv.js/
  1621. * by zheng qian
  1622. *
  1623. * The EMBL builder is from
  1624. * <https://www.npmjs.com/package/simple-ebml-builder>
  1625. * by ryiwamoto
  1626. */
  1627.  
  1628. class BiliMonkey {
  1629. constructor(playerWin, option = BiliMonkey.optionDefaults) {
  1630. this.playerWin = playerWin;
  1631. this.protocol = playerWin.location.protocol;
  1632. this.cid = null;
  1633. this.flvs = null;
  1634. this.mp4 = null;
  1635. this.ass = null;
  1636. this.flvFormatName = null;
  1637. this.mp4FormatName = null;
  1638. this.fallbackFormatName = null;
  1639. this.cidAsyncContainer = new AsyncContainer();
  1640. this.cidAsyncContainer.then(cid => { this.cid = cid; this.ass = this.getASS(); });
  1641. if (typeof top.cid === 'string') this.cidAsyncContainer.resolve(top.cid);
  1642.  
  1643. /***
  1644. * cache + proxy = Service Worker
  1645. * Hope bilibili will have a SW as soon as possible.
  1646. * partial = Stream
  1647. * Hope the fetch API will be stabilized as soon as possible.
  1648. * If you are using your grandpa's browser, do not enable these functions.
  1649. */
  1650. this.cache = option.cache;
  1651. this.partial = option.partial;
  1652. this.proxy = option.proxy;
  1653. this.blocker = option.blocker;
  1654. this.font = option.font;
  1655. this.option = option;
  1656. if (this.cache && (!(this.cache instanceof CacheDB))) this.cache = new CacheDB('biliMonkey', 'flv', 'name');
  1657.  
  1658. this.flvsDetailedFetch = [];
  1659. this.flvsBlob = [];
  1660.  
  1661. this.defaultFormatPromise = null;
  1662. this.queryInfoMutex = new Mutex();
  1663.  
  1664. this.destroy = new HookedFunction();
  1665. }
  1666.  
  1667. /***
  1668. * Guide: for ease of debug, please use format name(flv720) instead of format value(64) unless necessary
  1669. * Guide: for ease of html concat, please use string format value('64') instead of number(parseInt('64'))
  1670. */
  1671. lockFormat(format) {
  1672. // null => uninitialized
  1673. // async pending => another one is working on it
  1674. // async resolve => that guy just finished work
  1675. // sync value => someone already finished work
  1676. const toast = this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-top')[0];
  1677. if (toast) toast.style.visibility = 'hidden';
  1678. if (format == this.fallbackFormatName) return null;
  1679. switch (format) {
  1680. // Single writer is not a must.
  1681. // Plus, if one writer fail, others should be able to overwrite its garbage.
  1682. case 'flv_p60':
  1683. case 'flv720_p60':
  1684. case 'hdflv2':
  1685. case 'flv':
  1686. case 'flv720':
  1687. case 'flv480':
  1688. case 'flv360':
  1689. //if (this.flvs) return this.flvs;
  1690. return this.flvs = new AsyncContainer();
  1691. case 'hdmp4':
  1692. case 'mp4':
  1693. //if (this.mp4) return this.mp4;
  1694. return this.mp4 = new AsyncContainer();
  1695. default:
  1696. throw `lockFormat error: ${format} is a unrecognizable format`;
  1697. }
  1698. }
  1699.  
  1700. resolveFormat(res, shouldBe) {
  1701. const toast = this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-top')[0];
  1702. if (toast) {
  1703. toast.style.visibility = '';
  1704. if (toast.children.length) toast.children[0].style.visibility = 'hidden';
  1705. const video = this.playerWin.document.getElementsByTagName('video')[0];
  1706. if (video) {
  1707. const h = () => {
  1708. if (toast.children.length) toast.children[0].style.visibility = 'hidden';
  1709. };
  1710. video.addEventListener('emptied', h, { once: true });
  1711. setTimeout(() => video.removeEventListener('emptied', h), 500);
  1712. }
  1713.  
  1714. }
  1715. if (res.format == this.fallbackFormatName) return null;
  1716. switch (res.format) {
  1717. case 'flv_p60':
  1718. case 'flv720_p60':
  1719. case 'hdflv2':
  1720. case 'flv':
  1721. case 'flv720':
  1722. case 'flv480':
  1723. case 'flv360':
  1724. if (shouldBe && shouldBe != res.format) {
  1725. this.flvs = null;
  1726. throw `URL interface error: response is not ${shouldBe}`;
  1727. }
  1728. return this.flvs = this.flvs.resolve(res.durl.map(e => e.url.replace('http:', this.protocol)));
  1729. case 'hdmp4':
  1730. case 'mp4':
  1731. if (shouldBe && shouldBe != res.format) {
  1732. this.mp4 = null;
  1733. throw `URL interface error: response is not ${shouldBe}`;
  1734. }
  1735. return this.mp4 = this.mp4.resolve(res.durl[0].url.replace('http:', this.protocol));
  1736. default:
  1737. throw `resolveFormat error: ${res.format} is a unrecognizable format`;
  1738. }
  1739. }
  1740.  
  1741. getVIPStatus() {
  1742. const data = this.playerWin.sessionStorage.getItem('bili_login_status');
  1743. try {
  1744. return JSON.parse(data).some(e => e instanceof Object && e.vipStatus);
  1745. }
  1746. catch (e) {
  1747. return false;
  1748. }
  1749. }
  1750.  
  1751. async getASS(clickableFormat) {
  1752. if (this.ass) return this.ass;
  1753. this.ass = new Promise(async resolve => {
  1754. // 1. cid
  1755. if (!this.cid) this.cid = this.playerWin.cid;
  1756.  
  1757. // 2. options
  1758. const bilibili_player_settings = this.playerWin.localStorage.bilibili_player_settings && JSON.parse(this.playerWin.localStorage.bilibili_player_settings);
  1759.  
  1760. // 2.1 blocker
  1761. let danmaku = await BiliMonkey.fetchDanmaku(this.cid);
  1762. if (bilibili_player_settings && this.blocker) {
  1763. const i = bilibili_player_settings.block.list.map(e => e.v).join('|');
  1764. if (i) {
  1765. const regexp = new RegExp(i);
  1766. danmaku = danmaku.filter(e => !regexp.test(e.text));
  1767. }
  1768. }
  1769.  
  1770. // 2.2 font
  1771. const option = bilibili_player_settings && this.font && {
  1772. 'fontFamily': bilibili_player_settings.setting_config['fontfamily'] != 'custom' ? bilibili_player_settings.setting_config['fontfamily'].split(/, ?/) : bilibili_player_settings.setting_config['fontfamilycustom'].split(/, ?/),
  1773. 'fontSize': parseFloat(bilibili_player_settings.setting_config['fontsize']),
  1774. 'textOpacity': parseFloat(bilibili_player_settings.setting_config['opacity']),
  1775. 'bold': bilibili_player_settings.setting_config['bold'] ? 1 : 0,
  1776. } || undefined;
  1777.  
  1778. // 2.3 resolution
  1779. if (this.option.resolution) {
  1780. Object.assign(option, {
  1781. 'resolutionX': +this.option.resolutionX || 560,
  1782. 'resolutionY': +this.option.resolutionY || 420
  1783. });
  1784. }
  1785.  
  1786. // 3. generate
  1787. resolve(this.ass = top.URL.createObjectURL(await new ASSConverter(option).genASSBlob(
  1788. danmaku, top.document.title, top.location.href
  1789. )));
  1790. });
  1791. return this.ass;
  1792. }
  1793.  
  1794. async queryInfo(format) {
  1795. return this.queryInfoMutex.lockAndAwait(async () => {
  1796. switch (format) {
  1797. case 'video':
  1798. if (this.flvs)
  1799. return this.video_format;
  1800.  
  1801. const qn = (this.option.enableVideoMaxResolution && this.option.videoMaxResolution) || "116";
  1802. const api_url = `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&otype=json&qn=${qn}`;
  1803.  
  1804. let re = await fetch(api_url, { credentials: 'include' });
  1805.  
  1806. let data = (await re.json()).data;
  1807. // console.log(data)
  1808. let durls = data.durl;
  1809.  
  1810. if (!durls) {
  1811. const _zc = window.Gc || window.zc ||
  1812. Object.values(window).filter(
  1813. x => typeof x == "string" && x.includes("[Info]")
  1814. )[0];
  1815.  
  1816. data = JSON.parse(
  1817. _zc.split("\n").filter(
  1818. x => x.startsWith("{")
  1819. )[0]
  1820. );
  1821.  
  1822. const _data_X = data.Y || data.X ||
  1823. Object.values(data).filter(
  1824. x => typeof x == "object" && Object.prototype.toString.call(x) == "[object Object]"
  1825. )[0];
  1826.  
  1827. durls = _data_X.segments || [_data_X];
  1828. }
  1829.  
  1830. // console.log(data)
  1831.  
  1832. let flvs = durls.map(url_obj => url_obj.url.replace("http://", "https://"));
  1833.  
  1834. this.flvs = flvs;
  1835.  
  1836. let video_format = data.format && (data.format.match(/mp4|flv/) || [])[0];
  1837.  
  1838. this.video_format = video_format;
  1839.  
  1840. return video_format
  1841. case 'ass':
  1842. if (this.ass)
  1843. return this.ass;
  1844. else
  1845. return this.getASS(this.flvFormatName);
  1846. default:
  1847. throw `Bilimonkey: What is format ${format}?`;
  1848. }
  1849. });
  1850. }
  1851.  
  1852. hangPlayer() {
  1853. this.playerWin.document.getElementsByTagName('video')[0].src = "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAsxtZGF0AAACrgYF//+q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE0OCByMjY0MyA1YzY1NzA0IC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxNSAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTEgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAADmWIhABf/qcv4FM6/0nHAAAC7G1vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAAAAoAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAIWdHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAAAAoAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAQAAAAEAAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAAKAAAAAAAAQAAAAABjm1kaWEAAAAgbWRoZAAAAAAAAAAAAAAAAAAAMgAAAAIAFccAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAATltaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAD5c3RibAAAAJVzdHNkAAAAAAAAAAEAAACFYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAQABAASAAAAEgAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABj//wAAAC9hdmNDAWQACv/hABZnZAAKrNlehAAAAwAEAAADAMg8SJZYAQAGaOvjyyLAAAAAGHN0dHMAAAAAAAAAAQAAAAEAAAIAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAABAAAAAQAAABRzdHN6AAAAAAAAAsQAAAABAAAAFHN0Y28AAAAAAAAAAQAAADAAAABidWR0YQAAAFptZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAAC1pbHN0AAAAJal0b28AAAAdZGF0YQAAAAEAAAAATGF2ZjU2LjQwLjEwMQ==";
  1854. }
  1855.  
  1856. async loadFLVFromCache(index) {
  1857. if (!this.cache) return;
  1858. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1859. let name = this.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1860. let item = await this.cache.getData(name);
  1861. if (!item) return;
  1862. return this.flvsBlob[index] = item.data;
  1863. }
  1864.  
  1865. async loadPartialFLVFromCache(index) {
  1866. if (!this.cache) return;
  1867. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1868. let name = this.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1869. name = 'PC_' + name;
  1870. let item = await this.cache.getData(name);
  1871. if (!item) return;
  1872. return item.data;
  1873. }
  1874.  
  1875. async loadAllFLVFromCache() {
  1876. if (!this.cache) return;
  1877. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1878.  
  1879. let promises = [];
  1880. for (let i = 0; i < this.flvs.length; i++) promises.push(this.loadFLVFromCache(i));
  1881.  
  1882. return Promise.all(promises);
  1883. }
  1884.  
  1885. async saveFLVToCache(index, blob) {
  1886. if (!this.cache) return;
  1887. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1888. let name = this.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1889. return this.cache.addData({ name, data: blob });
  1890. }
  1891.  
  1892. async savePartialFLVToCache(index, blob) {
  1893. if (!this.cache) return;
  1894. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1895. let name = this.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1896. name = 'PC_' + name;
  1897. return this.cache.putData({ name, data: blob });
  1898. }
  1899.  
  1900. async cleanPartialFLVInCache(index) {
  1901. if (!this.cache) return;
  1902. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1903. let name = this.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1904. name = 'PC_' + name;
  1905. return this.cache.deleteData(name);
  1906. }
  1907.  
  1908. async getFLV(index, progressHandler) {
  1909. if (this.flvsBlob[index]) return this.flvsBlob[index];
  1910.  
  1911. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1912. this.flvsBlob[index] = (async () => {
  1913. let cache = await this.loadFLVFromCache(index);
  1914. if (cache) return this.flvsBlob[index] = cache;
  1915. let partialFLVFromCache = await this.loadPartialFLVFromCache(index);
  1916.  
  1917. let burl = this.flvs[index];
  1918. if (partialFLVFromCache) burl += `&bstart=${partialFLVFromCache.size}`;
  1919. let opt = {
  1920. fetch: this.playerWin.fetch,
  1921. method: 'GET',
  1922. mode: 'cors',
  1923. cache: 'default',
  1924. referrerPolicy: 'no-referrer-when-downgrade',
  1925. cacheLoaded: partialFLVFromCache ? partialFLVFromCache.size : 0,
  1926. headers: partialFLVFromCache && (!burl.includes('wsTime')) ? { Range: `bytes=${partialFLVFromCache.size}-` } : undefined
  1927. };
  1928. opt.onprogress = progressHandler;
  1929. opt.onerror = opt.onabort = ({ target, type }) => {
  1930. let partialFLV = target.getPartialBlob();
  1931. if (partialFLVFromCache) partialFLV = new Blob([partialFLVFromCache, partialFLV]);
  1932. this.savePartialFLVToCache(index, partialFLV);
  1933. };
  1934.  
  1935. let fch = new DetailedFetchBlob(burl, opt);
  1936. this.flvsDetailedFetch[index] = fch;
  1937. let fullFLV = await fch.getBlob();
  1938. this.flvsDetailedFetch[index] = undefined;
  1939. if (partialFLVFromCache) {
  1940. fullFLV = new Blob([partialFLVFromCache, fullFLV]);
  1941. this.cleanPartialFLVInCache(index);
  1942. }
  1943. this.saveFLVToCache(index, fullFLV);
  1944. return (this.flvsBlob[index] = fullFLV);
  1945. })();
  1946. return this.flvsBlob[index];
  1947. }
  1948.  
  1949. async abortFLV(index) {
  1950. if (this.flvsDetailedFetch[index]) return this.flvsDetailedFetch[index].abort();
  1951. }
  1952.  
  1953. async getAllFLVs(progressHandler) {
  1954. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1955. let promises = [];
  1956. for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLV(i, progressHandler));
  1957. return Promise.all(promises);
  1958. }
  1959.  
  1960. async cleanAllFLVsInCache() {
  1961. if (!this.cache) return;
  1962. if (!this.flvs) throw 'BiliMonkey: info uninitialized';
  1963.  
  1964. let ret = [];
  1965. for (let flv of this.flvs) {
  1966. let name = flv.match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  1967. ret.push(await this.cache.deleteData(name));
  1968. ret.push(await this.cache.deleteData('PC_' + name));
  1969. }
  1970.  
  1971. return ret;
  1972. }
  1973.  
  1974. async setupProxy(res, onsuccess) {
  1975. if (!this.setupProxy._fetch) {
  1976. const _fetch = this.setupProxy._fetch = this.playerWin.fetch;
  1977. this.playerWin.fetch = function (input, init) {
  1978. if (!input.slice || input.slice(0, 5) != 'blob:') {
  1979. return _fetch(input, init);
  1980. }
  1981. let bstart = input.indexOf('?bstart=');
  1982. if (bstart < 0) {
  1983. return _fetch(input, init);
  1984. }
  1985. if (!init.headers instanceof Headers) init.headers = new Headers(init.headers || {});
  1986. init.headers.set('Range', `bytes=${input.slice(bstart + 8)}-`);
  1987. return _fetch(input.slice(0, bstart), init)
  1988. };
  1989. this.destroy.addCallback(() => this.playerWin.fetch = _fetch);
  1990. }
  1991.  
  1992. await this.loadAllFLVFromCache();
  1993. let resProxy = Object.assign({}, res);
  1994. for (let i = 0; i < this.flvsBlob.length; i++) {
  1995. if (this.flvsBlob[i]) resProxy.durl[i].url = this.playerWin.URL.createObjectURL(this.flvsBlob[i]);
  1996. }
  1997. return onsuccess(resProxy);
  1998. }
  1999.  
  2000. static async fetchDanmaku(cid) {
  2001. return ASSConverter.parseXML(
  2002. await new Promise((resolve, reject) => {
  2003. const e = new XMLHttpRequest();
  2004. e.onload = () => resolve(e.responseText);
  2005. e.onerror = reject;
  2006. e.open('get', `https://comment.bilibili.com/${cid}.xml`);
  2007. e.send();
  2008. })
  2009. );
  2010. }
  2011.  
  2012. static async getAllPageDefaultFormats(playerWin = top, monkey) {
  2013. // bilibili has a misconfigured lazy loading => keep trying
  2014. /** @type {{cid: number; part?: string; index?: string; }[]} */
  2015. const list = await new Promise(resolve => {
  2016. const i = setInterval(() => {
  2017. const ret = playerWin.player.getPlaylist();
  2018. if (ret) {
  2019. clearInterval(i);
  2020. resolve(ret);
  2021. }
  2022. }, 500);
  2023. });
  2024.  
  2025. const queryInfoMutex = new Mutex();
  2026.  
  2027. // from the first page
  2028. playerWin.player.next(1);
  2029.  
  2030. const retPromises = list.map((x, n) => (async () => {
  2031. await queryInfoMutex.lock();
  2032.  
  2033. const cid = x.cid;
  2034. const danmuku = await new ASSConverter().genASSBlob(
  2035. await BiliMonkey.fetchDanmaku(cid), top.document.title, top.location.href
  2036. );
  2037.  
  2038. const qn = (monkey.option.enableVideoMaxResolution && monkey.option.videoMaxResolution) || "116";
  2039. const api_url = `https://api.bilibili.com/x/player/playurl?avid=${aid}&cid=${cid}&otype=json&qn=${qn}`;
  2040. const r = await fetch(api_url, { credentials: 'include' });
  2041. const res = (await r.json()).data;
  2042.  
  2043. if (!res.durl) {
  2044. const _getDataList = () => {
  2045. const _zc = playerWin.Gc || playerWin.zc ||
  2046. Object.values(playerWin).filter(
  2047. x => typeof x == "string" && x.includes("[Info]")
  2048. )[0];
  2049. return _zc.split("\n").filter(
  2050. x => x.startsWith("{")
  2051. )
  2052. };
  2053.  
  2054. await new Promise(resolve => {
  2055. const i = setInterval(() => {
  2056. const dataSize = new Set(
  2057. _getDataList()
  2058. ).size;
  2059.  
  2060. if (list.length == 1 || dataSize == n + 2) {
  2061. clearInterval(i);
  2062. resolve();
  2063. }
  2064. }, 100);
  2065. });
  2066.  
  2067. const data = JSON.parse(
  2068. _getDataList().pop()
  2069. );
  2070.  
  2071. const _data_X = data.Y || data.X ||
  2072. Object.values(data).filter(
  2073. x => typeof x == "object" && Object.prototype.toString.call(x) == "[object Object]"
  2074. )[0];
  2075.  
  2076. res.durl = _data_X.segments || [_data_X];
  2077. }
  2078.  
  2079. queryInfoMutex.unlock();
  2080. playerWin.player.next();
  2081.  
  2082. return ({
  2083. durl: res.durl.map(({ url }) => url.replace('http:', playerWin.location.protocol)),
  2084. danmuku,
  2085. name: x.part || x.index || playerWin.document.title.replace("_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili", ""),
  2086. outputName: res.durl[0].url.match(/\d+-\d+(?:\d|-|hd)*(?=\.flv)/) ?
  2087. /***
  2088. * see #28
  2089. * Firefox lookbehind assertion not implemented https://bugzilla.mozilla.org/show_bug.cgi?id=1225665
  2090. * try replace /-\d+(?=(?:\d|-|hd)*\.flv)/ => /(?<=\d+)-\d+(?=(?:\d|-|hd)*\.flv)/ in the future
  2091. */
  2092. res.durl[0].url.match(/\d+-\d+(?:\d|-|hd)*(?=\.flv)/)[0].replace(/-\d+(?=(?:\d|-|hd)*\.flv)/, '')
  2093. : res.durl[0].url.match(/\d(?:\d|-|hd)*(?=\.mp4)/) ?
  2094. res.durl[0].url.match(/\d(?:\d|-|hd)*(?=\.mp4)/)[0]
  2095. : cid,
  2096. cid,
  2097. res,
  2098. });
  2099. })());
  2100.  
  2101. const ret = await Promise.all(retPromises);
  2102.  
  2103. return ret;
  2104. }
  2105.  
  2106. static async getBiliShortVideoInfo() {
  2107. const video_id = location.pathname.match(/\/video\/(\d+)/)[1];
  2108. const api_url = `https://api.vc.bilibili.com/clip/v1/video/detail?video_id=${video_id}&need_playurl=1`;
  2109.  
  2110. const req = await fetch(api_url, { credentials: 'include' });
  2111. const data = (await req.json()).data;
  2112. const { video_playurl, first_pic: cover_img } = data.item;
  2113.  
  2114. return { video_playurl: video_playurl.replace("http://", "https://"), cover_img }
  2115. }
  2116.  
  2117. static formatToValue(format) {
  2118. if (format == 'does_not_exist') throw `formatToValue: cannot lookup does_not_exist`;
  2119. if (typeof BiliMonkey.formatToValue.dict == 'undefined') BiliMonkey.formatToValue.dict = {
  2120. 'flv_p60': '116',
  2121. 'flv720_p60': '74',
  2122. 'flv': '80',
  2123. 'flv720': '64',
  2124. 'flv480': '32',
  2125. 'flv360': '15',
  2126.  
  2127. // legacy - late 2017
  2128. 'hdflv2': '112',
  2129. 'hdmp4': '64', // data-value is still '64' instead of '48'. '48',
  2130. 'mp4': '16',
  2131. };
  2132. return BiliMonkey.formatToValue.dict[format] || null;
  2133. }
  2134.  
  2135. static valueToFormat(value) {
  2136. if (typeof BiliMonkey.valueToFormat.dict == 'undefined') BiliMonkey.valueToFormat.dict = {
  2137. '116': 'flv_p60',
  2138. '74': 'flv720_p60',
  2139. '80': 'flv',
  2140. '64': 'flv720',
  2141. '32': 'flv480',
  2142. '15': 'flv360',
  2143.  
  2144. // legacy - late 2017
  2145. '112': 'hdflv2',
  2146. '48': 'hdmp4',
  2147. '16': 'mp4',
  2148.  
  2149. // legacy - early 2017
  2150. '3': 'flv',
  2151. '2': 'hdmp4',
  2152. '1': 'mp4',
  2153. };
  2154. return BiliMonkey.valueToFormat.dict[value] || null;
  2155. }
  2156.  
  2157. static get optionDescriptions() {
  2158. return [
  2159. // 1. cache
  2160. ['cache', '关标签页不清缓存:保留完全下载好的分段到缓存,忘记另存为也没关系。'],
  2161. ['partial', '断点续传:点击“取消”保留部分下载的分段到缓存,忘记点击会弹窗确认。'],
  2162. ['proxy', '用缓存加速播放器:如果缓存里有完全下载好的分段,直接喂给网页播放器,不重新访问网络。小水管利器,播放只需500k流量。如果实在搞不清怎么播放ASS弹幕,也可以就这样用。'],
  2163.  
  2164. // 2. customizing
  2165. ['blocker', '弹幕过滤:在网页播放器里设置的屏蔽词也对下载的弹幕生效。'],
  2166. ['font', '自定义字体:在网页播放器里设置的字体、大小、加粗、透明度也对下载的弹幕生效。'],
  2167. ['resolution', '(测)自定义弹幕画布分辨率:仅对下载的弹幕生效。(默认值: 560 x 420)'],
  2168. ];
  2169. }
  2170.  
  2171. static get resolutionPreferenceOptions() {
  2172. return [
  2173. ['高清 1080P60 (大会员)', '116'],
  2174. ['高清 1080P+ (大会员)', '112'],
  2175. ['高清 720P60 (大会员)', '74'],
  2176. ['高清 1080P', '80'],
  2177. ['高清 720P', '64'],
  2178. ['清晰 480P', '32'],
  2179. ['流畅 360P', '16'],
  2180. ]
  2181. }
  2182.  
  2183. static get optionDefaults() {
  2184. return {
  2185. // 1. automation
  2186. autoDefault: true,
  2187. autoFLV: false,
  2188. autoMP4: false,
  2189.  
  2190. // 2. cache
  2191. cache: true,
  2192. partial: true,
  2193. proxy: true,
  2194.  
  2195. // 3. customizing
  2196. blocker: true,
  2197. font: true,
  2198. resolution: false,
  2199. resolutionX: 560,
  2200. resolutionY: 420,
  2201. videoMaxResolution: "116",
  2202. enableVideoMaxResolution: false,
  2203. }
  2204. }
  2205.  
  2206. static _UNIT_TEST() {
  2207. return (async () => {
  2208. let playerWin = await BiliUserJS.getPlayerWin();
  2209. window.m = new BiliMonkey(playerWin);
  2210.  
  2211. console.warn('data race test');
  2212. m.queryInfo('video');
  2213. console.log(m.queryInfo('video'));
  2214.  
  2215. //location.reload();
  2216. })();
  2217. }
  2218. }
  2219.  
  2220. /***
  2221. * BiliPolyfill
  2222. * A bilibili user script
  2223. * Copyright (C) 2018 Qli5. All Rights Reserved.
  2224. *
  2225. * @author qli5 <goodlq11[at](163|gmail).com>
  2226. *
  2227. * This Source Code Form is subject to the terms of the Mozilla Public
  2228. * License, v. 2.0. If a copy of the MPL was not distributed with this
  2229. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  2230. */
  2231.  
  2232. class BiliPolyfill {
  2233. /***
  2234. * Assumption: aid, cid, pageno does not change during lifecycle
  2235. * Create a new BiliPolyfill if assumption breaks
  2236. */
  2237. constructor(playerWin, option = BiliPolyfill.optionDefaults, hintInfo = () => { }) {
  2238. this.playerWin = playerWin;
  2239. this.option = option;
  2240. this.hintInfo = hintInfo;
  2241.  
  2242. this.video = null;
  2243.  
  2244. this.series = [];
  2245. this.userdata = { oped: {}, restore: {} };
  2246.  
  2247. this.destroy = new HookedFunction();
  2248. this.playerWin.addEventListener('beforeunload', this.destroy);
  2249. this.destroy.addCallback(() => this.playerWin.removeEventListener('beforeunload', this.destroy));
  2250. }
  2251.  
  2252. saveUserdata() {
  2253. this.option.setStorage('biliPolyfill', JSON.stringify(this.userdata));
  2254. }
  2255.  
  2256. retrieveUserdata() {
  2257. try {
  2258. this.userdata = this.option.getStorage('biliPolyfill');
  2259. if (this.userdata.length > 1073741824) top.alert('BiliPolyfill脚本数据已经快满了,在播放器上右键->BiliPolyfill->片头片尾->检视数据,删掉一些吧。');
  2260. this.userdata = JSON.parse(this.userdata);
  2261. }
  2262. catch (e) { }
  2263. finally {
  2264. if (!this.userdata) this.userdata = {};
  2265. if (!(this.userdata.oped instanceof Object)) this.userdata.oped = {};
  2266. if (!(this.userdata.restore instanceof Object)) this.userdata.restore = {};
  2267. }
  2268. }
  2269.  
  2270. async setFunctions({ videoRefresh = false } = {}) {
  2271. // 1. initialize
  2272. this.video = await this.getPlayerVideo();
  2273.  
  2274. // 2. if not enabled, run the process without real actions
  2275. if (!this.option.betabeta) return this.getPlayerMenu();
  2276.  
  2277. // 3. set up functions that are cid static
  2278. if (!videoRefresh) {
  2279. this.retrieveUserdata();
  2280. if (this.option.badgeWatchLater) {
  2281. await this.getWatchLaterBtn();
  2282. this.badgeWatchLater();
  2283. }
  2284. if (this.option.scroll) this.scrollToPlayer();
  2285.  
  2286. if (this.option.series) this.inferNextInSeries();
  2287.  
  2288. if (this.option.recommend) this.showRecommendTab();
  2289. if (this.option.focus) this.focusOnPlayer();
  2290. if (this.option.restorePrevent) this.restorePreventShade();
  2291. if (this.option.restoreDanmuku) this.restoreDanmukuSwitch();
  2292. if (this.option.restoreSpeed) this.restoreSpeed();
  2293. if (this.option.restoreWide) {
  2294. await this.getWideScreenBtn();
  2295. this.restoreWideScreen();
  2296. }
  2297. if (this.option.autoResume) this.autoResume();
  2298. if (this.option.autoPlay) this.autoPlay();
  2299. if (this.option.autoFullScreen) this.autoFullScreen();
  2300. if (this.option.limitedKeydown) this.limitedKeydownFullScreenPlay();
  2301. this.destroy.addCallback(() => this.saveUserdata());
  2302. }
  2303.  
  2304. // 4. set up functions that are binded to the video DOM
  2305. if (this.option.dblclick) this.dblclickFullScreen();
  2306. if (this.option.electric) this.reallocateElectricPanel();
  2307. if (this.option.oped) this.skipOPED();
  2308. this.video.addEventListener('emptied', () => this.setFunctions({ videoRefresh: true }), { once: true });
  2309.  
  2310. // 5. set up functions that require everything to be ready
  2311. await this.getPlayerMenu();
  2312. if (this.option.menuFocus) this.menuFocusOnPlayer();
  2313.  
  2314. // 6. set up experimental functions
  2315. if (this.option.speech) top.document.body.addEventListener('click', e => e.detail > 2 && this.speechRecognition());
  2316. }
  2317.  
  2318. async inferNextInSeries() {
  2319. // 1. find current title
  2320. const title = top.document.getElementsByTagName('h1')[0].textContent.replace(/\(\d+\)$/, '').trim();
  2321.  
  2322. // 2. find current ep number
  2323. const ep = title.match(/\d+(?=[^\d]*$)/);
  2324. if (!ep) return this.series = [];
  2325.  
  2326. // 3. current title - current ep number => series common title
  2327. const seriesTitle = title.slice(0, title.lastIndexOf(ep)).trim();
  2328.  
  2329. // 4. find sibling ep number
  2330. const epNumber = parseInt(ep);
  2331. const epSibling = ep[0] == '0' ?
  2332. [(epNumber - 1).toString().padStart(ep.length, '0'), (epNumber + 1).toString().padStart(ep.length, '0')] :
  2333. [(epNumber - 1).toString(), (epNumber + 1).toString()];
  2334.  
  2335. // 5. build search keywords
  2336. // [self, seriesTitle + epSibling, epSibling]
  2337. const keywords = [title, ...epSibling.map(e => seriesTitle + e), ...epSibling];
  2338.  
  2339. // 6. find mid
  2340. const midParent = top.document.getElementById('r-info-rank') || top.document.querySelector('.user');
  2341. if (!midParent) return this.series = [];
  2342. const mid = midParent.children[0].href.match(/\d+/)[0];
  2343.  
  2344. // 7. fetch query
  2345. const vlist = await Promise.all(keywords.map(keyword => new Promise((resolve, reject) => {
  2346. const req = new XMLHttpRequest();
  2347. req.onload = () => resolve((req.response.status && req.response.data.vlist) || []);
  2348. req.onerror = reject;
  2349. req.open('get', `https://space.bilibili.com/ajax/member/getSubmitVideos?mid=${mid}&keyword=${keyword}`);
  2350. req.responseType = 'json';
  2351. req.send();
  2352. })));
  2353.  
  2354. // 8. verify current video exists
  2355. vlist[0] = vlist[0].filter(e => e.title == title);
  2356. if (!vlist[0][0]) { console && console.warn('BiliPolyfill: inferNextInSeries: cannot find current video in mid space'); return this.series = []; }
  2357.  
  2358. // 9. if seriesTitle + epSibling qurey has reasonable results => pick
  2359. this.series = [vlist[1].find(e => e.created < vlist[0][0].created), vlist[2].reverse().find(e => e.created > vlist[0][0].created)];
  2360.  
  2361. // 10. fallback: if epSibling qurey has reasonable results => pick
  2362. if (!this.series[0]) this.series[0] = vlist[3].find(e => e.created < vlist[0][0].created);
  2363. if (!this.series[1]) this.series[1] = vlist[4].reverse().find(e => e.created > vlist[0][0].created);
  2364.  
  2365. return this.series;
  2366. }
  2367.  
  2368. badgeWatchLater() {
  2369. // 1. find watchlater button
  2370. const li = top.document.getElementById('i_menu_watchLater_btn') || top.document.getElementById('i_menu_later_btn') || top.document.querySelector('li.nav-item[report-id=playpage_watchlater]');
  2371. if (!li) return;
  2372.  
  2373. // 2. initialize watchlater panel
  2374. const observer = new MutationObserver(() => {
  2375.  
  2376. // 3. hide watchlater panel
  2377. observer.disconnect();
  2378. li.children[1].style.visibility = 'hidden';
  2379.  
  2380. // 4. loading => wait
  2381. if (li.children[1].children[0].children[0].className == 'm-w-loading') {
  2382. const observer = new MutationObserver(() => {
  2383.  
  2384. // 5. clean up watchlater panel
  2385. observer.disconnect();
  2386. li.dispatchEvent(new Event('mouseleave'));
  2387. setTimeout(() => li.children[1].style.visibility = '', 700);
  2388.  
  2389. // 6.1 empty list => do nothing
  2390. if (li.children[1].children[0].children[0].className == 'no-data') return;
  2391.  
  2392. // 6.2 otherwise => append div
  2393. const div = top.document.createElement('div');
  2394. div.className = 'num';
  2395. if (li.children[1].children[0].children[0].children.length > 5) {
  2396. div.textContent = '5+';
  2397. }
  2398. else {
  2399. div.textContent = li.children[1].children[0].children[0].children.length;
  2400. }
  2401. li.children[0].append(div);
  2402. this.destroy.addCallback(() => div.remove());
  2403. });
  2404. observer.observe(li.children[1].children[0], { childList: true });
  2405. }
  2406.  
  2407. // 4.2 otherwise => error
  2408. else {
  2409. throw 'badgeWatchLater: cannot find m-w-loading panel';
  2410. }
  2411. });
  2412. observer.observe(li, { childList: true });
  2413. li.dispatchEvent(new Event('mouseenter'));
  2414. }
  2415.  
  2416. dblclickFullScreen() {
  2417. this.video.addEventListener('dblclick', () =>
  2418. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click()
  2419. );
  2420. }
  2421.  
  2422. scrollToPlayer() {
  2423. if (top.scrollY < 200) top.document.getElementById('bofqi').scrollIntoView();
  2424. }
  2425.  
  2426. showRecommendTab() {
  2427. const h = this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-filter-btn-recommend');
  2428. if (h) h.click();
  2429. }
  2430.  
  2431. getCoverImage() { // 番剧用原来的方法只能获取到番剧的封面,改用API可以获取到每集的封面
  2432. const _jq = top.window.jQuery;
  2433. const view_url = "https://api.bilibili.com/x/web-interface/view?aid=" + aid;
  2434.  
  2435. try {
  2436. let view_res = _jq.ajax({ url: view_url, async: false });
  2437. let view_json = JSON.parse(view_res.responseText);
  2438. return view_json.data.pic.replace("http://", "https://")
  2439. }
  2440. catch (e) {
  2441. return null
  2442. }
  2443. }
  2444.  
  2445. reallocateElectricPanel() {
  2446. // 1. autopart == wait => ok
  2447. if (!this.playerWin.localStorage.bilibili_player_settings) return;
  2448. if (!this.playerWin.localStorage.bilibili_player_settings.includes('"autopart":1') && !this.option.electricSkippable) return;
  2449.  
  2450. // 2. wait for electric panel
  2451. this.video.addEventListener('ended', () => {
  2452. setTimeout(() => {
  2453. // 3. click skip
  2454. const electricPanel = this.playerWin.document.getElementsByClassName('bilibili-player-electric-panel')[0];
  2455. if (!electricPanel) return;
  2456. electricPanel.children[2].click();
  2457.  
  2458. // 4. but display a fake electric panel
  2459. electricPanel.style.display = 'block';
  2460. electricPanel.style.zIndex = 233;
  2461.  
  2462. // 5. and perform a fake countdown
  2463. let countdown = 5;
  2464. const h = setInterval(() => {
  2465. // 5.1 yield to next part hint
  2466. if (this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-item-jump')[0]) electricPanel.style.zIndex = '';
  2467.  
  2468. // 5.2 countdown > 0 => update textContent
  2469. if (countdown > 0) {
  2470. electricPanel.children[2].children[0].textContent = `0${countdown}`;
  2471. countdown--;
  2472. }
  2473.  
  2474. // 5.3 countdown == 0 => clean up
  2475. else {
  2476. clearInterval(h);
  2477. electricPanel.remove();
  2478. }
  2479. }, 1000);
  2480. }, 0);
  2481. }, { once: true });
  2482. }
  2483.  
  2484. /**
  2485. * As of March 2018:
  2486. * opacity:
  2487. * bilibili_player_settings.setting_config.opacity
  2488. * persist :)
  2489. * preventshade:
  2490. * bilibili_player_settings.setting_config.preventshade
  2491. * will be overwritten
  2492. * bilibili has a broken setting roaming scheme where the preventshade default is always used
  2493. * type_bottom, type_scroll, type_top:
  2494. * bilibili_player_settings.setting_config.type_(bottom|scroll|top)
  2495. * sessionStorage ONLY
  2496. * not sure if it is a feature or a bug
  2497. * danmaku switch:
  2498. * not stored
  2499. * videospeed:
  2500. * bilibili_player_settings.video_status.videospeed
  2501. * sessionStorage ONLY
  2502. * same as above
  2503. * widescreen:
  2504. * same as above
  2505. */
  2506. restorePreventShade() {
  2507. // 1. restore option should be an array
  2508. if (!Array.isArray(this.userdata.restore.preventShade)) this.userdata.restore.preventShade = [];
  2509.  
  2510. // 2. find corresponding option index
  2511. const index = top.location.href.includes('bangumi') ? 0 : 1;
  2512.  
  2513. // 3. MUST initialize setting panel before click
  2514. let danmaku_btn = this.playerWin.document.getElementsByClassName('bilibili-player-video-btn-danmaku')[0];
  2515. if (!danmaku_btn) return;
  2516. danmaku_btn.dispatchEvent(new Event('mouseover'));
  2517.  
  2518. // 4. restore if true
  2519. const input = this.playerWin.document.getElementsByName('ctlbar_danmuku_prevent')[0];
  2520. if (this.userdata.restore.preventShade[index] && !input.nextElementSibling.classList.contains('bpui-state-active')) {
  2521. input.click();
  2522. }
  2523.  
  2524. // 5. clean up setting panel
  2525. this.playerWin.document.getElementsByClassName('bilibili-player-video-btn-danmaku')[0].dispatchEvent(new Event('mouseout'));
  2526.  
  2527. // 6. memorize option
  2528. this.destroy.addCallback(() => {
  2529. this.userdata.restore.preventShade[index] = input.nextElementSibling.classList.contains('bpui-state-active');
  2530. });
  2531. }
  2532.  
  2533. restoreDanmukuSwitch() {
  2534. // 1. restore option should be an array
  2535. if (!Array.isArray(this.userdata.restore.danmukuSwitch)) this.userdata.restore.danmukuSwitch = [];
  2536. if (!Array.isArray(this.userdata.restore.danmukuTopSwitch)) this.userdata.restore.danmukuTopSwitch = [];
  2537. if (!Array.isArray(this.userdata.restore.danmukuBottomSwitch)) this.userdata.restore.danmukuBottomSwitch = [];
  2538. if (!Array.isArray(this.userdata.restore.danmukuScrollSwitch)) this.userdata.restore.danmukuScrollSwitch = [];
  2539.  
  2540. // 2. find corresponding option index
  2541. const index = top.location.href.includes('bangumi') ? 0 : 1;
  2542.  
  2543. // 3. MUST initialize setting panel before click
  2544. let danmaku_btn = this.playerWin.document.getElementsByClassName('bilibili-player-video-btn-danmaku')[0];
  2545. if (!danmaku_btn) return;
  2546. danmaku_btn.dispatchEvent(new Event('mouseover'));
  2547.  
  2548. // 4. restore if true
  2549. // 4.1 danmukuSwitch
  2550. const danmukuSwitchDiv = this.playerWin.document.getElementsByClassName('bilibili-player-video-btn-danmaku')[0];
  2551. if (this.userdata.restore.danmukuSwitch[index] && !danmukuSwitchDiv.classList.contains('video-state-danmaku-off')) {
  2552. danmukuSwitchDiv.click();
  2553. }
  2554.  
  2555. // 4.2 danmukuTopSwitch danmukuBottomSwitch danmukuScrollSwitch
  2556. const [danmukuTopSwitchDiv, danmukuBottomSwitchDiv, danmukuScrollSwitchDiv] = this.playerWin.document.getElementsByClassName('bilibili-player-danmaku-setting-lite-type-list')[0].children;
  2557. if (this.userdata.restore.danmukuTopSwitch[index] && !danmukuTopSwitchDiv.classList.contains('disabled')) {
  2558. danmukuTopSwitchDiv.click();
  2559. }
  2560. if (this.userdata.restore.danmukuBottomSwitch[index] && !danmukuBottomSwitchDiv.classList.contains('disabled')) {
  2561. danmukuBottomSwitchDiv.click();
  2562. }
  2563. if (this.userdata.restore.danmukuScrollSwitch[index] && !danmukuScrollSwitchDiv.classList.contains('disabled')) {
  2564. danmukuScrollSwitchDiv.click();
  2565. }
  2566.  
  2567. // 5. clean up setting panel
  2568. this.playerWin.document.getElementsByClassName('bilibili-player-video-btn-danmaku')[0].dispatchEvent(new Event('mouseout'));
  2569.  
  2570. // 6. memorize final option
  2571. this.destroy.addCallback(() => {
  2572. this.userdata.restore.danmukuSwitch[index] = danmukuSwitchDiv.classList.contains('video-state-danmaku-off');
  2573. this.userdata.restore.danmukuTopSwitch[index] = danmukuTopSwitchDiv.classList.contains('disabled');
  2574. this.userdata.restore.danmukuBottomSwitch[index] = danmukuBottomSwitchDiv.classList.contains('disabled');
  2575. this.userdata.restore.danmukuScrollSwitch[index] = danmukuScrollSwitchDiv.classList.contains('disabled');
  2576. });
  2577. }
  2578.  
  2579. restoreSpeed() {
  2580. // 1. restore option should be an array
  2581. if (!Array.isArray(this.userdata.restore.speed)) this.userdata.restore.speed = [];
  2582.  
  2583. // 2. find corresponding option index
  2584. const index = top.location.href.includes('bangumi') ? 0 : 1;
  2585.  
  2586. // 3. restore if different
  2587. if (this.userdata.restore.speed[index] && this.userdata.restore.speed[index] != this.video.playbackRate) {
  2588. this.video.playbackRate = this.userdata.restore.speed[index];
  2589. }
  2590.  
  2591. // 4. memorize option
  2592. this.destroy.addCallback(() => {
  2593. this.userdata.restore.speed[index] = this.video.playbackRate;
  2594. });
  2595. }
  2596.  
  2597. restoreWideScreen() {
  2598. // 1. restore option should be an array
  2599. if (!Array.isArray(this.userdata.restore.wideScreen)) this.userdata.restore.wideScreen = [];
  2600.  
  2601. // 2. find corresponding option index
  2602. const index = top.location.href.includes('bangumi') ? 0 : 1;
  2603.  
  2604. // 3. restore if different
  2605. const i = this.playerWin.document.querySelector('.bilibili-player-video-btn-widescreen');
  2606. if (this.userdata.restore.wideScreen[index] && !i.classList.contains('closed') && !i.firstElementChild.classList.contains('icon-24wideon')) {
  2607. i.click();
  2608. }
  2609.  
  2610. // 4. memorize option
  2611. this.destroy.addCallback(() => {
  2612. this.userdata.restore.wideScreen[index] = i.classList.contains('closed') || i.firstElementChild.classList.contains('icon-24wideon');
  2613. });
  2614. }
  2615.  
  2616. loadOffineSubtitles() {
  2617. // NO. NOBODY WILL NEED THIS。
  2618. // Hint: https://github.com/jamiees2/ass-to-vtt
  2619. throw 'Not implemented';
  2620. }
  2621.  
  2622. autoResume() {
  2623. // 1. wait for canplay => wait for resume popup
  2624. const h = () => {
  2625. // 2. parse resume popup
  2626. const span = this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-text span:nth-child(2)');
  2627. if (!span) return;
  2628. const [min, sec] = span.textContent.split(':');
  2629. if (!min || !sec) return;
  2630.  
  2631. // 3. parse last playback progress
  2632. const time = parseInt(min) * 60 + parseInt(sec);
  2633.  
  2634. // 3.1 still far from end => reasonable to resume => click
  2635. if (time < this.video.duration - 10) {
  2636. // 3.1.1 already playing => no need to pause => simply jump
  2637. if (!this.video.paused || this.video.autoplay) {
  2638. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-jump').click();
  2639. }
  2640.  
  2641. // 3.1.2 paused => should remain paused after jump => hook video.play
  2642. else {
  2643. const play = this.video.play;
  2644. this.video.play = () => setTimeout(() => {
  2645. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2646. this.video.play = play;
  2647. }, 0);
  2648. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-jump').click();
  2649. }
  2650. }
  2651.  
  2652. // 3.2 near end => silent popup
  2653. else {
  2654. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-close').click();
  2655. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom').children[0].style.visibility = 'hidden';
  2656. }
  2657. };
  2658. this.video.addEventListener('canplay', h, { once: true });
  2659. setTimeout(() => this.video && this.video.removeEventListener && this.video.removeEventListener('canplay', h), 3000);
  2660. }
  2661.  
  2662. autoPlay() {
  2663. this.video.autoplay = true;
  2664. setTimeout(() => {
  2665. if (this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2666. }, 0);
  2667. }
  2668.  
  2669. autoFullScreen() {
  2670. if (this.playerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off'))
  2671. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
  2672. }
  2673.  
  2674. getCollectionId() {
  2675. return (top.location.pathname.match(/av\d+/) || top.location.hash.match(/av\d+/) || top.document.querySelector('div.bangumi-info a').href).toString();
  2676. }
  2677.  
  2678. markOPEDPosition(index) {
  2679. const collectionId = this.getCollectionId();
  2680. if (!Array.isArray(this.userdata.oped[collectionId])) this.userdata.oped[collectionId] = [];
  2681. this.userdata.oped[collectionId][index] = this.video.currentTime;
  2682. }
  2683.  
  2684. clearOPEDPosition() {
  2685. const collectionId = this.getCollectionId();
  2686. this.userdata.oped[collectionId] = undefined;
  2687. }
  2688.  
  2689. skipOPED() {
  2690. // 1. find corresponding userdata
  2691. const collectionId = this.getCollectionId();
  2692. if (!Array.isArray(this.userdata.oped[collectionId]) || !this.userdata.oped[collectionId].length) return;
  2693.  
  2694. /**
  2695. * structure:
  2696. * listen for time update -> || <- skip -> || <- remove event listenner
  2697. */
  2698.  
  2699. // 2. | 0 <- opening -> oped[collectionId][1] | <- play --
  2700. if (!this.userdata.oped[collectionId][0] && this.userdata.oped[collectionId][1]) {
  2701. const h = () => {
  2702. if (this.video.currentTime >= this.userdata.oped[collectionId][1] - 1) {
  2703. this.video.removeEventListener('timeupdate', h);
  2704. }
  2705. else {
  2706. this.video.currentTime = this.userdata.oped[collectionId][1];
  2707. this.hintInfo('BiliPolyfill: 已跳过片头');
  2708. }
  2709. };
  2710. this.video.addEventListener('timeupdate', h);
  2711. }
  2712.  
  2713. // 3. | <- play -> | oped[collectionId][0] <- opening -> oped[collectionId][1] | <- play --
  2714. if (this.userdata.oped[collectionId][0] && this.userdata.oped[collectionId][1]) {
  2715. const h = () => {
  2716. if (this.video.currentTime >= this.userdata.oped[collectionId][1] - 1) {
  2717. this.video.removeEventListener('timeupdate', h);
  2718. }
  2719. else if (this.video.currentTime > this.userdata.oped[collectionId][0]) {
  2720. this.video.currentTime = this.userdata.oped[collectionId][1];
  2721. this.hintInfo('BiliPolyfill: 已跳过片头');
  2722. }
  2723. };
  2724. this.video.addEventListener('timeupdate', h);
  2725. }
  2726.  
  2727. // 4. -- play -> | oped[collectionId][2] <- ending -> end |
  2728. if (this.userdata.oped[collectionId][2] && !this.userdata.oped[collectionId][3]) {
  2729. const h = () => {
  2730. if (this.video.currentTime >= this.video.duration - 1) {
  2731. this.video.removeEventListener('timeupdate', h);
  2732. }
  2733. else if (this.video.currentTime > this.userdata.oped[collectionId][2]) {
  2734. this.video.currentTime = this.video.duration;
  2735. this.hintInfo('BiliPolyfill: 已跳过片尾');
  2736. }
  2737. };
  2738. this.video.addEventListener('timeupdate', h);
  2739. }
  2740.  
  2741. // 5.-- play -> | oped[collectionId][2] <- ending -> oped[collectionId][3] | <- play -> end |
  2742. if (this.userdata.oped[collectionId][2] && this.userdata.oped[collectionId][3]) {
  2743. const h = () => {
  2744. if (this.video.currentTime >= this.userdata.oped[collectionId][3] - 1) {
  2745. this.video.removeEventListener('timeupdate', h);
  2746. }
  2747. else if (this.video.currentTime > this.userdata.oped[collectionId][2]) {
  2748. this.video.currentTime = this.userdata.oped[collectionId][3];
  2749. this.hintInfo('BiliPolyfill: 已跳过片尾');
  2750. }
  2751. };
  2752. this.video.addEventListener('timeupdate', h);
  2753. }
  2754. }
  2755.  
  2756. setVideoSpeed(speed) {
  2757. if (speed < 0 || speed > 10) return;
  2758. this.video.playbackRate = speed;
  2759. }
  2760.  
  2761. focusOnPlayer() {
  2762. let player = this.playerWin.document.getElementsByClassName('bilibili-player-video-progress')[0];
  2763. if (player) player.click();
  2764. }
  2765.  
  2766. menuFocusOnPlayer() {
  2767. this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0].addEventListener('click', () =>
  2768. setTimeout(() => this.focusOnPlayer(), 0)
  2769. );
  2770. }
  2771.  
  2772. limitedKeydownFullScreenPlay() {
  2773. // 1. listen for any user guesture
  2774. const h = e => {
  2775. // 2. not real user guesture => do nothing
  2776. if (!e.isTrusted) return;
  2777.  
  2778. // 3. key down is Enter => full screen play
  2779. if (e.key == 'Enter') {
  2780. // 3.1 full screen
  2781. if (this.playerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off')) {
  2782. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
  2783. }
  2784.  
  2785. // 3.2 play
  2786. if (this.video.paused) {
  2787. if (this.video.readyState) {
  2788. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2789. }
  2790. else {
  2791. this.video.addEventListener('canplay', () => {
  2792. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2793. }, { once: true });
  2794. }
  2795. }
  2796. }
  2797.  
  2798. // 4. clean up listener
  2799. top.document.removeEventListener('keydown', h);
  2800. top.document.removeEventListener('click', h);
  2801. };
  2802. top.document.addEventListener('keydown', h);
  2803. top.document.addEventListener('click', h);
  2804. }
  2805.  
  2806. speechRecognition() {
  2807. // 1. polyfill
  2808. const SpeechRecognition = top.SpeechRecognition || top.webkitSpeechRecognition;
  2809. const SpeechGrammarList = top.SpeechGrammarList || top.webkitSpeechGrammarList;
  2810.  
  2811. // 2. give hint
  2812. alert('Yahaha! You found me!\nBiliTwin支持的语音命令: 播放 暂停 全屏 关闭 加速 减速 下一集\nChrome may support Cantonese or Hakka as well. See BiliPolyfill::speechRecognition.');
  2813. if (!SpeechRecognition || !SpeechGrammarList) alert('浏览器太旧啦~彩蛋没法运行~');
  2814.  
  2815. // 3. setup recognition
  2816. const player = ['播放', '暂停', '全屏', '关闭', '加速', '减速', '下一集'];
  2817. const grammar = '#JSGF V1.0; grammar player; public <player> = ' + player.join(' | ') + ' ;';
  2818. const recognition = new SpeechRecognition();
  2819. const speechRecognitionList = new SpeechGrammarList();
  2820. speechRecognitionList.addFromString(grammar, 1);
  2821. recognition.grammars = speechRecognitionList;
  2822. // cmn: Mandarin(Putonghua), yue: Cantonese, hak: Hakka
  2823. // See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
  2824. recognition.lang = 'cmn';
  2825. recognition.continuous = true;
  2826. recognition.interimResults = false;
  2827. recognition.maxAlternatives = 1;
  2828. recognition.start();
  2829. recognition.onresult = e => {
  2830. const last = e.results.length - 1;
  2831. const transcript = e.results[last][0].transcript;
  2832. switch (transcript) {
  2833. case '播放':
  2834. if (this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2835. this.hintInfo(`BiliPolyfill: 语音:播放`);
  2836. break;
  2837. case '暂停':
  2838. if (!this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
  2839. this.hintInfo(`BiliPolyfill: 语音:暂停`);
  2840. break;
  2841. case '全屏':
  2842. this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
  2843. this.hintInfo(`BiliPolyfill: 语音:全屏`);
  2844. break;
  2845. case '关闭':
  2846. top.close();
  2847. break;
  2848. case '加速':
  2849. this.setVideoSpeed(2);
  2850. this.hintInfo(`BiliPolyfill: 语音:加速`);
  2851. break;
  2852. case '减速':
  2853. this.setVideoSpeed(0.5);
  2854. this.hintInfo(`BiliPolyfill: 语音:减速`);
  2855. break;
  2856. case '下一集':
  2857. this.video.dispatchEvent(new Event('ended'));
  2858. default:
  2859. this.hintInfo(`BiliPolyfill: 语音:"${transcript}"?`);
  2860. break;
  2861. }
  2862. typeof console == "object" && console.log(e.results);
  2863. typeof console == "object" && console.log(`transcript:${transcript} confidence:${e.results[0][0].confidence}`);
  2864. };
  2865. }
  2866.  
  2867. substitudeFullscreenPlayer(option) {
  2868. // 1. check param
  2869. if (!option) throw 'usage: substitudeFullscreenPlayer({cid, aid[, p][, ...otherOptions]})';
  2870. if (!option.cid) throw 'player init: cid missing';
  2871. if (!option.aid) throw 'player init: aid missing';
  2872.  
  2873. // 2. hook exitFullscreen
  2874. const playerDoc = this.playerWin.document;
  2875. const hook = [playerDoc.webkitExitFullscreen, playerDoc.mozExitFullScreen, playerDoc.msExitFullscreen, playerDoc.exitFullscreen];
  2876. playerDoc.webkitExitFullscreen = playerDoc.mozExitFullScreen = playerDoc.msExitFullscreen = playerDoc.exitFullscreen = () => { };
  2877.  
  2878. // 3. substitude player
  2879. this.playerWin.player.destroy();
  2880. this.playerWin.player = new bilibiliPlayer(option);
  2881. if (option.p) this.playerWin.callAppointPart(option.p);
  2882.  
  2883. // 4. restore exitFullscreen
  2884. [playerDoc.webkitExitFullscreen, playerDoc.mozExitFullScreen, playerDoc.msExitFullscreen, playerDoc.exitFullscreen] = hook;
  2885. }
  2886.  
  2887. async getPlayerVideo() {
  2888. if (this.playerWin.document.getElementsByTagName('video').length) {
  2889. return this.video = this.playerWin.document.getElementsByTagName('video')[0];
  2890. }
  2891. else {
  2892. return new Promise(resolve => {
  2893. const observer = new MutationObserver(() => {
  2894. if (this.playerWin.document.getElementsByTagName('video').length) {
  2895. observer.disconnect();
  2896. resolve(this.video = this.playerWin.document.getElementsByTagName('video')[0]);
  2897. }
  2898. });
  2899. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  2900. });
  2901. }
  2902. }
  2903.  
  2904. async getPlayerMenu() {
  2905. if (this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black').length) {
  2906. return this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0];
  2907. }
  2908. else {
  2909. return new Promise(resolve => {
  2910. const observer = new MutationObserver(() => {
  2911. if (this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black').length) {
  2912. observer.disconnect();
  2913. resolve(this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0]);
  2914. }
  2915. });
  2916. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  2917. });
  2918. }
  2919. }
  2920.  
  2921. async getWatchLaterBtn() {
  2922. let li = top.document.getElementById('i_menu_watchLater_btn') || top.document.getElementById('i_menu_later_btn') || top.document.querySelector('li.nav-item[report-id=playpage_watchlater]');
  2923.  
  2924. if (!document.cookie.includes("DedeUserID")) return; // 未登录(不可用)
  2925.  
  2926. if (!li) {
  2927. return new Promise(resolve => {
  2928. const observer = new MutationObserver(() => {
  2929. li = top.document.getElementById('i_menu_watchLater_btn') || top.document.getElementById('i_menu_later_btn') || top.document.querySelector('li.nav-item[report-id=playpage_watchlater]');
  2930. if (li) {
  2931. observer.disconnect();
  2932. resolve(li);
  2933. }
  2934. });
  2935. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  2936. });
  2937. }
  2938. }
  2939.  
  2940. async getWideScreenBtn() {
  2941. let li = top.document.querySelector('.bilibili-player-video-btn-widescreen');
  2942.  
  2943. if (!li) {
  2944. return new Promise(resolve => {
  2945. const observer = new MutationObserver(() => {
  2946. li = top.document.querySelector('.bilibili-player-video-btn-widescreen');
  2947. if (li) {
  2948. observer.disconnect();
  2949. resolve(li);
  2950. }
  2951. });
  2952. observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
  2953. });
  2954. }
  2955. }
  2956.  
  2957. static async openMinimizedPlayer(option = { cid: top.cid, aid: top.aid, playerWin: top }) {
  2958. // 1. check param
  2959. if (!option) throw 'usage: openMinimizedPlayer({cid[, aid]})';
  2960. if (!option.cid) throw 'player init: cid missing';
  2961. if (!option.aid) option.aid = top.aid;
  2962. if (!option.playerWin) option.playerWin = top;
  2963.  
  2964. // 2. open a new window
  2965. const miniPlayerWin = top.open(`//www.bilibili.com/blackboard/html5player.html?cid=${option.cid}&aid=${option.aid}&crossDomain=${top.document.domain != 'www.bilibili.com' ? 'true' : ''}`, undefined, ' ');
  2966.  
  2967. // 3. bangumi => request referrer must match => hook response of current page
  2968. const res = top.location.href.includes('bangumi') && await new Promise(resolve => {
  2969. const jq = option.playerWin.jQuery;
  2970. const _ajax = jq.ajax;
  2971.  
  2972. jq.ajax = function (a, c) {
  2973. if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined; } if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
  2974. a.success = resolve;
  2975. jq.ajax = _ajax;
  2976. }
  2977. return _ajax.call(jq, a, c);
  2978. };
  2979. option.playerWin.player.reloadAccess();
  2980. });
  2981.  
  2982. // 4. wait for miniPlayerWin load
  2983. await new Promise(resolve => {
  2984. // 4.1 check for every500ms
  2985. const i = setInterval(() => miniPlayerWin.document.getElementById('bilibiliPlayer') && resolve(), 500);
  2986.  
  2987. // 4.2 explict event listener
  2988. miniPlayerWin.addEventListener('load', resolve, { once: true });
  2989.  
  2990. // 4.3 timeout after 6s
  2991. setTimeout(() => {
  2992. clearInterval(i);
  2993. miniPlayerWin.removeEventListener('load', resolve);
  2994. resolve();
  2995. }, 6000);
  2996. });
  2997. // 4.4 cannot find bilibiliPlayer => load timeout
  2998. const playerDiv = miniPlayerWin.document.getElementById('bilibiliPlayer');
  2999. if (!playerDiv) { console.warn('openMinimizedPlayer: document load timeout'); return; }
  3000.  
  3001. // 5. need to inject response => new bilibiliPlayer
  3002. if (res) {
  3003. await new Promise(resolve => {
  3004. const jq = miniPlayerWin.jQuery;
  3005. const _ajax = jq.ajax;
  3006.  
  3007. jq.ajax = function (a, c) {
  3008. if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined; } if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
  3009. a.success(res);
  3010. jq.ajax = _ajax;
  3011. resolve();
  3012. }
  3013. else {
  3014. return _ajax.call(jq, a, c);
  3015. }
  3016. };
  3017. miniPlayerWin.player = new miniPlayerWin.bilibiliPlayer({ cid: option.cid, aid: option.aid });
  3018. // miniPlayerWin.eval(`player = new bilibiliPlayer({ cid: ${option.cid}, aid: ${option.aid} })`);
  3019. // console.log(`player = new bilibiliPlayer({ cid: ${option.cid}, aid: ${option.aid} })`);
  3020. });
  3021. }
  3022.  
  3023. // 6. wait for bilibiliPlayer load
  3024. await new Promise(resolve => {
  3025. if (miniPlayerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen')) resolve();
  3026. else {
  3027. const observer = new MutationObserver(() => {
  3028. if (miniPlayerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen')) {
  3029. observer.disconnect();
  3030. resolve();
  3031. }
  3032. });
  3033. observer.observe(playerDiv, { childList: true });
  3034. }
  3035. });
  3036.  
  3037. // 7. adopt full screen player style withour really trigger full screen
  3038. // 7.1 hook requestFullscreen
  3039. const hook = [playerDiv.webkitRequestFullscreen, playerDiv.mozRequestFullScreen, playerDiv.msRequestFullscreen, playerDiv.requestFullscreen];
  3040. playerDiv.webkitRequestFullscreen = playerDiv.mozRequestFullScreen = playerDiv.msRequestFullscreen = playerDiv.requestFullscreen = () => { };
  3041.  
  3042. // 7.2 adopt full screen player style
  3043. if (miniPlayerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off'))
  3044. miniPlayerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
  3045.  
  3046. // 7.3 restore requestFullscreen
  3047. [playerDiv.webkitRequestFullscreen, playerDiv.mozRequestFullScreen, playerDiv.msRequestFullscreen, playerDiv.requestFullscreen] = hook;
  3048. }
  3049.  
  3050. static async biligameInit() {
  3051. const game_id = location.href.match(/id=(\d+)/)[1];
  3052. const api_url = `https://line1-h5-pc-api.biligame.com/game/detail/gameinfo?game_base_id=${game_id}`;
  3053.  
  3054. const req = await fetch(api_url, { credentials: 'include' });
  3055. const data = (await req.json()).data;
  3056.  
  3057. const aid = data.video_url;
  3058. const video_url = `https://www.bilibili.com/video/av${aid}`;
  3059.  
  3060. const tabs = document.querySelector(".tab-head");
  3061. const tab = document.createElement("a");
  3062. tab.href = video_url;
  3063. tab.textContent = "查看视频";
  3064. tab.target = "_blank";
  3065. tabs.appendChild(tab);
  3066. }
  3067.  
  3068. static showBangumiCoverImage() {
  3069. const imgElement = document.querySelector(".media-preview img");
  3070. if (!imgElement) return;
  3071.  
  3072. imgElement.style.cursor = "pointer";
  3073.  
  3074. imgElement.onclick = () => {
  3075. const cover_img = imgElement.src.match(/.+?\.(png|jpg)/)[0];
  3076. top.window.open(cover_img, '_blank');
  3077. };
  3078. }
  3079.  
  3080. static secondToReadable(s) {
  3081. if (s > 60) return `${parseInt(s / 60)}分${parseInt(s % 60)}秒`;
  3082. else return `${parseInt(s % 60)}秒`;
  3083. }
  3084.  
  3085. static clearAllUserdata(playerWin = top) {
  3086. if (playerWin.GM_setValue) return GM_setValue('biliPolyfill', '');
  3087. if (playerWin.GM.setValue) return GM.setValue('biliPolyfill', '');
  3088. playerWin.localStorage.removeItem('biliPolyfill');
  3089. }
  3090.  
  3091. static get optionDescriptions() {
  3092. return [
  3093. ['betabeta', '增强组件总开关 <---------更加懒得测试了,反正以后B站也会自己提供这些功能。也许吧。'],
  3094.  
  3095. // 1. user interface
  3096. ['badgeWatchLater', '稍后再看添加数字角标'],
  3097. ['recommend', '弹幕列表换成相关视频'],
  3098. ['electric', '整合充电榜与换P倒计时'],
  3099. ['electricSkippable', '跳过充电榜', 'disabled'],
  3100.  
  3101. // 2. automation
  3102. ['scroll', '自动滚动到播放器'],
  3103. ['focus', '自动聚焦到播放器(新页面直接按空格会播放而不是向下滚动)'],
  3104. ['menuFocus', '关闭菜单后聚焦到播放器'],
  3105. ['restorePrevent', '记住防挡字幕'],
  3106. ['restoreDanmuku', '记住弹幕开关(顶端/底端/滚动/全部)'],
  3107. ['restoreSpeed', '记住播放速度'],
  3108. ['restoreWide', '记住宽屏'],
  3109. ['autoResume', '自动跳转上次看到'],
  3110. ['autoPlay', '自动播放'],
  3111. ['autoFullScreen', '自动全屏'],
  3112. ['oped', '标记后自动跳OP/ED'],
  3113. ['series', '尝试自动找上下集'],
  3114.  
  3115. // 3. interaction
  3116. ['limitedKeydown', '首次回车键可全屏自动播放'],
  3117. ['dblclick', '双击全屏'],
  3118.  
  3119. // 4. easter eggs
  3120. ['speech', '(彩蛋)(需墙外)任意三击鼠标左键开启语音识别'],
  3121. ];
  3122. }
  3123.  
  3124. static get optionDefaults() {
  3125. return {
  3126. betabeta: false,
  3127.  
  3128. // 1. user interface
  3129. badgeWatchLater: true,
  3130. recommend: true,
  3131. electric: true,
  3132. electricSkippable: false,
  3133.  
  3134. // 2. automation
  3135. scroll: true,
  3136. focus: false,
  3137. menuFocus: true,
  3138. restorePrevent: false,
  3139. restoreDanmuku: false,
  3140. restoreSpeed: true,
  3141. restoreWide: true,
  3142. autoResume: true,
  3143. autoPlay: false,
  3144. autoFullScreen: false,
  3145. oped: true,
  3146. series: true,
  3147.  
  3148. // 3. interaction
  3149. limitedKeydown: true,
  3150. dblclick: true,
  3151.  
  3152. // 4. easter eggs
  3153. speech: false,
  3154. }
  3155. }
  3156.  
  3157. static _UNIT_TEST() {
  3158. console.warn('This test is impossible.');
  3159. console.warn('You need to close the tab, reopen it, etc.');
  3160. console.warn('Maybe you also want to test between bideo parts, etc.');
  3161. console.warn('I am too lazy to find workarounds.');
  3162. }
  3163. }
  3164.  
  3165. /***
  3166. * Copyright (C) 2018 Qli5. All Rights Reserved.
  3167. *
  3168. * @author qli5 <goodlq11[at](163|gmail).com>
  3169. *
  3170. * This Source Code Form is subject to the terms of the Mozilla Public
  3171. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3172. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  3173. */
  3174.  
  3175. class Exporter {
  3176. static exportIDM(urls, referrer = top.location.origin) {
  3177. return urls.map(url => `<\r\n${url}\r\nreferer: ${referrer}\r\n>\r\n`).join('');
  3178. }
  3179.  
  3180. static exportM3U8(urls, referrer = top.location.origin, userAgent = top.navigator.userAgent) {
  3181. return '#EXTM3U\n' + urls.map(url => `#EXTVLCOPT:http-referrer=${referrer}\n#EXTVLCOPT:http-user-agent=${userAgent}\n#EXTINF:-1\n${url}\n`).join('');
  3182. }
  3183.  
  3184. static exportAria2(urls, referrer = top.location.origin) {
  3185. return urls.map(url => `${url}\r\n referer=${referrer}\r\n`).join('');
  3186. }
  3187.  
  3188. static async sendToAria2RPC(urls, referrer = top.location.origin, target = 'http://127.0.0.1:6800/jsonrpc') {
  3189. const h = 'referer';
  3190. const method = 'POST';
  3191.  
  3192. while (1) {
  3193. const token_array = target.match(/\/\/((.+)@)/);
  3194. const body = JSON.stringify(urls.map((url, id) => {
  3195. const params = [
  3196. [url],
  3197. { [h]: referrer }
  3198. ];
  3199.  
  3200. if (token_array) {
  3201. params.unshift(token_array[2]);
  3202. target = target.replace(token_array[1], "");
  3203. }
  3204.  
  3205. return ({
  3206. id,
  3207. jsonrpc: 2,
  3208. method: "aria2.addUri",
  3209. params
  3210. })
  3211. }));
  3212.  
  3213. try {
  3214. const res = await (await fetch(target, { method, body })).json();
  3215. if (res.error || res[0].error) {
  3216. throw new Error((res.error || res[0].error).message)
  3217. }
  3218. else {
  3219. return res;
  3220. }
  3221. }
  3222. catch (e) {
  3223. target = top.prompt(`Aria2 connection failed${!e.message.includes("fetch") ? `: ${e.message}.\n` : ". "}Please provide a valid server address:`, target);
  3224. if (!target) return null;
  3225. }
  3226. }
  3227. }
  3228.  
  3229. static copyToClipboard(text) {
  3230. const textarea = document.createElement('textarea');
  3231. document.body.appendChild(textarea);
  3232. textarea.value = text;
  3233. textarea.select();
  3234. document.execCommand('copy');
  3235. document.body.removeChild(textarea);
  3236. }
  3237. }
  3238.  
  3239. /***
  3240. * Copyright (C) 2018 Qli5. All Rights Reserved.
  3241. *
  3242. * @author qli5 <goodlq11[at](163|gmail).com>
  3243. *
  3244. * This Source Code Form is subject to the terms of the Mozilla Public
  3245. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3246. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  3247. */
  3248.  
  3249. class TwentyFourDataView extends DataView {
  3250. getUint24(byteOffset, littleEndian) {
  3251. if (littleEndian) throw 'littleEndian int24 not implemented';
  3252. return this.getUint32(byteOffset - 1) & 0x00FFFFFF;
  3253. }
  3254.  
  3255. setUint24(byteOffset, value, littleEndian) {
  3256. if (littleEndian) throw 'littleEndian int24 not implemented';
  3257. if (value > 0x00FFFFFF) throw 'setUint24: number out of range';
  3258. let msb = value >> 16;
  3259. let lsb = value & 0xFFFF;
  3260. this.setUint8(byteOffset, msb);
  3261. this.setUint16(byteOffset + 1, lsb);
  3262. }
  3263.  
  3264. indexOf(search, startOffset = 0, endOffset = this.byteLength - search.length + 1) {
  3265. // I know it is NAIVE
  3266. if (search.charCodeAt) {
  3267. for (let i = startOffset; i < endOffset; i++) {
  3268. if (this.getUint8(i) != search.charCodeAt(0)) continue;
  3269. let found = 1;
  3270. for (let j = 0; j < search.length; j++) {
  3271. if (this.getUint8(i + j) != search.charCodeAt(j)) {
  3272. found = 0;
  3273. break;
  3274. }
  3275. }
  3276. if (found) return i;
  3277. }
  3278. return -1;
  3279. }
  3280. else {
  3281. for (let i = startOffset; i < endOffset; i++) {
  3282. if (this.getUint8(i) != search[0]) continue;
  3283. let found = 1;
  3284. for (let j = 0; j < search.length; j++) {
  3285. if (this.getUint8(i + j) != search[j]) {
  3286. found = 0;
  3287. break;
  3288. }
  3289. }
  3290. if (found) return i;
  3291. }
  3292. return -1;
  3293. }
  3294. }
  3295. }
  3296.  
  3297. /***
  3298. * Copyright (C) 2018 Qli5. All Rights Reserved.
  3299. *
  3300. * @author qli5 <goodlq11[at](163|gmail).com>
  3301. *
  3302. * This Source Code Form is subject to the terms of the Mozilla Public
  3303. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3304. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  3305. */
  3306.  
  3307. class FLVTag {
  3308. constructor(dataView, currentOffset = 0) {
  3309. this.tagHeader = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset, 11);
  3310. this.tagData = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11, this.dataSize);
  3311. this.previousSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11 + this.dataSize, 4);
  3312. }
  3313.  
  3314. get tagType() {
  3315. return this.tagHeader.getUint8(0);
  3316. }
  3317.  
  3318. get dataSize() {
  3319. return this.tagHeader.getUint24(1);
  3320. }
  3321.  
  3322. get timestamp() {
  3323. return this.tagHeader.getUint24(4);
  3324. }
  3325.  
  3326. get timestampExtension() {
  3327. return this.tagHeader.getUint8(7);
  3328. }
  3329.  
  3330. get streamID() {
  3331. return this.tagHeader.getUint24(8);
  3332. }
  3333.  
  3334. stripKeyframesScriptData() {
  3335. let hasKeyframes = 'hasKeyframes\x01';
  3336. if (this.tagType != 0x12) throw 'can not strip non-scriptdata\'s keyframes';
  3337.  
  3338. let index;
  3339. index = this.tagData.indexOf(hasKeyframes);
  3340. if (index != -1) {
  3341. //0x0101 => 0x0100
  3342. this.tagData.setUint8(index + hasKeyframes.length, 0x00);
  3343. }
  3344.  
  3345. // Well, I think it is unnecessary
  3346. /*index = this.tagData.indexOf(keyframes)
  3347. if (index != -1) {
  3348. this.dataSize = index;
  3349. this.tagHeader.setUint24(1, index);
  3350. this.tagData = new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset, index);
  3351. }*/
  3352. }
  3353.  
  3354. getDuration() {
  3355. if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
  3356.  
  3357. let duration = 'duration\x00';
  3358. let index = this.tagData.indexOf(duration);
  3359. if (index == -1) throw 'can not get flv meta duration';
  3360.  
  3361. index += 9;
  3362. return this.tagData.getFloat64(index);
  3363. }
  3364.  
  3365. getDurationAndView() {
  3366. if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
  3367.  
  3368. let duration = 'duration\x00';
  3369. let index = this.tagData.indexOf(duration);
  3370. if (index == -1) throw 'can not get flv meta duration';
  3371.  
  3372. index += 9;
  3373. return {
  3374. duration: this.tagData.getFloat64(index),
  3375. durationDataView: new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset + index, 8)
  3376. };
  3377. }
  3378.  
  3379. getCombinedTimestamp() {
  3380. return (this.timestampExtension << 24 | this.timestamp);
  3381. }
  3382.  
  3383. setCombinedTimestamp(timestamp) {
  3384. if (timestamp < 0) throw 'timestamp < 0';
  3385. this.tagHeader.setUint8(7, timestamp >> 24);
  3386. this.tagHeader.setUint24(4, timestamp & 0x00FFFFFF);
  3387. }
  3388. }
  3389.  
  3390. /***
  3391. * Copyright (C) 2018 Qli5. All Rights Reserved.
  3392. *
  3393. * @author qli5 <goodlq11[at](163|gmail).com>
  3394. *
  3395. * This Source Code Form is subject to the terms of the Mozilla Public
  3396. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3397. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  3398. *
  3399. * The FLV merge utility is a Javascript translation of
  3400. * https://github.com/grepmusic/flvmerge
  3401. * by grepmusic
  3402. */
  3403.  
  3404. /**
  3405. * A simple flv parser
  3406. */
  3407. class FLV {
  3408. constructor(dataView) {
  3409. if (dataView.indexOf('FLV', 0, 1) != 0) throw 'Invalid FLV header';
  3410. this.header = new TwentyFourDataView(dataView.buffer, dataView.byteOffset, 9);
  3411. this.firstPreviousTagSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + 9, 4);
  3412.  
  3413. this.tags = [];
  3414. let offset = this.headerLength + 4;
  3415. while (offset < dataView.byteLength) {
  3416. let tag = new FLVTag(dataView, offset);
  3417. // debug for scrpit data tag
  3418. // if (tag.tagType != 0x08 && tag.tagType != 0x09)
  3419. offset += 11 + tag.dataSize + 4;
  3420. this.tags.push(tag);
  3421. }
  3422.  
  3423. if (offset != dataView.byteLength) throw 'FLV unexpected end of file';
  3424. }
  3425.  
  3426. get type() {
  3427. return 'FLV';
  3428. }
  3429.  
  3430. get version() {
  3431. return this.header.getUint8(3);
  3432. }
  3433.  
  3434. get typeFlag() {
  3435. return this.header.getUint8(4);
  3436. }
  3437.  
  3438. get headerLength() {
  3439. return this.header.getUint32(5);
  3440. }
  3441.  
  3442. static merge(flvs) {
  3443. if (flvs.length < 1) throw 'Usage: FLV.merge([flvs])';
  3444. let blobParts = [];
  3445. let basetimestamp = [0, 0];
  3446. let lasttimestamp = [0, 0];
  3447. let duration = 0.0;
  3448. let durationDataView;
  3449.  
  3450. blobParts.push(flvs[0].header);
  3451. blobParts.push(flvs[0].firstPreviousTagSize);
  3452.  
  3453. for (let flv of flvs) {
  3454. let bts = duration * 1000;
  3455. basetimestamp[0] = lasttimestamp[0];
  3456. basetimestamp[1] = lasttimestamp[1];
  3457. bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
  3458. let foundDuration = 0;
  3459. for (let tag of flv.tags) {
  3460. if (tag.tagType == 0x12 && !foundDuration) {
  3461. duration += tag.getDuration();
  3462. foundDuration = 1;
  3463. if (flv == flvs[0]) {
  3464. ({ duration, durationDataView } = tag.getDurationAndView());
  3465. tag.stripKeyframesScriptData();
  3466. blobParts.push(tag.tagHeader);
  3467. blobParts.push(tag.tagData);
  3468. blobParts.push(tag.previousSize);
  3469. }
  3470. }
  3471. else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
  3472. lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
  3473. tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
  3474. blobParts.push(tag.tagHeader);
  3475. blobParts.push(tag.tagData);
  3476. blobParts.push(tag.previousSize);
  3477. }
  3478. }
  3479. }
  3480. durationDataView.setFloat64(0, duration);
  3481.  
  3482. return new Blob(blobParts);
  3483. }
  3484.  
  3485. static async mergeBlobs(blobs) {
  3486. // Blobs can be swapped to disk, while Arraybuffers can not.
  3487. // This is a RAM saving workaround. Somewhat.
  3488. if (blobs.length < 1) throw 'Usage: FLV.mergeBlobs([blobs])';
  3489. let ret = [];
  3490. let basetimestamp = [0, 0];
  3491. let lasttimestamp = [0, 0];
  3492. let duration = 0.0;
  3493. let durationDataView;
  3494.  
  3495. for (let blob of blobs) {
  3496. let bts = duration * 1000;
  3497. basetimestamp[0] = lasttimestamp[0];
  3498. basetimestamp[1] = lasttimestamp[1];
  3499. bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
  3500. let foundDuration = 0;
  3501.  
  3502. let flv = await new Promise((resolve, reject) => {
  3503. let fr = new FileReader();
  3504. fr.onload = () => resolve(new FLV(new TwentyFourDataView(fr.result)));
  3505. fr.readAsArrayBuffer(blob);
  3506. fr.onerror = reject;
  3507. });
  3508.  
  3509. let modifiedMediaTags = [];
  3510. for (let tag of flv.tags) {
  3511. if (tag.tagType == 0x12 && !foundDuration) {
  3512. duration += tag.getDuration();
  3513. foundDuration = 1;
  3514. if (blob == blobs[0]) {
  3515. ret.push(flv.header, flv.firstPreviousTagSize);
  3516. ({ duration, durationDataView } = tag.getDurationAndView());
  3517. tag.stripKeyframesScriptData();
  3518. ret.push(tag.tagHeader);
  3519. ret.push(tag.tagData);
  3520. ret.push(tag.previousSize);
  3521. }
  3522. }
  3523. else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
  3524. lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
  3525. tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
  3526. modifiedMediaTags.push(tag.tagHeader, tag.tagData, tag.previousSize);
  3527. }
  3528. }
  3529. ret.push(new Blob(modifiedMediaTags));
  3530. }
  3531. durationDataView.setFloat64(0, duration);
  3532.  
  3533. return new Blob(ret);
  3534. }
  3535. }
  3536.  
  3537. var embeddedHTML = `<html>
  3538.  
  3539. <body>
  3540. <p>
  3541. 加载文件…… loading files...
  3542. <progress value="0" max="100" id="fileProgress"></progress>
  3543. </p>
  3544. <p>
  3545. 构建mkv…… building mkv...
  3546. <progress value="0" max="100" id="mkvProgress"></progress>
  3547. </p>
  3548. <p>
  3549. <a id="a" download="merged.mkv">merged.mkv</a>
  3550. </p>
  3551. <footer>
  3552. author qli5 &lt;goodlq11[at](163|gmail).com&gt;
  3553. </footer>
  3554. <script>
  3555. var FLVASS2MKV = (function () {
  3556. 'use strict';
  3557.  
  3558. /***
  3559. * Copyright (C) 2018 Qli5. All Rights Reserved.
  3560. *
  3561. * @author qli5 <goodlq11[at](163|gmail).com>
  3562. *
  3563. * This Source Code Form is subject to the terms of the Mozilla Public
  3564. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3565. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  3566. */
  3567.  
  3568. const _navigator = typeof navigator === 'object' && navigator || { userAgent: 'chrome' };
  3569.  
  3570. const _Blob = typeof Blob === 'function' && Blob || class {
  3571. constructor(array) {
  3572. return Buffer.concat(array.map(Buffer.from.bind(Buffer)));
  3573. }
  3574. };
  3575.  
  3576. const _TextEncoder = typeof TextEncoder === 'function' && TextEncoder || class {
  3577. /**
  3578. * @param {string} chunk
  3579. * @returns {Uint8Array}
  3580. */
  3581. encode(chunk) {
  3582. return Buffer.from(chunk, 'utf-8');
  3583. }
  3584. };
  3585.  
  3586. const _TextDecoder = typeof TextDecoder === 'function' && TextDecoder || class extends require('string_decoder').StringDecoder {
  3587. /**
  3588. * @param {ArrayBuffer} chunk
  3589. * @returns {string}
  3590. */
  3591. decode(chunk) {
  3592. return this.end(Buffer.from(chunk));
  3593. }
  3594. };
  3595.  
  3596. /***
  3597. * The FLV demuxer is from flv.js
  3598. *
  3599. * Copyright (C) 2016 Bilibili. All Rights Reserved.
  3600. *
  3601. * @author zheng qian <xqq@xqq.im>
  3602. *
  3603. * Licensed under the Apache License, Version 2.0 (the "License");
  3604. * you may not use this file except in compliance with the License.
  3605. * You may obtain a copy of the License at
  3606. *
  3607. * http://www.apache.org/licenses/LICENSE-2.0
  3608. *
  3609. * Unless required by applicable law or agreed to in writing, software
  3610. * distributed under the License is distributed on an "AS IS" BASIS,
  3611. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  3612. * See the License for the specific language governing permissions and
  3613. * limitations under the License.
  3614. */
  3615.  
  3616. // import FLVDemuxer from 'flv.js/src/demux/flv-demuxer.js';
  3617. // ..import Log from '../utils/logger.js';
  3618. const Log = {
  3619. e: console.error.bind(console),
  3620. w: console.warn.bind(console),
  3621. i: console.log.bind(console),
  3622. v: console.log.bind(console),
  3623. };
  3624.  
  3625. // ..import AMF from './amf-parser.js';
  3626. // ....import Log from '../utils/logger.js';
  3627. // ....import decodeUTF8 from '../utils/utf8-conv.js';
  3628. function checkContinuation(uint8array, start, checkLength) {
  3629. let array = uint8array;
  3630. if (start + checkLength < array.length) {
  3631. while (checkLength--) {
  3632. if ((array[++start] & 0xC0) !== 0x80)
  3633. return false;
  3634. }
  3635. return true;
  3636. } else {
  3637. return false;
  3638. }
  3639. }
  3640.  
  3641. function decodeUTF8(uint8array) {
  3642. let out = [];
  3643. let input = uint8array;
  3644. let i = 0;
  3645. let length = uint8array.length;
  3646.  
  3647. while (i < length) {
  3648. if (input[i] < 0x80) {
  3649. out.push(String.fromCharCode(input[i]));
  3650. ++i;
  3651. continue;
  3652. } else if (input[i] < 0xC0) {
  3653. // fallthrough
  3654. } else if (input[i] < 0xE0) {
  3655. if (checkContinuation(input, i, 1)) {
  3656. let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F);
  3657. if (ucs4 >= 0x80) {
  3658. out.push(String.fromCharCode(ucs4 & 0xFFFF));
  3659. i += 2;
  3660. continue;
  3661. }
  3662. }
  3663. } else if (input[i] < 0xF0) {
  3664. if (checkContinuation(input, i, 2)) {
  3665. let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F;
  3666. if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) {
  3667. out.push(String.fromCharCode(ucs4 & 0xFFFF));
  3668. i += 3;
  3669. continue;
  3670. }
  3671. }
  3672. } else if (input[i] < 0xF8) {
  3673. if (checkContinuation(input, i, 3)) {
  3674. let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12
  3675. | (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F);
  3676. if (ucs4 > 0x10000 && ucs4 < 0x110000) {
  3677. ucs4 -= 0x10000;
  3678. out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800));
  3679. out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00));
  3680. i += 4;
  3681. continue;
  3682. }
  3683. }
  3684. }
  3685. out.push(String.fromCharCode(0xFFFD));
  3686. ++i;
  3687. }
  3688.  
  3689. return out.join('');
  3690. }
  3691.  
  3692. // ....import {IllegalStateException} from '../utils/exception.js';
  3693. class IllegalStateException extends Error { }
  3694.  
  3695. let le = (function () {
  3696. let buf = new ArrayBuffer(2);
  3697. (new DataView(buf)).setInt16(0, 256, true); // little-endian write
  3698. return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
  3699. })();
  3700.  
  3701. class AMF {
  3702.  
  3703. static parseScriptData(arrayBuffer, dataOffset, dataSize) {
  3704. let data = {};
  3705.  
  3706. try {
  3707. let name = AMF.parseValue(arrayBuffer, dataOffset, dataSize);
  3708. let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
  3709.  
  3710. data[name.data] = value.data;
  3711. } catch (e) {
  3712. Log.e('AMF', e.toString());
  3713. }
  3714.  
  3715. return data;
  3716. }
  3717.  
  3718. static parseObject(arrayBuffer, dataOffset, dataSize) {
  3719. if (dataSize < 3) {
  3720. throw new IllegalStateException('Data not enough when parse ScriptDataObject');
  3721. }
  3722. let name = AMF.parseString(arrayBuffer, dataOffset, dataSize);
  3723. let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
  3724. let isObjectEnd = value.objectEnd;
  3725.  
  3726. return {
  3727. data: {
  3728. name: name.data,
  3729. value: value.data
  3730. },
  3731. size: name.size + value.size,
  3732. objectEnd: isObjectEnd
  3733. };
  3734. }
  3735.  
  3736. static parseVariable(arrayBuffer, dataOffset, dataSize) {
  3737. return AMF.parseObject(arrayBuffer, dataOffset, dataSize);
  3738. }
  3739.  
  3740. static parseString(arrayBuffer, dataOffset, dataSize) {
  3741. if (dataSize < 2) {
  3742. throw new IllegalStateException('Data not enough when parse String');
  3743. }
  3744. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  3745. let length = v.getUint16(0, !le);
  3746.  
  3747. let str;
  3748. if (length > 0) {
  3749. str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 2, length));
  3750. } else {
  3751. str = '';
  3752. }
  3753.  
  3754. return {
  3755. data: str,
  3756. size: 2 + length
  3757. };
  3758. }
  3759.  
  3760. static parseLongString(arrayBuffer, dataOffset, dataSize) {
  3761. if (dataSize < 4) {
  3762. throw new IllegalStateException('Data not enough when parse LongString');
  3763. }
  3764. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  3765. let length = v.getUint32(0, !le);
  3766.  
  3767. let str;
  3768. if (length > 0) {
  3769. str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 4, length));
  3770. } else {
  3771. str = '';
  3772. }
  3773.  
  3774. return {
  3775. data: str,
  3776. size: 4 + length
  3777. };
  3778. }
  3779.  
  3780. static parseDate(arrayBuffer, dataOffset, dataSize) {
  3781. if (dataSize < 10) {
  3782. throw new IllegalStateException('Data size invalid when parse Date');
  3783. }
  3784. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  3785. let timestamp = v.getFloat64(0, !le);
  3786. let localTimeOffset = v.getInt16(8, !le);
  3787. timestamp += localTimeOffset * 60 * 1000; // get UTC time
  3788.  
  3789. return {
  3790. data: new Date(timestamp),
  3791. size: 8 + 2
  3792. };
  3793. }
  3794.  
  3795. static parseValue(arrayBuffer, dataOffset, dataSize) {
  3796. if (dataSize < 1) {
  3797. throw new IllegalStateException('Data not enough when parse Value');
  3798. }
  3799.  
  3800. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  3801.  
  3802. let offset = 1;
  3803. let type = v.getUint8(0);
  3804. let value;
  3805. let objectEnd = false;
  3806.  
  3807. try {
  3808. switch (type) {
  3809. case 0: // Number(Double) type
  3810. value = v.getFloat64(1, !le);
  3811. offset += 8;
  3812. break;
  3813. case 1: { // Boolean type
  3814. let b = v.getUint8(1);
  3815. value = b ? true : false;
  3816. offset += 1;
  3817. break;
  3818. }
  3819. case 2: { // String type
  3820. let amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
  3821. value = amfstr.data;
  3822. offset += amfstr.size;
  3823. break;
  3824. }
  3825. case 3: { // Object(s) type
  3826. value = {};
  3827. let terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd
  3828. if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
  3829. terminal = 3;
  3830. }
  3831. while (offset < dataSize - 4) { // 4 === type(UI8) + ScriptDataObjectEnd(UI24)
  3832. let amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
  3833. if (amfobj.objectEnd)
  3834. break;
  3835. value[amfobj.data.name] = amfobj.data.value;
  3836. offset += amfobj.size;
  3837. }
  3838. if (offset <= dataSize - 3) {
  3839. let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
  3840. if (marker === 9) {
  3841. offset += 3;
  3842. }
  3843. }
  3844. break;
  3845. }
  3846. case 8: { // ECMA array type (Mixed array)
  3847. value = {};
  3848. offset += 4; // ECMAArrayLength(UI32)
  3849. let terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd
  3850. if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
  3851. terminal = 3;
  3852. }
  3853. while (offset < dataSize - 8) { // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24)
  3854. let amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
  3855. if (amfvar.objectEnd)
  3856. break;
  3857. value[amfvar.data.name] = amfvar.data.value;
  3858. offset += amfvar.size;
  3859. }
  3860. if (offset <= dataSize - 3) {
  3861. let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
  3862. if (marker === 9) {
  3863. offset += 3;
  3864. }
  3865. }
  3866. break;
  3867. }
  3868. case 9: // ScriptDataObjectEnd
  3869. value = undefined;
  3870. offset = 1;
  3871. objectEnd = true;
  3872. break;
  3873. case 10: { // Strict array type
  3874. // ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf
  3875. value = [];
  3876. let strictArrayLength = v.getUint32(1, !le);
  3877. offset += 4;
  3878. for (let i = 0; i < strictArrayLength; i++) {
  3879. let val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset);
  3880. value.push(val.data);
  3881. offset += val.size;
  3882. }
  3883. break;
  3884. }
  3885. case 11: { // Date type
  3886. let date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1);
  3887. value = date.data;
  3888. offset += date.size;
  3889. break;
  3890. }
  3891. case 12: { // Long string type
  3892. let amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
  3893. value = amfLongStr.data;
  3894. offset += amfLongStr.size;
  3895. break;
  3896. }
  3897. default:
  3898. // ignore and skip
  3899. offset = dataSize;
  3900. Log.w('AMF', 'Unsupported AMF value type ' + type);
  3901. }
  3902. } catch (e) {
  3903. Log.e('AMF', e.toString());
  3904. }
  3905.  
  3906. return {
  3907. data: value,
  3908. size: offset,
  3909. objectEnd: objectEnd
  3910. };
  3911. }
  3912.  
  3913. }
  3914.  
  3915. // ..import SPSParser from './sps-parser.js';
  3916. // ....import ExpGolomb from './exp-golomb.js';
  3917. // ......import {IllegalStateException, InvalidArgumentException} from '../utils/exception.js';
  3918. class InvalidArgumentException extends Error { }
  3919.  
  3920. class ExpGolomb {
  3921.  
  3922. constructor(uint8array) {
  3923. this.TAG = 'ExpGolomb';
  3924.  
  3925. this._buffer = uint8array;
  3926. this._buffer_index = 0;
  3927. this._total_bytes = uint8array.byteLength;
  3928. this._total_bits = uint8array.byteLength * 8;
  3929. this._current_word = 0;
  3930. this._current_word_bits_left = 0;
  3931. }
  3932.  
  3933. destroy() {
  3934. this._buffer = null;
  3935. }
  3936.  
  3937. _fillCurrentWord() {
  3938. let buffer_bytes_left = this._total_bytes - this._buffer_index;
  3939. if (buffer_bytes_left <= 0)
  3940. throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available');
  3941.  
  3942. let bytes_read = Math.min(4, buffer_bytes_left);
  3943. let word = new Uint8Array(4);
  3944. word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));
  3945. this._current_word = new DataView(word.buffer).getUint32(0, false);
  3946.  
  3947. this._buffer_index += bytes_read;
  3948. this._current_word_bits_left = bytes_read * 8;
  3949. }
  3950.  
  3951. readBits(bits) {
  3952. if (bits > 32)
  3953. throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!');
  3954.  
  3955. if (bits <= this._current_word_bits_left) {
  3956. let result = this._current_word >>> (32 - bits);
  3957. this._current_word <<= bits;
  3958. this._current_word_bits_left -= bits;
  3959. return result;
  3960. }
  3961.  
  3962. let result = this._current_word_bits_left ? this._current_word : 0;
  3963. result = result >>> (32 - this._current_word_bits_left);
  3964. let bits_need_left = bits - this._current_word_bits_left;
  3965.  
  3966. this._fillCurrentWord();
  3967. let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);
  3968.  
  3969. let result2 = this._current_word >>> (32 - bits_read_next);
  3970. this._current_word <<= bits_read_next;
  3971. this._current_word_bits_left -= bits_read_next;
  3972.  
  3973. result = (result << bits_read_next) | result2;
  3974. return result;
  3975. }
  3976.  
  3977. readBool() {
  3978. return this.readBits(1) === 1;
  3979. }
  3980.  
  3981. readByte() {
  3982. return this.readBits(8);
  3983. }
  3984.  
  3985. _skipLeadingZero() {
  3986. let zero_count;
  3987. for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {
  3988. if (0 !== (this._current_word & (0x80000000 >>> zero_count))) {
  3989. this._current_word <<= zero_count;
  3990. this._current_word_bits_left -= zero_count;
  3991. return zero_count;
  3992. }
  3993. }
  3994. this._fillCurrentWord();
  3995. return zero_count + this._skipLeadingZero();
  3996. }
  3997.  
  3998. readUEG() { // unsigned exponential golomb
  3999. let leading_zeros = this._skipLeadingZero();
  4000. return this.readBits(leading_zeros + 1) - 1;
  4001. }
  4002.  
  4003. readSEG() { // signed exponential golomb
  4004. let value = this.readUEG();
  4005. if (value & 0x01) {
  4006. return (value + 1) >>> 1;
  4007. } else {
  4008. return -1 * (value >>> 1);
  4009. }
  4010. }
  4011.  
  4012. }
  4013.  
  4014. class SPSParser {
  4015.  
  4016. static _ebsp2rbsp(uint8array) {
  4017. let src = uint8array;
  4018. let src_length = src.byteLength;
  4019. let dst = new Uint8Array(src_length);
  4020. let dst_idx = 0;
  4021.  
  4022. for (let i = 0; i < src_length; i++) {
  4023. if (i >= 2) {
  4024. // Unescape: Skip 0x03 after 00 00
  4025. if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
  4026. continue;
  4027. }
  4028. }
  4029. dst[dst_idx] = src[i];
  4030. dst_idx++;
  4031. }
  4032.  
  4033. return new Uint8Array(dst.buffer, 0, dst_idx);
  4034. }
  4035.  
  4036. static parseSPS(uint8array) {
  4037. let rbsp = SPSParser._ebsp2rbsp(uint8array);
  4038. let gb = new ExpGolomb(rbsp);
  4039.  
  4040. gb.readByte();
  4041. let profile_idc = gb.readByte(); // profile_idc
  4042. gb.readByte(); // constraint_set_flags[5] + reserved_zero[3]
  4043. let level_idc = gb.readByte(); // level_idc
  4044. gb.readUEG(); // seq_parameter_set_id
  4045.  
  4046. let profile_string = SPSParser.getProfileString(profile_idc);
  4047. let level_string = SPSParser.getLevelString(level_idc);
  4048. let chroma_format_idc = 1;
  4049. let chroma_format = 420;
  4050. let chroma_format_table = [0, 420, 422, 444];
  4051. let bit_depth = 8;
  4052.  
  4053. if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 ||
  4054. profile_idc === 244 || profile_idc === 44 || profile_idc === 83 ||
  4055. profile_idc === 86 || profile_idc === 118 || profile_idc === 128 ||
  4056. profile_idc === 138 || profile_idc === 144) {
  4057.  
  4058. chroma_format_idc = gb.readUEG();
  4059. if (chroma_format_idc === 3) {
  4060. gb.readBits(1); // separate_colour_plane_flag
  4061. }
  4062. if (chroma_format_idc <= 3) {
  4063. chroma_format = chroma_format_table[chroma_format_idc];
  4064. }
  4065.  
  4066. bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8
  4067. gb.readUEG(); // bit_depth_chroma_minus8
  4068. gb.readBits(1); // qpprime_y_zero_transform_bypass_flag
  4069. if (gb.readBool()) { // seq_scaling_matrix_present_flag
  4070. let scaling_list_count = (chroma_format_idc !== 3) ? 8 : 12;
  4071. for (let i = 0; i < scaling_list_count; i++) {
  4072. if (gb.readBool()) { // seq_scaling_list_present_flag
  4073. if (i < 6) {
  4074. SPSParser._skipScalingList(gb, 16);
  4075. } else {
  4076. SPSParser._skipScalingList(gb, 64);
  4077. }
  4078. }
  4079. }
  4080. }
  4081. }
  4082. gb.readUEG(); // log2_max_frame_num_minus4
  4083. let pic_order_cnt_type = gb.readUEG();
  4084. if (pic_order_cnt_type === 0) {
  4085. gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4
  4086. } else if (pic_order_cnt_type === 1) {
  4087. gb.readBits(1); // delta_pic_order_always_zero_flag
  4088. gb.readSEG(); // offset_for_non_ref_pic
  4089. gb.readSEG(); // offset_for_top_to_bottom_field
  4090. let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();
  4091. for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
  4092. gb.readSEG(); // offset_for_ref_frame
  4093. }
  4094. }
  4095. gb.readUEG(); // max_num_ref_frames
  4096. gb.readBits(1); // gaps_in_frame_num_value_allowed_flag
  4097.  
  4098. let pic_width_in_mbs_minus1 = gb.readUEG();
  4099. let pic_height_in_map_units_minus1 = gb.readUEG();
  4100.  
  4101. let frame_mbs_only_flag = gb.readBits(1);
  4102. if (frame_mbs_only_flag === 0) {
  4103. gb.readBits(1); // mb_adaptive_frame_field_flag
  4104. }
  4105. gb.readBits(1); // direct_8x8_inference_flag
  4106.  
  4107. let frame_crop_left_offset = 0;
  4108. let frame_crop_right_offset = 0;
  4109. let frame_crop_top_offset = 0;
  4110. let frame_crop_bottom_offset = 0;
  4111.  
  4112. let frame_cropping_flag = gb.readBool();
  4113. if (frame_cropping_flag) {
  4114. frame_crop_left_offset = gb.readUEG();
  4115. frame_crop_right_offset = gb.readUEG();
  4116. frame_crop_top_offset = gb.readUEG();
  4117. frame_crop_bottom_offset = gb.readUEG();
  4118. }
  4119.  
  4120. let sar_width = 1, sar_height = 1;
  4121. let fps = 0, fps_fixed = true, fps_num = 0, fps_den = 0;
  4122.  
  4123. let vui_parameters_present_flag = gb.readBool();
  4124. if (vui_parameters_present_flag) {
  4125. if (gb.readBool()) { // aspect_ratio_info_present_flag
  4126. let aspect_ratio_idc = gb.readByte();
  4127. let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];
  4128. let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1];
  4129.  
  4130. if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {
  4131. sar_width = sar_w_table[aspect_ratio_idc - 1];
  4132. sar_height = sar_h_table[aspect_ratio_idc - 1];
  4133. } else if (aspect_ratio_idc === 255) {
  4134. sar_width = gb.readByte() << 8 | gb.readByte();
  4135. sar_height = gb.readByte() << 8 | gb.readByte();
  4136. }
  4137. }
  4138.  
  4139. if (gb.readBool()) { // overscan_info_present_flag
  4140. gb.readBool(); // overscan_appropriate_flag
  4141. }
  4142. if (gb.readBool()) { // video_signal_type_present_flag
  4143. gb.readBits(4); // video_format & video_full_range_flag
  4144. if (gb.readBool()) { // colour_description_present_flag
  4145. gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
  4146. }
  4147. }
  4148. if (gb.readBool()) { // chroma_loc_info_present_flag
  4149. gb.readUEG(); // chroma_sample_loc_type_top_field
  4150. gb.readUEG(); // chroma_sample_loc_type_bottom_field
  4151. }
  4152. if (gb.readBool()) { // timing_info_present_flag
  4153. let num_units_in_tick = gb.readBits(32);
  4154. let time_scale = gb.readBits(32);
  4155. fps_fixed = gb.readBool(); // fixed_frame_rate_flag
  4156.  
  4157. fps_num = time_scale;
  4158. fps_den = num_units_in_tick * 2;
  4159. fps = fps_num / fps_den;
  4160. }
  4161. }
  4162.  
  4163. let sarScale = 1;
  4164. if (sar_width !== 1 || sar_height !== 1) {
  4165. sarScale = sar_width / sar_height;
  4166. }
  4167.  
  4168. let crop_unit_x = 0, crop_unit_y = 0;
  4169. if (chroma_format_idc === 0) {
  4170. crop_unit_x = 1;
  4171. crop_unit_y = 2 - frame_mbs_only_flag;
  4172. } else {
  4173. let sub_wc = (chroma_format_idc === 3) ? 1 : 2;
  4174. let sub_hc = (chroma_format_idc === 1) ? 2 : 1;
  4175. crop_unit_x = sub_wc;
  4176. crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);
  4177. }
  4178.  
  4179. let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;
  4180. let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);
  4181.  
  4182. codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
  4183. codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
  4184.  
  4185. let present_width = Math.ceil(codec_width * sarScale);
  4186.  
  4187. gb.destroy();
  4188. gb = null;
  4189.  
  4190. return {
  4191. profile_string: profile_string, // baseline, high, high10, ...
  4192. level_string: level_string, // 3, 3.1, 4, 4.1, 5, 5.1, ...
  4193. bit_depth: bit_depth, // 8bit, 10bit, ...
  4194. chroma_format: chroma_format, // 4:2:0, 4:2:2, ...
  4195. chroma_format_string: SPSParser.getChromaFormatString(chroma_format),
  4196.  
  4197. frame_rate: {
  4198. fixed: fps_fixed,
  4199. fps: fps,
  4200. fps_den: fps_den,
  4201. fps_num: fps_num
  4202. },
  4203.  
  4204. sar_ratio: {
  4205. width: sar_width,
  4206. height: sar_height
  4207. },
  4208.  
  4209. codec_size: {
  4210. width: codec_width,
  4211. height: codec_height
  4212. },
  4213.  
  4214. present_size: {
  4215. width: present_width,
  4216. height: codec_height
  4217. }
  4218. };
  4219. }
  4220.  
  4221. static _skipScalingList(gb, count) {
  4222. let last_scale = 8, next_scale = 8;
  4223. let delta_scale = 0;
  4224. for (let i = 0; i < count; i++) {
  4225. if (next_scale !== 0) {
  4226. delta_scale = gb.readSEG();
  4227. next_scale = (last_scale + delta_scale + 256) % 256;
  4228. }
  4229. last_scale = (next_scale === 0) ? last_scale : next_scale;
  4230. }
  4231. }
  4232.  
  4233. static getProfileString(profile_idc) {
  4234. switch (profile_idc) {
  4235. case 66:
  4236. return 'Baseline';
  4237. case 77:
  4238. return 'Main';
  4239. case 88:
  4240. return 'Extended';
  4241. case 100:
  4242. return 'High';
  4243. case 110:
  4244. return 'High10';
  4245. case 122:
  4246. return 'High422';
  4247. case 244:
  4248. return 'High444';
  4249. default:
  4250. return 'Unknown';
  4251. }
  4252. }
  4253.  
  4254. static getLevelString(level_idc) {
  4255. return (level_idc / 10).toFixed(1);
  4256. }
  4257.  
  4258. static getChromaFormatString(chroma) {
  4259. switch (chroma) {
  4260. case 420:
  4261. return '4:2:0';
  4262. case 422:
  4263. return '4:2:2';
  4264. case 444:
  4265. return '4:4:4';
  4266. default:
  4267. return 'Unknown';
  4268. }
  4269. }
  4270.  
  4271. }
  4272.  
  4273. // ..import DemuxErrors from './demux-errors.js';
  4274. const DemuxErrors = {
  4275. OK: 'OK',
  4276. FORMAT_ERROR: 'FormatError',
  4277. FORMAT_UNSUPPORTED: 'FormatUnsupported',
  4278. CODEC_UNSUPPORTED: 'CodecUnsupported'
  4279. };
  4280.  
  4281. // ..import MediaInfo from '../core/media-info.js';
  4282. class MediaInfo {
  4283.  
  4284. constructor() {
  4285. this.mimeType = null;
  4286. this.duration = null;
  4287.  
  4288. this.hasAudio = null;
  4289. this.hasVideo = null;
  4290. this.audioCodec = null;
  4291. this.videoCodec = null;
  4292. this.audioDataRate = null;
  4293. this.videoDataRate = null;
  4294.  
  4295. this.audioSampleRate = null;
  4296. this.audioChannelCount = null;
  4297.  
  4298. this.width = null;
  4299. this.height = null;
  4300. this.fps = null;
  4301. this.profile = null;
  4302. this.level = null;
  4303. this.chromaFormat = null;
  4304. this.sarNum = null;
  4305. this.sarDen = null;
  4306.  
  4307. this.metadata = null;
  4308. this.segments = null; // MediaInfo[]
  4309. this.segmentCount = null;
  4310. this.hasKeyframesIndex = null;
  4311. this.keyframesIndex = null;
  4312. }
  4313.  
  4314. isComplete() {
  4315. let audioInfoComplete = (this.hasAudio === false) ||
  4316. (this.hasAudio === true &&
  4317. this.audioCodec != null &&
  4318. this.audioSampleRate != null &&
  4319. this.audioChannelCount != null);
  4320.  
  4321. let videoInfoComplete = (this.hasVideo === false) ||
  4322. (this.hasVideo === true &&
  4323. this.videoCodec != null &&
  4324. this.width != null &&
  4325. this.height != null &&
  4326. this.fps != null &&
  4327. this.profile != null &&
  4328. this.level != null &&
  4329. this.chromaFormat != null &&
  4330. this.sarNum != null &&
  4331. this.sarDen != null);
  4332.  
  4333. // keyframesIndex may not be present
  4334. return this.mimeType != null &&
  4335. this.duration != null &&
  4336. this.metadata != null &&
  4337. this.hasKeyframesIndex != null &&
  4338. audioInfoComplete &&
  4339. videoInfoComplete;
  4340. }
  4341.  
  4342. isSeekable() {
  4343. return this.hasKeyframesIndex === true;
  4344. }
  4345.  
  4346. getNearestKeyframe(milliseconds) {
  4347. if (this.keyframesIndex == null) {
  4348. return null;
  4349. }
  4350.  
  4351. let table = this.keyframesIndex;
  4352. let keyframeIdx = this._search(table.times, milliseconds);
  4353.  
  4354. return {
  4355. index: keyframeIdx,
  4356. milliseconds: table.times[keyframeIdx],
  4357. fileposition: table.filepositions[keyframeIdx]
  4358. };
  4359. }
  4360.  
  4361. _search(list, value) {
  4362. let idx = 0;
  4363.  
  4364. let last = list.length - 1;
  4365. let mid = 0;
  4366. let lbound = 0;
  4367. let ubound = last;
  4368.  
  4369. if (value < list[0]) {
  4370. idx = 0;
  4371. lbound = ubound + 1; // skip search
  4372. }
  4373.  
  4374. while (lbound <= ubound) {
  4375. mid = lbound + Math.floor((ubound - lbound) / 2);
  4376. if (mid === last || (value >= list[mid] && value < list[mid + 1])) {
  4377. idx = mid;
  4378. break;
  4379. } else if (list[mid] < value) {
  4380. lbound = mid + 1;
  4381. } else {
  4382. ubound = mid - 1;
  4383. }
  4384. }
  4385.  
  4386. return idx;
  4387. }
  4388.  
  4389. }
  4390.  
  4391. function ReadBig32(array, index) {
  4392. return ((array[index] << 24) |
  4393. (array[index + 1] << 16) |
  4394. (array[index + 2] << 8) |
  4395. (array[index + 3]));
  4396. }
  4397.  
  4398. class FLVDemuxer {
  4399.  
  4400. /**
  4401. * Create a new FLV demuxer
  4402. * @param {Object} probeData
  4403. * @param {boolean} probeData.match
  4404. * @param {number} probeData.consumed
  4405. * @param {number} probeData.dataOffset
  4406. * @param {booleam} probeData.hasAudioTrack
  4407. * @param {boolean} probeData.hasVideoTrack
  4408. * @param {*} config
  4409. */
  4410. constructor(probeData, config) {
  4411. this.TAG = 'FLVDemuxer';
  4412.  
  4413. this._config = config;
  4414.  
  4415. this._onError = null;
  4416. this._onMediaInfo = null;
  4417. this._onTrackMetadata = null;
  4418. this._onDataAvailable = null;
  4419.  
  4420. this._dataOffset = probeData.dataOffset;
  4421. this._firstParse = true;
  4422. this._dispatch = false;
  4423.  
  4424. this._hasAudio = probeData.hasAudioTrack;
  4425. this._hasVideo = probeData.hasVideoTrack;
  4426.  
  4427. this._hasAudioFlagOverrided = false;
  4428. this._hasVideoFlagOverrided = false;
  4429.  
  4430. this._audioInitialMetadataDispatched = false;
  4431. this._videoInitialMetadataDispatched = false;
  4432.  
  4433. this._mediaInfo = new MediaInfo();
  4434. this._mediaInfo.hasAudio = this._hasAudio;
  4435. this._mediaInfo.hasVideo = this._hasVideo;
  4436. this._metadata = null;
  4437. this._audioMetadata = null;
  4438. this._videoMetadata = null;
  4439.  
  4440. this._naluLengthSize = 4;
  4441. this._timestampBase = 0; // int32, in milliseconds
  4442. this._timescale = 1000;
  4443. this._duration = 0; // int32, in milliseconds
  4444. this._durationOverrided = false;
  4445. this._referenceFrameRate = {
  4446. fixed: true,
  4447. fps: 23.976,
  4448. fps_num: 23976,
  4449. fps_den: 1000
  4450. };
  4451.  
  4452. this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];
  4453.  
  4454. this._mpegSamplingRates = [
  4455. 96000, 88200, 64000, 48000, 44100, 32000,
  4456. 24000, 22050, 16000, 12000, 11025, 8000, 7350
  4457. ];
  4458.  
  4459. this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0];
  4460. this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0];
  4461. this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0];
  4462.  
  4463. this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1];
  4464. this._mpegAudioL2BitRateTable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1];
  4465. this._mpegAudioL3BitRateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1];
  4466.  
  4467. this._videoTrack = { type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0 };
  4468. this._audioTrack = { type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0 };
  4469.  
  4470. this._littleEndian = (function () {
  4471. let buf = new ArrayBuffer(2);
  4472. (new DataView(buf)).setInt16(0, 256, true); // little-endian write
  4473. return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
  4474. })();
  4475. }
  4476.  
  4477. destroy() {
  4478. this._mediaInfo = null;
  4479. this._metadata = null;
  4480. this._audioMetadata = null;
  4481. this._videoMetadata = null;
  4482. this._videoTrack = null;
  4483. this._audioTrack = null;
  4484.  
  4485. this._onError = null;
  4486. this._onMediaInfo = null;
  4487. this._onTrackMetadata = null;
  4488. this._onDataAvailable = null;
  4489. }
  4490.  
  4491. /**
  4492. * Probe the flv data
  4493. * @param {ArrayBuffer} buffer
  4494. * @returns {Object} - probeData to be feed into constructor
  4495. */
  4496. static probe(buffer) {
  4497. let data = new Uint8Array(buffer);
  4498. let mismatch = { match: false };
  4499.  
  4500. if (data[0] !== 0x46 || data[1] !== 0x4C || data[2] !== 0x56 || data[3] !== 0x01) {
  4501. return mismatch;
  4502. }
  4503.  
  4504. let hasAudio = ((data[4] & 4) >>> 2) !== 0;
  4505. let hasVideo = (data[4] & 1) !== 0;
  4506.  
  4507. let offset = ReadBig32(data, 5);
  4508.  
  4509. if (offset < 9) {
  4510. return mismatch;
  4511. }
  4512.  
  4513. return {
  4514. match: true,
  4515. consumed: offset,
  4516. dataOffset: offset,
  4517. hasAudioTrack: hasAudio,
  4518. hasVideoTrack: hasVideo
  4519. };
  4520. }
  4521.  
  4522. bindDataSource(loader) {
  4523. loader.onDataArrival = this.parseChunks.bind(this);
  4524. return this;
  4525. }
  4526.  
  4527. // prototype: function(type: string, metadata: any): void
  4528. get onTrackMetadata() {
  4529. return this._onTrackMetadata;
  4530. }
  4531.  
  4532. set onTrackMetadata(callback) {
  4533. this._onTrackMetadata = callback;
  4534. }
  4535.  
  4536. // prototype: function(mediaInfo: MediaInfo): void
  4537. get onMediaInfo() {
  4538. return this._onMediaInfo;
  4539. }
  4540.  
  4541. set onMediaInfo(callback) {
  4542. this._onMediaInfo = callback;
  4543. }
  4544.  
  4545. // prototype: function(type: number, info: string): void
  4546. get onError() {
  4547. return this._onError;
  4548. }
  4549.  
  4550. set onError(callback) {
  4551. this._onError = callback;
  4552. }
  4553.  
  4554. // prototype: function(videoTrack: any, audioTrack: any): void
  4555. get onDataAvailable() {
  4556. return this._onDataAvailable;
  4557. }
  4558.  
  4559. set onDataAvailable(callback) {
  4560. this._onDataAvailable = callback;
  4561. }
  4562.  
  4563. // timestamp base for output samples, must be in milliseconds
  4564. get timestampBase() {
  4565. return this._timestampBase;
  4566. }
  4567.  
  4568. set timestampBase(base) {
  4569. this._timestampBase = base;
  4570. }
  4571.  
  4572. get overridedDuration() {
  4573. return this._duration;
  4574. }
  4575.  
  4576. // Force-override media duration. Must be in milliseconds, int32
  4577. set overridedDuration(duration) {
  4578. this._durationOverrided = true;
  4579. this._duration = duration;
  4580. this._mediaInfo.duration = duration;
  4581. }
  4582.  
  4583. // Force-override audio track present flag, boolean
  4584. set overridedHasAudio(hasAudio) {
  4585. this._hasAudioFlagOverrided = true;
  4586. this._hasAudio = hasAudio;
  4587. this._mediaInfo.hasAudio = hasAudio;
  4588. }
  4589.  
  4590. // Force-override video track present flag, boolean
  4591. set overridedHasVideo(hasVideo) {
  4592. this._hasVideoFlagOverrided = true;
  4593. this._hasVideo = hasVideo;
  4594. this._mediaInfo.hasVideo = hasVideo;
  4595. }
  4596.  
  4597. resetMediaInfo() {
  4598. this._mediaInfo = new MediaInfo();
  4599. }
  4600.  
  4601. _isInitialMetadataDispatched() {
  4602. if (this._hasAudio && this._hasVideo) { // both audio & video
  4603. return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched;
  4604. }
  4605. if (this._hasAudio && !this._hasVideo) { // audio only
  4606. return this._audioInitialMetadataDispatched;
  4607. }
  4608. if (!this._hasAudio && this._hasVideo) { // video only
  4609. return this._videoInitialMetadataDispatched;
  4610. }
  4611. return false;
  4612. }
  4613.  
  4614. // function parseChunks(chunk: ArrayBuffer, byteStart: number): number;
  4615. parseChunks(chunk, byteStart) {
  4616. if (!this._onError || !this._onMediaInfo || !this._onTrackMetadata || !this._onDataAvailable) {
  4617. throw new IllegalStateException('Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified');
  4618. }
  4619.  
  4620. // qli5: fix nonzero byteStart
  4621. let offset = byteStart || 0;
  4622. let le = this._littleEndian;
  4623.  
  4624. if (byteStart === 0) { // buffer with FLV header
  4625. if (chunk.byteLength > 13) {
  4626. let probeData = FLVDemuxer.probe(chunk);
  4627. offset = probeData.dataOffset;
  4628. } else {
  4629. return 0;
  4630. }
  4631. }
  4632.  
  4633. if (this._firstParse) { // handle PreviousTagSize0 before Tag1
  4634. this._firstParse = false;
  4635. if (offset !== this._dataOffset) {
  4636. Log.w(this.TAG, 'First time parsing but chunk byteStart invalid!');
  4637. }
  4638.  
  4639. let v = new DataView(chunk, offset);
  4640. let prevTagSize0 = v.getUint32(0, !le);
  4641. if (prevTagSize0 !== 0) {
  4642. Log.w(this.TAG, 'PrevTagSize0 !== 0 !!!');
  4643. }
  4644. offset += 4;
  4645. }
  4646.  
  4647. while (offset < chunk.byteLength) {
  4648. this._dispatch = true;
  4649.  
  4650. let v = new DataView(chunk, offset);
  4651.  
  4652. if (offset + 11 + 4 > chunk.byteLength) {
  4653. // data not enough for parsing an flv tag
  4654. break;
  4655. }
  4656.  
  4657. let tagType = v.getUint8(0);
  4658. let dataSize = v.getUint32(0, !le) & 0x00FFFFFF;
  4659.  
  4660. if (offset + 11 + dataSize + 4 > chunk.byteLength) {
  4661. // data not enough for parsing actual data body
  4662. break;
  4663. }
  4664.  
  4665. if (tagType !== 8 && tagType !== 9 && tagType !== 18) {
  4666. Log.w(this.TAG, \`Unsupported tag type \${tagType}, skipped\`);
  4667. // consume the whole tag (skip it)
  4668. offset += 11 + dataSize + 4;
  4669. continue;
  4670. }
  4671.  
  4672. let ts2 = v.getUint8(4);
  4673. let ts1 = v.getUint8(5);
  4674. let ts0 = v.getUint8(6);
  4675. let ts3 = v.getUint8(7);
  4676.  
  4677. let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);
  4678.  
  4679. let streamId = v.getUint32(7, !le) & 0x00FFFFFF;
  4680. if (streamId !== 0) {
  4681. Log.w(this.TAG, 'Meet tag which has StreamID != 0!');
  4682. }
  4683.  
  4684. let dataOffset = offset + 11;
  4685.  
  4686. switch (tagType) {
  4687. case 8: // Audio
  4688. this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
  4689. break;
  4690. case 9: // Video
  4691. this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
  4692. break;
  4693. case 18: // ScriptDataObject
  4694. this._parseScriptData(chunk, dataOffset, dataSize);
  4695. break;
  4696. }
  4697.  
  4698. let prevTagSize = v.getUint32(11 + dataSize, !le);
  4699. if (prevTagSize !== 11 + dataSize) {
  4700. Log.w(this.TAG, \`Invalid PrevTagSize \${prevTagSize}\`);
  4701. }
  4702.  
  4703. offset += 11 + dataSize + 4; // tagBody + dataSize + prevTagSize
  4704. }
  4705.  
  4706. // dispatch parsed frames to consumer (typically, the remuxer)
  4707. if (this._isInitialMetadataDispatched()) {
  4708. if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
  4709. this._onDataAvailable(this._audioTrack, this._videoTrack);
  4710. }
  4711. }
  4712.  
  4713. return offset; // consumed bytes, just equals latest offset index
  4714. }
  4715.  
  4716. _parseScriptData(arrayBuffer, dataOffset, dataSize) {
  4717. let scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);
  4718.  
  4719. if (scriptData.hasOwnProperty('onMetaData')) {
  4720. if (scriptData.onMetaData == null || typeof scriptData.onMetaData !== 'object') {
  4721. Log.w(this.TAG, 'Invalid onMetaData structure!');
  4722. return;
  4723. }
  4724. if (this._metadata) {
  4725. Log.w(this.TAG, 'Found another onMetaData tag!');
  4726. }
  4727. this._metadata = scriptData;
  4728. let onMetaData = this._metadata.onMetaData;
  4729.  
  4730. if (typeof onMetaData.hasAudio === 'boolean') { // hasAudio
  4731. if (this._hasAudioFlagOverrided === false) {
  4732. this._hasAudio = onMetaData.hasAudio;
  4733. this._mediaInfo.hasAudio = this._hasAudio;
  4734. }
  4735. }
  4736. if (typeof onMetaData.hasVideo === 'boolean') { // hasVideo
  4737. if (this._hasVideoFlagOverrided === false) {
  4738. this._hasVideo = onMetaData.hasVideo;
  4739. this._mediaInfo.hasVideo = this._hasVideo;
  4740. }
  4741. }
  4742. if (typeof onMetaData.audiodatarate === 'number') { // audiodatarate
  4743. this._mediaInfo.audioDataRate = onMetaData.audiodatarate;
  4744. }
  4745. if (typeof onMetaData.videodatarate === 'number') { // videodatarate
  4746. this._mediaInfo.videoDataRate = onMetaData.videodatarate;
  4747. }
  4748. if (typeof onMetaData.width === 'number') { // width
  4749. this._mediaInfo.width = onMetaData.width;
  4750. }
  4751. if (typeof onMetaData.height === 'number') { // height
  4752. this._mediaInfo.height = onMetaData.height;
  4753. }
  4754. if (typeof onMetaData.duration === 'number') { // duration
  4755. if (!this._durationOverrided) {
  4756. let duration = Math.floor(onMetaData.duration * this._timescale);
  4757. this._duration = duration;
  4758. this._mediaInfo.duration = duration;
  4759. }
  4760. } else {
  4761. this._mediaInfo.duration = 0;
  4762. }
  4763. if (typeof onMetaData.framerate === 'number') { // framerate
  4764. let fps_num = Math.floor(onMetaData.framerate * 1000);
  4765. if (fps_num > 0) {
  4766. let fps = fps_num / 1000;
  4767. this._referenceFrameRate.fixed = true;
  4768. this._referenceFrameRate.fps = fps;
  4769. this._referenceFrameRate.fps_num = fps_num;
  4770. this._referenceFrameRate.fps_den = 1000;
  4771. this._mediaInfo.fps = fps;
  4772. }
  4773. }
  4774. if (typeof onMetaData.keyframes === 'object') { // keyframes
  4775. this._mediaInfo.hasKeyframesIndex = true;
  4776. let keyframes = onMetaData.keyframes;
  4777. this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes);
  4778. onMetaData.keyframes = null; // keyframes has been extracted, remove it
  4779. } else {
  4780. this._mediaInfo.hasKeyframesIndex = false;
  4781. }
  4782. this._dispatch = false;
  4783. this._mediaInfo.metadata = onMetaData;
  4784. Log.v(this.TAG, 'Parsed onMetaData');
  4785. if (this._mediaInfo.isComplete()) {
  4786. this._onMediaInfo(this._mediaInfo);
  4787. }
  4788. }
  4789. }
  4790.  
  4791. _parseKeyframesIndex(keyframes) {
  4792. let times = [];
  4793. let filepositions = [];
  4794.  
  4795. // ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord)
  4796. for (let i = 1; i < keyframes.times.length; i++) {
  4797. let time = this._timestampBase + Math.floor(keyframes.times[i] * 1000);
  4798. times.push(time);
  4799. filepositions.push(keyframes.filepositions[i]);
  4800. }
  4801.  
  4802. return {
  4803. times: times,
  4804. filepositions: filepositions
  4805. };
  4806. }
  4807.  
  4808. _parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
  4809. if (dataSize <= 1) {
  4810. Log.w(this.TAG, 'Flv: Invalid audio packet, missing SoundData payload!');
  4811. return;
  4812. }
  4813.  
  4814. if (this._hasAudioFlagOverrided === true && this._hasAudio === false) {
  4815. // If hasAudio: false indicated explicitly in MediaDataSource,
  4816. // Ignore all the audio packets
  4817. return;
  4818. }
  4819.  
  4820. let le = this._littleEndian;
  4821. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  4822.  
  4823. let soundSpec = v.getUint8(0);
  4824.  
  4825. let soundFormat = soundSpec >>> 4;
  4826. if (soundFormat !== 2 && soundFormat !== 10) { // MP3 or AAC
  4827. this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat);
  4828. return;
  4829. }
  4830.  
  4831. let soundRate = 0;
  4832. let soundRateIndex = (soundSpec & 12) >>> 2;
  4833. if (soundRateIndex >= 0 && soundRateIndex <= 4) {
  4834. soundRate = this._flvSoundRateTable[soundRateIndex];
  4835. } else {
  4836. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex);
  4837. return;
  4838. }
  4839. let soundType = (soundSpec & 1);
  4840.  
  4841.  
  4842. let meta = this._audioMetadata;
  4843. let track = this._audioTrack;
  4844.  
  4845. if (!meta) {
  4846. if (this._hasAudio === false && this._hasAudioFlagOverrided === false) {
  4847. this._hasAudio = true;
  4848. this._mediaInfo.hasAudio = true;
  4849. }
  4850.  
  4851. // initial metadata
  4852. meta = this._audioMetadata = {};
  4853. meta.type = 'audio';
  4854. meta.id = track.id;
  4855. meta.timescale = this._timescale;
  4856. meta.duration = this._duration;
  4857. meta.audioSampleRate = soundRate;
  4858. meta.channelCount = (soundType === 0 ? 1 : 2);
  4859. }
  4860.  
  4861. if (soundFormat === 10) { // AAC
  4862. let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1);
  4863. if (aacData == undefined) {
  4864. return;
  4865. }
  4866.  
  4867. if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig)
  4868. if (meta.config) {
  4869. Log.w(this.TAG, 'Found another AudioSpecificConfig!');
  4870. }
  4871. let misc = aacData.data;
  4872. meta.audioSampleRate = misc.samplingRate;
  4873. meta.channelCount = misc.channelCount;
  4874. meta.codec = misc.codec;
  4875. meta.originalCodec = misc.originalCodec;
  4876. meta.config = misc.config;
  4877. // added by qli5
  4878. meta.configRaw = misc.configRaw;
  4879. // The decode result of an aac sample is 1024 PCM samples
  4880. meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale;
  4881. Log.v(this.TAG, 'Parsed AudioSpecificConfig');
  4882.  
  4883. if (this._isInitialMetadataDispatched()) {
  4884. // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer
  4885. if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
  4886. this._onDataAvailable(this._audioTrack, this._videoTrack);
  4887. }
  4888. } else {
  4889. this._audioInitialMetadataDispatched = true;
  4890. }
  4891. // then notify new metadata
  4892. this._dispatch = false;
  4893. this._onTrackMetadata('audio', meta);
  4894.  
  4895. let mi = this._mediaInfo;
  4896. mi.audioCodec = meta.originalCodec;
  4897. mi.audioSampleRate = meta.audioSampleRate;
  4898. mi.audioChannelCount = meta.channelCount;
  4899. if (mi.hasVideo) {
  4900. if (mi.videoCodec != null) {
  4901. mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
  4902. }
  4903. } else {
  4904. mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
  4905. }
  4906. if (mi.isComplete()) {
  4907. this._onMediaInfo(mi);
  4908. }
  4909. } else if (aacData.packetType === 1) { // AAC raw frame data
  4910. let dts = this._timestampBase + tagTimestamp;
  4911. let aacSample = { unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts };
  4912. track.samples.push(aacSample);
  4913. track.length += aacData.data.length;
  4914. } else {
  4915. Log.e(this.TAG, \`Flv: Unsupported AAC data type \${aacData.packetType}\`);
  4916. }
  4917. } else if (soundFormat === 2) { // MP3
  4918. if (!meta.codec) {
  4919. // We need metadata for mp3 audio track, extract info from frame header
  4920. let misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true);
  4921. if (misc == undefined) {
  4922. return;
  4923. }
  4924. meta.audioSampleRate = misc.samplingRate;
  4925. meta.channelCount = misc.channelCount;
  4926. meta.codec = misc.codec;
  4927. meta.originalCodec = misc.originalCodec;
  4928. // The decode result of an mp3 sample is 1152 PCM samples
  4929. meta.refSampleDuration = 1152 / meta.audioSampleRate * meta.timescale;
  4930. Log.v(this.TAG, 'Parsed MPEG Audio Frame Header');
  4931.  
  4932. this._audioInitialMetadataDispatched = true;
  4933. this._onTrackMetadata('audio', meta);
  4934.  
  4935. let mi = this._mediaInfo;
  4936. mi.audioCodec = meta.codec;
  4937. mi.audioSampleRate = meta.audioSampleRate;
  4938. mi.audioChannelCount = meta.channelCount;
  4939. mi.audioDataRate = misc.bitRate;
  4940. if (mi.hasVideo) {
  4941. if (mi.videoCodec != null) {
  4942. mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
  4943. }
  4944. } else {
  4945. mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
  4946. }
  4947. if (mi.isComplete()) {
  4948. this._onMediaInfo(mi);
  4949. }
  4950. }
  4951.  
  4952. // This packet is always a valid audio packet, extract it
  4953. let data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false);
  4954. if (data == undefined) {
  4955. return;
  4956. }
  4957. let dts = this._timestampBase + tagTimestamp;
  4958. let mp3Sample = { unit: data, length: data.byteLength, dts: dts, pts: dts };
  4959. track.samples.push(mp3Sample);
  4960. track.length += data.length;
  4961. }
  4962. }
  4963.  
  4964. _parseAACAudioData(arrayBuffer, dataOffset, dataSize) {
  4965. if (dataSize <= 1) {
  4966. Log.w(this.TAG, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!');
  4967. return;
  4968. }
  4969.  
  4970. let result = {};
  4971. let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
  4972.  
  4973. result.packetType = array[0];
  4974.  
  4975. if (array[0] === 0) {
  4976. result.data = this._parseAACAudioSpecificConfig(arrayBuffer, dataOffset + 1, dataSize - 1);
  4977. } else {
  4978. result.data = array.subarray(1);
  4979. }
  4980.  
  4981. return result;
  4982. }
  4983.  
  4984. _parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {
  4985. let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
  4986. let config = null;
  4987.  
  4988. /* Audio Object Type:
  4989. 0: Null
  4990. 1: AAC Main
  4991. 2: AAC LC
  4992. 3: AAC SSR (Scalable Sample Rate)
  4993. 4: AAC LTP (Long Term Prediction)
  4994. 5: HE-AAC / SBR (Spectral Band Replication)
  4995. 6: AAC Scalable
  4996. */
  4997.  
  4998. let audioObjectType = 0;
  4999. let originalAudioObjectType = 0;
  5000. let samplingIndex = 0;
  5001. let extensionSamplingIndex = null;
  5002.  
  5003. // 5 bits
  5004. audioObjectType = originalAudioObjectType = array[0] >>> 3;
  5005. // 4 bits
  5006. samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);
  5007. if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {
  5008. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!');
  5009. return;
  5010. }
  5011.  
  5012. let samplingFrequence = this._mpegSamplingRates[samplingIndex];
  5013.  
  5014. // 4 bits
  5015. let channelConfig = (array[1] & 0x78) >>> 3;
  5016. if (channelConfig < 0 || channelConfig >= 8) {
  5017. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid channel configuration');
  5018. return;
  5019. }
  5020.  
  5021. if (audioObjectType === 5) { // HE-AAC?
  5022. // 4 bits
  5023. extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);
  5024. }
  5025.  
  5026. // workarounds for various browsers
  5027. let userAgent = _navigator.userAgent.toLowerCase();
  5028.  
  5029. if (userAgent.indexOf('firefox') !== -1) {
  5030. // firefox: use SBR (HE-AAC) if freq less than 24kHz
  5031. if (samplingIndex >= 6) {
  5032. audioObjectType = 5;
  5033. config = new Array(4);
  5034. extensionSamplingIndex = samplingIndex - 3;
  5035. } else { // use LC-AAC
  5036. audioObjectType = 2;
  5037. config = new Array(2);
  5038. extensionSamplingIndex = samplingIndex;
  5039. }
  5040. } else if (userAgent.indexOf('android') !== -1) {
  5041. // android: always use LC-AAC
  5042. audioObjectType = 2;
  5043. config = new Array(2);
  5044. extensionSamplingIndex = samplingIndex;
  5045. } else {
  5046. // for other browsers, e.g. chrome...
  5047. // Always use HE-AAC to make it easier to switch aac codec profile
  5048. audioObjectType = 5;
  5049. extensionSamplingIndex = samplingIndex;
  5050. config = new Array(4);
  5051.  
  5052. if (samplingIndex >= 6) {
  5053. extensionSamplingIndex = samplingIndex - 3;
  5054. } else if (channelConfig === 1) { // Mono channel
  5055. audioObjectType = 2;
  5056. config = new Array(2);
  5057. extensionSamplingIndex = samplingIndex;
  5058. }
  5059. }
  5060.  
  5061. config[0] = audioObjectType << 3;
  5062. config[0] |= (samplingIndex & 0x0F) >>> 1;
  5063. config[1] = (samplingIndex & 0x0F) << 7;
  5064. config[1] |= (channelConfig & 0x0F) << 3;
  5065. if (audioObjectType === 5) {
  5066. config[1] |= ((extensionSamplingIndex & 0x0F) >>> 1);
  5067. config[2] = (extensionSamplingIndex & 0x01) << 7;
  5068. // extended audio object type: force to 2 (LC-AAC)
  5069. config[2] |= (2 << 2);
  5070. config[3] = 0;
  5071. }
  5072.  
  5073. return {
  5074. // configRaw: added by qli5
  5075. configRaw: array,
  5076. config: config,
  5077. samplingRate: samplingFrequence,
  5078. channelCount: channelConfig,
  5079. codec: 'mp4a.40.' + audioObjectType,
  5080. originalCodec: 'mp4a.40.' + originalAudioObjectType
  5081. };
  5082. }
  5083.  
  5084. _parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) {
  5085. if (dataSize < 4) {
  5086. Log.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!');
  5087. return;
  5088. }
  5089.  
  5090. let le = this._littleEndian;
  5091. let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
  5092. let result = null;
  5093.  
  5094. if (requestHeader) {
  5095. if (array[0] !== 0xFF) {
  5096. return;
  5097. }
  5098. let ver = (array[1] >>> 3) & 0x03;
  5099. let layer = (array[1] & 0x06) >> 1;
  5100.  
  5101. let bitrate_index = (array[2] & 0xF0) >>> 4;
  5102. let sampling_freq_index = (array[2] & 0x0C) >>> 2;
  5103.  
  5104. let channel_mode = (array[3] >>> 6) & 0x03;
  5105. let channel_count = channel_mode !== 3 ? 2 : 1;
  5106.  
  5107. let sample_rate = 0;
  5108. let bit_rate = 0;
  5109.  
  5110. let codec = 'mp3';
  5111.  
  5112. switch (ver) {
  5113. case 0: // MPEG 2.5
  5114. sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index];
  5115. break;
  5116. case 2: // MPEG 2
  5117. sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index];
  5118. break;
  5119. case 3: // MPEG 1
  5120. sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index];
  5121. break;
  5122. }
  5123.  
  5124. switch (layer) {
  5125. case 1: // Layer 3
  5126. if (bitrate_index < this._mpegAudioL3BitRateTable.length) {
  5127. bit_rate = this._mpegAudioL3BitRateTable[bitrate_index];
  5128. }
  5129. break;
  5130. case 2: // Layer 2
  5131. if (bitrate_index < this._mpegAudioL2BitRateTable.length) {
  5132. bit_rate = this._mpegAudioL2BitRateTable[bitrate_index];
  5133. }
  5134. break;
  5135. case 3: // Layer 1
  5136. if (bitrate_index < this._mpegAudioL1BitRateTable.length) {
  5137. bit_rate = this._mpegAudioL1BitRateTable[bitrate_index];
  5138. }
  5139. break;
  5140. }
  5141.  
  5142. result = {
  5143. bitRate: bit_rate,
  5144. samplingRate: sample_rate,
  5145. channelCount: channel_count,
  5146. codec: codec,
  5147. originalCodec: codec
  5148. };
  5149. } else {
  5150. result = array;
  5151. }
  5152.  
  5153. return result;
  5154. }
  5155.  
  5156. _parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) {
  5157. if (dataSize <= 1) {
  5158. Log.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!');
  5159. return;
  5160. }
  5161.  
  5162. if (this._hasVideoFlagOverrided === true && this._hasVideo === false) {
  5163. // If hasVideo: false indicated explicitly in MediaDataSource,
  5164. // Ignore all the video packets
  5165. return;
  5166. }
  5167.  
  5168. let spec = (new Uint8Array(arrayBuffer, dataOffset, dataSize))[0];
  5169.  
  5170. let frameType = (spec & 240) >>> 4;
  5171. let codecId = spec & 15;
  5172.  
  5173. if (codecId !== 7) {
  5174. this._onError(DemuxErrors.CODEC_UNSUPPORTED, \`Flv: Unsupported codec in video frame: \${codecId}\`);
  5175. return;
  5176. }
  5177.  
  5178. this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, dataSize - 1, tagTimestamp, tagPosition, frameType);
  5179. }
  5180.  
  5181. _parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType) {
  5182. if (dataSize < 4) {
  5183. Log.w(this.TAG, 'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime');
  5184. return;
  5185. }
  5186.  
  5187. let le = this._littleEndian;
  5188. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  5189.  
  5190. let packetType = v.getUint8(0);
  5191. let cts = v.getUint32(0, !le) & 0x00FFFFFF;
  5192.  
  5193. if (packetType === 0) { // AVCDecoderConfigurationRecord
  5194. this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4);
  5195. } else if (packetType === 1) { // One or more Nalus
  5196. this._parseAVCVideoData(arrayBuffer, dataOffset + 4, dataSize - 4, tagTimestamp, tagPosition, frameType, cts);
  5197. } else if (packetType === 2) {
  5198. // empty, AVC end of sequence
  5199. } else {
  5200. this._onError(DemuxErrors.FORMAT_ERROR, \`Flv: Invalid video packet type \${packetType}\`);
  5201. return;
  5202. }
  5203. }
  5204.  
  5205. _parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {
  5206. if (dataSize < 7) {
  5207. Log.w(this.TAG, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');
  5208. return;
  5209. }
  5210.  
  5211. let meta = this._videoMetadata;
  5212. let track = this._videoTrack;
  5213. let le = this._littleEndian;
  5214. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  5215.  
  5216. if (!meta) {
  5217. if (this._hasVideo === false && this._hasVideoFlagOverrided === false) {
  5218. this._hasVideo = true;
  5219. this._mediaInfo.hasVideo = true;
  5220. }
  5221.  
  5222. meta = this._videoMetadata = {};
  5223. meta.type = 'video';
  5224. meta.id = track.id;
  5225. meta.timescale = this._timescale;
  5226. meta.duration = this._duration;
  5227. } else {
  5228. if (typeof meta.avcc !== 'undefined') {
  5229. Log.w(this.TAG, 'Found another AVCDecoderConfigurationRecord!');
  5230. }
  5231. }
  5232.  
  5233. let version = v.getUint8(0); // configurationVersion
  5234. let avcProfile = v.getUint8(1); // avcProfileIndication
  5235. let profileCompatibility = v.getUint8(2); // profile_compatibility
  5236. let avcLevel = v.getUint8(3); // AVCLevelIndication
  5237.  
  5238. if (version !== 1 || avcProfile === 0) {
  5239. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');
  5240. return;
  5241. }
  5242.  
  5243. this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
  5244. if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) { // holy shit!!!
  5245. this._onError(DemuxErrors.FORMAT_ERROR, \`Flv: Strange NaluLengthSizeMinusOne: \${this._naluLengthSize - 1}\`);
  5246. return;
  5247. }
  5248.  
  5249. let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
  5250. if (spsCount === 0) {
  5251. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');
  5252. return;
  5253. } else if (spsCount > 1) {
  5254. Log.w(this.TAG, \`Flv: Strange AVCDecoderConfigurationRecord: SPS Count = \${spsCount}\`);
  5255. }
  5256.  
  5257. let offset = 6;
  5258.  
  5259. for (let i = 0; i < spsCount; i++) {
  5260. let len = v.getUint16(offset, !le); // sequenceParameterSetLength
  5261. offset += 2;
  5262.  
  5263. if (len === 0) {
  5264. continue;
  5265. }
  5266.  
  5267. // Notice: Nalu without startcode header (00 00 00 01)
  5268. let sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
  5269. offset += len;
  5270.  
  5271. let config = SPSParser.parseSPS(sps);
  5272. if (i !== 0) {
  5273. // ignore other sps's config
  5274. continue;
  5275. }
  5276.  
  5277. meta.codecWidth = config.codec_size.width;
  5278. meta.codecHeight = config.codec_size.height;
  5279. meta.presentWidth = config.present_size.width;
  5280. meta.presentHeight = config.present_size.height;
  5281.  
  5282. meta.profile = config.profile_string;
  5283. meta.level = config.level_string;
  5284. meta.bitDepth = config.bit_depth;
  5285. meta.chromaFormat = config.chroma_format;
  5286. meta.sarRatio = config.sar_ratio;
  5287. meta.frameRate = config.frame_rate;
  5288.  
  5289. if (config.frame_rate.fixed === false ||
  5290. config.frame_rate.fps_num === 0 ||
  5291. config.frame_rate.fps_den === 0) {
  5292. meta.frameRate = this._referenceFrameRate;
  5293. }
  5294.  
  5295. let fps_den = meta.frameRate.fps_den;
  5296. let fps_num = meta.frameRate.fps_num;
  5297. meta.refSampleDuration = meta.timescale * (fps_den / fps_num);
  5298.  
  5299. let codecArray = sps.subarray(1, 4);
  5300. let codecString = 'avc1.';
  5301. for (let j = 0; j < 3; j++) {
  5302. let h = codecArray[j].toString(16);
  5303. if (h.length < 2) {
  5304. h = '0' + h;
  5305. }
  5306. codecString += h;
  5307. }
  5308. meta.codec = codecString;
  5309.  
  5310. let mi = this._mediaInfo;
  5311. mi.width = meta.codecWidth;
  5312. mi.height = meta.codecHeight;
  5313. mi.fps = meta.frameRate.fps;
  5314. mi.profile = meta.profile;
  5315. mi.level = meta.level;
  5316. mi.chromaFormat = config.chroma_format_string;
  5317. mi.sarNum = meta.sarRatio.width;
  5318. mi.sarDen = meta.sarRatio.height;
  5319. mi.videoCodec = codecString;
  5320.  
  5321. if (mi.hasAudio) {
  5322. if (mi.audioCodec != null) {
  5323. mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
  5324. }
  5325. } else {
  5326. mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + '"';
  5327. }
  5328. if (mi.isComplete()) {
  5329. this._onMediaInfo(mi);
  5330. }
  5331. }
  5332.  
  5333. let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
  5334. if (ppsCount === 0) {
  5335. this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');
  5336. return;
  5337. } else if (ppsCount > 1) {
  5338. Log.w(this.TAG, \`Flv: Strange AVCDecoderConfigurationRecord: PPS Count = \${ppsCount}\`);
  5339. }
  5340.  
  5341. offset++;
  5342.  
  5343. for (let i = 0; i < ppsCount; i++) {
  5344. let len = v.getUint16(offset, !le); // pictureParameterSetLength
  5345. offset += 2;
  5346.  
  5347. if (len === 0) {
  5348. continue;
  5349. }
  5350.  
  5351. // pps is useless for extracting video information
  5352. offset += len;
  5353. }
  5354.  
  5355. meta.avcc = new Uint8Array(dataSize);
  5356. meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0);
  5357. Log.v(this.TAG, 'Parsed AVCDecoderConfigurationRecord');
  5358.  
  5359. if (this._isInitialMetadataDispatched()) {
  5360. // flush parsed frames
  5361. if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
  5362. this._onDataAvailable(this._audioTrack, this._videoTrack);
  5363. }
  5364. } else {
  5365. this._videoInitialMetadataDispatched = true;
  5366. }
  5367. // notify new metadata
  5368. this._dispatch = false;
  5369. this._onTrackMetadata('video', meta);
  5370. }
  5371.  
  5372. _parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType, cts) {
  5373. let le = this._littleEndian;
  5374. let v = new DataView(arrayBuffer, dataOffset, dataSize);
  5375.  
  5376. let units = [], length = 0;
  5377.  
  5378. let offset = 0;
  5379. const lengthSize = this._naluLengthSize;
  5380. let dts = this._timestampBase + tagTimestamp;
  5381. let keyframe = (frameType === 1); // from FLV Frame Type constants
  5382. let refIdc = 1; // added by qli5
  5383.  
  5384. while (offset < dataSize) {
  5385. if (offset + 4 >= dataSize) {
  5386. Log.w(this.TAG, \`Malformed Nalu near timestamp \${dts}, offset = \${offset}, dataSize = \${dataSize}\`);
  5387. break; // data not enough for next Nalu
  5388. }
  5389. // Nalu with length-header (AVC1)
  5390. let naluSize = v.getUint32(offset, !le); // Big-Endian read
  5391. if (lengthSize === 3) {
  5392. naluSize >>>= 8;
  5393. }
  5394. if (naluSize > dataSize - lengthSize) {
  5395. Log.w(this.TAG, \`Malformed Nalus near timestamp \${dts}, NaluSize > DataSize!\`);
  5396. return;
  5397. }
  5398.  
  5399. let unitType = v.getUint8(offset + lengthSize) & 0x1F;
  5400. // added by qli5
  5401. refIdc = v.getUint8(offset + lengthSize) & 0x60;
  5402.  
  5403. if (unitType === 5) { // IDR
  5404. keyframe = true;
  5405. }
  5406.  
  5407. let data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);
  5408. let unit = { type: unitType, data: data };
  5409. units.push(unit);
  5410. length += data.byteLength;
  5411.  
  5412. offset += lengthSize + naluSize;
  5413. }
  5414.  
  5415. if (units.length) {
  5416. let track = this._videoTrack;
  5417. let avcSample = {
  5418. units: units,
  5419. length: length,
  5420. isKeyframe: keyframe,
  5421. refIdc: refIdc,
  5422. dts: dts,
  5423. cts: cts,
  5424. pts: (dts + cts)
  5425. };
  5426. if (keyframe) {
  5427. avcSample.fileposition = tagPosition;
  5428. }
  5429. track.samples.push(avcSample);
  5430. track.length += length;
  5431. }
  5432. }
  5433.  
  5434. }
  5435.  
  5436. /***
  5437. * Copyright (C) 2018 Qli5. All Rights Reserved.
  5438. *
  5439. * @author qli5 <goodlq11[at](163|gmail).com>
  5440. *
  5441. * This Source Code Form is subject to the terms of the Mozilla Public
  5442. * License, v. 2.0. If a copy of the MPL was not distributed with this
  5443. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  5444. */
  5445.  
  5446. class ASS {
  5447. /**
  5448. * Extract sections from ass string
  5449. * @param {string} str
  5450. * @returns {Object} - object from sections
  5451. */
  5452. static extractSections(str) {
  5453. const regex = /^\\ufeff?\\[(.*)\\]\$/mg;
  5454. let match;
  5455. let matchArr = [];
  5456. while ((match = regex.exec(str)) !== null) {
  5457. matchArr.push({ name: match[1], index: match.index });
  5458. }
  5459. let ret = {};
  5460. matchArr.forEach((match, i) => ret[match.name] = str.slice(match.index, matchArr[i + 1] && matchArr[i + 1].index));
  5461. return ret;
  5462. }
  5463.  
  5464. /**
  5465. * Extract subtitle lines from section Events
  5466. * @param {string} str
  5467. * @returns {Array<Object>} - array of subtitle lines
  5468. */
  5469. static extractSubtitleLines(str) {
  5470. const lines = str.split(/\\r\\n+/);
  5471. if (lines[0] != '[Events]' && lines[0] != '[events]') throw new Error('ASSDemuxer: section is not [Events]');
  5472. if (lines[1].indexOf('Format:') != 0 && lines[1].indexOf('format:') != 0) throw new Error('ASSDemuxer: cannot find Format definition in section [Events]');
  5473.  
  5474. const format = lines[1].slice(lines[1].indexOf(':') + 1).split(',').map(e => e.trim());
  5475. return lines.slice(2).map(e => {
  5476. let j = {};
  5477. e.replace(/[d|D]ialogue:\\s*/, '')
  5478. .match(new RegExp(new Array(format.length - 1).fill('(.*?),').join('') + '(.*)'))
  5479. .slice(1)
  5480. .forEach((k, index) => j[format[index]] = k);
  5481. return j;
  5482. });
  5483. }
  5484.  
  5485. /**
  5486. * Create a new ASS Demuxer
  5487. */
  5488. constructor() {
  5489. this.info = '';
  5490. this.styles = '';
  5491. this.events = '';
  5492. this.eventsHeader = '';
  5493. this.pictures = '';
  5494. this.fonts = '';
  5495. this.lines = '';
  5496. }
  5497.  
  5498. get header() {
  5499. // return this.info + this.styles + this.eventsHeader;
  5500. return this.info + this.styles;
  5501. }
  5502.  
  5503. /**
  5504. * Load a file from an arraybuffer of a string
  5505. * @param {(ArrayBuffer|string)} chunk
  5506. */
  5507. parseFile(chunk) {
  5508. const str = typeof chunk == 'string' ? chunk : new _TextDecoder('utf-8').decode(chunk);
  5509. for (let [i, j] of Object.entries(ASS.extractSections(str))) {
  5510. if (i.match(/Script Info(?:mation)?/i)) this.info = j;
  5511. else if (i.match(/V4\\+? Styles?/i)) this.styles = j;
  5512. else if (i.match(/Events?/i)) this.events = j;
  5513. else if (i.match(/Pictures?/i)) this.pictures = j;
  5514. else if (i.match(/Fonts?/i)) this.fonts = j;
  5515. }
  5516. this.eventsHeader = this.events.split('\\n', 2).join('\\n') + '\\n';
  5517. this.lines = ASS.extractSubtitleLines(this.events);
  5518. return this;
  5519. }
  5520. }
  5521.  
  5522. /** Detect free variable \`global\` from Node.js. */
  5523. var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
  5524.  
  5525. /** Detect free variable \`self\`. */
  5526. var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
  5527.  
  5528. /** Used as a reference to the global object. */
  5529. var root = freeGlobal || freeSelf || Function('return this')();
  5530.  
  5531. /** Built-in value references. */
  5532. var Symbol = root.Symbol;
  5533.  
  5534. /** Used for built-in method references. */
  5535. var objectProto = Object.prototype;
  5536.  
  5537. /** Used to check objects for own properties. */
  5538. var hasOwnProperty = objectProto.hasOwnProperty;
  5539.  
  5540. /**
  5541. * Used to resolve the
  5542. * [\`toStringTag\`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
  5543. * of values.
  5544. */
  5545. var nativeObjectToString = objectProto.toString;
  5546.  
  5547. /** Built-in value references. */
  5548. var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
  5549.  
  5550. /**
  5551. * A specialized version of \`baseGetTag\` which ignores \`Symbol.toStringTag\` values.
  5552. *
  5553. * @private
  5554. * @param {*} value The value to query.
  5555. * @returns {string} Returns the raw \`toStringTag\`.
  5556. */
  5557. function getRawTag(value) {
  5558. var isOwn = hasOwnProperty.call(value, symToStringTag),
  5559. tag = value[symToStringTag];
  5560.  
  5561. try {
  5562. value[symToStringTag] = undefined;
  5563. var unmasked = true;
  5564. } catch (e) {}
  5565.  
  5566. var result = nativeObjectToString.call(value);
  5567. if (unmasked) {
  5568. if (isOwn) {
  5569. value[symToStringTag] = tag;
  5570. } else {
  5571. delete value[symToStringTag];
  5572. }
  5573. }
  5574. return result;
  5575. }
  5576.  
  5577. /** Used for built-in method references. */
  5578. var objectProto\$1 = Object.prototype;
  5579.  
  5580. /**
  5581. * Used to resolve the
  5582. * [\`toStringTag\`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
  5583. * of values.
  5584. */
  5585. var nativeObjectToString\$1 = objectProto\$1.toString;
  5586.  
  5587. /**
  5588. * Converts \`value\` to a string using \`Object.prototype.toString\`.
  5589. *
  5590. * @private
  5591. * @param {*} value The value to convert.
  5592. * @returns {string} Returns the converted string.
  5593. */
  5594. function objectToString(value) {
  5595. return nativeObjectToString\$1.call(value);
  5596. }
  5597.  
  5598. /** \`Object#toString\` result references. */
  5599. var nullTag = '[object Null]',
  5600. undefinedTag = '[object Undefined]';
  5601.  
  5602. /** Built-in value references. */
  5603. var symToStringTag\$1 = Symbol ? Symbol.toStringTag : undefined;
  5604.  
  5605. /**
  5606. * The base implementation of \`getTag\` without fallbacks for buggy environments.
  5607. *
  5608. * @private
  5609. * @param {*} value The value to query.
  5610. * @returns {string} Returns the \`toStringTag\`.
  5611. */
  5612. function baseGetTag(value) {
  5613. if (value == null) {
  5614. return value === undefined ? undefinedTag : nullTag;
  5615. }
  5616. return (symToStringTag\$1 && symToStringTag\$1 in Object(value))
  5617. ? getRawTag(value)
  5618. : objectToString(value);
  5619. }
  5620.  
  5621. /**
  5622. * Checks if \`value\` is the
  5623. * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
  5624. * of \`Object\`. (e.g. arrays, functions, objects, regexes, \`new Number(0)\`, and \`new String('')\`)
  5625. *
  5626. * @static
  5627. * @memberOf _
  5628. * @since 0.1.0
  5629. * @category Lang
  5630. * @param {*} value The value to check.
  5631. * @returns {boolean} Returns \`true\` if \`value\` is an object, else \`false\`.
  5632. * @example
  5633. *
  5634. * _.isObject({});
  5635. * // => true
  5636. *
  5637. * _.isObject([1, 2, 3]);
  5638. * // => true
  5639. *
  5640. * _.isObject(_.noop);
  5641. * // => true
  5642. *
  5643. * _.isObject(null);
  5644. * // => false
  5645. */
  5646. function isObject(value) {
  5647. var type = typeof value;
  5648. return value != null && (type == 'object' || type == 'function');
  5649. }
  5650.  
  5651. /** \`Object#toString\` result references. */
  5652. var asyncTag = '[object AsyncFunction]',
  5653. funcTag = '[object Function]',
  5654. genTag = '[object GeneratorFunction]',
  5655. proxyTag = '[object Proxy]';
  5656.  
  5657. /**
  5658. * Checks if \`value\` is classified as a \`Function\` object.
  5659. *
  5660. * @static
  5661. * @memberOf _
  5662. * @since 0.1.0
  5663. * @category Lang
  5664. * @param {*} value The value to check.
  5665. * @returns {boolean} Returns \`true\` if \`value\` is a function, else \`false\`.
  5666. * @example
  5667. *
  5668. * _.isFunction(_);
  5669. * // => true
  5670. *
  5671. * _.isFunction(/abc/);
  5672. * // => false
  5673. */
  5674. function isFunction(value) {
  5675. if (!isObject(value)) {
  5676. return false;
  5677. }
  5678. // The use of \`Object#toString\` avoids issues with the \`typeof\` operator
  5679. // in Safari 9 which returns 'object' for typed arrays and other constructors.
  5680. var tag = baseGetTag(value);
  5681. return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
  5682. }
  5683.  
  5684. /** Used to detect overreaching core-js shims. */
  5685. var coreJsData = root['__core-js_shared__'];
  5686.  
  5687. /** Used to detect methods masquerading as native. */
  5688. var maskSrcKey = (function() {
  5689. var uid = /[^.]+\$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
  5690. return uid ? ('Symbol(src)_1.' + uid) : '';
  5691. }());
  5692.  
  5693. /**
  5694. * Checks if \`func\` has its source masked.
  5695. *
  5696. * @private
  5697. * @param {Function} func The function to check.
  5698. * @returns {boolean} Returns \`true\` if \`func\` is masked, else \`false\`.
  5699. */
  5700. function isMasked(func) {
  5701. return !!maskSrcKey && (maskSrcKey in func);
  5702. }
  5703.  
  5704. /** Used for built-in method references. */
  5705. var funcProto = Function.prototype;
  5706.  
  5707. /** Used to resolve the decompiled source of functions. */
  5708. var funcToString = funcProto.toString;
  5709.  
  5710. /**
  5711. * Converts \`func\` to its source code.
  5712. *
  5713. * @private
  5714. * @param {Function} func The function to convert.
  5715. * @returns {string} Returns the source code.
  5716. */
  5717. function toSource(func) {
  5718. if (func != null) {
  5719. try {
  5720. return funcToString.call(func);
  5721. } catch (e) {}
  5722. try {
  5723. return (func + '');
  5724. } catch (e) {}
  5725. }
  5726. return '';
  5727. }
  5728.  
  5729. /**
  5730. * Used to match \`RegExp\`
  5731. * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
  5732. */
  5733. var reRegExpChar = /[\\\\^\$.*+?()[\\]{}|]/g;
  5734.  
  5735. /** Used to detect host constructors (Safari). */
  5736. var reIsHostCtor = /^\\[object .+?Constructor\\]\$/;
  5737.  
  5738. /** Used for built-in method references. */
  5739. var funcProto\$1 = Function.prototype,
  5740. objectProto\$2 = Object.prototype;
  5741.  
  5742. /** Used to resolve the decompiled source of functions. */
  5743. var funcToString\$1 = funcProto\$1.toString;
  5744.  
  5745. /** Used to check objects for own properties. */
  5746. var hasOwnProperty\$1 = objectProto\$2.hasOwnProperty;
  5747.  
  5748. /** Used to detect if a method is native. */
  5749. var reIsNative = RegExp('^' +
  5750. funcToString\$1.call(hasOwnProperty\$1).replace(reRegExpChar, '\\\\\$&')
  5751. .replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '\$1.*?') + '\$'
  5752. );
  5753.  
  5754. /**
  5755. * The base implementation of \`_.isNative\` without bad shim checks.
  5756. *
  5757. * @private
  5758. * @param {*} value The value to check.
  5759. * @returns {boolean} Returns \`true\` if \`value\` is a native function,
  5760. * else \`false\`.
  5761. */
  5762. function baseIsNative(value) {
  5763. if (!isObject(value) || isMasked(value)) {
  5764. return false;
  5765. }
  5766. var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
  5767. return pattern.test(toSource(value));
  5768. }
  5769.  
  5770. /**
  5771. * Gets the value at \`key\` of \`object\`.
  5772. *
  5773. * @private
  5774. * @param {Object} [object] The object to query.
  5775. * @param {string} key The key of the property to get.
  5776. * @returns {*} Returns the property value.
  5777. */
  5778. function getValue(object, key) {
  5779. return object == null ? undefined : object[key];
  5780. }
  5781.  
  5782. /**
  5783. * Gets the native function at \`key\` of \`object\`.
  5784. *
  5785. * @private
  5786. * @param {Object} object The object to query.
  5787. * @param {string} key The key of the method to get.
  5788. * @returns {*} Returns the function if it's native, else \`undefined\`.
  5789. */
  5790. function getNative(object, key) {
  5791. var value = getValue(object, key);
  5792. return baseIsNative(value) ? value : undefined;
  5793. }
  5794.  
  5795. /* Built-in method references that are verified to be native. */
  5796. var nativeCreate = getNative(Object, 'create');
  5797.  
  5798. /**
  5799. * Removes all key-value entries from the hash.
  5800. *
  5801. * @private
  5802. * @name clear
  5803. * @memberOf Hash
  5804. */
  5805. function hashClear() {
  5806. this.__data__ = nativeCreate ? nativeCreate(null) : {};
  5807. this.size = 0;
  5808. }
  5809.  
  5810. /**
  5811. * Removes \`key\` and its value from the hash.
  5812. *
  5813. * @private
  5814. * @name delete
  5815. * @memberOf Hash
  5816. * @param {Object} hash The hash to modify.
  5817. * @param {string} key The key of the value to remove.
  5818. * @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
  5819. */
  5820. function hashDelete(key) {
  5821. var result = this.has(key) && delete this.__data__[key];
  5822. this.size -= result ? 1 : 0;
  5823. return result;
  5824. }
  5825.  
  5826. /** Used to stand-in for \`undefined\` hash values. */
  5827. var HASH_UNDEFINED = '__lodash_hash_undefined__';
  5828.  
  5829. /** Used for built-in method references. */
  5830. var objectProto\$3 = Object.prototype;
  5831.  
  5832. /** Used to check objects for own properties. */
  5833. var hasOwnProperty\$2 = objectProto\$3.hasOwnProperty;
  5834.  
  5835. /**
  5836. * Gets the hash value for \`key\`.
  5837. *
  5838. * @private
  5839. * @name get
  5840. * @memberOf Hash
  5841. * @param {string} key The key of the value to get.
  5842. * @returns {*} Returns the entry value.
  5843. */
  5844. function hashGet(key) {
  5845. var data = this.__data__;
  5846. if (nativeCreate) {
  5847. var result = data[key];
  5848. return result === HASH_UNDEFINED ? undefined : result;
  5849. }
  5850. return hasOwnProperty\$2.call(data, key) ? data[key] : undefined;
  5851. }
  5852.  
  5853. /** Used for built-in method references. */
  5854. var objectProto\$4 = Object.prototype;
  5855.  
  5856. /** Used to check objects for own properties. */
  5857. var hasOwnProperty\$3 = objectProto\$4.hasOwnProperty;
  5858.  
  5859. /**
  5860. * Checks if a hash value for \`key\` exists.
  5861. *
  5862. * @private
  5863. * @name has
  5864. * @memberOf Hash
  5865. * @param {string} key The key of the entry to check.
  5866. * @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
  5867. */
  5868. function hashHas(key) {
  5869. var data = this.__data__;
  5870. return nativeCreate ? (data[key] !== undefined) : hasOwnProperty\$3.call(data, key);
  5871. }
  5872.  
  5873. /** Used to stand-in for \`undefined\` hash values. */
  5874. var HASH_UNDEFINED\$1 = '__lodash_hash_undefined__';
  5875.  
  5876. /**
  5877. * Sets the hash \`key\` to \`value\`.
  5878. *
  5879. * @private
  5880. * @name set
  5881. * @memberOf Hash
  5882. * @param {string} key The key of the value to set.
  5883. * @param {*} value The value to set.
  5884. * @returns {Object} Returns the hash instance.
  5885. */
  5886. function hashSet(key, value) {
  5887. var data = this.__data__;
  5888. this.size += this.has(key) ? 0 : 1;
  5889. data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED\$1 : value;
  5890. return this;
  5891. }
  5892.  
  5893. /**
  5894. * Creates a hash object.
  5895. *
  5896. * @private
  5897. * @constructor
  5898. * @param {Array} [entries] The key-value pairs to cache.
  5899. */
  5900. function Hash(entries) {
  5901. var index = -1,
  5902. length = entries == null ? 0 : entries.length;
  5903.  
  5904. this.clear();
  5905. while (++index < length) {
  5906. var entry = entries[index];
  5907. this.set(entry[0], entry[1]);
  5908. }
  5909. }
  5910.  
  5911. // Add methods to \`Hash\`.
  5912. Hash.prototype.clear = hashClear;
  5913. Hash.prototype['delete'] = hashDelete;
  5914. Hash.prototype.get = hashGet;
  5915. Hash.prototype.has = hashHas;
  5916. Hash.prototype.set = hashSet;
  5917.  
  5918. /**
  5919. * Removes all key-value entries from the list cache.
  5920. *
  5921. * @private
  5922. * @name clear
  5923. * @memberOf ListCache
  5924. */
  5925. function listCacheClear() {
  5926. this.__data__ = [];
  5927. this.size = 0;
  5928. }
  5929.  
  5930. /**
  5931. * Performs a
  5932. * [\`SameValueZero\`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
  5933. * comparison between two values to determine if they are equivalent.
  5934. *
  5935. * @static
  5936. * @memberOf _
  5937. * @since 4.0.0
  5938. * @category Lang
  5939. * @param {*} value The value to compare.
  5940. * @param {*} other The other value to compare.
  5941. * @returns {boolean} Returns \`true\` if the values are equivalent, else \`false\`.
  5942. * @example
  5943. *
  5944. * var object = { 'a': 1 };
  5945. * var other = { 'a': 1 };
  5946. *
  5947. * _.eq(object, object);
  5948. * // => true
  5949. *
  5950. * _.eq(object, other);
  5951. * // => false
  5952. *
  5953. * _.eq('a', 'a');
  5954. * // => true
  5955. *
  5956. * _.eq('a', Object('a'));
  5957. * // => false
  5958. *
  5959. * _.eq(NaN, NaN);
  5960. * // => true
  5961. */
  5962. function eq(value, other) {
  5963. return value === other || (value !== value && other !== other);
  5964. }
  5965.  
  5966. /**
  5967. * Gets the index at which the \`key\` is found in \`array\` of key-value pairs.
  5968. *
  5969. * @private
  5970. * @param {Array} array The array to inspect.
  5971. * @param {*} key The key to search for.
  5972. * @returns {number} Returns the index of the matched value, else \`-1\`.
  5973. */
  5974. function assocIndexOf(array, key) {
  5975. var length = array.length;
  5976. while (length--) {
  5977. if (eq(array[length][0], key)) {
  5978. return length;
  5979. }
  5980. }
  5981. return -1;
  5982. }
  5983.  
  5984. /** Used for built-in method references. */
  5985. var arrayProto = Array.prototype;
  5986.  
  5987. /** Built-in value references. */
  5988. var splice = arrayProto.splice;
  5989.  
  5990. /**
  5991. * Removes \`key\` and its value from the list cache.
  5992. *
  5993. * @private
  5994. * @name delete
  5995. * @memberOf ListCache
  5996. * @param {string} key The key of the value to remove.
  5997. * @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
  5998. */
  5999. function listCacheDelete(key) {
  6000. var data = this.__data__,
  6001. index = assocIndexOf(data, key);
  6002.  
  6003. if (index < 0) {
  6004. return false;
  6005. }
  6006. var lastIndex = data.length - 1;
  6007. if (index == lastIndex) {
  6008. data.pop();
  6009. } else {
  6010. splice.call(data, index, 1);
  6011. }
  6012. --this.size;
  6013. return true;
  6014. }
  6015.  
  6016. /**
  6017. * Gets the list cache value for \`key\`.
  6018. *
  6019. * @private
  6020. * @name get
  6021. * @memberOf ListCache
  6022. * @param {string} key The key of the value to get.
  6023. * @returns {*} Returns the entry value.
  6024. */
  6025. function listCacheGet(key) {
  6026. var data = this.__data__,
  6027. index = assocIndexOf(data, key);
  6028.  
  6029. return index < 0 ? undefined : data[index][1];
  6030. }
  6031.  
  6032. /**
  6033. * Checks if a list cache value for \`key\` exists.
  6034. *
  6035. * @private
  6036. * @name has
  6037. * @memberOf ListCache
  6038. * @param {string} key The key of the entry to check.
  6039. * @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
  6040. */
  6041. function listCacheHas(key) {
  6042. return assocIndexOf(this.__data__, key) > -1;
  6043. }
  6044.  
  6045. /**
  6046. * Sets the list cache \`key\` to \`value\`.
  6047. *
  6048. * @private
  6049. * @name set
  6050. * @memberOf ListCache
  6051. * @param {string} key The key of the value to set.
  6052. * @param {*} value The value to set.
  6053. * @returns {Object} Returns the list cache instance.
  6054. */
  6055. function listCacheSet(key, value) {
  6056. var data = this.__data__,
  6057. index = assocIndexOf(data, key);
  6058.  
  6059. if (index < 0) {
  6060. ++this.size;
  6061. data.push([key, value]);
  6062. } else {
  6063. data[index][1] = value;
  6064. }
  6065. return this;
  6066. }
  6067.  
  6068. /**
  6069. * Creates an list cache object.
  6070. *
  6071. * @private
  6072. * @constructor
  6073. * @param {Array} [entries] The key-value pairs to cache.
  6074. */
  6075. function ListCache(entries) {
  6076. var index = -1,
  6077. length = entries == null ? 0 : entries.length;
  6078.  
  6079. this.clear();
  6080. while (++index < length) {
  6081. var entry = entries[index];
  6082. this.set(entry[0], entry[1]);
  6083. }
  6084. }
  6085.  
  6086. // Add methods to \`ListCache\`.
  6087. ListCache.prototype.clear = listCacheClear;
  6088. ListCache.prototype['delete'] = listCacheDelete;
  6089. ListCache.prototype.get = listCacheGet;
  6090. ListCache.prototype.has = listCacheHas;
  6091. ListCache.prototype.set = listCacheSet;
  6092.  
  6093. /* Built-in method references that are verified to be native. */
  6094. var Map = getNative(root, 'Map');
  6095.  
  6096. /**
  6097. * Removes all key-value entries from the map.
  6098. *
  6099. * @private
  6100. * @name clear
  6101. * @memberOf MapCache
  6102. */
  6103. function mapCacheClear() {
  6104. this.size = 0;
  6105. this.__data__ = {
  6106. 'hash': new Hash,
  6107. 'map': new (Map || ListCache),
  6108. 'string': new Hash
  6109. };
  6110. }
  6111.  
  6112. /**
  6113. * Checks if \`value\` is suitable for use as unique object key.
  6114. *
  6115. * @private
  6116. * @param {*} value The value to check.
  6117. * @returns {boolean} Returns \`true\` if \`value\` is suitable, else \`false\`.
  6118. */
  6119. function isKeyable(value) {
  6120. var type = typeof value;
  6121. return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
  6122. ? (value !== '__proto__')
  6123. : (value === null);
  6124. }
  6125.  
  6126. /**
  6127. * Gets the data for \`map\`.
  6128. *
  6129. * @private
  6130. * @param {Object} map The map to query.
  6131. * @param {string} key The reference key.
  6132. * @returns {*} Returns the map data.
  6133. */
  6134. function getMapData(map, key) {
  6135. var data = map.__data__;
  6136. return isKeyable(key)
  6137. ? data[typeof key == 'string' ? 'string' : 'hash']
  6138. : data.map;
  6139. }
  6140.  
  6141. /**
  6142. * Removes \`key\` and its value from the map.
  6143. *
  6144. * @private
  6145. * @name delete
  6146. * @memberOf MapCache
  6147. * @param {string} key The key of the value to remove.
  6148. * @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
  6149. */
  6150. function mapCacheDelete(key) {
  6151. var result = getMapData(this, key)['delete'](key);
  6152. this.size -= result ? 1 : 0;
  6153. return result;
  6154. }
  6155.  
  6156. /**
  6157. * Gets the map value for \`key\`.
  6158. *
  6159. * @private
  6160. * @name get
  6161. * @memberOf MapCache
  6162. * @param {string} key The key of the value to get.
  6163. * @returns {*} Returns the entry value.
  6164. */
  6165. function mapCacheGet(key) {
  6166. return getMapData(this, key).get(key);
  6167. }
  6168.  
  6169. /**
  6170. * Checks if a map value for \`key\` exists.
  6171. *
  6172. * @private
  6173. * @name has
  6174. * @memberOf MapCache
  6175. * @param {string} key The key of the entry to check.
  6176. * @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
  6177. */
  6178. function mapCacheHas(key) {
  6179. return getMapData(this, key).has(key);
  6180. }
  6181.  
  6182. /**
  6183. * Sets the map \`key\` to \`value\`.
  6184. *
  6185. * @private
  6186. * @name set
  6187. * @memberOf MapCache
  6188. * @param {string} key The key of the value to set.
  6189. * @param {*} value The value to set.
  6190. * @returns {Object} Returns the map cache instance.
  6191. */
  6192. function mapCacheSet(key, value) {
  6193. var data = getMapData(this, key),
  6194. size = data.size;
  6195.  
  6196. data.set(key, value);
  6197. this.size += data.size == size ? 0 : 1;
  6198. return this;
  6199. }
  6200.  
  6201. /**
  6202. * Creates a map cache object to store key-value pairs.
  6203. *
  6204. * @private
  6205. * @constructor
  6206. * @param {Array} [entries] The key-value pairs to cache.
  6207. */
  6208. function MapCache(entries) {
  6209. var index = -1,
  6210. length = entries == null ? 0 : entries.length;
  6211.  
  6212. this.clear();
  6213. while (++index < length) {
  6214. var entry = entries[index];
  6215. this.set(entry[0], entry[1]);
  6216. }
  6217. }
  6218.  
  6219. // Add methods to \`MapCache\`.
  6220. MapCache.prototype.clear = mapCacheClear;
  6221. MapCache.prototype['delete'] = mapCacheDelete;
  6222. MapCache.prototype.get = mapCacheGet;
  6223. MapCache.prototype.has = mapCacheHas;
  6224. MapCache.prototype.set = mapCacheSet;
  6225.  
  6226. /** Error message constants. */
  6227. var FUNC_ERROR_TEXT = 'Expected a function';
  6228.  
  6229. /**
  6230. * Creates a function that memoizes the result of \`func\`. If \`resolver\` is
  6231. * provided, it determines the cache key for storing the result based on the
  6232. * arguments provided to the memoized function. By default, the first argument
  6233. * provided to the memoized function is used as the map cache key. The \`func\`
  6234. * is invoked with the \`this\` binding of the memoized function.
  6235. *
  6236. * **Note:** The cache is exposed as the \`cache\` property on the memoized
  6237. * function. Its creation may be customized by replacing the \`_.memoize.Cache\`
  6238. * constructor with one whose instances implement the
  6239. * [\`Map\`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
  6240. * method interface of \`clear\`, \`delete\`, \`get\`, \`has\`, and \`set\`.
  6241. *
  6242. * @static
  6243. * @memberOf _
  6244. * @since 0.1.0
  6245. * @category Function
  6246. * @param {Function} func The function to have its output memoized.
  6247. * @param {Function} [resolver] The function to resolve the cache key.
  6248. * @returns {Function} Returns the new memoized function.
  6249. * @example
  6250. *
  6251. * var object = { 'a': 1, 'b': 2 };
  6252. * var other = { 'c': 3, 'd': 4 };
  6253. *
  6254. * var values = _.memoize(_.values);
  6255. * values(object);
  6256. * // => [1, 2]
  6257. *
  6258. * values(other);
  6259. * // => [3, 4]
  6260. *
  6261. * object.a = 2;
  6262. * values(object);
  6263. * // => [1, 2]
  6264. *
  6265. * // Modify the result cache.
  6266. * values.cache.set(object, ['a', 'b']);
  6267. * values(object);
  6268. * // => ['a', 'b']
  6269. *
  6270. * // Replace \`_.memoize.Cache\`.
  6271. * _.memoize.Cache = WeakMap;
  6272. */
  6273. function memoize(func, resolver) {
  6274. if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
  6275. throw new TypeError(FUNC_ERROR_TEXT);
  6276. }
  6277. var memoized = function() {
  6278. var args = arguments,
  6279. key = resolver ? resolver.apply(this, args) : args[0],
  6280. cache = memoized.cache;
  6281.  
  6282. if (cache.has(key)) {
  6283. return cache.get(key);
  6284. }
  6285. var result = func.apply(this, args);
  6286. memoized.cache = cache.set(key, result) || cache;
  6287. return result;
  6288. };
  6289. memoized.cache = new (memoize.Cache || MapCache);
  6290. return memoized;
  6291. }
  6292.  
  6293. // Expose \`MapCache\`.
  6294. memoize.Cache = MapCache;
  6295.  
  6296. const numberToByteArray = (num, byteLength = getNumberByteLength(num)) => {
  6297. var byteArray;
  6298. if (byteLength == 1) {
  6299. byteArray = new DataView(new ArrayBuffer(1));
  6300. byteArray.setUint8(0, num);
  6301. }
  6302. else if (byteLength == 2) {
  6303. byteArray = new DataView(new ArrayBuffer(2));
  6304. byteArray.setUint16(0, num);
  6305. }
  6306. else if (byteLength == 3) {
  6307. byteArray = new DataView(new ArrayBuffer(3));
  6308. byteArray.setUint8(0, num >> 16);
  6309. byteArray.setUint16(1, num & 0xffff);
  6310. }
  6311. else if (byteLength == 4) {
  6312. byteArray = new DataView(new ArrayBuffer(4));
  6313. byteArray.setUint32(0, num);
  6314. }
  6315. else if (num < 0xffffffff) {
  6316. byteArray = new DataView(new ArrayBuffer(5));
  6317. byteArray.setUint32(1, num);
  6318. }
  6319. else if (byteLength == 5) {
  6320. byteArray = new DataView(new ArrayBuffer(5));
  6321. byteArray.setUint8(0, num / 0x100000000 | 0);
  6322. byteArray.setUint32(1, num % 0x100000000);
  6323. }
  6324. else if (byteLength == 6) {
  6325. byteArray = new DataView(new ArrayBuffer(6));
  6326. byteArray.setUint16(0, num / 0x100000000 | 0);
  6327. byteArray.setUint32(2, num % 0x100000000);
  6328. }
  6329. else if (byteLength == 7) {
  6330. byteArray = new DataView(new ArrayBuffer(7));
  6331. byteArray.setUint8(0, num / 0x1000000000000 | 0);
  6332. byteArray.setUint16(1, num / 0x100000000 & 0xffff);
  6333. byteArray.setUint32(3, num % 0x100000000);
  6334. }
  6335. else if (byteLength == 8) {
  6336. byteArray = new DataView(new ArrayBuffer(8));
  6337. byteArray.setUint32(0, num / 0x100000000 | 0);
  6338. byteArray.setUint32(4, num % 0x100000000);
  6339. }
  6340. else {
  6341. throw new Error("EBML.typedArrayUtils.numberToByteArray: byte length must be less than or equal to 8");
  6342. }
  6343. return new Uint8Array(byteArray.buffer);
  6344. };
  6345. const stringToByteArray = memoize((str) => {
  6346. return Uint8Array.from(Array.from(str).map(_ => _.codePointAt(0)));
  6347. });
  6348. function getNumberByteLength(num) {
  6349. if (num < 0) {
  6350. throw new Error("EBML.typedArrayUtils.getNumberByteLength: negative number not implemented");
  6351. }
  6352. else if (num < 0x100) {
  6353. return 1;
  6354. }
  6355. else if (num < 0x10000) {
  6356. return 2;
  6357. }
  6358. else if (num < 0x1000000) {
  6359. return 3;
  6360. }
  6361. else if (num < 0x100000000) {
  6362. return 4;
  6363. }
  6364. else if (num < 0x10000000000) {
  6365. return 5;
  6366. }
  6367. else if (num < 0x1000000000000) {
  6368. return 6;
  6369. }
  6370. else if (num < 0x20000000000000) {
  6371. return 7;
  6372. }
  6373. else {
  6374. throw new Error("EBML.typedArrayUtils.getNumberByteLength: number exceeds Number.MAX_SAFE_INTEGER");
  6375. }
  6376. }
  6377. const int16Bit = memoize((num) => {
  6378. const ab = new ArrayBuffer(2);
  6379. new DataView(ab).setInt16(0, num);
  6380. return new Uint8Array(ab);
  6381. });
  6382. const float32bit = memoize((num) => {
  6383. const ab = new ArrayBuffer(4);
  6384. new DataView(ab).setFloat32(0, num);
  6385. return new Uint8Array(ab);
  6386. });
  6387. const dumpBytes = (b) => {
  6388. return Array.from(new Uint8Array(b)).map(_ => \`0x\${_.toString(16)}\`).join(", ");
  6389. };
  6390.  
  6391. class Value {
  6392. constructor(bytes) {
  6393. this.bytes = bytes;
  6394. }
  6395. write(buf, pos) {
  6396. buf.set(this.bytes, pos);
  6397. return pos + this.bytes.length;
  6398. }
  6399. countSize() {
  6400. return this.bytes.length;
  6401. }
  6402. }
  6403. class Element {
  6404. constructor(id, children, isSizeUnknown) {
  6405. this.id = id;
  6406. this.children = children;
  6407. const bodySize = this.children.reduce((p, c) => p + c.countSize(), 0);
  6408. this.sizeMetaData = isSizeUnknown ?
  6409. UNKNOWN_SIZE :
  6410. vintEncode(numberToByteArray(bodySize, getEBMLByteLength(bodySize)));
  6411. this.size = this.id.length + this.sizeMetaData.length + bodySize;
  6412. }
  6413. write(buf, pos) {
  6414. buf.set(this.id, pos);
  6415. buf.set(this.sizeMetaData, pos + this.id.length);
  6416. return this.children.reduce((p, c) => c.write(buf, p), pos + this.id.length + this.sizeMetaData.length);
  6417. }
  6418. countSize() {
  6419. return this.size;
  6420. }
  6421. }
  6422. const bytes = memoize((data) => {
  6423. return new Value(data);
  6424. });
  6425. const number = memoize((num) => {
  6426. return bytes(numberToByteArray(num));
  6427. });
  6428. const vintEncodedNumber = memoize((num) => {
  6429. return bytes(vintEncode(numberToByteArray(num, getEBMLByteLength(num))));
  6430. });
  6431. const int16 = memoize((num) => {
  6432. return bytes(int16Bit(num));
  6433. });
  6434. const float = memoize((num) => {
  6435. return bytes(float32bit(num));
  6436. });
  6437. const string = memoize((str) => {
  6438. return bytes(stringToByteArray(str));
  6439. });
  6440. const element = (id, child) => {
  6441. return new Element(id, Array.isArray(child) ? child : [child], false);
  6442. };
  6443. const unknownSizeElement = (id, child) => {
  6444. return new Element(id, Array.isArray(child) ? child : [child], true);
  6445. };
  6446. const build = (v) => {
  6447. const b = new Uint8Array(v.countSize());
  6448. v.write(b, 0);
  6449. return b;
  6450. };
  6451. const getEBMLByteLength = (num) => {
  6452. if (num < 0x7f) {
  6453. return 1;
  6454. }
  6455. else if (num < 0x3fff) {
  6456. return 2;
  6457. }
  6458. else if (num < 0x1fffff) {
  6459. return 3;
  6460. }
  6461. else if (num < 0xfffffff) {
  6462. return 4;
  6463. }
  6464. else if (num < 0x7ffffffff) {
  6465. return 5;
  6466. }
  6467. else if (num < 0x3ffffffffff) {
  6468. return 6;
  6469. }
  6470. else if (num < 0x1ffffffffffff) {
  6471. return 7;
  6472. }
  6473. else if (num < 0x20000000000000) {
  6474. return 8;
  6475. }
  6476. else if (num < 0xffffffffffffff) {
  6477. throw new Error("EBMLgetEBMLByteLength: number exceeds Number.MAX_SAFE_INTEGER");
  6478. }
  6479. else {
  6480. throw new Error("EBMLgetEBMLByteLength: data size must be less than or equal to " + (Math.pow(2, 56) - 2));
  6481. }
  6482. };
  6483. const UNKNOWN_SIZE = new Uint8Array([0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
  6484. const vintEncode = (byteArray) => {
  6485. byteArray[0] = getSizeMask(byteArray.length) | byteArray[0];
  6486. return byteArray;
  6487. };
  6488. const getSizeMask = (byteLength) => {
  6489. return 0x80 >> (byteLength - 1);
  6490. };
  6491.  
  6492. /**
  6493. * @see https://www.matroska.org/technical/specs/index.html
  6494. */
  6495. const ID = {
  6496. EBML: Uint8Array.of(0x1A, 0x45, 0xDF, 0xA3),
  6497. EBMLVersion: Uint8Array.of(0x42, 0x86),
  6498. EBMLReadVersion: Uint8Array.of(0x42, 0xF7),
  6499. EBMLMaxIDLength: Uint8Array.of(0x42, 0xF2),
  6500. EBMLMaxSizeLength: Uint8Array.of(0x42, 0xF3),
  6501. DocType: Uint8Array.of(0x42, 0x82),
  6502. DocTypeVersion: Uint8Array.of(0x42, 0x87),
  6503. DocTypeReadVersion: Uint8Array.of(0x42, 0x85),
  6504. Void: Uint8Array.of(0xEC),
  6505. CRC32: Uint8Array.of(0xBF),
  6506. Segment: Uint8Array.of(0x18, 0x53, 0x80, 0x67),
  6507. SeekHead: Uint8Array.of(0x11, 0x4D, 0x9B, 0x74),
  6508. Seek: Uint8Array.of(0x4D, 0xBB),
  6509. SeekID: Uint8Array.of(0x53, 0xAB),
  6510. SeekPosition: Uint8Array.of(0x53, 0xAC),
  6511. Info: Uint8Array.of(0x15, 0x49, 0xA9, 0x66),
  6512. SegmentUID: Uint8Array.of(0x73, 0xA4),
  6513. SegmentFilename: Uint8Array.of(0x73, 0x84),
  6514. PrevUID: Uint8Array.of(0x3C, 0xB9, 0x23),
  6515. PrevFilename: Uint8Array.of(0x3C, 0x83, 0xAB),
  6516. NextUID: Uint8Array.of(0x3E, 0xB9, 0x23),
  6517. NextFilename: Uint8Array.of(0x3E, 0x83, 0xBB),
  6518. SegmentFamily: Uint8Array.of(0x44, 0x44),
  6519. ChapterTranslate: Uint8Array.of(0x69, 0x24),
  6520. ChapterTranslateEditionUID: Uint8Array.of(0x69, 0xFC),
  6521. ChapterTranslateCodec: Uint8Array.of(0x69, 0xBF),
  6522. ChapterTranslateID: Uint8Array.of(0x69, 0xA5),
  6523. TimecodeScale: Uint8Array.of(0x2A, 0xD7, 0xB1),
  6524. Duration: Uint8Array.of(0x44, 0x89),
  6525. DateUTC: Uint8Array.of(0x44, 0x61),
  6526. Title: Uint8Array.of(0x7B, 0xA9),
  6527. MuxingApp: Uint8Array.of(0x4D, 0x80),
  6528. WritingApp: Uint8Array.of(0x57, 0x41),
  6529. Cluster: Uint8Array.of(0x1F, 0x43, 0xB6, 0x75),
  6530. Timecode: Uint8Array.of(0xE7),
  6531. SilentTracks: Uint8Array.of(0x58, 0x54),
  6532. SilentTrackNumber: Uint8Array.of(0x58, 0xD7),
  6533. Position: Uint8Array.of(0xA7),
  6534. PrevSize: Uint8Array.of(0xAB),
  6535. SimpleBlock: Uint8Array.of(0xA3),
  6536. BlockGroup: Uint8Array.of(0xA0),
  6537. Block: Uint8Array.of(0xA1),
  6538. BlockAdditions: Uint8Array.of(0x75, 0xA1),
  6539. BlockMore: Uint8Array.of(0xA6),
  6540. BlockAddID: Uint8Array.of(0xEE),
  6541. BlockAdditional: Uint8Array.of(0xA5),
  6542. BlockDuration: Uint8Array.of(0x9B),
  6543. ReferencePriority: Uint8Array.of(0xFA),
  6544. ReferenceBlock: Uint8Array.of(0xFB),
  6545. CodecState: Uint8Array.of(0xA4),
  6546. DiscardPadding: Uint8Array.of(0x75, 0xA2),
  6547. Slices: Uint8Array.of(0x8E),
  6548. TimeSlice: Uint8Array.of(0xE8),
  6549. LaceNumber: Uint8Array.of(0xCC),
  6550. Tracks: Uint8Array.of(0x16, 0x54, 0xAE, 0x6B),
  6551. TrackEntry: Uint8Array.of(0xAE),
  6552. TrackNumber: Uint8Array.of(0xD7),
  6553. TrackUID: Uint8Array.of(0x73, 0xC5),
  6554. TrackType: Uint8Array.of(0x83),
  6555. FlagEnabled: Uint8Array.of(0xB9),
  6556. FlagDefault: Uint8Array.of(0x88),
  6557. FlagForced: Uint8Array.of(0x55, 0xAA),
  6558. FlagLacing: Uint8Array.of(0x9C),
  6559. MinCache: Uint8Array.of(0x6D, 0xE7),
  6560. MaxCache: Uint8Array.of(0x6D, 0xF8),
  6561. DefaultDuration: Uint8Array.of(0x23, 0xE3, 0x83),
  6562. DefaultDecodedFieldDuration: Uint8Array.of(0x23, 0x4E, 0x7A),
  6563. MaxBlockAdditionID: Uint8Array.of(0x55, 0xEE),
  6564. Name: Uint8Array.of(0x53, 0x6E),
  6565. Language: Uint8Array.of(0x22, 0xB5, 0x9C),
  6566. CodecID: Uint8Array.of(0x86),
  6567. CodecPrivate: Uint8Array.of(0x63, 0xA2),
  6568. CodecName: Uint8Array.of(0x25, 0x86, 0x88),
  6569. AttachmentLink: Uint8Array.of(0x74, 0x46),
  6570. CodecDecodeAll: Uint8Array.of(0xAA),
  6571. TrackOverlay: Uint8Array.of(0x6F, 0xAB),
  6572. CodecDelay: Uint8Array.of(0x56, 0xAA),
  6573. SeekPreRoll: Uint8Array.of(0x56, 0xBB),
  6574. TrackTranslate: Uint8Array.of(0x66, 0x24),
  6575. TrackTranslateEditionUID: Uint8Array.of(0x66, 0xFC),
  6576. TrackTranslateCodec: Uint8Array.of(0x66, 0xBF),
  6577. TrackTranslateTrackID: Uint8Array.of(0x66, 0xA5),
  6578. Video: Uint8Array.of(0xE0),
  6579. FlagInterlaced: Uint8Array.of(0x9A),
  6580. FieldOrder: Uint8Array.of(0x9D),
  6581. StereoMode: Uint8Array.of(0x53, 0xB8),
  6582. AlphaMode: Uint8Array.of(0x53, 0xC0),
  6583. PixelWidth: Uint8Array.of(0xB0),
  6584. PixelHeight: Uint8Array.of(0xBA),
  6585. PixelCropBottom: Uint8Array.of(0x54, 0xAA),
  6586. PixelCropTop: Uint8Array.of(0x54, 0xBB),
  6587. PixelCropLeft: Uint8Array.of(0x54, 0xCC),
  6588. PixelCropRight: Uint8Array.of(0x54, 0xDD),
  6589. DisplayWidth: Uint8Array.of(0x54, 0xB0),
  6590. DisplayHeight: Uint8Array.of(0x54, 0xBA),
  6591. DisplayUnit: Uint8Array.of(0x54, 0xB2),
  6592. AspectRatioType: Uint8Array.of(0x54, 0xB3),
  6593. ColourSpace: Uint8Array.of(0x2E, 0xB5, 0x24),
  6594. Colour: Uint8Array.of(0x55, 0xB0),
  6595. MatrixCoefficients: Uint8Array.of(0x55, 0xB1),
  6596. BitsPerChannel: Uint8Array.of(0x55, 0xB2),
  6597. ChromaSubsamplingHorz: Uint8Array.of(0x55, 0xB3),
  6598. ChromaSubsamplingVert: Uint8Array.of(0x55, 0xB4),
  6599. CbSubsamplingHorz: Uint8Array.of(0x55, 0xB5),
  6600. CbSubsamplingVert: Uint8Array.of(0x55, 0xB6),
  6601. ChromaSitingHorz: Uint8Array.of(0x55, 0xB7),
  6602. ChromaSitingVert: Uint8Array.of(0x55, 0xB8),
  6603. Range: Uint8Array.of(0x55, 0xB9),
  6604. TransferCharacteristics: Uint8Array.of(0x55, 0xBA),
  6605. Primaries: Uint8Array.of(0x55, 0xBB),
  6606. MaxCLL: Uint8Array.of(0x55, 0xBC),
  6607. MaxFALL: Uint8Array.of(0x55, 0xBD),
  6608. MasteringMetadata: Uint8Array.of(0x55, 0xD0),
  6609. PrimaryRChromaticityX: Uint8Array.of(0x55, 0xD1),
  6610. PrimaryRChromaticityY: Uint8Array.of(0x55, 0xD2),
  6611. PrimaryGChromaticityX: Uint8Array.of(0x55, 0xD3),
  6612. PrimaryGChromaticityY: Uint8Array.of(0x55, 0xD4),
  6613. PrimaryBChromaticityX: Uint8Array.of(0x55, 0xD5),
  6614. PrimaryBChromaticityY: Uint8Array.of(0x55, 0xD6),
  6615. WhitePointChromaticityX: Uint8Array.of(0x55, 0xD7),
  6616. WhitePointChromaticityY: Uint8Array.of(0x55, 0xD8),
  6617. LuminanceMax: Uint8Array.of(0x55, 0xD9),
  6618. LuminanceMin: Uint8Array.of(0x55, 0xDA),
  6619. Audio: Uint8Array.of(0xE1),
  6620. SamplingFrequency: Uint8Array.of(0xB5),
  6621. OutputSamplingFrequency: Uint8Array.of(0x78, 0xB5),
  6622. Channels: Uint8Array.of(0x9F),
  6623. BitDepth: Uint8Array.of(0x62, 0x64),
  6624. TrackOperation: Uint8Array.of(0xE2),
  6625. TrackCombinePlanes: Uint8Array.of(0xE3),
  6626. TrackPlane: Uint8Array.of(0xE4),
  6627. TrackPlaneUID: Uint8Array.of(0xE5),
  6628. TrackPlaneType: Uint8Array.of(0xE6),
  6629. TrackJoinBlocks: Uint8Array.of(0xE9),
  6630. TrackJoinUID: Uint8Array.of(0xED),
  6631. ContentEncodings: Uint8Array.of(0x6D, 0x80),
  6632. ContentEncoding: Uint8Array.of(0x62, 0x40),
  6633. ContentEncodingOrder: Uint8Array.of(0x50, 0x31),
  6634. ContentEncodingScope: Uint8Array.of(0x50, 0x32),
  6635. ContentEncodingType: Uint8Array.of(0x50, 0x33),
  6636. ContentCompression: Uint8Array.of(0x50, 0x34),
  6637. ContentCompAlgo: Uint8Array.of(0x42, 0x54),
  6638. ContentCompSettings: Uint8Array.of(0x42, 0x55),
  6639. ContentEncryption: Uint8Array.of(0x50, 0x35),
  6640. ContentEncAlgo: Uint8Array.of(0x47, 0xE1),
  6641. ContentEncKeyID: Uint8Array.of(0x47, 0xE2),
  6642. ContentSignature: Uint8Array.of(0x47, 0xE3),
  6643. ContentSigKeyID: Uint8Array.of(0x47, 0xE4),
  6644. ContentSigAlgo: Uint8Array.of(0x47, 0xE5),
  6645. ContentSigHashAlgo: Uint8Array.of(0x47, 0xE6),
  6646. Cues: Uint8Array.of(0x1C, 0x53, 0xBB, 0x6B),
  6647. CuePoint: Uint8Array.of(0xBB),
  6648. CueTime: Uint8Array.of(0xB3),
  6649. CueTrackPositions: Uint8Array.of(0xB7),
  6650. CueTrack: Uint8Array.of(0xF7),
  6651. CueClusterPosition: Uint8Array.of(0xF1),
  6652. CueRelativePosition: Uint8Array.of(0xF0),
  6653. CueDuration: Uint8Array.of(0xB2),
  6654. CueBlockNumber: Uint8Array.of(0x53, 0x78),
  6655. CueCodecState: Uint8Array.of(0xEA),
  6656. CueReference: Uint8Array.of(0xDB),
  6657. CueRefTime: Uint8Array.of(0x96),
  6658. Attachments: Uint8Array.of(0x19, 0x41, 0xA4, 0x69),
  6659. AttachedFile: Uint8Array.of(0x61, 0xA7),
  6660. FileDescription: Uint8Array.of(0x46, 0x7E),
  6661. FileName: Uint8Array.of(0x46, 0x6E),
  6662. FileMimeType: Uint8Array.of(0x46, 0x60),
  6663. FileData: Uint8Array.of(0x46, 0x5C),
  6664. FileUID: Uint8Array.of(0x46, 0xAE),
  6665. Chapters: Uint8Array.of(0x10, 0x43, 0xA7, 0x70),
  6666. EditionEntry: Uint8Array.of(0x45, 0xB9),
  6667. EditionUID: Uint8Array.of(0x45, 0xBC),
  6668. EditionFlagHidden: Uint8Array.of(0x45, 0xBD),
  6669. EditionFlagDefault: Uint8Array.of(0x45, 0xDB),
  6670. EditionFlagOrdered: Uint8Array.of(0x45, 0xDD),
  6671. ChapterAtom: Uint8Array.of(0xB6),
  6672. ChapterUID: Uint8Array.of(0x73, 0xC4),
  6673. ChapterStringUID: Uint8Array.of(0x56, 0x54),
  6674. ChapterTimeStart: Uint8Array.of(0x91),
  6675. ChapterTimeEnd: Uint8Array.of(0x92),
  6676. ChapterFlagHidden: Uint8Array.of(0x98),
  6677. ChapterFlagEnabled: Uint8Array.of(0x45, 0x98),
  6678. ChapterSegmentUID: Uint8Array.of(0x6E, 0x67),
  6679. ChapterSegmentEditionUID: Uint8Array.of(0x6E, 0xBC),
  6680. ChapterPhysicalEquiv: Uint8Array.of(0x63, 0xC3),
  6681. ChapterTrack: Uint8Array.of(0x8F),
  6682. ChapterTrackNumber: Uint8Array.of(0x89),
  6683. ChapterDisplay: Uint8Array.of(0x80),
  6684. ChapString: Uint8Array.of(0x85),
  6685. ChapLanguage: Uint8Array.of(0x43, 0x7C),
  6686. ChapCountry: Uint8Array.of(0x43, 0x7E),
  6687. ChapProcess: Uint8Array.of(0x69, 0x44),
  6688. ChapProcessCodecID: Uint8Array.of(0x69, 0x55),
  6689. ChapProcessPrivate: Uint8Array.of(0x45, 0x0D),
  6690. ChapProcessCommand: Uint8Array.of(0x69, 0x11),
  6691. ChapProcessTime: Uint8Array.of(0x69, 0x22),
  6692. ChapProcessData: Uint8Array.of(0x69, 0x33),
  6693. Tags: Uint8Array.of(0x12, 0x54, 0xC3, 0x67),
  6694. Tag: Uint8Array.of(0x73, 0x73),
  6695. Targets: Uint8Array.of(0x63, 0xC0),
  6696. TargetTypeValue: Uint8Array.of(0x68, 0xCA),
  6697. TargetType: Uint8Array.of(0x63, 0xCA),
  6698. TagTrackUID: Uint8Array.of(0x63, 0xC5),
  6699. TagEditionUID: Uint8Array.of(0x63, 0xC9),
  6700. TagChapterUID: Uint8Array.of(0x63, 0xC4),
  6701. TagAttachmentUID: Uint8Array.of(0x63, 0xC6),
  6702. SimpleTag: Uint8Array.of(0x67, 0xC8),
  6703. TagName: Uint8Array.of(0x45, 0xA3),
  6704. TagLanguage: Uint8Array.of(0x44, 0x7A),
  6705. TagDefault: Uint8Array.of(0x44, 0x84),
  6706. TagString: Uint8Array.of(0x44, 0x87),
  6707. TagBinary: Uint8Array.of(0x44, 0x85),
  6708. };
  6709.  
  6710.  
  6711.  
  6712. var EBML = /*#__PURE__*/Object.freeze({
  6713. Value: Value,
  6714. Element: Element,
  6715. bytes: bytes,
  6716. number: number,
  6717. vintEncodedNumber: vintEncodedNumber,
  6718. int16: int16,
  6719. float: float,
  6720. string: string,
  6721. element: element,
  6722. unknownSizeElement: unknownSizeElement,
  6723. build: build,
  6724. getEBMLByteLength: getEBMLByteLength,
  6725. UNKNOWN_SIZE: UNKNOWN_SIZE,
  6726. vintEncode: vintEncode,
  6727. getSizeMask: getSizeMask,
  6728. ID: ID,
  6729. numberToByteArray: numberToByteArray,
  6730. stringToByteArray: stringToByteArray,
  6731. getNumberByteLength: getNumberByteLength,
  6732. int16Bit: int16Bit,
  6733. float32bit: float32bit,
  6734. dumpBytes: dumpBytes
  6735. });
  6736.  
  6737. /***
  6738. * The EMBL builder is from simple-ebml-builder
  6739. *
  6740. * Copyright 2017 ryiwamoto
  6741. *
  6742. * @author ryiwamoto, qli5
  6743. *
  6744. * Permission is hereby granted, free of charge, to any person obtaining
  6745. * a copy of this software and associated documentation files (the
  6746. * "Software"), to deal in the Software without restriction, including
  6747. * without limitation the rights to use, copy, modify, merge, publish,
  6748. * distribute, sublicense, and/or sell copies of the Software, and to
  6749. * permit persons to whom the Software is furnished to do so, subject
  6750. * to the following conditions:
  6751. *
  6752. * The above copyright notice and this permission notice shall be
  6753. * included in all copies or substantial portions of the Software.
  6754. *
  6755. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  6756. * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  6757. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  6758. * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
  6759. * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
  6760. * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  6761. * DEALINGS IN THE SOFTWARE.
  6762. */
  6763.  
  6764. /***
  6765. * Copyright (C) 2018 Qli5. All Rights Reserved.
  6766. *
  6767. * @author qli5 <goodlq11[at](163|gmail).com>
  6768. *
  6769. * This Source Code Form is subject to the terms of the Mozilla Public
  6770. * License, v. 2.0. If a copy of the MPL was not distributed with this
  6771. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6772. */
  6773.  
  6774. class MKV {
  6775. constructor(config) {
  6776. this.min = true;
  6777. this.onprogress = null;
  6778. Object.assign(this, config);
  6779. this.segmentUID = MKV.randomBytes(16);
  6780. this.trackUIDBase = Math.trunc(Math.random() * 2 ** 16);
  6781. this.trackMetadata = { h264: null, aac: null, ass: null };
  6782. this.duration = 0;
  6783. this.blocks = { h264: [], aac: [], ass: [] };
  6784. }
  6785.  
  6786. static randomBytes(num) {
  6787. return Array.from(new Array(num), () => Math.trunc(Math.random() * 256));
  6788. }
  6789.  
  6790. static textToMS(str) {
  6791. const [, h, mm, ss, ms10] = str.match(/(\\d+):(\\d+):(\\d+).(\\d+)/);
  6792. return h * 3600000 + mm * 60000 + ss * 1000 + ms10 * 10;
  6793. }
  6794.  
  6795. static mimeToCodecID(str) {
  6796. if (str.startsWith('avc1')) {
  6797. return 'V_MPEG4/ISO/AVC';
  6798. }
  6799. else if (str.startsWith('mp4a')) {
  6800. return 'A_AAC';
  6801. }
  6802. else {
  6803. throw new Error(\`MKVRemuxer: unknown codec \${str}\`);
  6804. }
  6805. }
  6806.  
  6807. static uint8ArrayConcat(...array) {
  6808. // if (Array.isArray(array[0])) array = array[0];
  6809. if (array.length == 1) return array[0];
  6810. if (typeof Buffer != 'undefined') return Buffer.concat(array);
  6811. const ret = new Uint8Array(array.reduce((i, j) => i + j.byteLength, 0));
  6812. let length = 0;
  6813. for (let e of array) {
  6814. ret.set(e, length);
  6815. length += e.byteLength;
  6816. }
  6817. return ret;
  6818. }
  6819.  
  6820. addH264Metadata(h264) {
  6821. this.trackMetadata.h264 = {
  6822. codecId: MKV.mimeToCodecID(h264.codec),
  6823. codecPrivate: h264.avcc,
  6824. defaultDuration: h264.refSampleDuration * 1000000,
  6825. pixelWidth: h264.codecWidth,
  6826. pixelHeight: h264.codecHeight,
  6827. displayWidth: h264.presentWidth,
  6828. displayHeight: h264.presentHeight
  6829. };
  6830. this.duration = Math.max(this.duration, h264.duration);
  6831. }
  6832.  
  6833. addAACMetadata(aac) {
  6834. this.trackMetadata.aac = {
  6835. codecId: MKV.mimeToCodecID(aac.originalCodec),
  6836. codecPrivate: aac.configRaw,
  6837. defaultDuration: aac.refSampleDuration * 1000000,
  6838. samplingFrequence: aac.audioSampleRate,
  6839. channels: aac.channelCount
  6840. };
  6841. this.duration = Math.max(this.duration, aac.duration);
  6842. }
  6843.  
  6844. addASSMetadata(ass) {
  6845. this.trackMetadata.ass = {
  6846. codecId: 'S_TEXT/ASS',
  6847. codecPrivate: new _TextEncoder().encode(ass.header)
  6848. };
  6849. }
  6850.  
  6851. addH264Stream(h264) {
  6852. this.blocks.h264 = this.blocks.h264.concat(h264.samples.map(e => ({
  6853. track: 1,
  6854. frame: MKV.uint8ArrayConcat(...e.units.map(i => i.data)),
  6855. isKeyframe: e.isKeyframe,
  6856. discardable: Boolean(e.refIdc),
  6857. timestamp: e.pts,
  6858. simple: true,
  6859. })));
  6860. }
  6861.  
  6862. addAACStream(aac) {
  6863. this.blocks.aac = this.blocks.aac.concat(aac.samples.map(e => ({
  6864. track: 2,
  6865. frame: e.unit,
  6866. timestamp: e.pts,
  6867. simple: true,
  6868. })));
  6869. }
  6870.  
  6871. addASSStream(ass) {
  6872. this.blocks.ass = this.blocks.ass.concat(ass.lines.map((e, i) => ({
  6873. track: 3,
  6874. frame: new _TextEncoder().encode(\`\${i},\${e['Layer'] || ''},\${e['Style'] || ''},\${e['Name'] || ''},\${e['MarginL'] || ''},\${e['MarginR'] || ''},\${e['MarginV'] || ''},\${e['Effect'] || ''},\${e['Text'] || ''}\`),
  6875. timestamp: MKV.textToMS(e['Start']),
  6876. duration: MKV.textToMS(e['End']) - MKV.textToMS(e['Start']),
  6877. })));
  6878. }
  6879.  
  6880. build() {
  6881. return new _Blob([
  6882. this.buildHeader(),
  6883. this.buildBody()
  6884. ]);
  6885. }
  6886.  
  6887. buildHeader() {
  6888. return new _Blob([EBML.build(EBML.element(EBML.ID.EBML, [
  6889. EBML.element(EBML.ID.EBMLVersion, EBML.number(1)),
  6890. EBML.element(EBML.ID.EBMLReadVersion, EBML.number(1)),
  6891. EBML.element(EBML.ID.EBMLMaxIDLength, EBML.number(4)),
  6892. EBML.element(EBML.ID.EBMLMaxSizeLength, EBML.number(8)),
  6893. EBML.element(EBML.ID.DocType, EBML.string('matroska')),
  6894. EBML.element(EBML.ID.DocTypeVersion, EBML.number(4)),
  6895. EBML.element(EBML.ID.DocTypeReadVersion, EBML.number(2)),
  6896. ]))]);
  6897. }
  6898.  
  6899. buildBody() {
  6900. if (this.min) {
  6901. return new _Blob([EBML.build(EBML.element(EBML.ID.Segment, [
  6902. this.getSegmentInfo(),
  6903. this.getTracks(),
  6904. ...this.getClusterArray()
  6905. ]))]);
  6906. }
  6907. else {
  6908. return new _Blob([EBML.build(EBML.element(EBML.ID.Segment, [
  6909. this.getSeekHead(),
  6910. this.getVoid(4100),
  6911. this.getSegmentInfo(),
  6912. this.getTracks(),
  6913. this.getVoid(1100),
  6914. ...this.getClusterArray()
  6915. ]))]);
  6916. }
  6917. }
  6918.  
  6919. getSeekHead() {
  6920. return EBML.element(EBML.ID.SeekHead, [
  6921. EBML.element(EBML.ID.Seek, [
  6922. EBML.element(EBML.ID.SeekID, EBML.bytes(EBML.ID.Info)),
  6923. EBML.element(EBML.ID.SeekPosition, EBML.number(4050))
  6924. ]),
  6925. EBML.element(EBML.ID.Seek, [
  6926. EBML.element(EBML.ID.SeekID, EBML.bytes(EBML.ID.Tracks)),
  6927. EBML.element(EBML.ID.SeekPosition, EBML.number(4200))
  6928. ]),
  6929. ]);
  6930. }
  6931.  
  6932. getVoid(length = 2000) {
  6933. return EBML.element(EBML.ID.Void, EBML.bytes(new Uint8Array(length)));
  6934. }
  6935.  
  6936. getSegmentInfo() {
  6937. return EBML.element(EBML.ID.Info, [
  6938. EBML.element(EBML.ID.TimecodeScale, EBML.number(1000000)),
  6939. EBML.element(EBML.ID.MuxingApp, EBML.string('flv.js + assparser_qli5 -> simple-ebml-builder')),
  6940. EBML.element(EBML.ID.WritingApp, EBML.string('flvass2mkv.js by qli5')),
  6941. EBML.element(EBML.ID.Duration, EBML.float(this.duration)),
  6942. EBML.element(EBML.ID.SegmentUID, EBML.bytes(this.segmentUID)),
  6943. ]);
  6944. }
  6945.  
  6946. getTracks() {
  6947. return EBML.element(EBML.ID.Tracks, [
  6948. this.getVideoTrackEntry(),
  6949. this.getAudioTrackEntry(),
  6950. this.getSubtitleTrackEntry()
  6951. ]);
  6952. }
  6953.  
  6954. getVideoTrackEntry() {
  6955. return EBML.element(EBML.ID.TrackEntry, [
  6956. EBML.element(EBML.ID.TrackNumber, EBML.number(1)),
  6957. EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 1)),
  6958. EBML.element(EBML.ID.TrackType, EBML.number(0x01)),
  6959. EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
  6960. EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.h264.codecId)),
  6961. EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.h264.codecPrivate)),
  6962. EBML.element(EBML.ID.DefaultDuration, EBML.number(this.trackMetadata.h264.defaultDuration)),
  6963. EBML.element(EBML.ID.Language, EBML.string('und')),
  6964. EBML.element(EBML.ID.Video, [
  6965. EBML.element(EBML.ID.PixelWidth, EBML.number(this.trackMetadata.h264.pixelWidth)),
  6966. EBML.element(EBML.ID.PixelHeight, EBML.number(this.trackMetadata.h264.pixelHeight)),
  6967. EBML.element(EBML.ID.DisplayWidth, EBML.number(this.trackMetadata.h264.displayWidth)),
  6968. EBML.element(EBML.ID.DisplayHeight, EBML.number(this.trackMetadata.h264.displayHeight)),
  6969. ]),
  6970. ]);
  6971. }
  6972.  
  6973. getAudioTrackEntry() {
  6974. return EBML.element(EBML.ID.TrackEntry, [
  6975. EBML.element(EBML.ID.TrackNumber, EBML.number(2)),
  6976. EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 2)),
  6977. EBML.element(EBML.ID.TrackType, EBML.number(0x02)),
  6978. EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
  6979. EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.aac.codecId)),
  6980. EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.aac.codecPrivate)),
  6981. EBML.element(EBML.ID.DefaultDuration, EBML.number(this.trackMetadata.aac.defaultDuration)),
  6982. EBML.element(EBML.ID.Language, EBML.string('und')),
  6983. EBML.element(EBML.ID.Audio, [
  6984. EBML.element(EBML.ID.SamplingFrequency, EBML.float(this.trackMetadata.aac.samplingFrequence)),
  6985. EBML.element(EBML.ID.Channels, EBML.number(this.trackMetadata.aac.channels)),
  6986. ]),
  6987. ]);
  6988. }
  6989.  
  6990. getSubtitleTrackEntry() {
  6991. return EBML.element(EBML.ID.TrackEntry, [
  6992. EBML.element(EBML.ID.TrackNumber, EBML.number(3)),
  6993. EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 3)),
  6994. EBML.element(EBML.ID.TrackType, EBML.number(0x11)),
  6995. EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
  6996. EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.ass.codecId)),
  6997. EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.ass.codecPrivate)),
  6998. EBML.element(EBML.ID.Language, EBML.string('und')),
  6999. ]);
  7000. }
  7001.  
  7002. getClusterArray() {
  7003. // H264 codecState
  7004. this.blocks.h264[0].simple = false;
  7005. this.blocks.h264[0].codecState = this.trackMetadata.h264.codecPrivate;
  7006.  
  7007. let i = 0;
  7008. let j = 0;
  7009. let k = 0;
  7010. let clusterTimeCode = 0;
  7011. let clusterContent = [EBML.element(EBML.ID.Timecode, EBML.number(clusterTimeCode))];
  7012. let ret = [clusterContent];
  7013. const progressThrottler = Math.pow(2, Math.floor(Math.log(this.blocks.h264.length >> 7) / Math.log(2))) - 1;
  7014. for (i = 0; i < this.blocks.h264.length; i++) {
  7015. const e = this.blocks.h264[i];
  7016. for (; j < this.blocks.aac.length; j++) {
  7017. if (this.blocks.aac[j].timestamp < e.timestamp) {
  7018. clusterContent.push(this.getBlocks(this.blocks.aac[j], clusterTimeCode));
  7019. }
  7020. else {
  7021. break;
  7022. }
  7023. }
  7024. for (; k < this.blocks.ass.length; k++) {
  7025. if (this.blocks.ass[k].timestamp < e.timestamp) {
  7026. clusterContent.push(this.getBlocks(this.blocks.ass[k], clusterTimeCode));
  7027. }
  7028. else {
  7029. break;
  7030. }
  7031. }
  7032. if (e.isKeyframe/* || clusterContent.length > 72 */) {
  7033. // start new cluster
  7034. clusterTimeCode = e.timestamp;
  7035. clusterContent = [EBML.element(EBML.ID.Timecode, EBML.number(clusterTimeCode))];
  7036. ret.push(clusterContent);
  7037. }
  7038. clusterContent.push(this.getBlocks(e, clusterTimeCode));
  7039. if (this.onprogress && !(i & progressThrottler)) this.onprogress({ loaded: i, total: this.blocks.h264.length });
  7040. }
  7041. for (; j < this.blocks.aac.length; j++) clusterContent.push(this.getBlocks(this.blocks.aac[j], clusterTimeCode));
  7042. for (; k < this.blocks.ass.length; k++) clusterContent.push(this.getBlocks(this.blocks.ass[k], clusterTimeCode));
  7043. if (this.onprogress) this.onprogress({ loaded: i, total: this.blocks.h264.length });
  7044. if (ret[0].length == 1) ret.shift();
  7045. ret = ret.map(clusterContent => EBML.element(EBML.ID.Cluster, clusterContent));
  7046.  
  7047. return ret;
  7048. }
  7049.  
  7050. getBlocks(e, clusterTimeCode) {
  7051. if (e.simple) {
  7052. return EBML.element(EBML.ID.SimpleBlock, [
  7053. EBML.vintEncodedNumber(e.track),
  7054. EBML.int16(e.timestamp - clusterTimeCode),
  7055. EBML.bytes(e.isKeyframe ? [128] : [0]),
  7056. EBML.bytes(e.frame)
  7057. ]);
  7058. }
  7059. else {
  7060. let blockGroupContent = [EBML.element(EBML.ID.Block, [
  7061. EBML.vintEncodedNumber(e.track),
  7062. EBML.int16(e.timestamp - clusterTimeCode),
  7063. EBML.bytes([0]),
  7064. EBML.bytes(e.frame)
  7065. ])];
  7066. if (typeof e.duration != 'undefined') {
  7067. blockGroupContent.push(EBML.element(EBML.ID.BlockDuration, EBML.number(e.duration)));
  7068. }
  7069. if (typeof e.codecState != 'undefined') {
  7070. blockGroupContent.push(EBML.element(EBML.ID.CodecState, EBML.bytes(e.codecState)));
  7071. }
  7072. return EBML.element(EBML.ID.BlockGroup, blockGroupContent);
  7073. }
  7074. }
  7075. }
  7076.  
  7077. /***
  7078. * FLV + ASS => MKV transmuxer
  7079. * Demux FLV into H264 + AAC stream and ASS into line stream; then
  7080. * remux them into a MKV file.
  7081. *
  7082. * @author qli5 <goodlq11[at](163|gmail).com>
  7083. *
  7084. * This Source Code Form is subject to the terms of the Mozilla Public
  7085. * License, v. 2.0. If a copy of the MPL was not distributed with this
  7086. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7087. *
  7088. * The FLV demuxer is from flv.js <https://github.com/Bilibili/flv.js/>
  7089. * by zheng qian <xqq@xqq.im>, licensed under Apache 2.0.
  7090. *
  7091. * The EMBL builder is from simple-ebml-builder
  7092. * <https://www.npmjs.com/package/simple-ebml-builder> by ryiwamoto,
  7093. * licensed under MIT.
  7094. */
  7095.  
  7096. const FLVASS2MKV = class {
  7097. constructor(config = {}) {
  7098. this.onflvprogress = null;
  7099. this.onassprogress = null;
  7100. this.onurlrevokesafe = null;
  7101. this.onfileload = null;
  7102. this.onmkvprogress = null;
  7103. this.onload = null;
  7104. Object.assign(this, config);
  7105. this.mkvConfig = { onprogress: this.onmkvprogress };
  7106. Object.assign(this.mkvConfig, config.mkvConfig);
  7107. }
  7108.  
  7109. /**
  7110. * Demux FLV into H264 + AAC stream and ASS into line stream; then
  7111. * remux them into a MKV file.
  7112. * @param {Blob|string|ArrayBuffer} flv
  7113. * @param {Blob|string|ArrayBuffer} ass
  7114. */
  7115. async build(flv = './samples/gen_case.flv', ass = './samples/gen_case.ass') {
  7116. // load flv and ass as arraybuffer
  7117. await Promise.all([
  7118. new Promise((r, j) => {
  7119. if (flv instanceof _Blob) {
  7120. const e = new FileReader();
  7121. e.onprogress = this.onflvprogress;
  7122. e.onload = () => r(flv = e.result);
  7123. e.onerror = j;
  7124. e.readAsArrayBuffer(flv);
  7125. }
  7126. else if (typeof flv == 'string') {
  7127. const e = new XMLHttpRequest();
  7128. e.responseType = 'arraybuffer';
  7129. e.onprogress = this.onflvprogress;
  7130. e.onload = () => r(flv = e.response);
  7131. e.onerror = j;
  7132. e.open('get', flv);
  7133. e.send();
  7134. flv = 2; // onurlrevokesafe
  7135. }
  7136. else if (flv instanceof ArrayBuffer) {
  7137. r(flv);
  7138. }
  7139. else {
  7140. j(new TypeError('flvass2mkv: flv {Blob|string|ArrayBuffer}'));
  7141. }
  7142. if (typeof ass != 'string' && this.onurlrevokesafe) this.onurlrevokesafe();
  7143. }),
  7144. new Promise((r, j) => {
  7145. if (ass instanceof _Blob) {
  7146. const e = new FileReader();
  7147. e.onprogress = this.onflvprogress;
  7148. e.onload = () => r(ass = e.result);
  7149. e.onerror = j;
  7150. e.readAsArrayBuffer(ass);
  7151. }
  7152. else if (typeof ass == 'string') {
  7153. const e = new XMLHttpRequest();
  7154. e.responseType = 'arraybuffer';
  7155. e.onprogress = this.onflvprogress;
  7156. e.onload = () => r(ass = e.response);
  7157. e.onerror = j;
  7158. e.open('get', ass);
  7159. e.send();
  7160. ass = 2; // onurlrevokesafe
  7161. }
  7162. else if (ass instanceof ArrayBuffer) {
  7163. r(ass);
  7164. }
  7165. else {
  7166. j(new TypeError('flvass2mkv: ass {Blob|string|ArrayBuffer}'));
  7167. }
  7168. if (typeof flv != 'string' && this.onurlrevokesafe) this.onurlrevokesafe();
  7169. }),
  7170. ]);
  7171. if (this.onfileload) this.onfileload();
  7172.  
  7173. const mkv = new MKV(this.mkvConfig);
  7174.  
  7175. const assParser = new ASS();
  7176. ass = assParser.parseFile(ass);
  7177. mkv.addASSMetadata(ass);
  7178. mkv.addASSStream(ass);
  7179.  
  7180. const flvProbeData = FLVDemuxer.probe(flv);
  7181. const flvDemuxer = new FLVDemuxer(flvProbeData);
  7182. let mediaInfo = null;
  7183. let h264 = null;
  7184. let aac = null;
  7185. flvDemuxer.onDataAvailable = (...array) => {
  7186. array.forEach(e => {
  7187. if (e.type == 'video') h264 = e;
  7188. else if (e.type == 'audio') aac = e;
  7189. else throw new Error(\`MKVRemuxer: unrecoginzed data type \${e.type}\`);
  7190. });
  7191. };
  7192. flvDemuxer.onMediaInfo = i => mediaInfo = i;
  7193. flvDemuxer.onTrackMetadata = (i, e) => {
  7194. if (i == 'video') mkv.addH264Metadata(e);
  7195. else if (i == 'audio') mkv.addAACMetadata(e);
  7196. else throw new Error(\`MKVRemuxer: unrecoginzed metadata type \${i}\`);
  7197. };
  7198. flvDemuxer.onError = e => { throw new Error(e); };
  7199. const finalOffset = flvDemuxer.parseChunks(flv, flvProbeData.dataOffset);
  7200. if (finalOffset != flv.byteLength) throw new Error('FLVDemuxer: unexpected EOF');
  7201. mkv.addH264Stream(h264);
  7202. mkv.addAACStream(aac);
  7203.  
  7204. const ret = mkv.build();
  7205. if (this.onload) this.onload(ret);
  7206. return ret;
  7207. }
  7208. };
  7209.  
  7210. // if nodejs then test
  7211. if (typeof window == 'undefined') {
  7212. if (require.main == module) {
  7213. (async () => {
  7214. const fs = require('fs');
  7215. const assFileName = process.argv.slice(1).find(e => e.includes('.ass')) || './samples/gen_case.ass';
  7216. const flvFileName = process.argv.slice(1).find(e => e.includes('.flv')) || './samples/gen_case.flv';
  7217. const assFile = fs.readFileSync(assFileName).buffer;
  7218. const flvFile = fs.readFileSync(flvFileName).buffer;
  7219. fs.writeFileSync('out.mkv', await new FLVASS2MKV({ onmkvprogress: console.log.bind(console) }).build(flvFile, assFile));
  7220. })();
  7221. }
  7222. }
  7223.  
  7224. return FLVASS2MKV;
  7225.  
  7226. }());
  7227. //# sourceMappingURL=index.js.map
  7228.  
  7229. </script>
  7230. <script>
  7231. const fileProgress = document.getElementById('fileProgress');
  7232. const mkvProgress = document.getElementById('mkvProgress');
  7233. const a = document.getElementById('a');
  7234. window.exec = async (option, target) => {
  7235. const defaultOption = {
  7236. onflvprogress: ({ loaded, total }) => {
  7237. fileProgress.value = loaded;
  7238. fileProgress.max = total;
  7239. },
  7240. onfileload: () => {
  7241. console.timeEnd('file');
  7242. console.time('flvass2mkv');
  7243. },
  7244. onmkvprogress: ({ loaded, total }) => {
  7245. mkvProgress.value = loaded;
  7246. mkvProgress.max = total;
  7247. },
  7248. name: 'merged.mkv',
  7249. };
  7250. option = Object.assign(defaultOption, option);
  7251. target.download = a.download = a.textContent = option.name;
  7252. console.time('file');
  7253. const mkv = await new FLVASS2MKV(option).build(option.flv, option.ass);
  7254. console.timeEnd('flvass2mkv');
  7255. target.href = a.href = URL.createObjectURL(mkv);
  7256. target.textContent = "另存为MKV"
  7257. target.onclick = () => {
  7258. window.close()
  7259. }
  7260. return a.href
  7261. };
  7262. </script>
  7263. </body>
  7264.  
  7265. </html>`;
  7266.  
  7267. /***
  7268. * Copyright (C) 2018 Qli5. All Rights Reserved.
  7269. *
  7270. * @author qli5 <goodlq11[at](163|gmail).com>
  7271. *
  7272. * This Source Code Form is subject to the terms of the Mozilla Public
  7273. * License, v. 2.0. If a copy of the MPL was not distributed with this
  7274. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7275. */
  7276.  
  7277. class MKVTransmuxer {
  7278. constructor(option) {
  7279. this.workerWin = null;
  7280. this.option = option;
  7281. }
  7282.  
  7283. /**
  7284. * FLV + ASS => MKV entry point
  7285. * @param {Blob|string|ArrayBuffer} flv
  7286. * @param {Blob|string|ArrayBuffer} ass
  7287. * @param {string=} name
  7288. */
  7289. exec(flv, ass, name, target) {
  7290. if (target.textContent != "另存为MKV") {
  7291. target.textContent = "打包中";
  7292.  
  7293. // 1. Allocate for a new window
  7294. if (!this.workerWin) this.workerWin = top.open('', undefined, ' ');
  7295.  
  7296. // 2. Inject scripts
  7297. this.workerWin.document.write(embeddedHTML);
  7298. this.workerWin.document.close();
  7299.  
  7300. // 3. Invoke exec
  7301. if (!(this.option instanceof Object)) this.option = null;
  7302. this.workerWin.exec(Object.assign({}, this.option, { flv, ass, name }), target);
  7303. URL.revokeObjectURL(flv);
  7304. URL.revokeObjectURL(ass);
  7305.  
  7306. // 4. Free parent window
  7307. // if (top.confirm('MKV打包中……要关掉这个窗口,释放内存吗?')) {
  7308. // top.location = 'about:blank';
  7309. // }
  7310. }
  7311. }
  7312. }
  7313.  
  7314. /***
  7315. * Copyright (C) 2018 Qli5. All Rights Reserved.
  7316. *
  7317. * @author qli5 <goodlq11[at](163|gmail).com>
  7318. *
  7319. * This Source Code Form is subject to the terms of the Mozilla Public
  7320. * License, v. 2.0. If a copy of the MPL was not distributed with this
  7321. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  7322. */
  7323.  
  7324. class UI {
  7325. constructor(twin, option = UI.optionDefaults) {
  7326. this.twin = twin;
  7327. this.option = option;
  7328.  
  7329. this.destroy = new HookedFunction();
  7330. this.dom = {};
  7331. this.cidSessionDestroy = new HookedFunction();
  7332. this.cidSessionDom = {};
  7333.  
  7334. this.destroy.addCallback(this.cidSessionDestroy.bind(this));
  7335.  
  7336. this.destroy.addCallback(() => {
  7337. Object.values(this.dom).forEach(e => e.remove());
  7338. this.dom = {};
  7339. });
  7340. this.cidSessionDestroy.addCallback(() => {
  7341. Object.values(this.cidSessionDom).forEach(e => e.remove());
  7342. this.cidSessionDom = {};
  7343. });
  7344.  
  7345. this.styleClearance();
  7346. }
  7347.  
  7348. styleClearance() {
  7349. let ret = `
  7350. .bilibili-player-context-menu-container.black ul.bilitwin li.context-menu-function > a:hover {
  7351. background: rgba(255,255,255,.12);
  7352. transition: all .3s ease-in-out;
  7353. cursor: pointer;
  7354. }
  7355.  
  7356. .bilitwin a {
  7357. cursor: pointer;
  7358. color: #00a1d6;
  7359. }
  7360.  
  7361. .bilitwin a:hover {
  7362. color: #f25d8e;
  7363. }
  7364.  
  7365. .bilitwin button {
  7366. color: #fff;
  7367. cursor: pointer;
  7368. text-align: center;
  7369. border-radius: 4px;
  7370. background-color: #00a1d6;
  7371. vertical-align: middle;
  7372. border: 1px solid #00a1d6;
  7373. transition: .1s;
  7374. transition-property: background-color,border,color;
  7375. user-select: none;
  7376. }
  7377.  
  7378. .bilitwin button:hover {
  7379. background-color: #00b5e5;
  7380. border-color: #00b5e5;
  7381. }
  7382.  
  7383. .bilitwin progress {
  7384. -webkit-appearance: progress-bar;
  7385. -moz-appearance: progress-bar;
  7386. appearance: progress-bar;
  7387. }
  7388.  
  7389. .bilitwin input[type="checkbox" i] {
  7390. -webkit-appearance: checkbox;
  7391. -moz-appearance: checkbox;
  7392. appearance: checkbox;
  7393. }
  7394. `;
  7395.  
  7396. const style = document.createElement('style');
  7397. style.type = 'text/css';
  7398. style.textContent = ret;
  7399. document.head.append(style);
  7400.  
  7401. return this.dom.style = style;
  7402. }
  7403.  
  7404. cidSessionRender() {
  7405. this.buildTitle();
  7406.  
  7407. if (this.option.title) this.appendTitle();
  7408. if (this.option.menu) this.appendMenu();
  7409. }
  7410.  
  7411. // Title Append
  7412. buildTitle(monkey = this.twin.monkey) {
  7413. // 1. build videoA, assA
  7414. const fontSize = '15px';
  7415. const videoA = document.createElement('a');
  7416. videoA.style.fontSize = fontSize;
  7417. videoA.textContent = '\u89C6\u9891FLV';
  7418. const assA = document.createElement('a');
  7419.  
  7420. // 1.1 build videoA
  7421. assA.style.fontSize = fontSize;
  7422. assA.textContent = '\u5F39\u5E55ASS';
  7423. videoA.onmouseover = async () => {
  7424. // 1.1.1 give processing hint
  7425. videoA.textContent = '正在FLV';
  7426. videoA.onmouseover = null;
  7427.  
  7428. // 1.1.2 query video
  7429. const video_format = await monkey.queryInfo('video');
  7430.  
  7431. // 1.1.3 display video
  7432. videoA.textContent = `视频${video_format ? video_format.toUpperCase() : 'FLV'}`;
  7433. videoA.onclick = () => this.displayFLVDiv();
  7434. };
  7435.  
  7436. // 1.2 build assA
  7437. assA.onmouseover = async () => {
  7438. // 1.2.1 give processing hint
  7439. assA.textContent = '正在ASS';
  7440. assA.onmouseover = null;
  7441.  
  7442. // 1.2.2 query flv
  7443. assA.href = await monkey.queryInfo('ass');
  7444.  
  7445. // 1.2.3 response mp4
  7446. assA.textContent = '弹幕ASS';
  7447. if (monkey.mp4 && monkey.mp4.match) {
  7448. assA.download = monkey.mp4.match(/\d(?:\d|-|hd)*(?=\.mp4)/)[0] + '.ass';
  7449. } else {
  7450. assA.download = monkey.cid + '.ass';
  7451. }
  7452. };
  7453.  
  7454. // 2. save to cache
  7455. Object.assign(this.cidSessionDom, { videoA, assA });
  7456. return this.cidSessionDom;
  7457. }
  7458.  
  7459. appendTitle({ videoA, assA } = this.cidSessionDom) {
  7460. // 1. build div
  7461. const div = document.createElement('div');
  7462.  
  7463. // 2. append to title
  7464. div.addEventListener('click', e => e.stopPropagation());
  7465. div.className = 'bilitwin';
  7466. div.append(...[videoA, ' ', assA]);
  7467. const tminfo = document.querySelector('div.tminfo') || document.querySelector('div.info-second') || document.querySelector('div.video-data');
  7468. tminfo.style.float = 'none';
  7469. tminfo.after(div);
  7470.  
  7471. // 3. save to cache
  7472. this.cidSessionDom.titleDiv = div;
  7473.  
  7474. return div;
  7475. }
  7476.  
  7477. appendShortVideoTitle({ video_playurl, cover_img }) {
  7478. const fontSize = '15px';
  7479. const marginRight = '15px';
  7480. const videoA = document.createElement('a');
  7481. videoA.style.fontSize = fontSize;
  7482. videoA.style.marginRight = marginRight;
  7483. videoA.href = video_playurl;
  7484. videoA.target = '_blank';
  7485. videoA.textContent = '\u4E0B\u8F7D\u89C6\u9891';
  7486. const coverA = document.createElement('a');
  7487.  
  7488. coverA.style.fontSize = fontSize;
  7489. coverA.href = cover_img;
  7490. coverA.target = '_blank';
  7491. coverA.textContent = '\u83B7\u53D6\u5C01\u9762';
  7492. videoA.onclick = e => {
  7493. e.preventDefault();alert("请使用右键另存为下载视频");
  7494. };
  7495.  
  7496. const span = document.createElement('span');
  7497.  
  7498. span.addEventListener('click', e => e.stopPropagation());
  7499. span.className = 'bilitwin';
  7500. span.append(...[videoA, ' ', coverA]);
  7501. const infoDiv = document.querySelector('div.base-info div.info');
  7502. infoDiv.appendChild(span);
  7503. }
  7504.  
  7505. buildFLVDiv(monkey = this.twin.monkey, flvs = monkey.flvs, cache = monkey.cache, format = monkey.video_format) {
  7506. // 1. build video splits
  7507. const flvTrs = flvs.map((href, index) => {
  7508. const tr = document.createElement('tr');
  7509. {
  7510. const td1 = document.createElement('td');
  7511. const a1 = document.createElement('a');
  7512. a1.href = href;
  7513. a1.download = cid + '-' + (index + 1) + '.' + (format || "flv");
  7514. a1.textContent = `视频分段 ${index + 1}`;
  7515. td1.append(a1);
  7516. tr.append(td1);
  7517. const td2 = document.createElement('td');
  7518. const a2 = document.createElement('a');
  7519.  
  7520. a2.onclick = e => this.downloadFLV({
  7521. monkey,
  7522. index,
  7523. a: e.target,
  7524. progress: tr.children[2].children[0]
  7525. });
  7526.  
  7527. a2.textContent = '\u7F13\u5B58\u672C\u6BB5';
  7528. td2.append(a2);
  7529. tr.append(td2);
  7530. const td3 = document.createElement('td');
  7531. const progress1 = document.createElement('progress');
  7532. progress1.setAttribute('value', '0');
  7533. progress1.setAttribute('max', '100');
  7534. progress1.textContent = '\u8FDB\u5EA6\u6761';
  7535. td3.append(progress1);
  7536. tr.append(td3);
  7537. }
  7538. return tr;
  7539. });
  7540.  
  7541. // 2. build exporter a
  7542. const exporterA = document.createElement('a');
  7543. if (this.option.aria2) {
  7544. exporterA.textContent = '导出Aria2';
  7545. exporterA.download = 'bilitwin.session';
  7546. exporterA.href = URL.createObjectURL(new Blob([Exporter.exportAria2(flvs, top.location.origin)]));
  7547. } else if (this.option.aria2RPC) {
  7548. exporterA.textContent = '发送Aria2 RPC';
  7549. exporterA.onclick = () => Exporter.sendToAria2RPC(flvs, top.location.origin);
  7550. } else if (this.option.m3u8) {
  7551. exporterA.textContent = '导出m3u8';
  7552. exporterA.download = 'bilitwin.m3u8';
  7553. exporterA.href = URL.createObjectURL(new Blob([Exporter.exportM3U8(flvs, top.location.origin, top.navigator.userAgent)]));
  7554. } else if (this.option.clipboard) {
  7555. exporterA.textContent = '全部复制到剪贴板';
  7556. exporterA.onclick = () => Exporter.copyToClipboard(flvs.join('\n'));
  7557. } else {
  7558. exporterA.textContent = '导出IDM';
  7559. exporterA.download = 'bilitwin.ef2';
  7560. exporterA.href = URL.createObjectURL(new Blob([Exporter.exportIDM(flvs, top.location.origin)]));
  7561. }
  7562.  
  7563. // 3. build body table
  7564. const table = document.createElement('table');
  7565. table.style.width = '100%';
  7566. table.style.lineHeight = '2em';
  7567. table.append(...flvTrs, (() => {
  7568. const tr1 = document.createElement('tr');
  7569. const td1 = document.createElement('td');
  7570. td1.append(...[exporterA]);
  7571. tr1.append(td1);
  7572. const td2 = document.createElement('td');
  7573. const a1 = document.createElement('a');
  7574.  
  7575. a1.onclick = e => format != "mp4" ? this.downloadAllFLVs({
  7576. a: e.target,
  7577. monkey,
  7578. table
  7579. }) : top.alert("不支持合并MP4视频");
  7580.  
  7581. a1.textContent = '\u7F13\u5B58\u5168\u90E8+\u81EA\u52A8\u5408\u5E76';
  7582. td2.append(a1);
  7583. tr1.append(td2);
  7584. const td3 = document.createElement('td');
  7585. const progress1 = document.createElement('progress');
  7586. progress1.setAttribute('value', '0');
  7587. progress1.setAttribute('max', flvs.length + 1);
  7588. progress1.textContent = '\u8FDB\u5EA6\u6761';
  7589. td3.append(progress1);
  7590. tr1.append(td3);
  7591. return tr1;
  7592. })(), (() => {
  7593. const tr1 = document.createElement('tr');
  7594. const td1 = document.createElement('td');
  7595. td1.colSpan = '3';
  7596. td1.textContent = '\u5408\u5E76\u529F\u80FD\u63A8\u8350\u914D\u7F6E\uFF1A\u81F3\u5C118G RAM\u3002\u628A\u81EA\u5DF1\u4E0B\u8F7D\u7684\u5206\u6BB5FLV\u62D6\u52A8\u5230\u8FD9\u91CC\uFF0C\u4E5F\u53EF\u4EE5\u5408\u5E76\u54E6~';
  7597. tr1.append(td1);
  7598. return tr1;
  7599. })(), cache ? (() => {
  7600. const tr1 = document.createElement('tr');
  7601. const td1 = document.createElement('td');
  7602. td1.colSpan = '3';
  7603. td1.textContent = '\u4E0B\u8F7D\u7684\u7F13\u5B58\u5206\u6BB5\u4F1A\u6682\u65F6\u505C\u7559\u5728\u7535\u8111\u91CC\uFF0C\u8FC7\u4E00\u6BB5\u65F6\u95F4\u4F1A\u81EA\u52A8\u6D88\u5931\u3002\u5EFA\u8BAE\u53EA\u5F00\u4E00\u4E2A\u6807\u7B7E\u9875\u3002';
  7604. tr1.append(td1);
  7605. return tr1;
  7606. })() : (() => {
  7607. const tr1 = document.createElement('tr');
  7608. const td1 = document.createElement('td');
  7609. td1.colSpan = '3';
  7610. td1.textContent = '\u5EFA\u8BAE\u53EA\u5F00\u4E00\u4E2A\u6807\u7B7E\u9875\u3002\u5173\u6389\u6807\u7B7E\u9875\u540E\uFF0C\u7F13\u5B58\u5C31\u4F1A\u88AB\u6E05\u7406\u3002\u522B\u5FD8\u4E86\u53E6\u5B58\u4E3A\uFF01';
  7611. tr1.append(td1);
  7612. return tr1;
  7613. })(), (() => {
  7614. const tr1 = document.createElement('tr');
  7615. const td1 = document.createElement('td');
  7616. td1.colSpan = '3';
  7617. this.displayQuota.bind(this)(td1);
  7618. tr1.append(td1);
  7619. return tr1;
  7620. })());
  7621. this.cidSessionDom.flvTable = table;
  7622.  
  7623. // 4. build container dlv
  7624. const div = UI.genDiv();
  7625. div.ondragenter = div.ondragover = e => UI.allowDrag(e);
  7626. div.ondrop = async e => {
  7627. // 4.1 allow drag
  7628. UI.allowDrag(e);
  7629.  
  7630. // 4.2 sort files if possible
  7631. const files = Array.from(e.dataTransfer.files);
  7632. if (files.every(e => e.name.search(/\d+-\d+(?:\d|-|hd)*\.flv/) != -1)) {
  7633. files.sort((a, b) => a.name.match(/\d+-(\d+)(?:\d|-|hd)*\.flv/)[1] - b.name.match(/\d+-(\d+)(?:\d|-|hd)*\.flv/)[1]);
  7634. }
  7635.  
  7636. // 4.3 give loaded files hint
  7637. table.append(...files.map(e => {
  7638. const tr1 = document.createElement('tr');
  7639. const td1 = document.createElement('td');
  7640. td1.colSpan = '3';
  7641. td1.textContent = e.name;
  7642. tr1.append(td1);
  7643. return tr1;
  7644. }));
  7645.  
  7646. // 4.4 determine output name
  7647. let outputName = files[0].name.match(/\d+-\d+(?:\d|-|hd)*\.flv/);
  7648. if (outputName) outputName = outputName[0].replace(/-\d/, "");else outputName = 'merge_' + files[0].name;
  7649.  
  7650. // 4.5 build output ui
  7651. const href = await this.twin.mergeFLVFiles(files);
  7652. table.append((() => {
  7653. const tr1 = document.createElement('tr');
  7654. const td1 = document.createElement('td');
  7655. td1.colSpan = '3';
  7656. const a1 = document.createElement('a');
  7657. a1.href = href;
  7658. a1.download = outputName;
  7659. a1.textContent = outputName;
  7660. td1.append(a1);
  7661. tr1.append(td1);
  7662. return tr1;
  7663. })());
  7664. };
  7665.  
  7666. // 5. build util buttons
  7667. div.append(table, (() => {
  7668. const button = document.createElement('button');
  7669. button.style.padding = '0.5em';
  7670. button.style.margin = '0.2em';
  7671.  
  7672. button.onclick = () => div.style.display = 'none';
  7673.  
  7674. button.textContent = '\u5173\u95ED';
  7675. return button;
  7676. })(), (() => {
  7677. const button = document.createElement('button');
  7678. button.style.padding = '0.5em';
  7679. button.style.margin = '0.2em';
  7680.  
  7681. button.onclick = () => monkey.cleanAllFLVsInCache();
  7682.  
  7683. button.textContent = '\u6E05\u7A7A\u8FD9\u4E2A\u89C6\u9891\u7684\u7F13\u5B58';
  7684. return button;
  7685. })(), (() => {
  7686. const button = document.createElement('button');
  7687. button.style.padding = '0.5em';
  7688. button.style.margin = '0.2em';
  7689.  
  7690. button.onclick = () => this.twin.clearCacheDB(cache);
  7691.  
  7692. button.textContent = '\u6E05\u7A7A\u6240\u6709\u89C6\u9891\u7684\u7F13\u5B58';
  7693. return button;
  7694. })());
  7695.  
  7696. // 6. cancel on destroy
  7697. this.cidSessionDestroy.addCallback(() => {
  7698. flvTrs.map(tr => {
  7699. const a = tr.children[1].children[0];
  7700. if (a.textContent == '取消') a.click();
  7701. });
  7702. });
  7703.  
  7704. return this.cidSessionDom.flvDiv = div;
  7705. }
  7706.  
  7707. displayFLVDiv(flvDiv = this.cidSessionDom.flvDiv) {
  7708. if (!flvDiv) {
  7709. flvDiv = this.buildFLVDiv();
  7710. document.body.append(flvDiv);
  7711. }
  7712. flvDiv.style.display = '';
  7713. return flvDiv;
  7714. }
  7715.  
  7716. async downloadAllFLVs({ a, monkey = this.twin.monkey, table = this.cidSessionDom.flvTable }) {
  7717. if (this.cidSessionDom.downloadAllTr) return;
  7718.  
  7719. // 1. hang player
  7720. monkey.hangPlayer();
  7721.  
  7722. // 2. give hang player hint
  7723. this.cidSessionDom.downloadAllTr = (() => {
  7724. const tr1 = document.createElement('tr');
  7725. const td1 = document.createElement('td');
  7726. td1.colSpan = '3';
  7727. td1.textContent = '\u5DF2\u5C4F\u853D\u7F51\u9875\u64AD\u653E\u5668\u7684\u7F51\u7EDC\u94FE\u63A5\u3002\u5237\u65B0\u9875\u9762\u53EF\u91CD\u65B0\u6FC0\u6D3B\u64AD\u653E\u5668\u3002';
  7728. tr1.append(td1);
  7729. return tr1;
  7730. })();
  7731. table.append(this.cidSessionDom.downloadAllTr);
  7732.  
  7733. // 3. click download all split
  7734. for (let i = 0; i < monkey.flvs.length; i++) {
  7735. if (table.rows[i].cells[1].children[0].textContent == '缓存本段') table.rows[i].cells[1].children[0].click();
  7736. }
  7737.  
  7738. // 4. set sprogress
  7739. const progress = a.parentElement.nextElementSibling.children[0];
  7740. progress.max = monkey.flvs.length + 1;
  7741. progress.value = 0;
  7742. for (let i = 0; i < monkey.flvs.length; i++) monkey.getFLV(i).then(e => progress.value++);
  7743.  
  7744. // 5. merge splits
  7745. const files = await monkey.getAllFLVs();
  7746. const href = await this.twin.mergeFLVFiles(files);
  7747. const ass = await monkey.ass;
  7748. const outputName = top.document.getElementsByTagName('h1')[0].textContent.trim();
  7749.  
  7750. // 6. build download all ui
  7751. progress.value++;
  7752. table.prepend((() => {
  7753. const tr1 = document.createElement('tr');
  7754. const td1 = document.createElement('td');
  7755. td1.colSpan = '3';
  7756. td1.style = 'border: 1px solid black';
  7757. const a1 = document.createElement('a');
  7758. a1.href = href;
  7759. a1.download = `${outputName}.flv`;
  7760.  
  7761. (a => {
  7762. if (this.option.autoDanmaku) a.onclick = () => a.nextElementSibling.click();
  7763. })(a1);
  7764.  
  7765. a1.textContent = '\u4FDD\u5B58\u5408\u5E76\u540EFLV';
  7766. td1.append(a1);
  7767. td1.append(' ');
  7768. const a2 = document.createElement('a');
  7769. a2.href = ass;
  7770. a2.download = `${outputName}.ass`;
  7771. a2.textContent = '\u5F39\u5E55ASS';
  7772. td1.append(a2);
  7773. td1.append(' ');
  7774. const a3 = document.createElement('a');
  7775.  
  7776. a3.onclick = e => new MKVTransmuxer().exec(href, ass, `${outputName}.mkv`, e.target);
  7777.  
  7778. a3.textContent = '\u6253\u5305MKV(\u8F6F\u5B57\u5E55\u5C01\u88C5)';
  7779. td1.append(a3);
  7780. td1.append(' ');
  7781. td1.append('\u8BB0\u5F97\u6E05\u7406\u5206\u6BB5\u7F13\u5B58\u54E6~');
  7782. tr1.append(td1);
  7783. return tr1;
  7784. })());
  7785.  
  7786. return href;
  7787. }
  7788.  
  7789. async downloadFLV({ a, monkey = this.twin.monkey, index, progress = {} }) {
  7790. // 1. add beforeUnloadHandler
  7791. const handler = e => UI.beforeUnloadHandler(e);
  7792. window.addEventListener('beforeunload', handler);
  7793.  
  7794. // 2. switch to cancel ui
  7795. a.textContent = '取消';
  7796. a.onclick = () => {
  7797. a.onclick = null;
  7798. window.removeEventListener('beforeunload', handler);
  7799. a.textContent = '已取消';
  7800. monkey.abortFLV(index);
  7801. };
  7802.  
  7803. // 3. try download
  7804. let url;
  7805. try {
  7806. url = await monkey.getFLV(index, (loaded, total) => {
  7807. progress.value = loaded;
  7808. progress.max = total;
  7809. });
  7810. url = URL.createObjectURL(url);
  7811. if (progress.value == 0) progress.value = progress.max = 1;
  7812. } catch (e) {
  7813. a.onclick = null;
  7814. window.removeEventListener('beforeunload', handler);
  7815. a.textContent = '错误';
  7816. throw e;
  7817. }
  7818.  
  7819. // 4. switch to complete ui
  7820. a.onclick = null;
  7821. window.removeEventListener('beforeunload', handler);
  7822. a.textContent = '另存为';
  7823. a.download = monkey.flvs[index].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  7824. a.href = url;
  7825. return url;
  7826. }
  7827.  
  7828. async displayQuota(td) {
  7829. return new Promise(resolve => {
  7830. const temporaryStorage = window.navigator.temporaryStorage || window.navigator.webkitTemporaryStorage || window.navigator.mozTemporaryStorage || window.navigator.msTemporaryStorage;
  7831. if (!temporaryStorage) return resolve(td.textContent = '这个浏览器不支持缓存呢~关掉标签页后,缓存马上就会消失哦');
  7832. temporaryStorage.queryUsageAndQuota((usage, quota) => resolve(td.textContent = `缓存已用空间:${Math.round(usage / 1048576)} MB / ${Math.round(quota / 1048576)} MB 也包括了B站本来的缓存`));
  7833. });
  7834. }
  7835.  
  7836. // Menu Append
  7837. appendMenu(playerWin = this.twin.playerWin) {
  7838. // 1. build monkey menu and polyfill menu
  7839. const monkeyMenu = this.buildMonkeyMenu();
  7840. const polyfillMenu = this.buildPolyfillMenu();
  7841.  
  7842. // 2. build ul
  7843. const ul = document.createElement('ul');
  7844.  
  7845. // 3. append to menu
  7846. ul.className = 'bilitwin';
  7847. ul.style.borderBottom = '1px solid rgba(255,255,255,.12)';
  7848. ul.append(...[monkeyMenu, polyfillMenu]);
  7849. const div = playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black bilibili-player-context-menu-origin')[0] || [...playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')].pop();
  7850. div.prepend(ul);
  7851.  
  7852. // 4. save to cache
  7853. this.cidSessionDom.menuUl = ul;
  7854.  
  7855. return ul;
  7856. }
  7857.  
  7858. buildMonkeyMenu({
  7859. playerWin = this.twin.playerWin,
  7860. BiliMonkey = this.twin.BiliMonkey,
  7861. monkey = this.twin.monkey,
  7862. videoA = this.cidSessionDom.videoA,
  7863. assA = this.cidSessionDom.assA
  7864. } = {}) {
  7865. let context_menu_videoA = document.createElement('li');
  7866.  
  7867. {
  7868. context_menu_videoA.className = 'context-menu-function';
  7869.  
  7870. context_menu_videoA.onmouseover = async ({ target: { lastChild: textNode } }) => {
  7871. if (videoA.onmouseover) await videoA.onmouseover();
  7872. if (textNode && textNode.textContent) {
  7873. textNode.textContent = textNode.textContent.slice(0, -3) + (monkey.video_format ? monkey.video_format.toUpperCase() : 'FLV');
  7874. }
  7875. };
  7876.  
  7877. context_menu_videoA.onclick = () => videoA.click();
  7878.  
  7879. const a1 = document.createElement('a');
  7880. a1.className = 'context-menu-a';
  7881. const span1 = document.createElement('span');
  7882. span1.className = 'video-contextmenu-icon';
  7883. a1.append(span1);
  7884. a1.append(' \u4E0B\u8F7D\u89C6\u9891FLV');
  7885. context_menu_videoA.append(a1);
  7886. }
  7887.  
  7888. Object.assign(this.cidSessionDom, { context_menu_videoA });
  7889.  
  7890. const li = document.createElement('li');
  7891. li.className = 'context-menu-menu bilitwin';
  7892.  
  7893. li.onclick = () => playerWin.document.getElementById('bilibiliPlayer').click();
  7894.  
  7895. const a1 = document.createElement('a');
  7896. a1.className = 'context-menu-a';
  7897. a1.append('BiliMonkey');
  7898. const span1 = document.createElement('span');
  7899. span1.className = 'bpui-icon bpui-icon-arrow-down';
  7900. span1.style = 'transform:rotate(-90deg);margin-top:3px;';
  7901. a1.append(span1);
  7902. li.append(a1);
  7903. const ul1 = document.createElement('ul');
  7904. ul1.append(context_menu_videoA);
  7905. const li1 = document.createElement('li');
  7906. li1.className = 'context-menu-function';
  7907.  
  7908. li1.onclick = async () => {
  7909. if (assA.onmouseover) await assA.onmouseover();
  7910. assA.click();
  7911. };
  7912.  
  7913. const a2 = document.createElement('a');
  7914. a2.className = 'context-menu-a';
  7915. const span2 = document.createElement('span');
  7916. span2.className = 'video-contextmenu-icon';
  7917. a2.append(span2);
  7918. a2.append(' \u4E0B\u8F7D\u5F39\u5E55ASS');
  7919. li1.append(a2);
  7920. ul1.append(li1);
  7921. const li2 = document.createElement('li');
  7922. li2.className = 'context-menu-function';
  7923.  
  7924. li2.onclick = () => this.displayOptionDiv();
  7925.  
  7926. const a3 = document.createElement('a');
  7927. a3.className = 'context-menu-a';
  7928. const span3 = document.createElement('span');
  7929. span3.className = 'video-contextmenu-icon';
  7930. a3.append(span3);
  7931. a3.append(' \u8BBE\u7F6E/\u5E2E\u52A9/\u5173\u4E8E');
  7932. li2.append(a3);
  7933. ul1.append(li2);
  7934. const li3 = document.createElement('li');
  7935. li3.className = 'context-menu-function';
  7936.  
  7937. li3.onclick = async () => UI.displayDownloadAllPageDefaultFormatsBody((await BiliMonkey.getAllPageDefaultFormats(playerWin, monkey)));
  7938.  
  7939. const a4 = document.createElement('a');
  7940. a4.className = 'context-menu-a';
  7941. const span4 = document.createElement('span');
  7942. span4.className = 'video-contextmenu-icon';
  7943. a4.append(span4);
  7944. a4.append(' (\u6D4B)\u6279\u91CF\u4E0B\u8F7D');
  7945. li3.append(a4);
  7946. ul1.append(li3);
  7947. const li4 = document.createElement('li');
  7948. li4.className = 'context-menu-function';
  7949.  
  7950. li4.onclick = async () => {
  7951. monkey.proxy = true;
  7952. monkey.flvs = null;
  7953. UI.hintInfo('请稍候,可能需要10秒时间……', playerWin);
  7954. // Yes, I AM lazy.
  7955. playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li[data-value="80"]').click();
  7956. await new Promise(r => playerWin.document.getElementsByTagName('video')[0].addEventListener('emptied', r));
  7957. return monkey.queryInfo('video');
  7958. };
  7959.  
  7960. const a5 = document.createElement('a');
  7961. a5.className = 'context-menu-a';
  7962. const span5 = document.createElement('span');
  7963. span5.className = 'video-contextmenu-icon';
  7964. a5.append(span5);
  7965. a5.append(' (\u6D4B)\u8F7D\u5165\u7F13\u5B58FLV');
  7966. li4.append(a5);
  7967. ul1.append(li4);
  7968. const li5 = document.createElement('li');
  7969. li5.className = 'context-menu-function';
  7970.  
  7971. li5.onclick = () => top.location.reload(true);
  7972.  
  7973. const a6 = document.createElement('a');
  7974. a6.className = 'context-menu-a';
  7975. const span6 = document.createElement('span');
  7976. span6.className = 'video-contextmenu-icon';
  7977. a6.append(span6);
  7978. a6.append(' (\u6D4B)\u5F3A\u5236\u5237\u65B0');
  7979. li5.append(a6);
  7980. ul1.append(li5);
  7981. const li6 = document.createElement('li');
  7982. li6.className = 'context-menu-function';
  7983.  
  7984. li6.onclick = () => this.cidSessionDestroy() && this.cidSessionRender();
  7985.  
  7986. const a7 = document.createElement('a');
  7987. a7.className = 'context-menu-a';
  7988. const span7 = document.createElement('span');
  7989. span7.className = 'video-contextmenu-icon';
  7990. a7.append(span7);
  7991. a7.append(' (\u6D4B)\u91CD\u542F\u811A\u672C');
  7992. li6.append(a7);
  7993. ul1.append(li6);
  7994. const li7 = document.createElement('li');
  7995. li7.className = 'context-menu-function';
  7996.  
  7997. li7.onclick = () => playerWin.player && playerWin.player.destroy();
  7998.  
  7999. const a8 = document.createElement('a');
  8000. a8.className = 'context-menu-a';
  8001. const span8 = document.createElement('span');
  8002. span8.className = 'video-contextmenu-icon';
  8003. a8.append(span8);
  8004. a8.append(' (\u6D4B)\u9500\u6BC1\u64AD\u653E\u5668');
  8005. li7.append(a8);
  8006. ul1.append(li7);
  8007. li.append(ul1);
  8008. return li;
  8009. }
  8010.  
  8011. buildPolyfillMenu({
  8012. playerWin = this.twin.playerWin,
  8013. BiliPolyfill = this.twin.BiliPolyfill,
  8014. polyfill = this.twin.polyfill
  8015. } = {}) {
  8016. let oped = [];
  8017. const refreshSession = new HookedFunction(() => oped = polyfill.userdata.oped[polyfill.getCollectionId()] || []); // as a convenient callback register
  8018. const li = document.createElement('li');
  8019. li.className = 'context-menu-menu bilitwin';
  8020.  
  8021. li.onclick = () => playerWin.document.getElementById('bilibiliPlayer').click();
  8022.  
  8023. const a1 = document.createElement('a');
  8024. a1.className = 'context-menu-a';
  8025.  
  8026. a1.onmouseover = () => refreshSession();
  8027.  
  8028. a1.append('BiliPolyfill');
  8029. a1.append(!polyfill.option.betabeta ? '(到设置开启)' : '');
  8030. const span1 = document.createElement('span');
  8031. span1.className = 'bpui-icon bpui-icon-arrow-down';
  8032. span1.style = 'transform:rotate(-90deg);margin-top:3px;';
  8033. a1.append(span1);
  8034. li.append(a1);
  8035. const ul1 = document.createElement('ul');
  8036. const li1 = document.createElement('li');
  8037. li1.className = 'context-menu-function';
  8038.  
  8039. li1.onclick = () => top.window.open(polyfill.getCoverImage(), '_blank');
  8040.  
  8041. const a2 = document.createElement('a');
  8042. a2.className = 'context-menu-a';
  8043. const span2 = document.createElement('span');
  8044. span2.className = 'video-contextmenu-icon';
  8045. a2.append(span2);
  8046. a2.append(' \u83B7\u53D6\u5C01\u9762');
  8047. li1.append(a2);
  8048. ul1.append(li1);
  8049. const li2 = document.createElement('li');
  8050. li2.className = 'context-menu-menu';
  8051. const a3 = document.createElement('a');
  8052. a3.className = 'context-menu-a';
  8053. const span3 = document.createElement('span');
  8054. span3.className = 'video-contextmenu-icon';
  8055. a3.append(span3);
  8056. a3.append(' \u66F4\u591A\u64AD\u653E\u901F\u5EA6');
  8057. const span4 = document.createElement('span');
  8058. span4.className = 'bpui-icon bpui-icon-arrow-down';
  8059. span4.style = 'transform:rotate(-90deg);margin-top:3px;';
  8060. a3.append(span4);
  8061. li2.append(a3);
  8062. const ul2 = document.createElement('ul');
  8063. const li3 = document.createElement('li');
  8064. li3.className = 'context-menu-function';
  8065.  
  8066. li3.onclick = () => {
  8067. polyfill.setVideoSpeed(0.1);
  8068. };
  8069.  
  8070. const a4 = document.createElement('a');
  8071. a4.className = 'context-menu-a';
  8072. const span5 = document.createElement('span');
  8073. span5.className = 'video-contextmenu-icon';
  8074. a4.append(span5);
  8075. a4.append(' 0.1');
  8076. li3.append(a4);
  8077. ul2.append(li3);
  8078. const li4 = document.createElement('li');
  8079. li4.className = 'context-menu-function';
  8080.  
  8081. li4.onclick = () => {
  8082. polyfill.setVideoSpeed(3);
  8083. };
  8084.  
  8085. const a5 = document.createElement('a');
  8086. a5.className = 'context-menu-a';
  8087. const span6 = document.createElement('span');
  8088. span6.className = 'video-contextmenu-icon';
  8089. a5.append(span6);
  8090. a5.append(' 3');
  8091. li4.append(a5);
  8092. ul2.append(li4);
  8093. const li5 = document.createElement('li');
  8094. li5.className = 'context-menu-function';
  8095.  
  8096. li5.onclick = e => polyfill.setVideoSpeed(e.children[0].children[1].value);
  8097.  
  8098. const a6 = document.createElement('a');
  8099. a6.className = 'context-menu-a';
  8100. const span7 = document.createElement('span');
  8101. span7.className = 'video-contextmenu-icon';
  8102. a6.append(span7);
  8103. a6.append(' \u70B9\u51FB\u786E\u8BA4');
  8104. const input = document.createElement('input');
  8105. input.type = 'text';
  8106. input.style = 'width: 35px; height: 70%';
  8107.  
  8108. input.onclick = e => e.stopPropagation();
  8109.  
  8110. (e => refreshSession.addCallback(() => e.value = polyfill.video.playbackRate))(input);
  8111.  
  8112. a6.append(input);
  8113. li5.append(a6);
  8114. ul2.append(li5);
  8115. li2.append(ul2);
  8116. ul1.append(li2);
  8117. const li6 = document.createElement('li');
  8118. li6.className = 'context-menu-menu';
  8119. const a7 = document.createElement('a');
  8120. a7.className = 'context-menu-a';
  8121. const span8 = document.createElement('span');
  8122. span8.className = 'video-contextmenu-icon';
  8123. a7.append(span8);
  8124. a7.append(' \u7247\u5934\u7247\u5C3E');
  8125. const span9 = document.createElement('span');
  8126. span9.className = 'bpui-icon bpui-icon-arrow-down';
  8127. span9.style = 'transform:rotate(-90deg);margin-top:3px;';
  8128. a7.append(span9);
  8129. li6.append(a7);
  8130. const ul3 = document.createElement('ul');
  8131. const li7 = document.createElement('li');
  8132. li7.className = 'context-menu-function';
  8133.  
  8134. li7.onclick = () => polyfill.markOPEDPosition(0);
  8135.  
  8136. const a8 = document.createElement('a');
  8137. a8.className = 'context-menu-a';
  8138. const span10 = document.createElement('span');
  8139. span10.className = 'video-contextmenu-icon';
  8140. a8.append(span10);
  8141. a8.append(' \u7247\u5934\u5F00\u59CB:');
  8142. const span11 = document.createElement('span');
  8143.  
  8144. (e => refreshSession.addCallback(() => e.textContent = oped[0] ? BiliPolyfill.secondToReadable(oped[0]) : '无'))(span11);
  8145.  
  8146. a8.append(span11);
  8147. li7.append(a8);
  8148. ul3.append(li7);
  8149. const li8 = document.createElement('li');
  8150. li8.className = 'context-menu-function';
  8151.  
  8152. li8.onclick = () => polyfill.markOPEDPosition(1);
  8153.  
  8154. const a9 = document.createElement('a');
  8155. a9.className = 'context-menu-a';
  8156. const span12 = document.createElement('span');
  8157. span12.className = 'video-contextmenu-icon';
  8158. a9.append(span12);
  8159. a9.append(' \u7247\u5934\u7ED3\u675F:');
  8160. const span13 = document.createElement('span');
  8161.  
  8162. (e => refreshSession.addCallback(() => e.textContent = oped[1] ? BiliPolyfill.secondToReadable(oped[1]) : '无'))(span13);
  8163.  
  8164. a9.append(span13);
  8165. li8.append(a9);
  8166. ul3.append(li8);
  8167. const li9 = document.createElement('li');
  8168. li9.className = 'context-menu-function';
  8169.  
  8170. li9.onclick = () => polyfill.markOPEDPosition(2);
  8171.  
  8172. const a10 = document.createElement('a');
  8173. a10.className = 'context-menu-a';
  8174. const span14 = document.createElement('span');
  8175. span14.className = 'video-contextmenu-icon';
  8176. a10.append(span14);
  8177. a10.append(' \u7247\u5C3E\u5F00\u59CB:');
  8178. const span15 = document.createElement('span');
  8179.  
  8180. (e => refreshSession.addCallback(() => e.textContent = oped[2] ? BiliPolyfill.secondToReadable(oped[2]) : '无'))(span15);
  8181.  
  8182. a10.append(span15);
  8183. li9.append(a10);
  8184. ul3.append(li9);
  8185. const li10 = document.createElement('li');
  8186. li10.className = 'context-menu-function';
  8187.  
  8188. li10.onclick = () => polyfill.markOPEDPosition(3);
  8189.  
  8190. const a11 = document.createElement('a');
  8191. a11.className = 'context-menu-a';
  8192. const span16 = document.createElement('span');
  8193. span16.className = 'video-contextmenu-icon';
  8194. a11.append(span16);
  8195. a11.append(' \u7247\u5C3E\u7ED3\u675F:');
  8196. const span17 = document.createElement('span');
  8197.  
  8198. (e => refreshSession.addCallback(() => e.textContent = oped[3] ? BiliPolyfill.secondToReadable(oped[3]) : '无'))(span17);
  8199.  
  8200. a11.append(span17);
  8201. li10.append(a11);
  8202. ul3.append(li10);
  8203. const li11 = document.createElement('li');
  8204. li11.className = 'context-menu-function';
  8205.  
  8206. li11.onclick = () => polyfill.clearOPEDPosition();
  8207.  
  8208. const a12 = document.createElement('a');
  8209. a12.className = 'context-menu-a';
  8210. const span18 = document.createElement('span');
  8211. span18.className = 'video-contextmenu-icon';
  8212. a12.append(span18);
  8213. a12.append(' \u53D6\u6D88\u6807\u8BB0');
  8214. li11.append(a12);
  8215. ul3.append(li11);
  8216. const li12 = document.createElement('li');
  8217. li12.className = 'context-menu-function';
  8218.  
  8219. li12.onclick = () => this.displayPolyfillDataDiv();
  8220.  
  8221. const a13 = document.createElement('a');
  8222. a13.className = 'context-menu-a';
  8223. const span19 = document.createElement('span');
  8224. span19.className = 'video-contextmenu-icon';
  8225. a13.append(span19);
  8226. a13.append(' \u68C0\u89C6\u6570\u636E/\u8BF4\u660E');
  8227. li12.append(a13);
  8228. ul3.append(li12);
  8229. li6.append(ul3);
  8230. ul1.append(li6);
  8231. const li13 = document.createElement('li');
  8232. li13.className = 'context-menu-menu';
  8233. const a14 = document.createElement('a');
  8234. a14.className = 'context-menu-a';
  8235. const span20 = document.createElement('span');
  8236. span20.className = 'video-contextmenu-icon';
  8237. a14.append(span20);
  8238. a14.append(' \u627E\u4E0A\u4E0B\u96C6');
  8239. const span21 = document.createElement('span');
  8240. span21.className = 'bpui-icon bpui-icon-arrow-down';
  8241. span21.style = 'transform:rotate(-90deg);margin-top:3px;';
  8242. a14.append(span21);
  8243. li13.append(a14);
  8244. const ul4 = document.createElement('ul');
  8245. const li14 = document.createElement('li');
  8246. li14.className = 'context-menu-function';
  8247.  
  8248. li14.onclick = () => {
  8249. if (polyfill.series[0]) {
  8250. top.window.open(`https://www.bilibili.com/video/av${polyfill.series[0].aid}`, '_blank');
  8251. }
  8252. };
  8253.  
  8254. const a15 = document.createElement('a');
  8255. a15.className = 'context-menu-a';
  8256. a15.style.width = 'initial';
  8257. const span22 = document.createElement('span');
  8258. span22.className = 'video-contextmenu-icon';
  8259. a15.append(span22);
  8260. const span23 = document.createElement('span');
  8261.  
  8262. (e => refreshSession.addCallback(() => e.textContent = polyfill.series[0] ? polyfill.series[0].title : '找不到'))(span23);
  8263.  
  8264. a15.append(span23);
  8265. li14.append(a15);
  8266. ul4.append(li14);
  8267. const li15 = document.createElement('li');
  8268. li15.className = 'context-menu-function';
  8269.  
  8270. li15.onclick = () => {
  8271. if (polyfill.series[1]) {
  8272. top.window.open(`https://www.bilibili.com/video/av${polyfill.series[1].aid}`, '_blank');
  8273. }
  8274. };
  8275.  
  8276. const a16 = document.createElement('a');
  8277. a16.className = 'context-menu-a';
  8278. a16.style.width = 'initial';
  8279. const span24 = document.createElement('span');
  8280. span24.className = 'video-contextmenu-icon';
  8281. a16.append(span24);
  8282. const span25 = document.createElement('span');
  8283.  
  8284. (e => refreshSession.addCallback(() => e.textContent = polyfill.series[1] ? polyfill.series[1].title : '找不到'))(span25);
  8285.  
  8286. a16.append(span25);
  8287. li15.append(a16);
  8288. ul4.append(li15);
  8289. li13.append(ul4);
  8290. ul1.append(li13);
  8291. const li16 = document.createElement('li');
  8292. li16.className = 'context-menu-function';
  8293.  
  8294. li16.onclick = () => BiliPolyfill.openMinimizedPlayer();
  8295.  
  8296. const a17 = document.createElement('a');
  8297. a17.className = 'context-menu-a';
  8298. const span26 = document.createElement('span');
  8299. span26.className = 'video-contextmenu-icon';
  8300. a17.append(span26);
  8301. a17.append(' \u5C0F\u7A97\u64AD\u653E');
  8302. li16.append(a17);
  8303. ul1.append(li16);
  8304. const li17 = document.createElement('li');
  8305. li17.className = 'context-menu-function';
  8306.  
  8307. li17.onclick = () => this.displayOptionDiv();
  8308.  
  8309. const a18 = document.createElement('a');
  8310. a18.className = 'context-menu-a';
  8311. const span27 = document.createElement('span');
  8312. span27.className = 'video-contextmenu-icon';
  8313. a18.append(span27);
  8314. a18.append(' \u8BBE\u7F6E/\u5E2E\u52A9/\u5173\u4E8E');
  8315. li17.append(a18);
  8316. ul1.append(li17);
  8317. const li18 = document.createElement('li');
  8318. li18.className = 'context-menu-function';
  8319.  
  8320. li18.onclick = () => polyfill.saveUserdata();
  8321.  
  8322. const a19 = document.createElement('a');
  8323. a19.className = 'context-menu-a';
  8324. const span28 = document.createElement('span');
  8325. span28.className = 'video-contextmenu-icon';
  8326. a19.append(span28);
  8327. a19.append(' (\u6D4B)\u7ACB\u5373\u4FDD\u5B58\u6570\u636E');
  8328. li18.append(a19);
  8329. ul1.append(li18);
  8330. const li19 = document.createElement('li');
  8331. li19.className = 'context-menu-function';
  8332.  
  8333. li19.onclick = () => {
  8334. BiliPolyfill.clearAllUserdata(playerWin);
  8335. polyfill.retrieveUserdata();
  8336. };
  8337.  
  8338. const a20 = document.createElement('a');
  8339. a20.className = 'context-menu-a';
  8340. const span29 = document.createElement('span');
  8341. span29.className = 'video-contextmenu-icon';
  8342. a20.append(span29);
  8343. a20.append(' (\u6D4B)\u5F3A\u5236\u6E05\u7A7A\u6570\u636E');
  8344. li19.append(a20);
  8345. ul1.append(li19);
  8346. li.append(ul1);
  8347. return li;
  8348. }
  8349.  
  8350. buildOptionDiv(twin = this.twin) {
  8351. const div = UI.genDiv();
  8352.  
  8353. div.append(this.buildMonkeyOptionTable(), this.buildPolyfillOptionTable(), this.buildUIOptionTable(), (() => {
  8354. const table1 = document.createElement('table');
  8355. table1.style.width = '100%';
  8356. table1.style.lineHeight = '2em';
  8357. const tr1 = document.createElement('tr');
  8358. const td1 = document.createElement('td');
  8359. td1.textContent = '\u8BBE\u7F6E\u81EA\u52A8\u4FDD\u5B58\uFF0C\u5237\u65B0\u540E\u751F\u6548\u3002';
  8360. tr1.append(td1);
  8361. table1.append(tr1);
  8362. const tr2 = document.createElement('tr');
  8363. const td2 = document.createElement('td');
  8364. td2.textContent = '\u89C6\u9891\u4E0B\u8F7D\u7EC4\u4EF6\u7684\u7F13\u5B58\u529F\u80FD\u53EA\u5728Windows+Chrome\u6D4B\u8BD5\u8FC7\uFF0C\u5982\u679C\u51FA\u73B0\u95EE\u9898\uFF0C\u8BF7\u5173\u95ED\u7F13\u5B58\u3002';
  8365. tr2.append(td2);
  8366. table1.append(tr2);
  8367. const tr3 = document.createElement('tr');
  8368. const td3 = document.createElement('td');
  8369. td3.textContent = '\u529F\u80FD\u589E\u5F3A\u7EC4\u4EF6\u5C3D\u91CF\u4FDD\u8BC1\u4E86\u517C\u5BB9\u6027\u3002\u4F46\u5982\u679C\u6709\u540C\u529F\u80FD\u811A\u672C/\u63D2\u4EF6\uFF0C\u8BF7\u5173\u95ED\u672C\u63D2\u4EF6\u7684\u5BF9\u5E94\u529F\u80FD\u3002';
  8370. tr3.append(td3);
  8371. table1.append(tr3);
  8372. const tr4 = document.createElement('tr');
  8373. const td4 = document.createElement('td');
  8374. td4.textContent = '\u8FD9\u4E2A\u811A\u672C\u4E43\u201C\u6309\u539F\u6837\u201D\u63D0\u4F9B\uFF0C\u4E0D\u9644\u5E26\u4EFB\u4F55\u660E\u793A\uFF0C\u6697\u793A\u6216\u6CD5\u5B9A\u7684\u4FDD\u8BC1\uFF0C\u5305\u62EC\u4F46\u4E0D\u9650\u4E8E\u5176\u6CA1\u6709\u7F3A\u9677\uFF0C\u9002\u5408\u7279\u5B9A\u76EE\u7684\u6216\u975E\u4FB5\u6743\u3002';
  8375. tr4.append(td4);
  8376. table1.append(tr4);
  8377. const tr5 = document.createElement('tr');
  8378. const td5 = document.createElement('td');
  8379. const a1 = document.createElement('a');
  8380. a1.href = 'https://gf.qytechs.cn/zh-CN/scripts/372516';
  8381. a1.target = '_blank';
  8382. a1.textContent = '\u66F4\u65B0';
  8383. td5.append(a1);
  8384. td5.append(' ');
  8385. const a2 = document.createElement('a');
  8386. a2.href = 'https://github.com/Xmader/bilitwin/issues';
  8387. a2.target = '_blank';
  8388. a2.textContent = '\u8BA8\u8BBA';
  8389. td5.append(a2);
  8390. td5.append(' ');
  8391. const a3 = document.createElement('a');
  8392. a3.href = 'https://github.com/Xmader/bilitwin/';
  8393. a3.target = '_blank';
  8394. a3.textContent = 'GitHub';
  8395. td5.append(a3);
  8396. td5.append(' ');
  8397. td5.append('Author: qli5. Copyright: qli5, 2014+, \u7530\u751F, grepmusic, xmader');
  8398. tr5.append(td5);
  8399. table1.append(tr5);
  8400. return table1;
  8401. })(), (() => {
  8402. const button = document.createElement('button');
  8403. button.style.padding = '0.5em';
  8404. button.style.margin = '0.2em';
  8405.  
  8406. button.onclick = () => div.style.display = 'none';
  8407.  
  8408. button.textContent = '\u5173\u95ED';
  8409. return button;
  8410. })(), (() => {
  8411. const button = document.createElement('button');
  8412. button.style.padding = '0.5em';
  8413. button.style.margin = '0.2em';
  8414.  
  8415. button.onclick = () => top.location.reload();
  8416.  
  8417. button.textContent = '\u4FDD\u5B58\u5E76\u5237\u65B0';
  8418. return button;
  8419. })(), (() => {
  8420. const button = document.createElement('button');
  8421. button.style.padding = '0.5em';
  8422. button.style.margin = '0.2em';
  8423.  
  8424. button.onclick = () => twin.resetOption() && top.location.reload();
  8425.  
  8426. button.textContent = '\u91CD\u7F6E\u5E76\u5237\u65B0';
  8427. return button;
  8428. })());
  8429.  
  8430. return this.dom.optionDiv = div;
  8431. }
  8432.  
  8433. buildMonkeyOptionTable(twin = this.twin, BiliMonkey = this.twin.BiliMonkey) {
  8434. const table = document.createElement('table');
  8435. {
  8436. table.style.width = '100%';
  8437. table.style.lineHeight = '2em';
  8438. const tr1 = document.createElement('tr');
  8439. const td1 = document.createElement('td');
  8440. td1.style = 'text-align:center';
  8441. td1.textContent = 'BiliMonkey\uFF08\u89C6\u9891\u6293\u53D6\u7EC4\u4EF6\uFF09';
  8442. tr1.append(td1);
  8443. table.append(tr1);
  8444. }
  8445.  
  8446. table.append(...BiliMonkey.optionDescriptions.map(([name, description]) => {
  8447. const tr1 = document.createElement('tr');
  8448. const label = document.createElement('label');
  8449. const input = document.createElement('input');
  8450. input.type = 'checkbox';
  8451. input.checked = twin.option[name];
  8452.  
  8453. input.onchange = e => {
  8454. twin.option[name] = e.target.checked;
  8455. twin.saveOption(twin.option);
  8456. };
  8457.  
  8458. label.append(input);
  8459. label.append(description);
  8460. tr1.append(label);
  8461. return tr1;
  8462. }));
  8463.  
  8464. table.append((() => {
  8465. const tr1 = document.createElement('tr');
  8466. const label = document.createElement('label');
  8467. const input = document.createElement('input');
  8468. input.type = 'number';
  8469. input.value = +twin.option["resolutionX"] || 560;
  8470. input.min = 480;
  8471.  
  8472. input.onchange = e => {
  8473. twin.option["resolutionX"] = +e.target.value;
  8474. twin.saveOption(twin.option);
  8475. };
  8476.  
  8477. label.append(input);
  8478. label.append(" x ");
  8479. const input1 = document.createElement('input');
  8480. input1.type = 'number';
  8481. input1.value = +twin.option["resolutionY"] || 420;
  8482. input1.min = 360;
  8483.  
  8484. input1.onchange = e => {
  8485. twin.option["resolutionY"] = +e.target.value;
  8486. twin.saveOption(twin.option);
  8487. };
  8488.  
  8489. label.append(input1);
  8490. tr1.append(label);
  8491. return tr1;
  8492. })());
  8493.  
  8494. table.append((() => {
  8495. const tr1 = document.createElement('tr');
  8496. const label = document.createElement('label');
  8497. const input = document.createElement('input');
  8498. input.type = 'checkbox';
  8499. input.checked = twin.option["enableVideoMaxResolution"];
  8500.  
  8501. input.onchange = e => {
  8502. twin.option["enableVideoMaxResolution"] = e.target.checked;
  8503. twin.saveOption(twin.option);
  8504. };
  8505.  
  8506. label.append(input);
  8507. label.append('\u81EA\u5B9A\u4E49\u4E0B\u8F7D\u7684\u89C6\u9891\u7684');
  8508. const b1 = document.createElement('b');
  8509. b1.textContent = '\u6700\u9AD8';
  8510. label.append(b1);
  8511. label.append('\u5206\u8FA8\u7387\uFF1A');
  8512. const select = document.createElement('select');
  8513.  
  8514. select.onchange = e => {
  8515. twin.option["videoMaxResolution"] = e.target.value;
  8516. twin.saveOption(twin.option);
  8517. };
  8518.  
  8519. select.append(...BiliMonkey.resolutionPreferenceOptions.map(([name, value]) => {
  8520. const option1 = document.createElement('option');
  8521. option1.value = value;
  8522. option1.selected = (twin.option["videoMaxResolution"] || "116") == value;
  8523. option1.textContent = name;
  8524. return option1;
  8525. }));
  8526. label.append(select);
  8527. tr1.append(label);
  8528. return tr1;
  8529. })());
  8530.  
  8531. return table;
  8532. }
  8533.  
  8534. buildPolyfillOptionTable(twin = this.twin, BiliPolyfill = this.twin.BiliPolyfill) {
  8535. const table = document.createElement('table');
  8536. {
  8537. table.style.width = '100%';
  8538. table.style.lineHeight = '2em';
  8539. const tr1 = document.createElement('tr');
  8540. const td1 = document.createElement('td');
  8541. td1.style = 'text-align:center';
  8542. td1.textContent = 'BiliPolyfill\uFF08\u529F\u80FD\u589E\u5F3A\u7EC4\u4EF6\uFF09';
  8543. tr1.append(td1);
  8544. table.append(tr1);
  8545. const tr2 = document.createElement('tr');
  8546. const td2 = document.createElement('td');
  8547. td2.style = 'text-align:center';
  8548. td2.textContent = '\u61D2\u9B3C\u4F5C\u8005\u8FD8\u5728\u6D4B\u8BD5\u7684\u65F6\u5019\uFF0CB\u7AD9\u5DF2\u7ECF\u4E0A\u7EBF\u4E86\u539F\u751F\u7684\u7A0D\u540E\u518D\u770B(\u0E51\u2022\u0300\u3142\u2022\u0301)\u0648\u2727';
  8549. tr2.append(td2);
  8550. table.append(tr2);
  8551. }
  8552.  
  8553. table.append(...BiliPolyfill.optionDescriptions.map(([name, description, disabled]) => {
  8554. const tr1 = document.createElement('tr');
  8555. const label = document.createElement('label');
  8556. label.style.textDecoration = disabled == 'disabled' ? 'line-through' : undefined;
  8557. const input = document.createElement('input');
  8558. input.type = 'checkbox';
  8559. input.checked = twin.option[name];
  8560.  
  8561. input.onchange = e => {
  8562. twin.option[name] = e.target.checked;
  8563. twin.saveOption(twin.option);
  8564. };
  8565.  
  8566. input.disabled = disabled == 'disabled';
  8567. label.append(input);
  8568. label.append(description);
  8569. tr1.append(label);
  8570. return tr1;
  8571. }));
  8572.  
  8573. return table;
  8574. }
  8575.  
  8576. buildUIOptionTable(twin = this.twin) {
  8577. const table = document.createElement('table');
  8578. {
  8579. table.style.width = '100%';
  8580. table.style.lineHeight = '2em';
  8581. const tr1 = document.createElement('tr');
  8582. const td1 = document.createElement('td');
  8583. td1.style = 'text-align:center';
  8584. td1.textContent = 'UI\uFF08\u7528\u6237\u754C\u9762\uFF09';
  8585. tr1.append(td1);
  8586. table.append(tr1);
  8587. }
  8588.  
  8589. table.append(...UI.optionDescriptions.map(([name, description]) => {
  8590. const tr1 = document.createElement('tr');
  8591. const label = document.createElement('label');
  8592. const input = document.createElement('input');
  8593. input.type = 'checkbox';
  8594. input.checked = twin.option[name];
  8595.  
  8596. input.onchange = e => {
  8597. twin.option[name] = e.target.checked;
  8598. twin.saveOption(twin.option);
  8599. };
  8600.  
  8601. label.append(input);
  8602. label.append(description);
  8603. tr1.append(label);
  8604. return tr1;
  8605. }));
  8606.  
  8607. return table;
  8608. }
  8609.  
  8610. displayOptionDiv(optionDiv = this.dom.optionDiv) {
  8611. if (!optionDiv) {
  8612. optionDiv = this.buildOptionDiv();
  8613. document.body.append(optionDiv);
  8614. }
  8615. optionDiv.style.display = '';
  8616. return optionDiv;
  8617. }
  8618.  
  8619. buildPolyfillDataDiv(polyfill = this.twin.polyfill) {
  8620. const textarea = document.createElement('textarea');
  8621.  
  8622. textarea.style.resize = 'vertical';
  8623. textarea.style.width = '100%';
  8624. textarea.style.height = '200px';
  8625. textarea.textContent = `
  8626. ${JSON.stringify(polyfill.userdata.oped).replace(/{/, '{\n').replace(/}/, '\n}').replace(/],/g, '],\n')}
  8627. `;
  8628. const div = UI.genDiv();
  8629.  
  8630. div.append((() => {
  8631. const p = document.createElement('p');
  8632. p.style.margin = '0.3em';
  8633. p.textContent = '\u8FD9\u91CC\u662F\u811A\u672C\u50A8\u5B58\u7684\u6570\u636E\u3002\u6240\u6709\u6570\u636E\u90FD\u53EA\u5B58\u5728\u6D4F\u89C8\u5668\u91CC\uFF0C\u522B\u4EBA\u4E0D\u77E5\u9053\uFF0CB\u7AD9\u4E5F\u4E0D\u77E5\u9053\uFF0C\u811A\u672C\u4F5C\u8005\u66F4\u4E0D\u77E5\u9053(\u8FD9\u4E2A\u5BB6\u4F19\u8FDE\u670D\u52A1\u5668\u90FD\u79DF\u4E0D\u8D77 \u6454';
  8634. return p;
  8635. })(), (() => {
  8636. const p = document.createElement('p');
  8637. p.style.margin = '0.3em';
  8638. p.textContent = 'B\u7AD9\u5DF2\u4E0A\u7EBF\u539F\u751F\u7684\u7A0D\u540E\u89C2\u770B\u529F\u80FD\u3002';
  8639. return p;
  8640. })(), (() => {
  8641. const p = document.createElement('p');
  8642. p.style.margin = '0.3em';
  8643. p.textContent = '\u8FD9\u91CC\u662F\u7247\u5934\u7247\u5C3E\u3002\u683C\u5F0F\u662F\uFF0Cav\u53F7\u6216\u756A\u5267\u53F7:[\u7247\u5934\u5F00\u59CB(\u9ED8\u8BA4=0),\u7247\u5934\u7ED3\u675F(\u9ED8\u8BA4=\u4E0D\u8DF3),\u7247\u5C3E\u5F00\u59CB(\u9ED8\u8BA4=\u4E0D\u8DF3),\u7247\u5C3E\u7ED3\u675F(\u9ED8\u8BA4=\u65E0\u7A77\u5927)]\u3002\u53EF\u4EE5\u4EFB\u610F\u586B\u5199null\uFF0C\u811A\u672C\u4F1A\u81EA\u52A8\u91C7\u7528\u9ED8\u8BA4\u503C\u3002';
  8644. return p;
  8645. })(), textarea, (() => {
  8646. const p = document.createElement('p');
  8647. p.style.margin = '0.3em';
  8648. p.textContent = '\u5F53\u7136\u53EF\u4EE5\u76F4\u63A5\u6E05\u7A7A\u5566\u3002\u53EA\u5220\u9664\u5176\u4E2D\u7684\u4E00\u4E9B\u884C\u7684\u8BDD\uFF0C\u4E00\u5B9A\u8981\u8BB0\u5F97\u5220\u6389\u591A\u4F59\u7684\u9017\u53F7\u3002';
  8649. return p;
  8650. })(), (() => {
  8651. const button = document.createElement('button');
  8652. button.style.padding = '0.5em';
  8653. button.style.margin = '0.2em';
  8654.  
  8655. button.onclick = () => div.remove();
  8656.  
  8657. button.textContent = '\u5173\u95ED';
  8658. return button;
  8659. })(), (() => {
  8660. const button = document.createElement('button');
  8661. button.style.padding = '0.5em';
  8662. button.style.margin = '0.2em';
  8663.  
  8664. button.onclick = e => {
  8665. if (!textarea.value) textarea.value = '{\n\n}';
  8666. textarea.value = textarea.value.replace(/,(\s|\n)*}/, '\n}').replace(/,(\s|\n),/g, ',\n').replace(/,(\s|\n)*]/g, ']');
  8667. const userdata = {};
  8668. try {
  8669. userdata.oped = JSON.parse(textarea.value);
  8670. } catch (e) {
  8671. alert('片头片尾: ' + e);throw e;
  8672. }
  8673. e.target.textContent = '格式没有问题!';
  8674. return userdata;
  8675. };
  8676.  
  8677. button.textContent = '\u9A8C\u8BC1\u683C\u5F0F';
  8678. return button;
  8679. })(), (() => {
  8680. const button = document.createElement('button');
  8681. button.style.padding = '0.5em';
  8682. button.style.margin = '0.2em';
  8683.  
  8684. button.onclick = e => {
  8685. polyfill.userdata = e.target.previousElementSibling.onclick({ target: e.target.previousElementSibling });
  8686. polyfill.saveUserdata();
  8687. e.target.textContent = '保存成功';
  8688. };
  8689.  
  8690. button.textContent = '\u5C1D\u8BD5\u4FDD\u5B58';
  8691. return button;
  8692. })());
  8693.  
  8694. return div;
  8695. }
  8696.  
  8697. displayPolyfillDataDiv(polyfill) {
  8698. const div = this.buildPolyfillDataDiv();
  8699. document.body.append(div);
  8700. div.style.display = 'block';
  8701.  
  8702. return div;
  8703. }
  8704.  
  8705. // Common
  8706. static buildDownloadAllPageDefaultFormatsBody(ret) {
  8707. const table = document.createElement('table');
  8708.  
  8709. table.onclick = e => e.stopPropagation();
  8710.  
  8711. for (const i of ret) {
  8712. table.append((() => {
  8713. const tr1 = document.createElement('tr');
  8714. const td1 = document.createElement('td');
  8715. td1.textContent = `
  8716. ${i.name}
  8717. `;
  8718. tr1.append(td1);
  8719. const td2 = document.createElement('td');
  8720. const a1 = document.createElement('a');
  8721. a1.href = i.durl[0];
  8722. a1.download = '';
  8723. a1.setAttribute('referrerpolicy', 'origin');
  8724. a1.textContent = i.durl[0].match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  8725. td2.append(a1);
  8726. tr1.append(td2);
  8727. const td3 = document.createElement('td');
  8728. const a2 = document.createElement('a');
  8729. a2.href = top.URL.createObjectURL(i.danmuku);
  8730. a2.download = `${i.outputName}.ass`;
  8731. a2.setAttribute('referrerpolicy', 'origin');
  8732. a2.textContent = `${i.outputName}.ass`;
  8733. td3.append(a2);
  8734. tr1.append(td3);
  8735. return tr1;
  8736. })(), ...i.durl.slice(1).map(href => {
  8737. const tr1 = document.createElement('tr');
  8738. const td1 = document.createElement('td');
  8739. td1.textContent = `
  8740. `;
  8741. tr1.append(td1);
  8742. const td2 = document.createElement('td');
  8743. const a1 = document.createElement('a');
  8744. a1.href = href;
  8745. a1.download = '';
  8746. a1.setAttribute('referrerpolicy', 'origin');
  8747. a1.textContent = href.match(/\d+-\d+(?:\d|-|hd)*\.(flv|mp4)/)[0];
  8748. td2.append(a1);
  8749. tr1.append(td2);
  8750. const td3 = document.createElement('td');
  8751. td3.textContent = `
  8752. `;
  8753. tr1.append(td3);
  8754. return tr1;
  8755. }), (() => {
  8756. const tr1 = document.createElement('tr');
  8757. const td1 = document.createElement('td');
  8758. td1.textContent = ` `;
  8759. tr1.append(td1);
  8760. return tr1;
  8761. })());
  8762. }
  8763.  
  8764. const fragment = document.createDocumentFragment();
  8765. const style1 = document.createElement('style');
  8766. style1.textContent = `
  8767. table {
  8768. width: 100%;
  8769. table-layout: fixed;
  8770. }
  8771. td {
  8772. overflow: hidden;
  8773. white-space: nowrap;
  8774. text-overflow: ellipsis;
  8775. text-align: center;
  8776. }
  8777. `;
  8778. fragment.append(style1);
  8779. const h1 = document.createElement('h1');
  8780. h1.textContent = '(\u6D4B\u8BD5) \u6279\u91CF\u6293\u53D6';
  8781. fragment.append(h1);
  8782. const ul1 = document.createElement('ul');
  8783. const li = document.createElement('li');
  8784. const p = document.createElement('p');
  8785. p.textContent = '\u6293\u53D6\u7684\u89C6\u9891\u7684\u6700\u9AD8\u5206\u8FA8\u7387\u53EF\u5728\u8BBE\u7F6E\u4E2D\u81EA\u5B9A\u4E49';
  8786. li.append(p);
  8787. ul1.append(li);
  8788. const li1 = document.createElement('li');
  8789. const p1 = document.createElement('p');
  8790. p1.textContent = '\u590D\u5236\u94FE\u63A5\u5730\u5740\u65E0\u6548\uFF0C\u8BF7\u5DE6\u952E\u5355\u51FB/\u53F3\u952E\u53E6\u5B58\u4E3A/\u53F3\u952E\u8C03\u7528\u4E0B\u8F7D\u5DE5\u5177';
  8791. li1.append(p1);
  8792. const p2 = document.createElement('p');
  8793. const em = document.createElement('em');
  8794. em.textContent = '\u5F00\u53D1\u8005\uFF1A\u9700\u8981\u6821\u9A8Creferrer\u548Cuser agent';
  8795. p2.append(em);
  8796. li1.append(p2);
  8797. ul1.append(li1);
  8798. const li2 = document.createElement('li');
  8799. const p3 = document.createElement('p');
  8800. p3.append('flv\u5408\u5E76');
  8801. const a1 = document.createElement('a');
  8802. a1.href = 'http://www.flvcd.com/teacher2.htm';
  8803. a1.textContent = '\u7855\u9F20';
  8804. p3.append(a1);
  8805. li2.append(p3);
  8806. const p4 = document.createElement('p');
  8807. p4.textContent = '\u6279\u91CF\u5408\u5E76\u5BF9\u5355\u6807\u7B7E\u9875\u8D1F\u8377\u592A\u5927';
  8808. li2.append(p4);
  8809. const p5 = document.createElement('p');
  8810. const em1 = document.createElement('em');
  8811. em1.textContent = '\u5F00\u53D1\u8005\uFF1A\u53EF\u4EE5\u7528webworker\uFF0C\u4F46\u662F\u6211\u6CA1\u9700\u6C42\uFF0C\u53C8\u61D2';
  8812. p5.append(em1);
  8813. li2.append(p5);
  8814. ul1.append(li2);
  8815. fragment.append(ul1);
  8816. fragment.append(table);
  8817. return fragment;
  8818. }
  8819.  
  8820. static displayDownloadAllPageDefaultFormatsBody(ret) {
  8821. top.document.open();
  8822. top.document.close();
  8823.  
  8824. top.document.body.append(UI.buildDownloadAllPageDefaultFormatsBody(ret));
  8825. return ret;
  8826. }
  8827.  
  8828. static displayDownloadAllPagePendingBody() {
  8829. top.document.open();
  8830. top.document.close();
  8831.  
  8832. top.document.body.append((() => {
  8833. const fragment = document.createDocumentFragment();
  8834. const h1 = document.createElement('h1');
  8835. h1.textContent = '(\u6D4B\u8BD5) \u6279\u91CF\u6293\u53D6';
  8836. fragment.append(h1);
  8837. const ul1 = document.createElement('ul');
  8838. const li = document.createElement('li');
  8839. const p = document.createElement('p');
  8840. p.textContent = '\u6293\u53D6\u4E2D\uFF0C\u8BF7\u7A0D\u5019\u2026\u2026';
  8841. li.append(p);
  8842. ul1.append(li);
  8843. fragment.append(ul1);
  8844. return fragment;
  8845. })());
  8846. }
  8847.  
  8848. static genDiv() {
  8849. const div1 = document.createElement('div');
  8850. div1.style.position = 'fixed';
  8851. div1.style.zIndex = '10036';
  8852. div1.style.top = '50%';
  8853. div1.style.marginTop = '-200px';
  8854. div1.style.left = '50%';
  8855. div1.style.marginLeft = '-320px';
  8856. div1.style.width = '540px';
  8857. div1.style.maxHeight = '400px';
  8858. div1.style.overflowY = 'auto';
  8859. div1.style.padding = '30px 50px';
  8860. div1.style.backgroundColor = 'white';
  8861. div1.style.borderRadius = '6px';
  8862. div1.style.boxShadow = 'rgba(0, 0, 0, 0.6) 1px 1px 40px 0px';
  8863. div1.style.display = 'none';
  8864. div1.addEventListener('click', e => e.stopPropagation());
  8865. div1.className = 'bilitwin';
  8866.  
  8867. return div1;
  8868. }
  8869.  
  8870. static requestH5Player() {
  8871. const h = document.querySelector('div.tminfo');
  8872. h.prepend('[[脚本需要HTML5播放器(弹幕列表右上角三个点的按钮切换)]] ');
  8873. }
  8874.  
  8875. static allowDrag(e) {
  8876. e.stopPropagation();
  8877. e.preventDefault();
  8878. }
  8879.  
  8880. static beforeUnloadHandler(e) {
  8881. return e.returnValue = '脚本还没做完工作,真的要退出吗?';
  8882. }
  8883.  
  8884. static hintInfo(text, playerWin) {
  8885. const div = document.createElement('div');
  8886. {
  8887. div.className = 'bilibili-player-video-toast-bottom';
  8888. const div1 = document.createElement('div');
  8889. div1.className = 'bilibili-player-video-toast-item';
  8890. const div2 = document.createElement('div');
  8891. div2.className = 'bilibili-player-video-toast-item-text';
  8892. const span1 = document.createElement('span');
  8893. span1.textContent = text;
  8894. div2.append(span1);
  8895. div1.append(div2);
  8896. div.append(div1);
  8897. }
  8898. playerWin.document.getElementsByClassName('bilibili-player-video-toast-wrp')[0].append(div);
  8899. setTimeout(() => div.remove(), 3000);
  8900. }
  8901.  
  8902. static get optionDescriptions() {
  8903. return [
  8904. // 1. automation
  8905. ['autoDanmaku', '下载视频也触发下载弹幕'],
  8906.  
  8907. // 2. user interface
  8908. ['title', '在视频标题旁添加链接'], ['menu', '在视频菜单栏添加链接'],
  8909.  
  8910. // 3. download
  8911. ['aria2', '导出aria2'], ['aria2RPC', '(请自行解决阻止混合活动内容的问题)发送到aria2 RPC'], ['m3u8', '(限VLC兼容播放器)导出m3u8'], ['clipboard', '(测)(请自行解决referrer)强制导出剪贴板']];
  8912. }
  8913.  
  8914. static get optionDefaults() {
  8915. return {
  8916. // 1. automation
  8917. autoDanmaku: false,
  8918.  
  8919. // 2. user interface
  8920. title: true,
  8921. menu: true,
  8922.  
  8923. // 3. download
  8924. aria2: false,
  8925. aria2RPC: false,
  8926. m3u8: false,
  8927. clipboard: false
  8928. };
  8929. }
  8930. }
  8931.  
  8932. /***
  8933. * Copyright (C) 2018 Qli5. All Rights Reserved.
  8934. *
  8935. * @author qli5 <goodlq11[at](163|gmail).com>
  8936. *
  8937. * This Source Code Form is subject to the terms of the Mozilla Public
  8938. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8939. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  8940. */
  8941.  
  8942. let debugOption = { debug: 1 };
  8943.  
  8944. class BiliTwin extends BiliUserJS {
  8945. static get debugOption() {
  8946. return debugOption;
  8947. }
  8948.  
  8949. static set debugOption(option) {
  8950. debugOption = option;
  8951. }
  8952.  
  8953. constructor(option = {}, ui) {
  8954. super();
  8955. this.BiliMonkey = BiliMonkey;
  8956. this.BiliPolyfill = BiliPolyfill;
  8957. this.playerWin = null;
  8958. this.monkey = null;
  8959. this.polifill = null;
  8960. this.ui = ui || new UI(this);
  8961. this.option = option;
  8962. }
  8963.  
  8964. async runCidSession() {
  8965. // 1. playerWin and option
  8966. try {
  8967. // you know what? it is a race, data race for jq! try not to yield to others!
  8968. this.playerWin = BiliUserJS.tryGetPlayerWinSync() || await BiliTwin.getPlayerWin();
  8969. }
  8970. catch (e) {
  8971. if (e == 'Need H5 Player') UI.requestH5Player();
  8972. throw e;
  8973. }
  8974. const href = location.href;
  8975. this.option = this.getOption();
  8976. if (this.option.debug) {
  8977. if (top.console) top.console.clear();
  8978. }
  8979.  
  8980. // 2. monkey and polyfill
  8981. this.monkey = new BiliMonkey(this.playerWin, this.option);
  8982. this.polyfill = new BiliPolyfill(this.playerWin, this.option, t => UI.hintInfo(t, this.playerWin));
  8983.  
  8984. const cidRefresh = BiliTwin.getCidRefreshPromise(this.playerWin);
  8985. const video = document.querySelector("video");
  8986. if (video) {
  8987. video.addEventListener('play', () => {
  8988. let event = new MouseEvent('contextmenu', {
  8989. 'bubbles': true
  8990. });
  8991.  
  8992. video.dispatchEvent(event);
  8993. video.dispatchEvent(event);
  8994. }, { once: true });
  8995. }
  8996. await this.polyfill.setFunctions();
  8997.  
  8998. // 3. async consistent => render UI
  8999. if (href == location.href) {
  9000. this.ui.option = this.option;
  9001. this.ui.cidSessionRender();
  9002.  
  9003. let videoA = this.ui.cidSessionDom.context_menu_videoA || this.ui.cidSessionDom.videoA;
  9004. if (videoA && videoA.onmouseover) videoA.onmouseover({ target: videoA.lastChild });
  9005. }
  9006. else {
  9007. cidRefresh.resolve();
  9008. }
  9009.  
  9010. // 4. debug
  9011. if (this.option.debug) {
  9012. [(top.unsafeWindow || top).monkey, (top.unsafeWindow || top).polyfill] = [this.monkey, this.polyfill];
  9013. }
  9014.  
  9015. // 5. refresh => session expire
  9016. await cidRefresh;
  9017. this.monkey.destroy();
  9018. this.polyfill.destroy();
  9019. this.ui.cidSessionDestroy();
  9020. }
  9021.  
  9022. async mergeFLVFiles(files) {
  9023. return URL.createObjectURL(await FLV.mergeBlobs(files));
  9024. }
  9025.  
  9026. async clearCacheDB(cache) {
  9027. if (cache) return cache.deleteEntireDB();
  9028. }
  9029.  
  9030. resetOption(option = this.option) {
  9031. option.setStorage('BiliTwin', JSON.stringify({}));
  9032. return this.option = {};
  9033. }
  9034.  
  9035. getOption(playerWin = this.playerWin) {
  9036. let rawOption = null;
  9037. try {
  9038. rawOption = JSON.parse(playerWin.localStorage.getItem('BiliTwin'));
  9039. }
  9040. catch (e) { }
  9041. finally {
  9042. if (!rawOption) rawOption = {};
  9043. rawOption.setStorage = (n, i) => playerWin.localStorage.setItem(n, i);
  9044. rawOption.getStorage = n => playerWin.localStorage.getItem(n);
  9045. return Object.assign(
  9046. {},
  9047. BiliMonkey.optionDefaults,
  9048. BiliPolyfill.optionDefaults,
  9049. UI.optionDefaults,
  9050. rawOption,
  9051. BiliTwin.debugOption,
  9052. );
  9053. }
  9054. }
  9055.  
  9056. saveOption(option = this.option) {
  9057. return option.setStorage('BiliTwin', JSON.stringify(option));
  9058. }
  9059.  
  9060. static async init() {
  9061. if (!document.body) return;
  9062.  
  9063. if (location.hostname == "www.biligame.com") {
  9064. return BiliPolyfill.biligameInit();
  9065. }
  9066. else if (location.pathname.startsWith("/bangumi/media/md")) {
  9067. return BiliPolyfill.showBangumiCoverImage();
  9068. }
  9069.  
  9070. BiliTwin.outdatedEngineClearance();
  9071. BiliTwin.firefoxClearance();
  9072.  
  9073. const twin = new BiliTwin();
  9074.  
  9075. if (location.hostname == "vc.bilibili.com") {
  9076. const vc_info = await BiliMonkey.getBiliShortVideoInfo();
  9077. return twin.ui.appendShortVideoTitle(vc_info);
  9078. }
  9079.  
  9080. while (1) {
  9081. await twin.runCidSession();
  9082. }
  9083. }
  9084.  
  9085. static outdatedEngineClearance() {
  9086. if (typeof Promise != 'function' || typeof MutationObserver != 'function') {
  9087. alert('这个浏览器实在太老了,脚本决定罢工。');
  9088. throw 'BiliTwin: browser outdated: Promise or MutationObserver unsupported';
  9089. }
  9090. }
  9091.  
  9092. static firefoxClearance() {
  9093. if (navigator.userAgent.includes('Firefox')) {
  9094. BiliTwin.debugOption.proxy = false;
  9095. if (!window.navigator.temporaryStorage && !window.navigator.mozTemporaryStorage) window.navigator.temporaryStorage = { queryUsageAndQuota: func => func(-1048576, 10484711424) };
  9096. }
  9097. }
  9098.  
  9099. static xpcWrapperClearance() {
  9100. if (top.unsafeWindow) {
  9101. Object.defineProperty(window, 'cid', {
  9102. configurable: true,
  9103. get: () => String(unsafeWindow.cid)
  9104. });
  9105. Object.defineProperty(window, 'player', {
  9106. configurable: true,
  9107. get: () => ({ destroy: unsafeWindow.player.destroy, reloadAccess: unsafeWindow.player.reloadAccess })
  9108. });
  9109. Object.defineProperty(window, 'jQuery', {
  9110. configurable: true,
  9111. get: () => unsafeWindow.jQuery,
  9112. });
  9113. Object.defineProperty(window, 'fetch', {
  9114. configurable: true,
  9115. get: () => unsafeWindow.fetch.bind(unsafeWindow),
  9116. set: _fetch => unsafeWindow.fetch = _fetch.bind(unsafeWindow)
  9117. });
  9118. }
  9119. }
  9120. }
  9121.  
  9122. BiliTwin.domContentLoadedThen(BiliTwin.init);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址