├── src ├── service │ ├── bilitwin.ts │ ├── bilitwin-ui.ts │ ├── bilitwin-keeper.ts │ ├── bilitwin-options.ts │ ├── bilitwin-store.ts │ └── bilimonkey-ass-handler.ts ├── shims-vue.d.ts ├── codec │ ├── flvass2mkv │ │ ├── util │ │ │ ├── simple-ebml-builder │ │ │ │ ├── index.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── typedArrayUtils.d.ts │ │ │ │ ├── README.md │ │ │ │ ├── ebml.d.ts │ │ │ │ ├── typedArrayUtils.js │ │ │ │ ├── ebml.js │ │ │ │ ├── id.d.ts │ │ │ │ └── id.js │ │ │ ├── rollup-plugin-multiline-string.js │ │ │ ├── ebml.js │ │ │ └── shim.js │ │ ├── .gitignore │ │ ├── verify │ │ │ ├── verify_header.js │ │ │ └── verify_block.js │ │ ├── README.md │ │ ├── package.json │ │ ├── demo.html │ │ ├── interface.entry.ts │ │ ├── gulpfile.js │ │ ├── demuxer │ │ │ └── ass.js │ │ └── index.entry.js │ └── flvparser │ │ ├── flv-tag.ts │ │ ├── flv-offset-stream.ts │ │ ├── flv.ts │ │ └── flv-stream.ts ├── store │ ├── modules │ │ ├── options.ts │ │ ├── bilipolyfill.ts │ │ ├── biliuserjs.ts │ │ └── bilimonkey.ts │ └── store.ts ├── util │ ├── playground.ts │ ├── async-mutation-observer.ts │ ├── cached-storage.ts │ ├── lib-util-streams │ │ ├── builtin-namespace-wrapper.ts │ │ ├── writableStream-types.ts │ │ ├── transformstream-types.ts │ │ ├── readablestream-types.ts │ │ ├── memory-stream.ts │ │ ├── random-stream.ts │ │ ├── console-stream.ts │ │ └── input-stream.ts │ ├── cache-db.ts │ ├── detailed-fetch-blob.ts │ ├── lib-cached-storage │ │ ├── common-cached-storage.ts │ │ ├── cached-dom-storage.ts │ │ ├── cached-extension-storage.ts │ │ └── cached-grease-storage.ts │ ├── lib-cache-db │ │ ├── common-cache-db.ts │ │ ├── base-mutable-cache-db.ts │ │ └── idb-cache-db.ts │ ├── polyfill.js │ ├── twenty-four-dataview.ts │ ├── event-socket.ts │ ├── common-types.ts │ ├── mutex.ts │ ├── lib-detailed-fetch-blob │ │ ├── base-detailed-fetch-blob.ts │ │ ├── stream-detailed-fetch-blob.ts │ │ └── firefox-detailed-fetch-blob.ts │ ├── async-control.ts │ ├── monitor-stream.ts │ ├── hooked-function.ts │ ├── on-event-target.ts │ ├── simple-event-target.ts │ └── event-duplex.ts ├── shims-tsx.d.ts ├── component │ ├── mkv-popup.vue │ ├── options-panel.vue │ ├── title-anchor.vue │ ├── menu-anchor.vue │ ├── base-panel-button.vue │ ├── base-panel.vue │ └── flv-panel.vue └── test-portal.ts ├── _config.yml ├── babel.config.js ├── .gitmodules ├── docs ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── CONTRIBUTING.md ├── .gitignore ├── tslint.json ├── test.html ├── tsconfig.json ├── package.json ├── start-local-debug-server.bat ├── release-README.md └── README.md /src/service/bilitwin.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/service/bilitwin-ui.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/service/bilitwin-keeper.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/service/bilitwin-options.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/service/bilitwin-store.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/codec/assconverter"] 2 | path = src/codec/assconverter 3 | url = https://github.com/liqi0816/ass-danmaku.git 4 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/index.js: -------------------------------------------------------------------------------- 1 | export * from "./ebml"; 2 | export * from "./id"; 3 | export * from "./typedArrayUtils"; 4 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./ebml"; 2 | export * from "./id"; 3 | export * from "./typedArrayUtils"; 4 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ### 哪一个页面? 8 | 9 | 10 | ### 出了什么问题? 11 | 12 | 13 | ### 用的浏览器是? 14 | 15 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/.gitignore: -------------------------------------------------------------------------------- 1 | # node 2 | node_modules/ 3 | 4 | # samples 5 | samples/ 6 | 7 | # build 8 | /index.js 9 | /index.js.map 10 | /interface.js 11 | /interface.js.map 12 | /embedded.html 13 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/verify/verify_header.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const gen = fs.readFileSync('../answer.mkv'); 3 | const out = fs.readFileSync('../out.mkv'); 4 | // verify header correctness 5 | fs.writeFileSync('../combined.mkv', Buffer.concat([gen.slice(0,36), out.slice(36)])); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /tsout 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw* 23 | test-bundle.js 24 | -------------------------------------------------------------------------------- /src/store/modules/options.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -------------------------------------------------------------------------------- /src/util/playground.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | -------------------------------------------------------------------------------- /src/store/modules/bilipolyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -------------------------------------------------------------------------------- /src/store/modules/biliuserjs.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/typedArrayUtils.d.ts: -------------------------------------------------------------------------------- 1 | export declare const numberToByteArray: (num: number, byteLength?: number) => Uint8Array; 2 | export declare const stringToByteArray: Function; 3 | export declare function getNumberByteLength(num: number): number; 4 | export declare const int16Bit: Function; 5 | export declare const float32bit: Function; 6 | export declare const dumpBytes: (b: ArrayBuffer) => string; 7 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/README.md: -------------------------------------------------------------------------------- 1 | Built from 2 | 3 | Unlikely to change in the future; therefore not a git submodule. 4 | 5 | ## How to dev: 6 | * node ^8.9.4 7 | * npm ^5.6.0 8 | ```bash 9 | git clone --branch esm --single-branch https://github.com/liqi0816/simple-ebml-builder-js.git 10 | cd simple-ebml-builder-js 11 | npx typescript 12 | cp ./build/es/*.js /wherever/you/like 13 | ``` 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 4], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Unit Test Page 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/store/store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import Vue from 'vue'; 11 | import Vuex from 'vuex'; 12 | 13 | Vue.use(Vuex); 14 | 15 | export default new Vuex.Store({ 16 | }) 17 | -------------------------------------------------------------------------------- /src/component/mkv-popup.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/component/options-panel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /src/component/title-anchor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 18 | 19 | 21 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/README.md: -------------------------------------------------------------------------------- 1 | # FLV + ASS => MKV transmuxer 2 | 3 | Demux FLV into H264 + AAC stream and ASS into line stream; then remux them into a MKV file. 4 | 5 | ## Quick Start 6 | 7 | samples/gen_case.ass 8 | samples/gen_case.flv 9 | 10 | node index.js 11 | 12 | =>out.mkv 13 | 14 | ## API 15 | 16 | @param {Blob|string|ArrayBuffer} flv 17 | @param {Blob|string|ArrayBuffer} ass 18 | FLVASS2MKV.prototype.build 19 | 20 | ## How to dev: 21 | 22 | * node ^8.9.4 23 | * npm ^5.6.0 24 | ```bash 25 | npm install 26 | npx gulp 27 | ``` -------------------------------------------------------------------------------- /src/component/menu-anchor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 19 | 20 | 21 | 23 | -------------------------------------------------------------------------------- /src/component/base-panel-button.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /src/test-portal.ts: -------------------------------------------------------------------------------- 1 | import { BiliMonkeyFLVHandler, BiliMonkeyFLVHandlerArray } from "./service/bilimonkey-flv-handler.js"; 2 | import { BiliUserJS, PlayerWindow } from './service/biliuserjs.js'; 3 | import { BiliMonkey } from './service/bilimonkey.js'; 4 | 5 | Object.assign(window, { 6 | BiliMonkeyFLVHandler, 7 | BiliMonkeyFLVHandlerArray, 8 | BiliUserJS, 9 | BiliMonkey 10 | }) 11 | 12 | ////////////////////////// 13 | 14 | let u = new BiliUserJS(); 15 | u.connect(top as PlayerWindow); 16 | 17 | var m = new BiliMonkey(u); 18 | u.pipeEventsThrough(m); 19 | m.activate(); 20 | 21 | ////////////////////////// 22 | 23 | Object.assign(window, { 24 | u, m 25 | }) 26 | -------------------------------------------------------------------------------- /src/util/async-mutation-observer.ts: -------------------------------------------------------------------------------- 1 | 2 | class AsyncMutationObserver extends Promise { 3 | observer: MutationObserver 4 | 5 | constructor() { 6 | let observer: MutationObserver; 7 | super(resolve => { 8 | observer = new MutationObserver(mutations => { 9 | resolve(mutations); 10 | observer.disconnect(); 11 | }) 12 | }); 13 | this.observer = observer!; 14 | } 15 | 16 | observe(target: Node, options: MutationObserverInit) { 17 | return this.observer.observe(target, options); 18 | } 19 | 20 | disconnect() { 21 | return this.observer.disconnect(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/rollup-plugin-multiline-string.js: -------------------------------------------------------------------------------- 1 | const { createFilter } = require('rollup-pluginutils'); 2 | 3 | module.exports = function string(opts = {}) { 4 | if (!opts.include) { 5 | throw Error('include option should be specified'); 6 | } 7 | 8 | const filter = createFilter(opts.include, opts.exclude); 9 | 10 | return { 11 | name: 'string', 12 | 13 | transform(code, id) { 14 | if (filter(id)) { 15 | return { 16 | code: `export default \`${code.replace(/\\|\`|\$/g, e => '\\' + e)}\`;`, 17 | map: { mappings: '' } 18 | }; 19 | } 20 | } 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/util/cached-storage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CachedExtensionStorage from './lib-cached-storage/cached-extension-storage.js'; 11 | import CachedGreaseStorage from './lib-cached-storage/cached-grease-storage.js'; 12 | import CachedDOMStorage from './lib-cached-storage/cached-dom-storage.js'; 13 | 14 | const CachedStorage = CachedDOMStorage; 15 | 16 | export default CachedStorage; 17 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/builtin-namespace-wrapper.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | export type BuiltinWritableStream = WritableStream 11 | export var BuiltinWritableStream: typeof WritableStream 12 | export type BuiltinReadableStream = ReadableStream 13 | export var BuiltinReadableStream: typeof WritableStream 14 | export type BuiltinTransformStream = {} 15 | export var BuiltinTransformStream: never 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "sourceMap": false, 6 | "allowJs": true, 7 | "strict": true, 8 | "noImplicitReturns": true, 9 | "importHelpers": true, 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "allowSyntheticDefaultImports": true, 14 | "lib": [ 15 | "esnext", 16 | "dom", 17 | "dom.iterable", 18 | "scripthost" 19 | ], 20 | "outDir": "./tsout" 21 | }, 22 | "include": [ 23 | "src/**/*.ts", 24 | "src/**/*.js", 25 | "src/**/*.vue" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } -------------------------------------------------------------------------------- /src/codec/flvass2mkv/verify/verify_block.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const ebml = require('ebml'); 3 | const ebmlBlock = require('ebml-block'); 4 | 5 | const simplified = []; 6 | const encoder = new ebml.Encoder(); 7 | const decoder = new ebml.Decoder(); 8 | const mkv = fs.readFileSync('../out.mkv'); 9 | 10 | decoder.write(mkv); 11 | let start = false; 12 | decoder.on('data', function (chunk) { 13 | // if (chunk[1].name == 'CodecPrivate') start = true; 14 | // if (start) { 15 | // console.log(chunk); 16 | // } 17 | if (chunk[1].name === 'SimpleBlock' || chunk[1].name === 'Block') { 18 | const block = ebmlBlock(chunk[1].data); 19 | if (block.timecode == 550 && block.frames[0].length == 117) { 20 | console.log(block.frames[0]); 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/util/cache-db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCacheDB from './lib-cache-db/common-cache-db.js'; 11 | import BaseMutableCacheDB from './lib-cache-db/base-mutable-cache-db.js'; 12 | import IDBCacheDB from './lib-cache-db/idb-cache-db.js'; 13 | import ChromeCacheDB from './lib-cache-db/chrome-cache-db.js'; 14 | import FirefoxCacheDB from './lib-cache-db/firefox-cache-db.js'; 15 | 16 | export { CommonCacheDB, BaseMutableCacheDB } 17 | export { IDBCacheDB, ChromeCacheDB } 18 | export default { IDBCacheDB, ChromeCacheDB } 19 | -------------------------------------------------------------------------------- /src/component/base-panel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 17 | 18 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bilitwin", 3 | "version": "2.0.0-alpha0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "vue": "^2.5.16", 12 | "vue-class-component": "^6.0.0", 13 | "vue-property-decorator": "^6.0.0", 14 | "vue-template-compiler": "^2.5.16", 15 | "vuex": "^3.0.1" 16 | }, 17 | "devDependencies": { 18 | "@types/filesystem": "0.0.28", 19 | "@types/jquery": "^3.3.5", 20 | "@vue/cli-plugin-babel": "^3.0.0-beta.15", 21 | "@vue/cli-plugin-typescript": "^3.0.0-beta.15", 22 | "@vue/cli-service": "^3.0.0-beta.15", 23 | "http-server": "^0.11.1", 24 | "rollup": "^0.63.4" 25 | }, 26 | "browserslist": [ 27 | "> 1%", 28 | "last 2 versions", 29 | "not ie <= 8" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flvass2mkv", 3 | "version": "0.0.1", 4 | "description": "flv + ass => mkv", 5 | "repository": { 6 | "private": true 7 | }, 8 | "author": "qli5", 9 | "license": "MPL-2.0", 10 | "main": "index.js", 11 | "dependencies": { 12 | "ebml": "^2.2.1", 13 | "ebml-block": "^1.1.1", 14 | "lodash-es": "^4.17.5" 15 | }, 16 | "devDependencies": { 17 | "flv.js": "^1.4.1", 18 | "gulp": "^3.9.1", 19 | "gulp-inline": "^0.1.3", 20 | "gulp-rename": "^1.2.2", 21 | "gulp-replace": "^0.6.1", 22 | "rollup": "^0.57.1", 23 | "rollup-plugin-node-resolve": "^3.0.3", 24 | "rollup-pluginutils": "^2.0.1", 25 | "vinyl-buffer": "^1.0.1", 26 | "vinyl-source-stream": "^2.0.0" 27 | }, 28 | "engines": { 29 | "node": "^8.10.0" 30 | }, 31 | "scripts": { 32 | "start": "node index.js", 33 | "install": "gulp" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/writableStream-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { BuiltinWritableStream } from './builtin-namespace-wrapper.js' 11 | 12 | export type WritableStream = BuiltinWritableStream 13 | 14 | export interface UnderlyingSink { 15 | start?: WritableStreamDefaultControllerCallback 16 | write?: WritableStreamChunkCallback 17 | close?: WritableStreamDefaultControllerCallback 18 | abort?: WritableStreamErrorCallback 19 | } 20 | 21 | declare const WritableStream: BuiltinWritableStream & { 22 | new(underlyingSink?: UnderlyingSink, strategy?: QueuingStrategy): WritableStream; 23 | } 24 | export { WritableStream } 25 | 26 | export default WritableStream; 27 | -------------------------------------------------------------------------------- /src/util/detailed-fetch-blob.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import BaseDetailedFetchBlob from './lib-detailed-fetch-blob/base-detailed-fetch-blob.js'; 11 | import StreamDetailedFetchBlob from './lib-detailed-fetch-blob/stream-detailed-fetch-blob.js'; 12 | import FirefoxDetailedFetchBlob from './lib-detailed-fetch-blob/firefox-detailed-fetch-blob.js'; 13 | 14 | /** 15 | * A more powerful fetch with 16 | * 1. onprogress handler 17 | * 2. partial response getter 18 | */ 19 | const DetailedFetchBlob: typeof BaseDetailedFetchBlob = StreamDetailedFetchBlob.isSupported ? StreamDetailedFetchBlob : FirefoxDetailedFetchBlob.isSupported ? FirefoxDetailedFetchBlob : StreamDetailedFetchBlob; 20 | 21 | export default DetailedFetchBlob; 22 | -------------------------------------------------------------------------------- /src/util/lib-cached-storage/common-cached-storage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | interface CommonCachedStorage { 11 | cache: { [key: string]: string | null } 12 | 13 | setItem(name: string, item: string): void | Promise 14 | setJSON(name: string, item: any): void | Promise 15 | getItem(name: string): string | null | Promise 16 | getJSON(name: string): any | null | Promise 17 | removeItem(name: string): void | Promise 18 | clear(): void | Promise 19 | clearCache(): void 20 | length: number | Promise 21 | keys(): Iterable | AsyncIterable 22 | values(): Iterable | AsyncIterable 23 | entries(): Iterable<[string, string]> | AsyncIterable<[string, string]> 24 | } 25 | 26 | export default CommonCachedStorage; 27 | -------------------------------------------------------------------------------- /src/component/flv-panel.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 29 | 30 | 36 | 37 | 38 | 40 | -------------------------------------------------------------------------------- /src/store/modules/bilimonkey.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { StoreOptions } from 'vuex'; 11 | 12 | const state = () => ({ 13 | mp4: '', 14 | ass: '', 15 | flvs: [] as string[], 16 | flvDescriptors: [] as { 17 | remote: string 18 | local: string 19 | blob: Blob 20 | loaded: number 21 | total: number 22 | controller: { abort(): void } 23 | }[] 24 | }) 25 | 26 | const initState = state(); 27 | 28 | type BiliMonkeyState = typeof initState 29 | 30 | interface BiliMonkeyDependencies { 31 | cid: string 32 | playerWin: string 33 | protocol: string 34 | options: string 35 | } 36 | 37 | const dependencyCollector = ({ cid, playerWin, protocol, options }: BiliMonkeyDependencies): StoreOptions => ({ 38 | state, 39 | mutations: { 40 | reset(state) { 41 | Object.assign(state, initState); 42 | } 43 | }, 44 | getters: { 45 | 46 | }, 47 | actions: { 48 | [cid]: { 49 | root: true, 50 | handler({ commit }) { return commit('reset'); } 51 | } 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/ebml.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * The EMBL builder is from simple-ebml-builder 3 | * 4 | * Copyright 2017 ryiwamoto 5 | * 6 | * @author ryiwamoto, qli5 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject 14 | * to the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 20 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 22 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 23 | * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 24 | * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | * DEALINGS IN THE SOFTWARE. 26 | */ 27 | 28 | import * as EBML from './simple-ebml-builder/index'; 29 | 30 | export * from './simple-ebml-builder/index'; 31 | export default EBML; 32 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/transformstream-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { BuiltinTransformStream } from './builtin-namespace-wrapper.js' 11 | import { ReadableStream } from './readablestream-types.js'; 12 | import { WritableStream } from './writablestream-types.js'; 13 | 14 | export interface TransformStream extends BuiltinTransformStream { 15 | readable: ReadableStream 16 | writable: WritableStream 17 | } 18 | 19 | export interface TransformStreamDefaultController { 20 | desiredSize: number 21 | enqueue(chunk: any): void 22 | error(reason: any): void 23 | terminate(): void 24 | } 25 | 26 | export interface Transformer { 27 | start?(controller: TransformStreamDefaultController): void 28 | transform?(chunk: any, controller: TransformStreamDefaultController): void 29 | flush?(controller: TransformStreamDefaultController): void 30 | } 31 | 32 | declare const TransformStream: { 33 | prototype: TransformStream 34 | new(transformer?: Transformer, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy): TransformStream 35 | } 36 | export { TransformStream } 37 | 38 | export default TransformStream 39 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/shim.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | const _navigator = typeof navigator === 'object' && navigator || { userAgent: 'chrome' }; 12 | 13 | const _Blob = typeof Blob === 'function' && Blob || class { 14 | constructor(array) { 15 | return Buffer.concat(array.map(Buffer.from.bind(Buffer))); 16 | } 17 | }; 18 | 19 | const _TextEncoder = typeof TextEncoder === 'function' && TextEncoder || class { 20 | /** 21 | * @param {string} chunk 22 | * @returns {Uint8Array} 23 | */ 24 | encode(chunk) { 25 | return Buffer.from(chunk, 'utf-8'); 26 | } 27 | }; 28 | 29 | const _TextDecoder = typeof TextDecoder === 'function' && TextDecoder || class extends require('string_decoder').StringDecoder { 30 | /** 31 | * @param {ArrayBuffer} chunk 32 | * @returns {string} 33 | */ 34 | decode(chunk) { 35 | return this.end(Buffer.from(chunk)); 36 | } 37 | } 38 | 39 | export { _navigator as navigator, _Blob as Blob, _TextEncoder as TextEncoder, _TextDecoder as TextDecoder }; 40 | export default { navigator: _navigator, Blob: _Blob, TextEncoder: _TextEncoder, TextDecoder: _TextDecoder }; 41 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/ebml.d.ts: -------------------------------------------------------------------------------- 1 | export interface EBMLData { 2 | write(buf: Uint8Array, pos: number): number; 3 | countSize(): number; 4 | } 5 | export declare class Value implements EBMLData { 6 | private bytes; 7 | constructor(bytes: Uint8Array); 8 | write(buf: Uint8Array, pos: number): number; 9 | countSize(): number; 10 | } 11 | export declare class Element implements EBMLData { 12 | private id; 13 | private children; 14 | private readonly size; 15 | private readonly sizeMetaData; 16 | constructor(id: Uint8Array, children: EBMLData[], isSizeUnknown: boolean); 17 | write(buf: Uint8Array, pos: number): number; 18 | countSize(): number; 19 | } 20 | export declare const bytes: Function; 21 | export declare const number: Function; 22 | export declare const vintEncodedNumber: Function; 23 | export declare const int16: Function; 24 | export declare const float: Function; 25 | export declare const string: Function; 26 | export declare const element: (id: Uint8Array, child: EBMLData | EBMLData[]) => EBMLData; 27 | export declare const unknownSizeElement: (id: Uint8Array, child: EBMLData | EBMLData[]) => EBMLData; 28 | export declare const build: (v: EBMLData) => Uint8Array; 29 | export declare const getEBMLByteLength: (num: number) => number; 30 | export declare const UNKNOWN_SIZE: Uint8Array; 31 | export declare const vintEncode: (byteArray: Uint8Array) => Uint8Array; 32 | export declare const getSizeMask: (byteLength: number) => number; 33 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/readablestream-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { BuiltinReadableStream } from './builtin-namespace-wrapper.js'; 11 | import { TransformStream } from './transformstream-types.js'; 12 | import { WritableStream } from './writablestream-types.js'; 13 | 14 | export interface PipeOptions { 15 | preventClose?: boolean 16 | preventAbort?: boolean 17 | preventCancel?: boolean 18 | } 19 | 20 | export interface ReadableStream extends BuiltinReadableStream { 21 | pipeThrough(transformStream: TransformStream, options?: PipeOptions): ReadableStream 22 | pipeTo(destination: WritableStream, options?: PipeOptions): Promise 23 | tee(): [ReadableStream, ReadableStream] 24 | } 25 | 26 | export interface ReadableStreamDefaultController { 27 | desiredSize: number 28 | enqueue(chunk: any): void 29 | error(reason: any): void 30 | close(): void 31 | } 32 | 33 | export interface UnderlyingSource { 34 | start?(controler: ReadableStreamDefaultController): void 35 | pull?(controller: ReadableStreamDefaultController): void 36 | cancel?(reason: any): void 37 | } 38 | 39 | declare const ReadableStream: { 40 | prototype: ReadableStream; 41 | new(source?: UnderlyingSource, strategy?: QueuingStrategy): ReadableStream; 42 | } 43 | export { ReadableStream } 44 | 45 | export default ReadableStream 46 | -------------------------------------------------------------------------------- /start-local-debug-server.bat: -------------------------------------------------------------------------------- 1 | echo see ./docs/CONTRIBUTING.md for usage 2 | 3 | call .\node_modules\.bin\tsc || exit /b 4 | 5 | call .\node_modules\.bin\rollup tsout/test-portal.js --file test-bundle.js --format iife || exit /b 6 | 7 | rem. > test-bundle.js.1 8 | echo // ==UserScript== >> test-bundle.js.1 9 | echo // @name (develop)bilibili merged flv+mp4+ass+enhance >> test-bundle.js.1 10 | echo // @namespace http://qli5.tk/ >> test-bundle.js.1 11 | echo // @homepageURL https://github.com/liqi0816/bilitwin/ >> test-bundle.js.1 12 | echo // @description bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,MKV打包,播放体验增强,原生appsecret,不借助其他网站 >> test-bundle.js.1 13 | echo // @match *://www.bilibili.com/video/av* >> test-bundle.js.1 14 | echo // @match *://bangumi.bilibili.com/anime/*/play* >> test-bundle.js.1 15 | echo // @match *://www.bilibili.com/bangumi/play/ep* >> test-bundle.js.1 16 | echo // @match *://www.bilibili.com/bangumi/play/ss* >> test-bundle.js.1 17 | echo // @match *://www.bilibili.com/watchlater/ >> test-bundle.js.1 18 | echo // @version 1.0 >> test-bundle.js.1 19 | echo // @author qli5 >> test-bundle.js.1 20 | echo // @copyright qli5, 2014+, 田生, grepmusic, zheng qian, ryiwamoto >> test-bundle.js.1 21 | echo // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/ >> test-bundle.js.1 22 | echo // @grant none >> test-bundle.js.1 23 | echo // @run-at document-start >> test-bundle.js.1 24 | echo // ==/UserScript== >> test-bundle.js.1 25 | type test-bundle.js >> test-bundle.js.1 26 | move /y test-bundle.js.1 test-bundle.js 27 | 28 | if "%~1" == "" call .\node_modules\.bin\http-server --cors -p 8081 -c-1 29 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/memory-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import WritableStream, { UnderlyingSink } from './writablestream-types'; 11 | 12 | class MemoryStream extends WritableStream { 13 | buffer: any[]; 14 | 15 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 16 | super({ 17 | start, close, abort, 18 | write: write ? 19 | (chunk, controler) => { 20 | this.buffer.push(chunk); 21 | return write(chunk, controler); 22 | } : 23 | chunk => this.buffer.push(chunk), 24 | }, strategy); 25 | this.buffer = []; 26 | } 27 | } 28 | 29 | class MemoryBlobStream extends WritableStream { 30 | buffer: any[]; 31 | 32 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 33 | super({ 34 | start, close, abort, 35 | write: write ? 36 | (chunk, controler) => { 37 | this.buffer.push(new Blob([chunk])); 38 | return write(chunk, controler); 39 | } : 40 | chunk => this.buffer.push(new Blob([chunk])), 41 | }, strategy); 42 | this.buffer = []; 43 | } 44 | } 45 | 46 | export { MemoryStream, MemoryBlobStream } 47 | export default MemoryStream; 48 | -------------------------------------------------------------------------------- /src/util/lib-cache-db/common-cache-db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | export interface StorageNavigator extends Navigator { 11 | storage?: { estimate(): Promise<{ usage: number, quota: number }> } 12 | webkitTemporaryStorage?: { queryUsageAndQuota(succb?: (usage: number, quota: number) => void, errcb?: (err: DOMException) => void): void } 13 | } 14 | 15 | declare const navigator: StorageNavigator 16 | export { navigator } 17 | 18 | export interface FileLike extends Blob { 19 | name: string 20 | } 21 | 22 | abstract class CommonCacheDB { 23 | constructor(public dbName: string, public storeName: string) { } 24 | 25 | abstract async createData(item: Blob, name: string): Promise 26 | abstract async createData(item: FileLike, name?: string): Promise 27 | 28 | abstract async setData(item: Blob, name: string): Promise 29 | abstract async setData(item: FileLike, name?: string): Promise 30 | 31 | abstract async getData(name: string): Promise 32 | 33 | abstract async hasData(name: string): Promise 34 | 35 | abstract async deleteData(name: string): Promise 36 | 37 | abstract async deleteAllData(): Promise 38 | 39 | abstract async deleteEntireDB(): Promise 40 | 41 | static get isSupported() { return false } 42 | 43 | static async quota() { return { usage: -1, quota: -1 } } 44 | } 45 | 46 | export default CommonCacheDB; 47 | -------------------------------------------------------------------------------- /src/util/polyfill.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | let _EventTarget = EventTarget; 11 | try { 12 | new EventTarget(); 13 | } catch (e) { 14 | _EventTarget = class { 15 | constructor() { 16 | const e = document.createDocumentFragment(); 17 | for (const name of Object.keys(EventTarget.prototype)) { 18 | this[name] = e[name].bind(e); 19 | } 20 | } 21 | } 22 | } 23 | 24 | const _AbortController = typeof AbortController === 'function' && AbortController || class { 25 | constructor() { 26 | this.signal = new _EventTarget(); 27 | this.signal.aborted = false; 28 | 29 | let onabort = null; 30 | Object.defineProperty(this.signal, 'onabort', { 31 | configurable: true, 32 | enumerable: true, 33 | get: () => onabort, 34 | set: e => { 35 | if (typeof e !== 'function') e = null; 36 | this.removeEventListener('abort', onabort); 37 | this.addEventListener('abort', e); 38 | onabort = e; 39 | }, 40 | }); 41 | } 42 | 43 | abort() { 44 | this.signal.dispatchEvent(new Event('abort')); 45 | this.signal.aborted = true; 46 | } 47 | }; 48 | 49 | import E from './playground.js'; 50 | const a = E.a 51 | 52 | export { _EventTarget as EventTarget, _AbortController as AbortController }; 53 | export default { EventTarget: _EventTarget, AbortController: _AbortController }; 54 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | 加载文件…… loading files... 6 | 7 |

8 |

9 | 构建mkv…… building mkv... 10 | 11 |

12 |

13 | merged.mkv 14 |

15 |
16 | author qli5 <goodlq11[at](163|gmail).com> 17 |
18 | 19 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/interface.entry.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | // @ts-ignore: this import statement will be handled by rollup 12 | import embeddedHTML from './embedded.html'; 13 | 14 | declare const embeddedHTML: string 15 | 16 | interface WorkerWinInit { 17 | onflvprogress?: (event: ProgressEvent) => void 18 | onfileload?: (event: ProgressEvent) => void 19 | onmkvprogress?: (event: ProgressEvent) => void 20 | name?: string 21 | flv: Blob | string | ArrayBuffer 22 | ass: Blob | string | ArrayBuffer 23 | } 24 | 25 | interface WorkerWin extends Window { 26 | exec(init: WorkerWinInit): Promise 27 | } 28 | 29 | class MKVTransmuxer { 30 | workerWin: WorkerWin | null 31 | option?: Partial 32 | 33 | constructor(option?: MKVTransmuxer['option']) { 34 | this.workerWin = null; 35 | this.option = option; 36 | } 37 | 38 | /** 39 | * FLV + ASS => MKV entry point 40 | */ 41 | exec(flv: Blob | string | ArrayBuffer, ass: Blob | string | ArrayBuffer, name?: string) { 42 | // 1. Allocate for a new window 43 | if (!this.workerWin) this.workerWin = top.open('', undefined, ' ') as WorkerWin; 44 | 45 | // 2. Inject scripts 46 | this.workerWin.document.write(embeddedHTML); 47 | this.workerWin.document.close(); 48 | 49 | // 3. Invoke exec 50 | if (!(this.option instanceof Object)) this.option = undefined; 51 | this.workerWin.exec(Object.assign({}, this.option, { flv, ass, name })); 52 | if (typeof flv === 'string') URL.revokeObjectURL(flv); 53 | if (typeof ass === 'string') URL.revokeObjectURL(ass); 54 | 55 | // 4. Free parent window 56 | // if (top.confirm('MKV打包中……要关掉这个窗口,释放内存吗?')) 57 | top.location.assign('about:blank'); 58 | } 59 | } 60 | 61 | export default MKVTransmuxer; 62 | -------------------------------------------------------------------------------- /src/util/twenty-four-dataview.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | class TwentyFourDataView extends DataView { 11 | getUint24(byteOffset: number, littleEndian = false) { 12 | if (littleEndian) { 13 | const msb = this.getUint16(byteOffset + 1) << 8; 14 | return msb | this.getUint8(byteOffset); 15 | } 16 | else if (byteOffset > this.byteOffset) { 17 | return this.getUint32(byteOffset - 1) & 0x00FFFFFF; 18 | } 19 | else { 20 | const msb = this.getUint8(byteOffset) << 16; 21 | return msb | this.getUint16(byteOffset + 1); 22 | } 23 | } 24 | 25 | setUint24(byteOffset: number, value: number, littleEndian = false) { 26 | if (littleEndian) { 27 | const msb = value >> 8; 28 | this.setUint16(byteOffset + 1, msb); 29 | this.setUint8(byteOffset, value); 30 | } 31 | else { 32 | const msb = value >> 16; 33 | this.setUint8(byteOffset, msb); 34 | this.setUint16(byteOffset + 1, value); 35 | } 36 | } 37 | 38 | indexOfSubArray(search: string | ArrayLike, startOffset = 0, endOffset = this.byteLength - search.length + 1) { 39 | if (typeof search[0] !== 'number') search = new TextEncoder().encode(search as string); 40 | // I know it is NAIVE 41 | for (let i = startOffset; i < endOffset; i++) { 42 | if (this.getUint8(i) != search[0]) continue; 43 | let found = 1; 44 | for (let j = 0; j < search.length; j++) { 45 | if (this.getUint8(i + j) != search[j]) { 46 | found = 0; 47 | break; 48 | } 49 | } 50 | if (found) return i; 51 | } 52 | return -1; 53 | } 54 | } 55 | 56 | export default TwentyFourDataView; 57 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Hi there, 2 | 3 | 多谢阁下对这脚本的兴趣。 4 | 5 | > 本来是自己写着玩,手动rollup,天天rebase,随意push -f,这是最好的!但是我想,我见到你们这样热情啊,这么乱糟糟的也不好。将来如果你们PR里有bug,你们要负责的。 6 | 7 | ## 源代码 8 | 请用 `git clone --recursive` 确保子模块正确初始化。源代码存放在 `./src`。 9 | 10 | ```bash 11 | git clone --recursive https://github.com/liqi0816/bilitwin.git 12 | ``` 13 | 14 | ## 构建 15 | 需要 [Node.js](https://nodejs.org) ^8.10.0 16 | 17 | ```bash 18 | cd /path/to/install 19 | npm install 20 | ``` 21 | 22 | ## 测试 23 | 看着几千行代码头皮发麻?彼此彼此! 24 | 25 | 需要 Chrome 64+ 26 | 27 | 1. 架设本地服务器,让B站能访问到本地代码 28 | 29 | ```bash 30 | npx http-server --cors -p 8081 -c-1 31 | ``` 32 | 33 | 2. 修改脚本,使用本地代码 34 | 35 | ```javascript 36 | // ==UserScript== 37 | // @name (local)bilibili merged flv+mp4+ass+enhance 38 | // @namespace http://qli5.tk/ 39 | // @homepageURL https://github.com/liqi0816/bilitwin/ 40 | // @description bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,MKV打包,播放体验增强,原生appsecret,不借助其他网站 41 | // @match *://www.bilibili.com/video/av* 42 | // @match *://bangumi.bilibili.com/anime/*/play* 43 | // @match *://www.bilibili.com/bangumi/play/ep* 44 | // @match *://www.bilibili.com/bangumi/play/ss* 45 | // @match *://www.bilibili.com/watchlater/ 46 | // @version 1.0 47 | // @author qli5 48 | // @copyright qli5, 2014+, 田生, grepmusic, zheng qian, ryiwamoto 49 | // @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/ 50 | // @grant none 51 | // @run-at document-start 52 | // ==/UserScript== 53 | 54 | const port = 8081; 55 | const script = document.createElement('script'); 56 | script.src = `http://127.0.0.1:${port}/src/bilitwin.entry.js`; 57 | script.type = 'module'; 58 | document.body.append(script); 59 | ``` 60 | 61 | 3. 到`devtools -> Sources -> 127.0.0.1:8081`里去找这些ES module吧! 62 | 63 | 4. 到`devtools -> Filesystem`添加本地权限,可以直接在浏览器里保存代码 64 | 65 | ## 工作流 66 | `master`和`develop`两个长期分支? 67 | 68 | * master至少应该是测试过的 69 | * 懒,B站改版,拖不下去了才会测试、更新 70 | 71 | 也懒的话,请一起用develop分支 72 | 73 | ## 坑 74 | 75 | * `./src/assconverter`是一个[git子模块](https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E5%AD%90%E6%A8%A1%E5%9D%97) 76 | * `./src/ui/ui.entry.js`使用了一个合理的[jsx](https://github.com/facebook/jsx)子集,推荐用[liqi0816/jsx-append-child](https://github.com/liqi0816/jsx-append-child)转译 77 | * `./src/flvass2mkv`是一个独立的子package 78 | * 把中间文件到处乱扔,这确实不是一个好习惯 79 | -------------------------------------------------------------------------------- /src/util/lib-cached-storage/cached-dom-storage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCachedStorage from './common-cached-storage.js'; 11 | 12 | class CachedDOMStorage implements CommonCachedStorage { 13 | storage: Storage 14 | cache: { [key: string]: string | null } 15 | 16 | constructor(storage = localStorage) { 17 | this.storage = storage; 18 | this.cache = {}; 19 | } 20 | 21 | setItem(name: string, item: string) { 22 | const string = '' + item; 23 | this.cache[name] = string; 24 | return this.storage.setItem(name, string); 25 | } 26 | 27 | setJSON(name: string, item: any) { 28 | const string = JSON.stringify(item); 29 | this.cache[name] = string; 30 | return this.storage.setItem(name, string); 31 | } 32 | 33 | getItem(name: string) { 34 | if (this.cache[name] === undefined) { 35 | return this.cache[name] = this.storage.getItem(name); 36 | } 37 | return this.cache[name]; 38 | } 39 | 40 | getJSON(name: string) { 41 | if (this.cache[name] === undefined) { 42 | this.cache[name] = this.storage.getItem(name); 43 | } 44 | return this.cache[name] === null ? null : JSON.parse(this.cache[name]!); 45 | } 46 | 47 | removeItem(name: string) { 48 | this.cache[name] = null; 49 | return this.storage.removeItem(name); 50 | } 51 | 52 | clear() { 53 | this.cache = {}; 54 | return this.storage.clear(); 55 | } 56 | 57 | clearCache() { 58 | this.cache = {}; 59 | } 60 | 61 | get length() { 62 | return this.storage.length; 63 | } 64 | 65 | * keys() { 66 | for (let i = 0; i < this.storage.length; i++) { 67 | yield this.storage.key(i) as string; 68 | } 69 | } 70 | 71 | values() { 72 | return Object.values(this.storage) as Iterable; 73 | } 74 | 75 | entries() { 76 | this.cache = { ...this.storage }; 77 | return Object.entries(this.cache) as Iterable<[string, string]>; 78 | } 79 | 80 | [Symbol.iterator]() { 81 | return this.entries()[Symbol.iterator]; 82 | } 83 | } 84 | 85 | export default CachedDOMStorage; 86 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/random-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import ReadableStream, { ReadableStreamDefaultController, UnderlyingSource } from './readablestream-types.js' 11 | 12 | export interface RandomStreamInit extends UnderlyingSource { 13 | chunkCount?: number 14 | chunkLength?: number 15 | } 16 | 17 | class RandomStream extends ReadableStream { 18 | chunkCount: number 19 | chunkLength: number 20 | controller: ReadableStreamDefaultController 21 | 22 | constructor({ start, pull, cancel, chunkCount = 20, chunkLength = 4 } = {} as RandomStreamInit, strategy?: QueuingStrategy) { 23 | let _controller = null as ReadableStreamDefaultController | null; 24 | super({ 25 | cancel, 26 | start: start ? 27 | controller => { 28 | _controller = controller; 29 | return start(controller); 30 | } : 31 | controller => _controller = controller, 32 | pull: pull ? 33 | controller => { 34 | const chunk = new Uint8Array(this.chunkLength); 35 | for (let i = 0; i < this.chunkLength; i++) { 36 | chunk[i] = Math.trunc(Math.random() * 256); 37 | } 38 | controller.enqueue(chunk); 39 | this.chunkCount--; 40 | if (!this.chunkCount) controller.close(); 41 | return pull(controller); 42 | } : 43 | controller => { 44 | const chunk = new Uint8Array(this.chunkLength); 45 | for (let i = 0; i < this.chunkLength; i++) { 46 | chunk[i] = Math.trunc(Math.random() * 256); 47 | } 48 | controller.enqueue(chunk); 49 | this.chunkCount--; 50 | if (!this.chunkCount) controller.close(); 51 | }, 52 | }, strategy); 53 | 54 | this.chunkCount = chunkCount; 55 | this.chunkLength = chunkLength; 56 | this.controller = _controller!; 57 | } 58 | 59 | close() { 60 | this.controller.close(); 61 | } 62 | } 63 | 64 | export default RandomStream; 65 | -------------------------------------------------------------------------------- /src/util/event-socket.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { SimpleEventMap, CommonEventTargetInterface, SimpleEventListener, SimpleEventTargetListenersList } from './simple-event-target.js'; 11 | import { listenersDictSymbol } from './simple-event-target.js'; 12 | import SimpleEventTarget from './simple-event-target.js'; 13 | 14 | const remoteListenersMapSymbol = Symbol('remoteListenersMap'); 15 | 16 | class EventSocket extends SimpleEventTarget { 17 | [remoteListenersMapSymbol] = new WeakMap() 18 | alive = true 19 | 20 | addEventType(...types: Type[]) { 21 | for (const type of types) { 22 | if (!this[listenersDictSymbol][type]) { 23 | this[listenersDictSymbol][type] = new Set() as SimpleEventTargetListenersList; 24 | this[listenersDictSymbol][type].onceListeners = new Set(); 25 | } 26 | } 27 | } 28 | 29 | connect(target: CommonEventTargetInterface) { 30 | let listener = this[remoteListenersMapSymbol].get(target); 31 | if (!listener) { 32 | listener = event => { 33 | if (this.alive) { 34 | this.dispatchEvent(event); 35 | } 36 | else { 37 | target.removeEventListener(event.type, listener!) 38 | } 39 | } 40 | this[remoteListenersMapSymbol].set(target, listener); 41 | } 42 | for (const type in this[listenersDictSymbol]) { 43 | target.addEventListener(type, listener); 44 | } 45 | } 46 | 47 | disconnect(target: CommonEventTargetInterface) { 48 | const listener = this[remoteListenersMapSymbol].get(target); 49 | if (!listener) return; 50 | this[remoteListenersMapSymbol].delete(target); 51 | for (const type in this[listenersDictSymbol]) { 52 | target.removeEventListener(type, listener); 53 | } 54 | } 55 | 56 | close() { 57 | for (const key of Object.getOwnPropertyNames(this)) { 58 | delete this[key as keyof this]; 59 | } 60 | for (const key of Object.getOwnPropertySymbols(this)) { 61 | delete this[key as keyof this]; 62 | } 63 | } 64 | } 65 | 66 | export { EventSocket, remoteListenersMapSymbol } 67 | export default EventSocket 68 | -------------------------------------------------------------------------------- /src/util/common-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | /** 11 | * Constructor type of T 12 | */ 13 | export interface Constructor { 14 | new(...args: any[]): T 15 | readonly prototype: T 16 | } 17 | 18 | /** 19 | * Combined progress marking type of T 20 | * 21 | * * null => uninitialized 22 | * * async pending => someone is working on it 23 | * * async resolve => someone just finished work 24 | * * sync value => someone already finished work 25 | */ 26 | export type AsyncOrSyncOrNull = Promise | T | null 27 | 28 | /** 29 | * Override properties `Override` in `Original` 30 | * (example: { a: number } => { a: boolean }) 31 | */ 32 | export type ForceOverride = 33 | Pick> & Override 34 | 35 | /** 36 | * Extend properties `Override` in `Original` 37 | * (example: { a: number } => { a: number | boolean }) 38 | */ 39 | export type ForceExtend = 40 | Pick> 41 | & Pick 42 | 43 | /** 44 | * Combination of `ForceOverride` and `ForceExtend` 45 | */ 46 | export type ForceShim = 47 | Pick> 48 | & Pick 49 | & Override 50 | 51 | /** 52 | * From T omit a set of properties K 53 | */ 54 | export type Omit = Pick> 55 | 56 | /** 57 | * From a Constructor pick all static methods 58 | */ 59 | export type PickStatic = Pick> 60 | 61 | /** 62 | * Generate type EventMap source strings 63 | */ 64 | export const generateEventMapSourceString = (str: string) => { 65 | const events = str.split('\n').filter(e => e).map(e => { 66 | const trim = e.trim(); 67 | const indexOf = trim.indexOf(':'); 68 | return [trim.slice(0, indexOf).trim(), trim.slice(indexOf + 1).trim()]; 69 | }) 70 | return [ 71 | events.map(([type, event]) => `on${type}: ${event}`).join('\n'), 72 | events.map(([type]) => `'${type}'`).join(', '), 73 | events.map(([type]) => `on${type}: CLASS_NAME["on${type}"]`).join('\n'), 74 | events.map(([type]) => `on${type} = null,`).join('\n'), 75 | events.map(([type]) => `this.on${type} = on${type};`).join('\n'), 76 | ].join('\n\n========\n\n'); 77 | } 78 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/console-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import WritableStream, { UnderlyingSink } from './writablestream-types'; 11 | 12 | class ConsoleStream extends WritableStream { 13 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 14 | super({ 15 | start, close, abort, 16 | write: write ? 17 | (chunk, controler) => { 18 | console.log(chunk); 19 | return write(chunk, controler); 20 | } : 21 | chunk => console.log(chunk), 22 | }, strategy); 23 | } 24 | } 25 | 26 | class ConsoleWarnStream extends WritableStream { 27 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 28 | super({ 29 | start, close, abort, 30 | write: write ? 31 | (chunk, controler) => { 32 | console.warn(chunk); 33 | return write(chunk, controler); 34 | } : 35 | chunk => console.warn(chunk), 36 | }, strategy); 37 | } 38 | } 39 | 40 | class ConsoleErrorStream extends WritableStream { 41 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 42 | super({ 43 | start, close, abort, 44 | write: write ? 45 | (chunk, controler) => { 46 | console.error(chunk); 47 | return write(chunk, controler); 48 | } : 49 | chunk => console.error(chunk), 50 | }, strategy); 51 | } 52 | } 53 | 54 | class ConsoleDirStream extends WritableStream { 55 | constructor({ start, write, close, abort } = {} as UnderlyingSink, strategy?: QueuingStrategy) { 56 | super({ 57 | start, close, abort, 58 | write: write ? 59 | (chunk, controler) => { 60 | console.dir(chunk); 61 | return write(chunk, controler); 62 | } : 63 | chunk => console.dir(chunk), 64 | }, strategy); 65 | } 66 | } 67 | 68 | const streams = { 69 | get stdout() { return new ConsoleStream() }, 70 | get stdwarn() { return new ConsoleWarnStream() }, 71 | get stderr() { return new ConsoleErrorStream() }, 72 | get stddir() { return new ConsoleDirStream() }, 73 | } 74 | 75 | export { ConsoleStream, ConsoleWarnStream, ConsoleErrorStream, ConsoleDirStream, streams } 76 | export default ConsoleStream; 77 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/gulpfile.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | const gulp = require('gulp'); 12 | const source = require('vinyl-source-stream'); 13 | const buffer = require('vinyl-buffer'); 14 | 15 | const rollup = require('rollup'); 16 | const nodeResolve = require('rollup-plugin-node-resolve'); 17 | const multilineString = require('./util/rollup-plugin-multiline-string'); 18 | 19 | const replace = require('gulp-replace'); 20 | const inline = require('gulp-inline'); 21 | const rename = require('gulp-rename'); 22 | 23 | gulp.task('default', ['build']); 24 | gulp.task('build', ['index.js', 'embedded.html', 'interface.js']); 25 | 26 | gulp.task('index.js', async () => { 27 | const bundle = await rollup.rollup({ 28 | input: './index.entry.js', 29 | plugins: [ 30 | nodeResolve() 31 | ] 32 | }); 33 | return bundle.write({ 34 | file: './index.js', 35 | format: 'iife', 36 | name: 'FLVASS2MKV', 37 | sourcemap: true, 38 | }) 39 | }); 40 | 41 | gulp.task('embedded.html', ['index.js'], () => { 42 | return gulp.src('./demo.html') 43 | .pipe(replace(/(?:window\.)?exec\(\);?/, '')) 44 | .pipe(inline()) 45 | .pipe(rename('embedded.html')) 46 | .pipe(gulp.dest('./')); 47 | }); 48 | 49 | gulp.task('interface.js', ['embedded.html'], async () => { 50 | const bundle = await rollup.rollup({ 51 | input: './interface.entry.js', 52 | plugins: [ 53 | multilineString({ include: './embedded.html' }) 54 | ] 55 | }); 56 | return bundle.write({ 57 | file: './interface.js', 58 | format: 'es', 59 | sourcemap: true, 60 | }) 61 | }); 62 | 63 | gulp.task('clean', () => { 64 | const fs = require('fs'); 65 | const cb = () => { }; 66 | fs.unlink('index.js', cb); 67 | fs.unlink('index.js.map', cb); 68 | fs.unlink('interface.js', cb); 69 | fs.unlink('interface.js.map', cb); 70 | fs.unlink('embedded.html', cb); 71 | }); 72 | 73 | // const browserify = require('browserify'); 74 | // const sourcemaps = require("gulp-sourcemaps"); 75 | 76 | gulp.task('browserify-index.bundle.js', () => { 77 | const b = browserify({ 78 | entries: './index.js', 79 | standalone: 'FLVASS2MKV', 80 | debug: true, 81 | bare: true, 82 | }); 83 | return b.bundle() 84 | .on('error', console.error.bind(console)) 85 | .pipe(source('index.bundle.js')) 86 | .pipe(buffer()) 87 | .pipe(sourcemaps.init({ loadMaps: true })) 88 | .pipe(sourcemaps.write()) 89 | .pipe(gulp.dest('./')); 90 | }); 91 | -------------------------------------------------------------------------------- /src/util/mutex.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | /** 11 | * A simple emulation of pthread_mutex 12 | */ 13 | class Mutex { 14 | queueTail: Promise 15 | resolveHead: () => void 16 | 17 | static readonly INIT_QUEUE_TAIL = Promise.resolve() 18 | static readonly INIT_RESOLVE_HEAD = () => { } 19 | constructor() { 20 | this.queueTail = Mutex.INIT_QUEUE_TAIL; 21 | this.resolveHead = Mutex.INIT_RESOLVE_HEAD; 22 | } 23 | 24 | /** 25 | * await mutex.lock = pthread_mutex_lock 26 | * @returns a promise that resolves when the lock is available 27 | */ 28 | async lock() { 29 | const queueTail = this.queueTail; 30 | let myResolve: () => void; 31 | this.queueTail = new Promise(resolve => myResolve = resolve); 32 | await queueTail; 33 | this.resolveHead = myResolve!; 34 | } 35 | 36 | /** 37 | * mutex.unlock = pthread_mutex_unlock 38 | */ 39 | unlock() { 40 | this.resolveHead(); 41 | } 42 | 43 | /** 44 | * a convenient method for 45 | * lock -> ret = await async -> unlock -> return ret 46 | * 47 | * @param promise async function or promise to wait for 48 | */ 49 | async lockAndAwait(promise: (() => Promise) | (() => T) | Promise | T) { 50 | await this.lock(); 51 | try { 52 | if (typeof promise == 'function') { 53 | return await promise(); 54 | } 55 | else { 56 | return await promise; 57 | } 58 | } 59 | finally { 60 | this.unlock(); 61 | } 62 | } 63 | } 64 | 65 | const _UNIT_TEST = () => { 66 | let m = new Mutex(); 67 | function sleep(time: number) { 68 | return new Promise(r => setTimeout(r, time)); 69 | } 70 | m.lockAndAwait(() => { 71 | console.warn('Check message timestamps.'); 72 | console.warn('Bad:'); 73 | console.warn('1 1 1 1 1:5s'); 74 | console.warn(' 1 1 1 1 1:10s'); 75 | console.warn('Good:'); 76 | console.warn('1 1 1 1 1:5s'); 77 | console.warn(' 1 1 1 1 1:10s'); 78 | }); 79 | m.lockAndAwait(async () => { 80 | await sleep(1000); 81 | await sleep(1000); 82 | await sleep(1000); 83 | await sleep(1000); 84 | await sleep(1000); 85 | }); 86 | m.lockAndAwait(async () => console.log('5s!')); 87 | m.lockAndAwait(async () => { 88 | await sleep(1000); 89 | await sleep(1000); 90 | await sleep(1000); 91 | await sleep(1000); 92 | await sleep(1000); 93 | }); 94 | m.lockAndAwait(async () => console.log('10s!')); 95 | }; 96 | 97 | export default Mutex; 98 | -------------------------------------------------------------------------------- /src/util/lib-detailed-fetch-blob/base-detailed-fetch-blob.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import OnEventTargetFactory from '../on-event-target.js'; 11 | import { SimpleEventListener } from '../simple-event-target.js'; 12 | 13 | export interface BaseDetailedFetchBlobInit { 14 | onprogress?: BaseDetailedFetchBlob['onprogress'] 15 | onabort?: BaseDetailedFetchBlob['onabort'] 16 | onerror?: BaseDetailedFetchBlob['onerror'] 17 | loaded?: BaseDetailedFetchBlob['loaded'] 18 | total?: BaseDetailedFetchBlob['total'] 19 | lengthComputable?: BaseDetailedFetchBlob['lengthComputable'] 20 | fetch?: typeof fetch 21 | XMLHttpRequest?: typeof XMLHttpRequest 22 | } 23 | 24 | abstract class BaseDetailedFetchBlob extends OnEventTargetFactory< 25 | { progress: ProgressEvent, abort: ProgressEvent, error: ProgressEvent }, 26 | { onprogress: ProgressEvent, onabort: ProgressEvent, onerror: ProgressEvent } 27 | >(['progress', 'abort', 'error']) implements PromiseLike { 28 | loaded: number 29 | total: number 30 | lengthComputable: boolean 31 | error: DOMException | null = null 32 | blob: Blob | null = null 33 | buffer: (Blob | ArrayBuffer)[] | null = [] 34 | abstract promise: Promise 35 | 36 | constructor(input: string | Request, { 37 | onprogress = null, 38 | onabort = null, 39 | onerror = null, 40 | loaded = 0, 41 | total = Infinity, 42 | lengthComputable = false, 43 | } = {} as BaseDetailedFetchBlobInit) { 44 | super(); 45 | this.onprogress = onprogress; 46 | this.onabort = onabort; 47 | this.onerror = onerror; 48 | this.loaded = loaded; 49 | this.total = total; 50 | this.lengthComputable = lengthComputable; 51 | } 52 | 53 | getPartialBlob() { 54 | return this.blob || new Blob(this.buffer || []); 55 | } 56 | 57 | async getBlob() { 58 | return this; 59 | } 60 | 61 | abstract abort(): void 62 | 63 | then(onfulfilled?: ((value: Blob) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null) { 64 | return this.promise.then(onfulfilled, onrejected); 65 | } 66 | 67 | catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null) { 68 | return this.promise.catch(onrejected) 69 | } 70 | 71 | finally(onfinally?: (() => void) | undefined | null) { 72 | return this.promise.finally(onfinally); 73 | } 74 | 75 | static get isSupported() { return false } 76 | } 77 | 78 | export { BaseDetailedFetchBlob }; 79 | export default BaseDetailedFetchBlob; 80 | -------------------------------------------------------------------------------- /src/util/lib-cached-storage/cached-extension-storage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCachedStorage from './common-cached-storage.js'; 11 | 12 | export interface StorageArea { 13 | get(name?: string | string[] | { [name: string]: any }): Promise<{ [name: string]: any }> 14 | set(name: { [name: string]: any }): Promise 15 | remove(name: string | string[] | { [name: string]: any }): Promise 16 | clear(): Promise 17 | } 18 | 19 | class CachedExtensionStorage implements CommonCachedStorage { 20 | storage: StorageArea 21 | cache: { [key: string]: string | null } 22 | 23 | constructor(storage: StorageArea) { 24 | this.storage = storage; 25 | this.cache = {}; 26 | } 27 | 28 | async setItem(name: string, item: string) { 29 | const string = '' + item; 30 | this.cache[name] = string; 31 | return await this.storage.set({ [name]: string }); 32 | } 33 | 34 | async setJSON(name: string, item: any) { 35 | const string = JSON.stringify(item); 36 | this.cache[name] = string; 37 | return await this.storage.set({ [name]: string }); 38 | } 39 | 40 | async getItem(name: string) { 41 | if (this.cache[name] === undefined) { 42 | return this.cache[name] = (await this.storage.get(name))[name] || null as string | null; 43 | } 44 | return this.cache[name]; 45 | } 46 | 47 | async getJSON(name: string) { 48 | if (this.cache[name] === undefined) { 49 | this.cache[name] = (await this.storage.get(name))[name] || null as string | null; 50 | } 51 | return this.cache[name] === null ? null : JSON.parse(this.cache[name]!); 52 | } 53 | 54 | async removeItem(name: string) { 55 | this.cache[name] = null; 56 | return await this.storage.remove(name); 57 | } 58 | 59 | async clear() { 60 | this.cache = {}; 61 | await this.storage.clear(); 62 | } 63 | 64 | clearCache() { 65 | this.cache = {}; 66 | } 67 | 68 | get length() { 69 | return this.storage.get().then(cache => { 70 | this.cache = cache; 71 | return Object.keys(cache).length; 72 | }); 73 | } 74 | 75 | async * keys() { 76 | this.cache = await this.storage.get(); 77 | yield* Object.keys(this.cache); 78 | } 79 | 80 | async * values() { 81 | this.cache = await this.storage.get(); 82 | yield* Object.values(this.cache) as string[]; 83 | } 84 | 85 | async * entries() { 86 | this.cache = await this.storage.get(); 87 | yield* Object.entries(this.cache) as [string, string][]; 88 | } 89 | 90 | [Symbol.asyncIterator]() { 91 | return this.entries()[Symbol.asyncIterator]; 92 | } 93 | } 94 | 95 | export default CachedExtensionStorage; 96 | -------------------------------------------------------------------------------- /src/util/lib-cached-storage/cached-grease-storage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCachedStorage from './common-cached-storage.js'; 11 | 12 | export interface GreaseStorage { 13 | setValue(name: string, item: string | number | boolean): Promise 14 | getValue(name: string): Promise 15 | getValue(name: string, def: T): Promise 16 | listValues(): Promise 17 | deleteValue(name: string): Promise 18 | } 19 | declare const GM: GreaseStorage 20 | export { GM } 21 | 22 | class CachedGreaseStorage implements CommonCachedStorage { 23 | storage: GreaseStorage 24 | cache: { [key: string]: string | null } 25 | 26 | constructor(storage = GM) { 27 | this.storage = storage; 28 | this.cache = {}; 29 | } 30 | 31 | async setItem(name: string, item: string) { 32 | const string = '' + item; 33 | this.cache[name] = string; 34 | return await this.storage.setValue(name, string); 35 | } 36 | 37 | async setJSON(name: string, item: any) { 38 | const string = JSON.stringify(item); 39 | this.cache[name] = string; 40 | return await this.storage.setValue(name, string); 41 | } 42 | 43 | async getItem(name: string) { 44 | if (this.cache[name] === undefined) { 45 | return this.cache[name] = await this.storage.getValue(name, null) as string | null; 46 | } 47 | return this.cache[name]; 48 | } 49 | 50 | async getJSON(name: string) { 51 | if (this.cache[name] === undefined) { 52 | this.cache[name] = await this.storage.getValue(name, null) as string | null; 53 | } 54 | return this.cache[name] === null ? null : JSON.parse(this.cache[name]!); 55 | } 56 | 57 | async removeItem(name: string) { 58 | this.cache[name] = null; 59 | return await this.storage.deleteValue(name); 60 | } 61 | 62 | async clear() { 63 | this.cache = {}; 64 | await Promise.all((await this.storage.listValues()).map(name => this.storage.deleteValue(name))); 65 | } 66 | 67 | clearCache() { 68 | this.cache = {}; 69 | } 70 | 71 | get length() { 72 | return this.storage.listValues().then(({ length }) => length); 73 | } 74 | 75 | async * keys() { 76 | yield* await this.storage.listValues() 77 | } 78 | 79 | async * values() { 80 | for (const name of await this.storage.listValues()) { 81 | yield this.getItem(name) as Promise; 82 | } 83 | } 84 | 85 | async * entries() { 86 | for (const name of await this.storage.listValues()) { 87 | yield [name, await this.getItem(name) as string] as [string, string]; 88 | } 89 | } 90 | 91 | [Symbol.asyncIterator]() { 92 | return this.entries()[Symbol.asyncIterator]; 93 | } 94 | } 95 | 96 | export default CachedGreaseStorage; 97 | -------------------------------------------------------------------------------- /release-README.md: -------------------------------------------------------------------------------- 1 | # 国产浏览器请点[这里](https://liqi0816.github.io/bilitwin/biliTwinBabelCompiled.user.js) 2 | 3 | # 脚本功能 4 | * BiliMonkey 5 | * 网络 6 | * 抓取FLV 7 | * 抓取MP4 8 | * 抓取弹幕 9 | * 缓存 10 | * 缓存FLV到本地 11 | * 断点续传 12 | * 用缓存加速播放器 13 | * 转码 14 | * 合并FLV 15 | * 弹幕转码ASS 16 | * 软字幕打包FLV+ASS为MKV 17 | * 集成 18 | * 下载合并一条龙 一键下载所有超清FLV分段并自动合并 19 | * 关标签页已下载的分段不消失 保留已经下载好的分段到缓存 20 | * 断点续传 也保留部分下载的分段到缓存 21 | * 用B站原生播放器播放下载好的缓存 如果发现缓存里有完整的分段,直接喂给网页播放器,不重新访问网络。小水管利器。如果实在搞不清怎么播放ASS弹幕,也可以就这样用。 22 | * BiliPolyfill 23 | * 界面 24 | * 稍后再看添加数字角标 25 | * 弹幕列表换成相关视频 26 | * 整合充电榜与换P倒计时 27 | * 自动化 28 | * 自动滚动到播放器 29 | * 自动聚焦到播放器 新页面直接按空格会播放而不是向下滚动 30 | * 关闭菜单后聚焦到播放器 31 | * 记住防挡字幕 32 | * 记住弹幕开关(顶端/底端/滚动/全部) 33 | * 记住播放速度 34 | * 记住宽屏 35 | * 自动跳转上次看到 36 | * 自动播放 37 | * 自动全屏 38 | * 标记后自动跳OP/ED 39 | * 尝试自动找上下集 40 | * 交互 41 | * 双击全屏 42 | * 首次回车键可全屏自动播放 43 | * 功能 44 | * 获取封面 45 | * 小窗播放 46 | * 自定义播放速度 47 | * 彩蛋 48 | * 不能 49 | * 破解地区限制 50 | * 破解10492 51 | * 其他需要服务器辅助的功能 鄙人木有服务器 (๑•́ ₃ •̀๑) 52 | 53 | # 需求 54 |
    55 |
  • 56 | B站 HTML5播放器 57 |
  • 58 |
  • 59 | 浏览器 60 |
    61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
    国产浏览器请用兼容版本
    Chrome作者在用
    Firefox应该OK
    Edge不造
    IENO
    85 |
  • 86 |
87 | 88 | # 更新/讨论 89 | * [Greasy Fork](https://greasyfork.org/zh-CN/scripts/27819) 90 | * [Github](https://github.com/liqi0816/bilitwin) 91 | * [文档](https://github.com/liqi0816/bilitwin/tree/master/docs) 92 | * 如果鄙人的代码太辣鸡,请一起来[Fork you](https://github.com/liqi0816/bilitwin)! 93 | 94 | # 特征 95 | * 轻量 96 | 新建一个书签,书签地址粘贴下面的代码,想用的时候点一下也可以使用。 97 | ```javascript 98 | javascript:(function(){f=document.createElement("script");f.setAttribute("src","https://liqi0816.github.io/bilitwin/biliTwinBabelCompiled.user.js");document.body.appendChild(f)})() 99 | ``` 100 | * 充分保障隐私 101 | 作者根本就没有服务器可以用来偷偷记下各位的奇怪癖好 102 | * 充分利用最快的B站视频源 103 | 数据皆由浏览器实时抓取 104 | 105 | *有用部分结束* 106 | 107 | ---------- 108 | 109 | 作者用的是Chrome,8G内存。 110 | 111 | 支持HTTPS,不借助第三方服务器,用原生的appsecret,不需要额外权限,用书签就可以运行。 112 | 113 | 模拟用户用原生鉴权方式加载视频,再也不怕B站改appkey或appsecret,该走哪个CDN就走哪个。 114 | 115 | 脚本用到了大量ES6功能和一些ES7功能。用着最新浏览器的同学,请把脚本从babel中解放出来! 116 | 117 | 懒得加的功能: 118 | * 边看边下载 119 | 一旦进度条鬼畜,下载就会拉肚子。 120 | * 超清FLV转MP4 121 | qianqian立过的flag,我就不立了。 122 | -------------------------------------------------------------------------------- /src/util/lib-detailed-fetch-blob/stream-detailed-fetch-blob.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { BaseDetailedFetchBlobInit } from './base-detailed-fetch-blob.js'; 11 | import BaseDetailedFetchBlob from './base-detailed-fetch-blob.js'; 12 | 13 | class StreamDetailedFetchBlob extends BaseDetailedFetchBlob { 14 | controller = new AbortController() 15 | promise: Promise 16 | 17 | constructor(input: string | Request, { 18 | onprogress = null, 19 | onabort = null, 20 | onerror = null, 21 | loaded = 0, 22 | total = Infinity, 23 | lengthComputable = false, 24 | fetch = top.fetch, 25 | ...init 26 | } = {} as BaseDetailedFetchBlobInit & RequestInit) { 27 | super(input, { onprogress, onabort, onerror, loaded, total, lengthComputable }); 28 | 29 | this.promise = (async () => { 30 | try { 31 | const { body, ok, status, statusText, headers } = await fetch(input, { ...init, signal: this.controller.signal } as RequestInit); 32 | if (!ok) throw new DOMException(`${status}: ${statusText}`, 'HTTPError'); 33 | if (!body) throw new DOMException('DetailedFetchBlob encountered a network error', 'NetworkError'); 34 | this.lengthComputable = headers.has('Content-Length'); 35 | this.total += this.lengthComputable && parseInt(headers.get('Content-Length')!) || Infinity; 36 | let last = Date.now(); 37 | for await (const chunk of this.iteratorify(body)) { 38 | this.loaded += chunk.length; 39 | this.buffer!.push(new Blob([chunk])); 40 | if (Date.now() - last > 500) { 41 | this.dispatchEvent(new ProgressEvent('progress', this)); 42 | last = Date.now(); 43 | } 44 | }; 45 | if (this.error) throw this.error; 46 | return this.blob = new Blob(this.buffer!); 47 | } 48 | catch (e) { 49 | if (!this.error) this.error = e; 50 | this.dispatchEvent(new ErrorEvent('error', this)); 51 | throw this.error; 52 | } 53 | finally { 54 | this.buffer = null; 55 | } 56 | })(); 57 | } 58 | 59 | iteratorify(body: ReadableStream) { 60 | const reader = body.getReader(); 61 | this.addEventListener('abort', reader.cancel.bind(reader, 'AbortError')); 62 | return { 63 | next: reader.read.bind(reader), 64 | return: reader.cancel.bind(reader), 65 | [Symbol.asyncIterator]() { return this }, 66 | }; 67 | } 68 | 69 | abort() { 70 | this.controller.abort(); 71 | this.error = new DOMException('DetailedFetchBlob was aborted', 'AbortError'); 72 | this.dispatchEvent(new ProgressEvent('abort', this)); 73 | } 74 | 75 | static get isSupported() { 76 | return typeof fetch === 'function' && typeof ReadableStream === 'function'; 77 | } 78 | } 79 | 80 | export default StreamDetailedFetchBlob; 81 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/demuxer/ass.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | */ 10 | 11 | import { TextDecoder } from '../util/shim.js'; 12 | 13 | class ASS { 14 | /** 15 | * Extract sections from ass string 16 | * @param {string} str 17 | * @returns {Object} - object from sections 18 | */ 19 | static extractSections(str) { 20 | const regex = /^\ufeff?\[(.*)\]$/mg; 21 | let match; 22 | let matchArr = []; 23 | while ((match = regex.exec(str)) !== null) { 24 | matchArr.push({ name: match[1], index: match.index }); 25 | } 26 | let ret = {}; 27 | matchArr.forEach((match, i) => ret[match.name] = str.slice(match.index, matchArr[i + 1] && matchArr[i + 1].index)); 28 | return ret; 29 | } 30 | 31 | /** 32 | * Extract subtitle lines from section Events 33 | * @param {string} str 34 | * @returns {Array} - array of subtitle lines 35 | */ 36 | static extractSubtitleLines(str) { 37 | const lines = str.split('\n'); 38 | if (lines[0] != '[Events]' && lines[0] != '[events]') throw new Error('ASSDemuxer: section is not [Events]'); 39 | if (lines[1].indexOf('Format:') != 0 && lines[1].indexOf('format:') != 0) throw new Error('ASSDemuxer: cannot find Format definition in section [Events]'); 40 | 41 | const format = lines[1].slice(lines[1].indexOf(':') + 1).split(',').map(e => e.trim()); 42 | return lines.slice(2).map(e => { 43 | let j = {}; 44 | e.replace(/[d|D]ialogue:\s*/, '') 45 | .match(new RegExp(new Array(format.length - 1).fill('(.*?),').join('') + '(.*)')) 46 | .slice(1) 47 | .forEach((k, index) => j[format[index]] = k) 48 | return j; 49 | }); 50 | } 51 | 52 | /** 53 | * Create a new ASS Demuxer 54 | */ 55 | constructor() { 56 | this.info = ''; 57 | this.styles = ''; 58 | this.events = ''; 59 | this.eventsHeader = ''; 60 | this.pictures = ''; 61 | this.fonts = ''; 62 | this.lines = ''; 63 | } 64 | 65 | get header() { 66 | // return this.info + this.styles + this.eventsHeader; 67 | return this.info + this.styles; 68 | } 69 | 70 | /** 71 | * Load a file from an arraybuffer of a string 72 | * @param {(ArrayBuffer|string)} chunk 73 | */ 74 | parseFile(chunk) { 75 | const str = typeof chunk == 'string' ? chunk : new TextDecoder('utf-8').decode(chunk); 76 | for (let [i, j] of Object.entries(ASS.extractSections(str))) { 77 | if (i.match(/Script Info(?:mation)?/i)) this.info = j; 78 | else if (i.match(/V4\+? Styles?/i)) this.styles = j; 79 | else if (i.match(/Events?/i)) this.events = j; 80 | else if (i.match(/Pictures?/i)) this.pictures = j; 81 | else if (i.match(/Fonts?/i)) this.fonts = j; 82 | } 83 | this.eventsHeader = this.events.split('\n', 2).join('\n') + '\n'; 84 | this.lines = ASS.extractSubtitleLines(this.events); 85 | return this; 86 | } 87 | } 88 | 89 | export { ASS }; 90 | export default ASS; 91 | -------------------------------------------------------------------------------- /src/codec/flvparser/flv-tag.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import TwentyFourDataView from '../../util/twenty-four-dataview.js'; 11 | 12 | class FLVTag { 13 | tagHeader: TwentyFourDataView 14 | tagData: TwentyFourDataView 15 | previousSize: TwentyFourDataView 16 | 17 | constructor(dataView: DataView, currentOffset = 0) { 18 | this.tagHeader = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset, 11); 19 | this.tagData = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11, this.dataSize); 20 | this.previousSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11 + this.dataSize, 4); 21 | } 22 | 23 | get tagType() { 24 | return this.tagHeader.getUint8(0); 25 | } 26 | 27 | get dataSize() { 28 | return this.tagHeader.getUint24(1); 29 | } 30 | 31 | get timestamp() { 32 | return this.tagHeader.getUint24(4); 33 | } 34 | 35 | get timestampExtension() { 36 | return this.tagHeader.getUint8(7); 37 | } 38 | 39 | get streamID() { 40 | return this.tagHeader.getUint24(8); 41 | } 42 | 43 | stripKeyframesScriptData() { 44 | if (this.tagType !== 0x12) throw new TypeError(`getDurationAndView: this.tagType should be 0x12 (ScriptData type) but get ${this.tagType}`); 45 | 46 | const anchorIndex = this.tagData.indexOfSubArray('hasKeyframes\x01'); 47 | if (anchorIndex === -1) return; 48 | 49 | //0x0101 => 0x0100 50 | const index = anchorIndex + 13; 51 | this.tagData.setUint8(index, 0x00); 52 | } 53 | 54 | getDuration() { 55 | if (this.tagType !== 0x12) throw new TypeError(`getDurationAndView: this.tagType should be 0x12 (ScriptData type) but get ${this.tagType}`); 56 | 57 | const anchorIndex = this.tagData.indexOfSubArray('duration\x00'); 58 | if (anchorIndex === -1) throw new Error('getDurationAndView: cannot find duration metainfo section'); 59 | 60 | const index = anchorIndex + 9; 61 | return this.tagData.getFloat64(index); 62 | } 63 | 64 | getDurationAndView() { 65 | if (this.tagType !== 0x12) throw new TypeError(`getDurationAndView: this.tagType should be 0x12 (ScriptData type) but get ${this.tagType}`); 66 | 67 | const anchorIndex = this.tagData.indexOfSubArray('duration\x00'); 68 | if (anchorIndex === -1) throw new Error('getDurationAndView: cannot find duration metainfo section'); 69 | 70 | const index = anchorIndex + 9; 71 | return { 72 | duration: this.tagData.getFloat64(index), 73 | durationDataView: new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset + index, 8) 74 | }; 75 | } 76 | 77 | getCombinedTimestamp() { 78 | return (this.timestampExtension << 24 | this.timestamp); 79 | } 80 | 81 | setCombinedTimestamp(timestamp: number) { 82 | if (timestamp < 0) throw new RangeError(`setCombinedTimestamp: parameter timestamp should be non-negative but get ${timestamp}`); 83 | this.tagHeader.setUint8(7, timestamp >> 24); 84 | this.tagHeader.setUint24(4, timestamp & 0x00FFFFFF); 85 | } 86 | } 87 | 88 | export default FLVTag; 89 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/typedArrayUtils.js: -------------------------------------------------------------------------------- 1 | import memoize from "lodash-es/memoize"; 2 | export const numberToByteArray = (num, byteLength = getNumberByteLength(num)) => { 3 | var byteArray; 4 | if (byteLength == 1) { 5 | byteArray = new DataView(new ArrayBuffer(1)); 6 | byteArray.setUint8(0, num); 7 | } 8 | else if (byteLength == 2) { 9 | byteArray = new DataView(new ArrayBuffer(2)); 10 | byteArray.setUint16(0, num); 11 | } 12 | else if (byteLength == 3) { 13 | byteArray = new DataView(new ArrayBuffer(3)); 14 | byteArray.setUint8(0, num >> 16); 15 | byteArray.setUint16(1, num & 0xffff); 16 | } 17 | else if (byteLength == 4) { 18 | byteArray = new DataView(new ArrayBuffer(4)); 19 | byteArray.setUint32(0, num); 20 | } 21 | else if (num < 0xffffffff) { 22 | byteArray = new DataView(new ArrayBuffer(5)); 23 | byteArray.setUint32(1, num); 24 | } 25 | else if (byteLength == 5) { 26 | byteArray = new DataView(new ArrayBuffer(5)); 27 | byteArray.setUint8(0, num / 0x100000000 | 0); 28 | byteArray.setUint32(1, num % 0x100000000); 29 | } 30 | else if (byteLength == 6) { 31 | byteArray = new DataView(new ArrayBuffer(6)); 32 | byteArray.setUint16(0, num / 0x100000000 | 0); 33 | byteArray.setUint32(2, num % 0x100000000); 34 | } 35 | else if (byteLength == 7) { 36 | byteArray = new DataView(new ArrayBuffer(7)); 37 | byteArray.setUint8(0, num / 0x1000000000000 | 0); 38 | byteArray.setUint16(1, num / 0x100000000 & 0xffff); 39 | byteArray.setUint32(3, num % 0x100000000); 40 | } 41 | else if (byteLength == 8) { 42 | byteArray = new DataView(new ArrayBuffer(8)); 43 | byteArray.setUint32(0, num / 0x100000000 | 0); 44 | byteArray.setUint32(4, num % 0x100000000); 45 | } 46 | else { 47 | throw new Error("EBML.typedArrayUtils.numberToByteArray: byte length must be less than or equal to 8"); 48 | } 49 | return new Uint8Array(byteArray.buffer); 50 | }; 51 | export const stringToByteArray = memoize((str) => { 52 | return Uint8Array.from(Array.from(str).map(_ => _.codePointAt(0))); 53 | }); 54 | export function getNumberByteLength(num) { 55 | if (num < 0) { 56 | throw new Error("EBML.typedArrayUtils.getNumberByteLength: negative number not implemented"); 57 | } 58 | else if (num < 0x100) { 59 | return 1; 60 | } 61 | else if (num < 0x10000) { 62 | return 2; 63 | } 64 | else if (num < 0x1000000) { 65 | return 3; 66 | } 67 | else if (num < 0x100000000) { 68 | return 4; 69 | } 70 | else if (num < 0x10000000000) { 71 | return 5; 72 | } 73 | else if (num < 0x1000000000000) { 74 | return 6; 75 | } 76 | else if (num < 0x20000000000000) { 77 | return 7; 78 | } 79 | else { 80 | throw new Error("EBML.typedArrayUtils.getNumberByteLength: number exceeds Number.MAX_SAFE_INTEGER"); 81 | } 82 | } 83 | export const int16Bit = memoize((num) => { 84 | const ab = new ArrayBuffer(2); 85 | new DataView(ab).setInt16(0, num); 86 | return new Uint8Array(ab); 87 | }); 88 | export const float32bit = memoize((num) => { 89 | const ab = new ArrayBuffer(4); 90 | new DataView(ab).setFloat32(0, num); 91 | return new Uint8Array(ab); 92 | }); 93 | export const dumpBytes = (b) => { 94 | return Array.from(new Uint8Array(b)).map(_ => `0x${_.toString(16)}`).join(", "); 95 | }; 96 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/ebml.js: -------------------------------------------------------------------------------- 1 | import memoize from "lodash-es/memoize"; 2 | import { numberToByteArray, stringToByteArray, int16Bit, float32bit } from "./typedArrayUtils"; 3 | export class Value { 4 | constructor(bytes) { 5 | this.bytes = bytes; 6 | } 7 | write(buf, pos) { 8 | buf.set(this.bytes, pos); 9 | return pos + this.bytes.length; 10 | } 11 | countSize() { 12 | return this.bytes.length; 13 | } 14 | } 15 | export class Element { 16 | constructor(id, children, isSizeUnknown) { 17 | this.id = id; 18 | this.children = children; 19 | const bodySize = this.children.reduce((p, c) => p + c.countSize(), 0); 20 | this.sizeMetaData = isSizeUnknown ? 21 | UNKNOWN_SIZE : 22 | vintEncode(numberToByteArray(bodySize, getEBMLByteLength(bodySize))); 23 | this.size = this.id.length + this.sizeMetaData.length + bodySize; 24 | } 25 | write(buf, pos) { 26 | buf.set(this.id, pos); 27 | buf.set(this.sizeMetaData, pos + this.id.length); 28 | return this.children.reduce((p, c) => c.write(buf, p), pos + this.id.length + this.sizeMetaData.length); 29 | } 30 | countSize() { 31 | return this.size; 32 | } 33 | } 34 | export const bytes = memoize((data) => { 35 | return new Value(data); 36 | }); 37 | export const number = memoize((num) => { 38 | return bytes(numberToByteArray(num)); 39 | }); 40 | export const vintEncodedNumber = memoize((num) => { 41 | return bytes(vintEncode(numberToByteArray(num, getEBMLByteLength(num)))); 42 | }); 43 | export const int16 = memoize((num) => { 44 | return bytes(int16Bit(num)); 45 | }); 46 | export const float = memoize((num) => { 47 | return bytes(float32bit(num)); 48 | }); 49 | export const string = memoize((str) => { 50 | return bytes(stringToByteArray(str)); 51 | }); 52 | export const element = (id, child) => { 53 | return new Element(id, Array.isArray(child) ? child : [child], false); 54 | }; 55 | export const unknownSizeElement = (id, child) => { 56 | return new Element(id, Array.isArray(child) ? child : [child], true); 57 | }; 58 | export const build = (v) => { 59 | const b = new Uint8Array(v.countSize()); 60 | v.write(b, 0); 61 | return b; 62 | }; 63 | export const getEBMLByteLength = (num) => { 64 | if (num < 0x7f) { 65 | return 1; 66 | } 67 | else if (num < 0x3fff) { 68 | return 2; 69 | } 70 | else if (num < 0x1fffff) { 71 | return 3; 72 | } 73 | else if (num < 0xfffffff) { 74 | return 4; 75 | } 76 | else if (num < 0x7ffffffff) { 77 | return 5; 78 | } 79 | else if (num < 0x3ffffffffff) { 80 | return 6; 81 | } 82 | else if (num < 0x1ffffffffffff) { 83 | return 7; 84 | } 85 | else if (num < 0x20000000000000) { 86 | return 8; 87 | } 88 | else if (num < 0xffffffffffffff) { 89 | throw new Error("EBMLgetEBMLByteLength: number exceeds Number.MAX_SAFE_INTEGER"); 90 | } 91 | else { 92 | throw new Error("EBMLgetEBMLByteLength: data size must be less than or equal to " + (Math.pow(2, 56) - 2)); 93 | } 94 | }; 95 | export const UNKNOWN_SIZE = new Uint8Array([0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]); 96 | export const vintEncode = (byteArray) => { 97 | byteArray[0] = getSizeMask(byteArray.length) | byteArray[0]; 98 | return byteArray; 99 | }; 100 | export const getSizeMask = (byteLength) => { 101 | return 0x80 >> (byteLength - 1); 102 | }; 103 | -------------------------------------------------------------------------------- /src/util/lib-detailed-fetch-blob/firefox-detailed-fetch-blob.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { Constructor, ForceShim } from '../common-types.js'; 11 | import { BaseDetailedFetchBlobInit } from './base-detailed-fetch-blob.js'; 12 | import BaseDetailedFetchBlob from './base-detailed-fetch-blob.js'; 13 | 14 | export type MozXMLHttpRequest = ForceShim void) }, 16 | { responseType: 'moz-chunked-arraybuffer' }> 17 | 18 | declare const window: Window & { 19 | XMLHttpRequest: Constructor & typeof XMLHttpRequest 20 | } 21 | export { window } 22 | 23 | /** 24 | * It has been two years. Firefox still do not have streams :( 25 | */ 26 | class FirefoxDetailedFetchBlob extends BaseDetailedFetchBlob { 27 | xhr!: MozXMLHttpRequest 28 | promise: Promise 29 | 30 | constructor(input: string, { 31 | onprogress = null, 32 | onabort = null, 33 | onerror = null, 34 | loaded = 0, 35 | total = Infinity, 36 | lengthComputable = false, 37 | XMLHttpRequest = window.XMLHttpRequest 38 | } = {} as BaseDetailedFetchBlobInit & { XMLHttpRequest: typeof window.XMLHttpRequest }) { 39 | super(input, { onprogress, onabort, onerror, loaded, total, lengthComputable }); 40 | const initLoaded = loaded; 41 | 42 | this.promise = new Promise((resolve, reject) => { 43 | this.xhr = new XMLHttpRequest(); 44 | this.xhr.responseType = 'moz-chunked-arraybuffer'; 45 | this.xhr.onloadstart = ({ total, lengthComputable }) => { 46 | this.total += total; 47 | this.lengthComputable = lengthComputable; 48 | } 49 | let last = Date.now(); 50 | this.xhr.onprogress = ({ loaded }) => { 51 | this.loaded = initLoaded + loaded; 52 | this.buffer!.push(new Blob([this.xhr.response])); 53 | if (Date.now() - last > 500) { 54 | this.dispatchEvent(new ProgressEvent('progress', this)); 55 | last = Date.now(); 56 | } 57 | }; 58 | this.xhr.onload = () => { 59 | this.blob = new Blob(this.buffer!); 60 | this.buffer = null; 61 | resolve(this.blob); 62 | } 63 | this.xhr.onerror = () => { 64 | this.error = new DOMException('firefoxDetailedFetchBlob', 'NetworkError'); 65 | this.dispatchEvent(new ErrorEvent('error', this)); 66 | reject(this.error); 67 | } 68 | this.xhr.onabort = () => { 69 | this.error = new DOMException('DetailedFetchBlob was aborted', 'AbortError'); 70 | this.dispatchEvent(new ProgressEvent('abort', this)); 71 | this.dispatchEvent(new ErrorEvent('error', this)); 72 | reject(this.error); 73 | } 74 | this.xhr.open('get', input); 75 | this.xhr.send(); 76 | }); 77 | } 78 | 79 | abort() { 80 | this.xhr.abort(); 81 | } 82 | 83 | static get isSupported() { 84 | const xhr = new XMLHttpRequest() as MozXMLHttpRequest; 85 | xhr.responseType = 'moz-chunked-arraybuffer'; 86 | return xhr.responseType === 'moz-chunked-arraybuffer'; 87 | } 88 | } 89 | 90 | export default FirefoxDetailedFetchBlob; 91 | -------------------------------------------------------------------------------- /src/util/lib-util-streams/input-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import ReadableStream, { ReadableStreamDefaultController } from './readablestream-types.js' 11 | 12 | class InputStream extends ReadableStream { 13 | controller: ReadableStreamDefaultController 14 | 15 | constructor(strategy?: QueuingStrategy) { 16 | let _controller = null as ReadableStreamDefaultController | null; 17 | super({ start: controller => _controller = controller }, strategy); 18 | 19 | this.controller = _controller!; 20 | } 21 | 22 | close() { 23 | this.controller.close(); 24 | } 25 | 26 | write(chunk: any) { 27 | this.controller.enqueue(chunk); 28 | } 29 | 30 | error(error: any) { 31 | this.controller.error(error); 32 | } 33 | } 34 | 35 | class BackpressureFrame { 36 | resolvePull?: () => void 37 | resolveWrite?: () => void 38 | next: BackpressureFrame | null 39 | done: Promise<[void, void]> 40 | 41 | constructor() { 42 | this.done = Promise.all([ 43 | new Promise(resolve => this.resolvePull = resolve), 44 | new Promise(resolve => this.resolveWrite = resolve), 45 | ]); 46 | this.next = null; 47 | } 48 | } 49 | 50 | class BackpressureInputStream extends ReadableStream { 51 | controller: ReadableStreamDefaultController 52 | pullFrame: BackpressureFrame 53 | writeFrame: BackpressureFrame 54 | 55 | constructor(strategy?: QueuingStrategy) { 56 | let _controller = null as ReadableStreamDefaultController | null; 57 | super({ 58 | start: controller => _controller = controller, 59 | pull: () => { 60 | const pullFrame = this.pullFrame; 61 | if (!pullFrame.next) { 62 | pullFrame.next = new BackpressureFrame(); 63 | } 64 | this.pullFrame = pullFrame.next; 65 | pullFrame.resolvePull!(); 66 | if (pullFrame.resolveWrite) return pullFrame.done; 67 | return /* void */; 68 | } 69 | }, strategy); 70 | 71 | this.controller = _controller!; 72 | this.pullFrame = this.writeFrame = new BackpressureFrame(); 73 | } 74 | 75 | close() { 76 | this.controller.close(); 77 | } 78 | 79 | async write(chunk: any) { 80 | const writeFrame = this.writeFrame; 81 | if (!writeFrame.next) { 82 | writeFrame.next = new BackpressureFrame(); 83 | } 84 | this.writeFrame = writeFrame.next; 85 | writeFrame.resolveWrite!(); 86 | if (writeFrame.resolvePull) await writeFrame.done; 87 | this.controller.enqueue(chunk); 88 | } 89 | 90 | error(error: any) { 91 | this.controller.error(error); 92 | } 93 | } 94 | 95 | const __UNIT_TEST = () => { 96 | class ConsoleStream extends WritableStream { 97 | constructor() { 98 | super({ 99 | write: (chunk: any) => new Promise(resolve => setTimeout(resolve, 1000)).then(() => console.log(chunk)), 100 | } as any); 101 | } 102 | } 103 | const streams = { 104 | get stdout() { return new ConsoleStream() }, 105 | } 106 | 107 | var is = new BackpressureInputStream(); 108 | is.pipeTo(streams.stdout); 109 | 110 | void (async () => { 111 | for (let i = 0; i < 5; i++) { 112 | is.write(i); console.warn(`${i} written`); 113 | } 114 | })(); 115 | } 116 | 117 | export { InputStream, BackpressureInputStream } 118 | export default InputStream 119 | -------------------------------------------------------------------------------- /src/util/async-control.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | /** 11 | * A promisified settimeout 12 | * 13 | * @param (default 0) time to sleep in ms 14 | */ 15 | const sleep = (ms = 0) => { 16 | return new Promise(resolve => setTimeout(resolve, ms)); 17 | } 18 | 19 | /** 20 | * Creates a Promise that resolves when the current macrotask queue has been 21 | * emptied 22 | */ 23 | const yieldThread = () => new Promise(setTimeout); 24 | 25 | /** 26 | * Creates a function that invokes `originalFunction`, with the `this` binding 27 | * and `arguments` of the created function, while there is no other pending 28 | * excutions of `originalFunction`. Simultaneous calls to the created function 29 | * return the result of the first pending `originalFunction` invocation. 30 | * 31 | * @param originalFunction async function to wrap 32 | */ 33 | const debounceAsync = Promise>(originalFunction: T) => { 34 | let currentExcution: ReturnType | null; 35 | const wrappedFunction = async function (this: any) { 36 | // 1. locked => return lock 37 | if (currentExcution) return currentExcution; 38 | 39 | // 2. released => apply 40 | currentExcution = originalFunction.apply(this, arguments); 41 | try { 42 | return await currentExcution; 43 | } 44 | finally { 45 | currentExcution = null; 46 | } 47 | }; 48 | return wrappedFunction as T; 49 | } 50 | 51 | const debounceAsync_UNIT_TEST = async () => { 52 | const goodnight = debounceAsync(sleep); 53 | for (let i = 0; i < 8; i++) { 54 | goodnight(5000).then(() => console.log(Date())); 55 | await sleep(500); 56 | } 57 | console.warn('Expected output: 8 identical datetime'); 58 | }; 59 | 60 | const endOfQueue = Promise.resolve(); 61 | const overrideResult = async (lastExcution: Promise) => { 62 | try { 63 | await lastExcution; 64 | } 65 | finally { 66 | return endOfQueue; 67 | } 68 | } 69 | 70 | /** 71 | * Creates a function that invokes `originalFunction`, with the `this` binding 72 | * and `arguments` of the created function, while there is no other pending 73 | * excutions of `originalFunction`. Simultaneous calls to the created function 74 | * will be queued up. 75 | * 76 | * @param {function} originalFunction async function to wrap 77 | */ 78 | const queueAsync = Promise>(originalFunction: T) => { 79 | let lastExcution = endOfQueue; 80 | const wrappedFunction = async function (this: any) { 81 | // 1. queue up 82 | const myExcution = lastExcution.then(() => originalFunction.apply(this, arguments)); 83 | 84 | // 2. update queue tail + swipe excution result from queue 85 | lastExcution = overrideResult(myExcution); 86 | 87 | // 3. return excution result 88 | return myExcution; 89 | }; 90 | return wrappedFunction as T; 91 | } 92 | 93 | const queueAsync_UNIT_TEST = () => { 94 | const badnight = queueAsync(i => sleep(i).then(() => { if (Math.random() > 0.5) throw new Error('uncaught error test: you should expect a console error message.') })); 95 | badnight(1000); 96 | badnight(1000); 97 | badnight(1000); 98 | badnight(1000); 99 | badnight(1000).finally(() => console.log('5s!')); 100 | badnight(1000); 101 | badnight(1000); 102 | badnight(1000); 103 | badnight(1000); 104 | badnight(1000).finally(() => console.log('10s!')); 105 | console.warn('Check message timestamps.'); 106 | console.warn('Bad:'); 107 | console.warn('1 1 1 1 1:5s'); 108 | console.warn(' 1 1 1 1 1:10s'); 109 | console.warn('Good:'); 110 | console.warn('1 1 1 1 1:5s'); 111 | console.warn(' 1 1 1 1 1:10s'); 112 | } 113 | 114 | export { sleep, yieldThread, debounceAsync, queueAsync }; 115 | export default { sleep, yieldThread, debounceAsync, queueAsync }; 116 | -------------------------------------------------------------------------------- /src/service/bilimonkey-ass-handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { OnEventTargetFactory } from '../util/on-event-target.js'; 11 | import { parseXML, genASSBlob } from '../codec/assconverter/interface.js'; 12 | import { SimpleBareEvent } from '../util/simple-event-target.js'; 13 | 14 | export type FetchFunctionType = typeof fetch 15 | 16 | export const enum BiliMonkeyASSHandlerReadyState { 17 | initialized, 18 | downloadinited, 19 | downloadstarted, 20 | loaded, 21 | } 22 | 23 | export interface BiliMonkeyASSHandlerInit { 24 | blocker?: RegExp, 25 | style?: { 26 | fontFamily?: string 27 | fontSize?: number 28 | textOpacity?: number 29 | bold?: boolean 30 | } 31 | protocol?: 'http:' | 'https:' | '' 32 | fetch?: FetchFunctionType 33 | } 34 | 35 | export type EventMap = { 36 | load: SimpleBareEvent 37 | downloadinit: SimpleBareEvent 38 | downloadstart: SimpleBareEvent 39 | } 40 | 41 | export type OnEventMap = { 42 | onload: SimpleBareEvent 43 | ondownloadinit: SimpleBareEvent 44 | ondownloadstart: SimpleBareEvent 45 | } 46 | 47 | const CACHE_URL_SYMBOL = Symbol('blobURL'); 48 | class BiliMonkeyASSHandler extends OnEventTargetFactory() { 49 | readonly originalURL: string 50 | 51 | cache: Blob | null 52 | blocker: RegExp | null 53 | style: { 54 | fontFamily?: string 55 | fontSize?: number 56 | textOpacity?: number 57 | bold?: boolean 58 | } 59 | [CACHE_URL_SYMBOL]: string | null 60 | 61 | fetch: FetchFunctionType 62 | currentDownload: Promise | null 63 | readyState: BiliMonkeyASSHandlerReadyState 64 | 65 | constructor(originalURL: string, { blocker = null, style: { fontFamily = undefined, fontSize = undefined, textOpacity = undefined, bold = undefined } = {}, protocol = '', fetch = top.fetch } = {} as BiliMonkeyASSHandlerInit) { 66 | super(); 67 | 68 | const indexOf = originalURL.indexOf(':') + 1; 69 | this.originalURL = indexOf ? `${protocol}${originalURL.slice(indexOf)}` : originalURL; 70 | 71 | this.cache = null; 72 | this.blocker = blocker; 73 | this.style = { fontFamily, fontSize, textOpacity, bold }; 74 | this[CACHE_URL_SYMBOL] = null; 75 | 76 | this.fetch = fetch; 77 | this.readyState = BiliMonkeyASSHandlerReadyState.initialized; 78 | this.currentDownload = null; 79 | this.addEventListener('downloadinit', () => this.readyState = BiliMonkeyASSHandlerReadyState.downloadinited); 80 | this.addEventListener('downloadstart', () => this.readyState = BiliMonkeyASSHandlerReadyState.downloadstarted); 81 | this.addEventListener('loaded', () => this.readyState = BiliMonkeyASSHandlerReadyState.loaded); 82 | } 83 | 84 | async getDownload() { 85 | if (this.cache) return this.cache; 86 | else return this.download(); 87 | } 88 | 89 | async getDownloadURL() { 90 | if (!this.cache) await this.download(); 91 | return this.cacheURL!; 92 | } 93 | 94 | async download() { 95 | return this.currentDownload = (async () => { 96 | const fetch = this.fetch; 97 | this.dispatchEvent({ type: 'downloadinit' }); 98 | const response = await fetch(this.originalURL); 99 | this.dispatchEvent({ type: 'downloadstart' }); 100 | 101 | let danmaku = parseXML(await response.text()); 102 | 103 | if (this.blocker) { 104 | danmaku = danmaku.filter(e => !this.blocker!.test(e.text)); 105 | } 106 | 107 | this.cache = await genASSBlob(danmaku, this.style); 108 | this.dispatchEvent({ type: 'load' }); 109 | return this.cache; 110 | })(); 111 | } 112 | 113 | get cacheURL() { 114 | if (!this.cache) return null; 115 | return this[CACHE_URL_SYMBOL] || (this[CACHE_URL_SYMBOL] = URL.createObjectURL(this.cache)); 116 | } 117 | 118 | toString() { 119 | return this.cacheURL || ''; 120 | } 121 | 122 | destroy() { 123 | if (this[CACHE_URL_SYMBOL]) { 124 | URL.revokeObjectURL(this[CACHE_URL_SYMBOL]!); 125 | this[CACHE_URL_SYMBOL] = null; 126 | } 127 | } 128 | } 129 | 130 | export { BiliMonkeyASSHandler }; 131 | export default BiliMonkeyASSHandler; 132 | -------------------------------------------------------------------------------- /src/util/lib-cache-db/base-mutable-cache-db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCacheDB, { FileLike } from './common-cache-db.js'; 11 | 12 | export interface MutationInit { 13 | name?: string 14 | offset?: number 15 | append?: boolean 16 | truncate?: boolean 17 | } 18 | 19 | export interface NamedMutationInit extends MutationInit { 20 | name: string 21 | } 22 | 23 | export interface NamedArrayBuffer extends ArrayBuffer { 24 | name: string 25 | } 26 | 27 | abstract class BaseMutableCacheDB implements CommonCacheDB { 28 | dbName: string 29 | storeName: string 30 | 31 | constructor(dbName: string, storeName: string) { 32 | this.dbName = dbName; 33 | this.storeName = storeName; 34 | } 35 | 36 | abstract async createData(item: FileLike | NamedArrayBuffer): Promise 37 | abstract async createData(item: Blob | ArrayBuffer, name: string): Promise 38 | abstract async createData(item: Blob | ArrayBuffer, options: NamedMutationInit): Promise 39 | 40 | abstract async setData(item: FileLike | NamedArrayBuffer, options?: MutationInit): Promise 41 | abstract async setData(item: Blob | ArrayBuffer, name: string, options?: MutationInit): Promise 42 | abstract async setData(item: Blob | ArrayBuffer, options: NamedMutationInit): Promise 43 | 44 | abstract async appendData(item: FileLike | NamedArrayBuffer, options?: MutationInit): Promise 45 | abstract async appendData(item: Blob | ArrayBuffer, name: string, options?: MutationInit): Promise 46 | abstract async appendData(item: Blob | ArrayBuffer, options: NamedMutationInit): Promise 47 | 48 | abstract async getData(name: string): Promise 49 | 50 | abstract async hasData(name: string): Promise 51 | 52 | abstract async deleteData(name: string): Promise 53 | 54 | abstract async renameData(name: string, newName: string): Promise 55 | 56 | abstract async createWriteStream(options: NamedMutationInit): Promise 57 | abstract async createWriteStream(name: string, options?: MutationInit): Promise 58 | 59 | async createReadStream(name: string) { 60 | const data = await this.getData(name) 61 | if (!data) return null; 62 | return new Response(data).body; 63 | } 64 | 65 | async getObjectURL(name: string) { 66 | const data = await this.getData(name); 67 | if (!data) return null; 68 | return URL.createObjectURL(data); 69 | } 70 | 71 | async getText(name: string) { 72 | const data = await this.getData(name); 73 | if (!data) return null; 74 | return new Promise((resolve, reject) => { 75 | const e = new FileReader(); 76 | e.onload = () => resolve(e.result); 77 | e.onerror = reject; 78 | e.readAsText(data); 79 | }); 80 | } 81 | 82 | async getJSON(name: string) { 83 | const data = await this.getData(name); 84 | if (!data) return null; 85 | return new Promise((resolve, reject) => { 86 | const e = new FileReader(); 87 | e.onload = () => resolve(JSON.parse(e.result)); 88 | e.onerror = reject; 89 | e.readAsText(data); 90 | }); 91 | } 92 | 93 | async getRespone(name: string) { 94 | const data = await this.getData(name) 95 | if (!data) return null; 96 | return new Response(data); 97 | } 98 | 99 | get getReadStream() { 100 | return this.createReadStream; 101 | } 102 | 103 | abstract async deleteAllData(): Promise 104 | 105 | abstract async deleteEntireDB(): Promise 106 | 107 | static get isSupported() { return false } 108 | 109 | static async cloneBlob(file: Blob & { name: USVString, type?: string, lastModified?: number }): Promise 110 | static async cloneBlob(file: Blob): Promise 111 | static async cloneBlob(file: Blob & { name?: USVString, type?: string, lastModified?: number }) { 112 | const ret = new Response(file).blob(); 113 | if (file.name) { 114 | return new File([await ret], file.name, file) 115 | } 116 | return ret; 117 | } 118 | 119 | static async quota() { return { usage: -1, quota: -1 } } 120 | } 121 | 122 | export { BaseMutableCacheDB } 123 | export default BaseMutableCacheDB; 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 正在施工。大约需要一个星期更新脚本。届时将放出可用版本。 2 | 3 | # 围观群众看得懂的东西 4 | 5 | * Chrome更新了一个超好用的东西,现在我们可以**几乎0内存合并FLV了**。 6 | 7 | * 之前我得了“**不重构就会死**”的病,不过终于找到药了! 8 | 9 | * 为了获得真·项目的经验,打算上**Vue全家桶**了。 10 | 11 | * 再拖更也不好意思了。 12 | 13 | * \#help-wanted\# 14 | 15 | # 目前的状态 16 | 17 | util已经差不多了,codec直接用以前的,但是service的工作量还很大。 18 | 19 | # 目录结构 20 | 21 | * util:工具 可复用的函数 22 | 23 | * codec:媒体文件(flv/ass/mkv)生成 24 | 25 | * service:与B站页面交互 从B站抓数据 在页面上模拟用户操作 监控页面状态 26 | 27 | * store:Vuex 转发service的数据到视图层 28 | 29 | * components:视图层 30 | 31 | # 开发指南 32 | 33 | * util:旧文件有JSDoc,新文件是TypeScript,易懂 34 | 35 | * codec:纯函数,易懂 36 | 37 | * service:重头戏来了 38 | 39 | 作为插件,页面本身的状态管理是不会管我们的,但是插件又依赖于页面,状态同步成了一团乱麻。 40 | 41 | 仔细思考了现有的技术后,我总结出,一个能**独立存在**的模块与外界交互的方式主要有以下几种 42 | 43 | * 向模块输入信息:设置对象属性,调用方法传参数,在输入上设置事件监听器/回调 44 | * 模块向外输出信息:获取对象属性,调用方法返回同步值,调用方法返回Promise,发射事件/调用回调 45 | 46 | 搜索了一番之后,我发现我想要的其实是一个`Promise.all`的事件版,或者`Observable.merge`的所有事件版。[问了一圈](https://segmentfault.com/q/1010000015424221),没找到。 47 | 48 | 所以只能自己造轮子。 49 | 50 | `util/event-duplex.js`就是成果。解释一下: 51 | 52 | ```typescript 53 | OnEventDuplexFactory(init?) 54 | ``` 55 | 56 | `InputEventMap`定义这个模块接受哪些事件作为输入,`OutputEventMap`定义这个模块发射哪些事件作为输出。这前两个类型虽然有默认值,但是还是强烈建议写上,至少可以作为文档。 57 | 58 | 与此同时,我想要给模块加上`onevent`这种比较方便的监听器添加方法。因为TypeScript的类型映射不能改属性名,所以只能用`OutOnEventMap`再次指明事件类型,内容和`OutputEventMap`一样,只不过属性名前面要加上`on`。因为JavaScript不能读取TypeScript类型,所以要额外再传递一次哪些事件要加上`onevent`属性,参数`init`接受`Iterable<事件名>`。这两个倒是可选的,只不过加上以后方便一些。 59 | 60 | 示例: 61 | 62 | ```typescript 63 | OnEventDuplexFactory< 64 | { click: MouseEvent }, 65 | { load: ProgressEvent }, 66 | { onload: ProgressEvent } 67 | >(['load']) 68 | ``` 69 | 70 | 会生成一个类,实现了(一个合理简化了的)`addEL`/`removeEL`/`dispatchE`,而且有`onload`属性,`addEL('load')`的时候也可以正确提示事件类型。那事件输入呢? 71 | 72 | `OnEventDuplex`实现了`[inputSocketSymbol]: EventSocket`接口,这个`inputSocketSymbol`也从`util/event-duplex.js`里导出了。`EventSocket`的目的就是提供`Promise.all`的事件版。因此,所有事件输入都应该通过`this[inputSocketSymbol].addEL/removeEL`实现。 73 | 74 | 但这不是转了一圈,更麻烦了呀? 75 | 76 | 接下来就是神奇之处了:`util/event-duplex.js`还导出了一个工具函数`pipeEventsThrough`。望文生义,它接受两个参数,第一个是事件源,第二个是实现了`[inputSocketSymbol]: EventSocket`接口的 ~~接盘侠~~ 事件目的地。 77 | 78 | ```javascript 79 | pipeEventsThrough(button, eventDuplex); 80 | ``` 81 | 82 | 这个函数会从`eventDuplex[inputSocketSymbol]`获取所有`eventDuplex`订阅过的事件,然后在`button`发射这些事件的时候,转发一份给`eventDuplex`。 83 | 84 | 注意,“获取所有`eventDuplex`订阅过的事件”这个行为是一次性的,所以我推荐把所有事件输入都提前到`constructor`里绑定好,或者至少调用`eventDuplex[inputSocketSymbol].addEventType`显式注册。**如果pipe之后再扩充事件列表,新事件并不会被转发**。这个时候需要重新`pipeEventsThrough`——别担心,[重复地注册监听器也只会触发一次](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener)。 85 | 86 | 所以现在我们可以有各种舒服的用法了: 87 | 88 | ```javascript 89 | pipeEventsThrough(button1, eventDuplex1); 90 | pipeEventsThrough(button2, eventDuplex1); 91 | pipeEventsThrough(button1, eventDuplex2); 92 | pipeEventsThrough(button2, eventDuplex2); 93 | ``` 94 | 95 | 想要取消? 96 | 97 | ```javascript 98 | eventDuplex1[inputSocketSymbol].disconnect(button1); 99 | ``` 100 | 101 | 如果上游是`EventDuplex`,还可以链式调用 102 | 103 | ```javascript 104 | pipeEventsThrough(button1, eventDuplex1) 105 | .pipeEventsThrough(eventDuplex2) 106 | .pipeEventsThrough(eventDuplex3) 107 | .addEventListener('load', console.log) 108 | ``` 109 | 110 | 很像RxJS,是吧?我承认,可能最主要的差别仅仅是我实现了一个`fromAllEvents`。 111 | 112 | 可能还有一个差别,RxJS与函数式结合得最好,如果管道有状态就很坑爹,更不要说管道上的方法了——当然,我喜欢函数式,学Haskell几乎应该是我大学最快乐的一门课了。但是函数式处理IO真的会变得很奇怪,以至于出了`do`这个语法糖。`EventDuplex`并不会偏向哪一种,设置属性或者调用方法也不会很奇怪,毕竟一开始就是`new`出来的,明示了这个模块是有状态的。所以 113 | 114 | ```javascript 115 | eventDuplex2.pause() 116 | console.log(eventDuplex2.currentState) 117 | console.log(await eventDuplex2.setState('buffer-empty')) 118 | eventDuplex2.resume() 119 | ``` 120 | 121 | 这样的代码也OK。 122 | 123 | 至于垃圾收集,很遗憾,我没有找到完美的解决方案,所以理论上和原生监听器一样,事件源保留监听器的引用,如果是事件源先被垃圾收集,监听器也会被收集,但如果事件源还在,想要先删掉监听器,需要显式`.removeEL`。不过我还是找到了一个妥协方案: 124 | 125 | ```javascript 126 | eventDuplex[inputSocketSymbol].close() 127 | ``` 128 | 129 | 这会把`EventSocket`标记为已关闭(其实就是`delete`了所有属性)。事件源**下一次**发射事件的时候,将会移除监听器。在此之前,将会泄露一个空对象(`EventSocket`)+一些很短小的函数(监听器)。 130 | 131 | 如果在`EventSocket`保留上游的引用,确实可以在`close`的时候自动清理,**但是上游可以被收集了的时候并不会通知下游,下游保留上游的引用会导致上游泄露**。考虑再三,还是决定与原生行为看齐。如果有更好的解决方案,请告诉我。 132 | 133 | 现在有了用着顺手的模块,我们可以开始解耦了。 134 | 135 | * BiliUserjs:监控页面状态(aid/cid/video/播放控制条/右键菜单ul),输入无,输出页面状态改变事件 136 | * BiliMonkey:抓取网络请求,输入cid/播放控制条,输出视频地址/下载进度/模块是否需要重建 137 | * BiliPolyfill:简化用户操作,输入aid/cid/video,输出待定/模块是否需要重建 138 | * bilitwin-keeper:监控模块状态,输入Monkey/Polyfill,适时重建模块,不输出 139 | * bilitwin-options:负责持久化用户设置,CRUD,可能返回Promise,不使用事件 140 | * bilitwin-store:vuex中介,输入Monkey/Polyfill,筛选需要的事件,然后写入vuex 141 | * bilitwin-ui:监控UI,如果UI被源页面一句`$.html()`清掉了负责补上,输入BiliUserjs,输出待定 142 | * bilitwin:负责启动上面一大堆,并且安装合适的pipe,同时充当IoC容器 143 | 144 | * store:实验性地学习一下Vuex怎么用,结构随便 145 | 146 | * components:解耦UI与服务,这样以后UI可以单独放出去自定义,方便人民群众fork 147 | -------------------------------------------------------------------------------- /src/codec/flvparser/flv-offset-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import TransformStream from '../../util/lib-util-streams/transformstream-types.js'; 11 | import { ReadableStream } from '../../util/lib-util-streams/readablestream-types.js'; 12 | 13 | /** 14 | * A TransformStream to offset flv timestamps 15 | * IN: ...tag objects (script tag, audio tag and video tag) piped to writeableStream 16 | * OUT: ...tag objects (script tag, audio tag and video tag) as readableStream 17 | * OUT2: ...tag DataView as readableStream 18 | */ 19 | class FLVOffsetStream extends TransformStream { 20 | timestampMax: number 21 | duration: number 22 | scriptData: Uint8Array | null 23 | 24 | constructor({ timestampOffset = 0, mediaTagsOnly = true, outputBinary = false } = {}, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy) { 25 | super({ 26 | transform: outputBinary ? 27 | (tag, controller) => { 28 | if (tag.tagType === 0x08 || tag.tagType === 0x09) { 29 | this.timestampMax = tag.getCombinedTimestamp(); 30 | tag.setCombinedTimestamp(this.timestampMax + timestampOffset); 31 | controller.enqueue(tag.tagHeader); 32 | controller.enqueue(tag.tagData); 33 | controller.enqueue(tag.previousSize); 34 | } 35 | else { 36 | if (tag.tagType === 0x12) { 37 | this.duration = tag.getDuration(); 38 | this.scriptData = tag; 39 | } 40 | if (!mediaTagsOnly) { 41 | controller.enqueue(tag.tagHeader); 42 | controller.enqueue(tag.tagData); 43 | controller.enqueue(tag.previousSize); 44 | } 45 | } 46 | } : 47 | (tag, controller) => { 48 | if (tag.tagType === 0x08 || tag.tagType === 0x09) { 49 | this.timestampMax = tag.getCombinedTimestamp(); 50 | tag.setCombinedTimestamp(this.timestampMax + timestampOffset); 51 | controller.enqueue(tag); 52 | } 53 | else { 54 | if (tag.tagType === 0x12) { 55 | this.duration = tag.getDuration(); 56 | this.scriptData = tag; 57 | } 58 | if (!mediaTagsOnly) { 59 | controller.enqueue(tag); 60 | } 61 | } 62 | } 63 | }, writableStrategy, readableStrategy); 64 | 65 | this.timestampMax = 0; 66 | this.duration = 0; 67 | this.scriptData = null; 68 | } 69 | 70 | 71 | /** 72 | * 73 | * @param flvStreams flv tag ReadableStreams 74 | * @param strategies writableStrategy = {}, readableStrategy = {} 75 | */ 76 | static mergeStream(flvStreams: ReadableStream[], writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy) { 77 | const { readable, writable } = new TransformStream({ 78 | start: async controller => { 79 | let duration = 0; 80 | let scriptData = null; 81 | const flvOffsetStreams = []; 82 | 83 | // 1. extract correct settings from scriptdata tag for each stream 84 | for (const flvStream of flvStreams) { 85 | // 1.1 read the first tag from stream (expected to be script tag) 86 | const reader = flvStream.getReader(); 87 | const { value } = await reader.read(); 88 | reader.releaseLock(); 89 | 90 | // 1.2 find a scriptdata tag template 91 | if (!scriptData) scriptData = value; 92 | 93 | // 1.3 accumlate duration and compute offset 94 | const timestampOffset = duration * 1000; 95 | duration += value.getDuration(); 96 | 97 | // 1.4 create offset stream pipe 98 | const flvOffsetStream = new FLVOffsetStream({ timestampOffset, outputBinary: true }); 99 | flvOffsetStreams.push(flvOffsetStream); 100 | (flvStream as ReadableStream).pipeTo(flvOffsetStream.writable); 101 | } 102 | 103 | // 2. output flv.header + flv.firstPreviousTagSize 104 | controller.enqueue(new Uint8Array([70, 76, 86, 1, 5, 0, 0, 0, 9, 0, 0, 0, 0])); 105 | 106 | // 3. output scriptData 107 | // 3.1 set correct duration 108 | scriptData.getDurationAndView().durationDataView.setFloat64(0, duration); 109 | 110 | // 3.2 remove keyframe section 111 | scriptData.stripKeyframesScriptData(); 112 | 113 | // 3.3 output everything 114 | controller.enqueue(scriptData.tagHeader); 115 | controller.enqueue(scriptData.tagData); 116 | controller.enqueue(scriptData.previousSize); 117 | 118 | // 4. create continious pipeline 119 | // BAD Programming. Error not propagated. 120 | (async () => { 121 | for (const flvOffsetStream of flvOffsetStreams) { 122 | await (flvOffsetStream.readable as ReadableStream).pipeTo(writable, { preventClose: true }); 123 | } 124 | writable.getWriter().close(); 125 | })().catch(controller.error.bind(controller)); 126 | } 127 | }, writableStrategy, readableStrategy); 128 | 129 | return readable; 130 | } 131 | } 132 | 133 | export default FLVOffsetStream; 134 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/index.entry.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * FLV + ASS => MKV transmuxer 3 | * Demux FLV into H264 + AAC stream and ASS into line stream; then 4 | * remux them into a MKV file. 5 | * 6 | * @author qli5 7 | * 8 | * This Source Code Form is subject to the terms of the Mozilla Public 9 | * License, v. 2.0. If a copy of the MPL was not distributed with this 10 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 11 | * 12 | * The FLV demuxer is from flv.js 13 | * by zheng qian , licensed under Apache 2.0. 14 | * 15 | * The EMBL builder is from simple-ebml-builder 16 | * by ryiwamoto, 17 | * licensed under MIT. 18 | */ 19 | 20 | import FLVDemuxer from './demuxer/flvdemuxer.js'; 21 | import ASS from './demuxer/ass.js'; 22 | import MKV from './remuxer/mkv.js'; 23 | import { Blob } from './util/shim.js'; 24 | 25 | const FLVASS2MKV = class { 26 | constructor(config = {}) { 27 | this.onflvprogress = null; 28 | this.onassprogress = null; 29 | this.onurlrevokesafe = null; 30 | this.onfileload = null; 31 | this.onmkvprogress = null; 32 | this.onload = null; 33 | Object.assign(this, config); 34 | this.mkvConfig = { onprogress: this.onmkvprogress }; 35 | Object.assign(this.mkvConfig, config.mkvConfig); 36 | } 37 | 38 | /** 39 | * Demux FLV into H264 + AAC stream and ASS into line stream; then 40 | * remux them into a MKV file. 41 | * @param {Blob|string|ArrayBuffer} flv 42 | * @param {Blob|string|ArrayBuffer} ass 43 | */ 44 | async build(flv = './samples/gen_case.flv', ass = './samples/gen_case.ass') { 45 | // load flv and ass as arraybuffer 46 | await Promise.all([ 47 | new Promise((r, j) => { 48 | if (flv instanceof Blob) { 49 | const e = new FileReader(); 50 | e.onprogress = this.onflvprogress; 51 | e.onload = () => r(flv = e.result); 52 | e.onerror = j; 53 | e.readAsArrayBuffer(flv); 54 | } 55 | else if (typeof flv == 'string') { 56 | const e = new XMLHttpRequest(); 57 | e.responseType = 'arraybuffer'; 58 | e.onprogress = this.onflvprogress; 59 | e.onload = () => r(flv = e.response); 60 | e.onerror = j; 61 | e.open('get', flv); 62 | e.send(); 63 | flv = 2; // onurlrevokesafe 64 | } 65 | else if (flv instanceof ArrayBuffer) { 66 | r(flv); 67 | } 68 | else { 69 | j(new TypeError('flvass2mkv: flv {Blob|string|ArrayBuffer}')); 70 | } 71 | if (typeof ass != 'string' && this.onurlrevokesafe) this.onurlrevokesafe(); 72 | }), 73 | new Promise((r, j) => { 74 | if (ass instanceof Blob) { 75 | const e = new FileReader(); 76 | e.onprogress = this.onflvprogress; 77 | e.onload = () => r(ass = e.result); 78 | e.onerror = j; 79 | e.readAsArrayBuffer(ass); 80 | } 81 | else if (typeof ass == 'string') { 82 | const e = new XMLHttpRequest(); 83 | e.responseType = 'arraybuffer'; 84 | e.onprogress = this.onflvprogress; 85 | e.onload = () => r(ass = e.response); 86 | e.onerror = j; 87 | e.open('get', ass); 88 | e.send(); 89 | ass = 2; // onurlrevokesafe 90 | } 91 | else if (ass instanceof ArrayBuffer) { 92 | r(ass); 93 | } 94 | else { 95 | j(new TypeError('flvass2mkv: ass {Blob|string|ArrayBuffer}')); 96 | } 97 | if (typeof flv != 'string' && this.onurlrevokesafe) this.onurlrevokesafe(); 98 | }), 99 | ]); 100 | if (this.onfileload) this.onfileload(); 101 | 102 | const mkv = new MKV(this.mkvConfig); 103 | 104 | const assParser = new ASS(); 105 | ass = assParser.parseFile(ass); 106 | mkv.addASSMetadata(ass); 107 | mkv.addASSStream(ass); 108 | 109 | const flvProbeData = FLVDemuxer.probe(flv); 110 | const flvDemuxer = new FLVDemuxer(flvProbeData); 111 | let mediaInfo = null; 112 | let h264 = null; 113 | let aac = null; 114 | flvDemuxer.onDataAvailable = (...array) => { 115 | array.forEach(e => { 116 | if (e.type == 'video') h264 = e; 117 | else if (e.type == 'audio') aac = e; 118 | else throw new Error(`MKVRemuxer: unrecoginzed data type ${e.type}`); 119 | }); 120 | }; 121 | flvDemuxer.onMediaInfo = i => mediaInfo = i; 122 | flvDemuxer.onTrackMetadata = (i, e) => { 123 | if (i == 'video') mkv.addH264Metadata(e); 124 | else if (i == 'audio') mkv.addAACMetadata(e); 125 | else throw new Error(`MKVRemuxer: unrecoginzed metadata type ${i}`); 126 | }; 127 | flvDemuxer.onError = e => { throw new Error(e); }; 128 | const finalOffset = flvDemuxer.parseChunks(flv, flvProbeData.dataOffset); 129 | if (finalOffset != flv.byteLength) throw new Error('FLVDemuxer: unexpected EOF'); 130 | mkv.addH264Stream(h264); 131 | mkv.addAACStream(aac); 132 | 133 | const ret = mkv.build(); 134 | if (this.onload) this.onload(ret); 135 | return ret; 136 | } 137 | }; 138 | 139 | // export { FLVASS2MKV }; 140 | export default FLVASS2MKV; 141 | 142 | // if nodejs then test 143 | if (typeof window == 'undefined') { 144 | if (require.main == module) { 145 | (async () => { 146 | const fs = require('fs'); 147 | const assFileName = process.argv.slice(1).find(e => e.includes('.ass')) || './samples/gen_case.ass'; 148 | const flvFileName = process.argv.slice(1).find(e => e.includes('.flv')) || './samples/gen_case.flv'; 149 | const assFile = fs.readFileSync(assFileName).buffer; 150 | const flvFile = fs.readFileSync(flvFileName).buffer; 151 | fs.writeFileSync('out.mkv', await new FLVASS2MKV({ onmkvprogress: console.log.bind(console) }).build(flvFile, assFile)); 152 | })(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/codec/flvparser/flv.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 9 | * 10 | * The FLV merge utility is a Javascript translation of 11 | * https://github.com/grepmusic/flvmerge 12 | * by grepmusic */ 13 | 14 | import TwentyFourDataView from '../../util/twenty-four-dataview.js'; 15 | import FLVTag from './flv-tag.js'; 16 | 17 | /** 18 | * A simple flv parser 19 | */ 20 | class FLV { 21 | header: TwentyFourDataView 22 | firstPreviousTagSize: TwentyFourDataView 23 | tags: FLVTag[] 24 | 25 | constructor(dataView: DataView) { 26 | if (dataView.getUint32(0) >> 8 !== 0x464C56) { 27 | throw new TypeError(`FLV construtor: FLV header signature should be 'FLV' but get ${dataView.getUint8(0)},${dataView.getUint8(1)},${dataView.getUint8(2)}`); 28 | } 29 | this.header = new TwentyFourDataView(dataView.buffer, dataView.byteOffset, 9); 30 | this.firstPreviousTagSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + 9, 4); 31 | 32 | this.tags = []; 33 | let offset = this.headerLength + 4; 34 | while (offset < dataView.byteLength) { 35 | const tag = new FLVTag(dataView, offset); 36 | // debug anchor for inspecting scrpit data tag 37 | // if (tag.tagType !== 0x08 && tag.tagType !== 0x09) { debugger } 38 | offset += 11 + tag.dataSize + 4; 39 | this.tags.push(tag); 40 | } 41 | 42 | if (offset !== dataView.byteLength) throw new Error(`FLV construtor: final offset should equal dataView.byteLength but get offset ${offset} !== byteLength ${dataView.byteLength}`); 43 | } 44 | 45 | get type() { 46 | return 'FLV'; 47 | } 48 | 49 | get version() { 50 | return this.header.getUint8(3); 51 | } 52 | 53 | get typeFlag() { 54 | return this.header.getUint8(4); 55 | } 56 | 57 | get headerLength() { 58 | return this.header.getUint32(5); 59 | } 60 | 61 | static merge(flvs: Iterable) { 62 | if (!flvs[Symbol.iterator]) throw new TypeError(`FLV.merge: parameter flvs expect Iterable but get ${flvs}`); 63 | let blobParts = []; 64 | let basetimestamp = [0, 0]; 65 | let lasttimestamp = [0, 0]; 66 | let duration = 0.0; 67 | let durationDataView: TwentyFourDataView; 68 | 69 | 70 | for (let flv of flvs) { 71 | let bts = duration * 1000; 72 | basetimestamp[0] = lasttimestamp[0]; 73 | basetimestamp[1] = lasttimestamp[1]; 74 | bts = Math.max(bts, basetimestamp[0], basetimestamp[1]); 75 | let foundDuration = 0; 76 | for (let tag of flv.tags) { 77 | if (tag.tagType === 0x12 && !foundDuration) { 78 | duration += tag.getDuration(); 79 | foundDuration = 1; 80 | if (blobParts.length === 0) { 81 | blobParts.push(flv.header, flv.firstPreviousTagSize); 82 | ({ duration, durationDataView } = tag.getDurationAndView()); 83 | tag.stripKeyframesScriptData(); 84 | blobParts.push(tag.tagHeader); 85 | blobParts.push(tag.tagData); 86 | blobParts.push(tag.previousSize); 87 | } 88 | } 89 | else if (tag.tagType === 0x08 || tag.tagType === 0x09) { 90 | lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp(); 91 | tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]); 92 | blobParts.push(tag.tagHeader); 93 | blobParts.push(tag.tagData); 94 | blobParts.push(tag.previousSize); 95 | } 96 | } 97 | } 98 | durationDataView!.setFloat64(0, duration); 99 | 100 | return new Blob(blobParts); 101 | } 102 | 103 | static async mergeBlobs(blobs: Iterable) { 104 | // Blobs can be swapped to disk, while Arraybuffers can not. 105 | // This is a RAM saving workaround. Somewhat. 106 | if (!blobs[Symbol.iterator]) throw new TypeError(`FLV.mergeBlobs: parameter blobs expect Iterable but get ${blobs}`); 107 | let ret = []; 108 | let basetimestamp = [0, 0]; 109 | let lasttimestamp = [0, 0]; 110 | let duration = 0.0; 111 | let durationDataView: TwentyFourDataView; 112 | 113 | for (let blob of blobs) { 114 | let bts = duration * 1000; 115 | basetimestamp[0] = lasttimestamp[0]; 116 | basetimestamp[1] = lasttimestamp[1]; 117 | bts = Math.max(bts, basetimestamp[0], basetimestamp[1]); 118 | let foundDuration = 0; 119 | 120 | let flv = await new Promise((resolve, reject) => { 121 | let fr = new FileReader(); 122 | fr.onload = () => resolve(new FLV(new TwentyFourDataView(fr.result))); 123 | fr.readAsArrayBuffer(blob); 124 | fr.onerror = reject; 125 | }); 126 | 127 | let modifiedMediaTags = []; 128 | for (let tag of flv.tags) { 129 | if (tag.tagType === 0x12 && !foundDuration) { 130 | duration += tag.getDuration(); 131 | foundDuration = 1; 132 | if (ret.length === 0) { 133 | ret.push(flv.header, flv.firstPreviousTagSize); 134 | ({ duration, durationDataView } = tag.getDurationAndView()); 135 | tag.stripKeyframesScriptData(); 136 | ret.push(tag.tagHeader); 137 | ret.push(tag.tagData); 138 | ret.push(tag.previousSize); 139 | } 140 | } 141 | else if (tag.tagType === 0x08 || tag.tagType === 0x09) { 142 | lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp(); 143 | tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]); 144 | modifiedMediaTags.push(tag.tagHeader, tag.tagData, tag.previousSize); 145 | } 146 | } 147 | ret.push(new Blob(modifiedMediaTags)); 148 | } 149 | durationDataView!.setFloat64(0, duration); 150 | 151 | return new Blob(ret); 152 | } 153 | } 154 | 155 | export default FLV; 156 | -------------------------------------------------------------------------------- /src/util/monitor-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import OnEventTargetFactory from './on-event-target.js'; 11 | import TransformStream, { TransformStreamDefaultController } from './lib-util-streams/transformstream-types.js'; 12 | 13 | export interface SimpleProgressEvent { 14 | type: string 15 | target: MonitorStream 16 | loaded: number 17 | total: number 18 | lengthComputable: boolean 19 | } 20 | 21 | export type EventMap = { 22 | loadstart: SimpleProgressEvent 23 | progress: SimpleProgressEvent 24 | // error: SimpleProgressEvent 25 | abort: SimpleProgressEvent 26 | load: SimpleProgressEvent 27 | } 28 | 29 | export type OnEventMap = { 30 | onloadstart: SimpleProgressEvent 31 | onprogress: SimpleProgressEvent 32 | // onerror: SimpleProgressEvent 33 | onabort: SimpleProgressEvent 34 | onload: SimpleProgressEvent 35 | } 36 | 37 | export interface MonitorStreamInit { 38 | onloadstart?: MonitorStream["onloadstart"] 39 | onprogress?: MonitorStream["onprogress"] 40 | onabort?: MonitorStream["onabort"] 41 | onload?: MonitorStream["onload"] 42 | throttle?: MonitorStream['throttle'] 43 | loaded?: MonitorStream['loaded'] 44 | total?: MonitorStream['total'] 45 | lengthComputable?: MonitorStream['lengthComputable'] 46 | progressInterval?: MonitorStream['progressInterval'] 47 | } 48 | 49 | class MonitorStream extends OnEventTargetFactory(['loadstart', 'progress', /* 'error', */ 'abort', 'load']).mixin(TransformStream) { 50 | throttle: number 51 | loaded: number 52 | total: number 53 | lengthComputable: boolean 54 | progressInterval: number 55 | controller: TransformStreamDefaultController 56 | 57 | constructor({ 58 | onloadstart = null, 59 | onprogress = null, 60 | onabort = null, 61 | onload = null, 62 | throttle = 0, 63 | loaded = 0, 64 | total = Infinity, 65 | lengthComputable = false, 66 | progressInterval = 1000, 67 | } = {} as MonitorStreamInit, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy) { 68 | let _controller = null as TransformStreamDefaultController | null; 69 | let progressLast = 0; 70 | let last = 0; 71 | super({ 72 | start: controller => { 73 | _controller = controller; 74 | }, 75 | 76 | transform: throttle ? 77 | async (chunk, controller) => { 78 | const now = Date.now(); 79 | if (now - progressLast > this.progressInterval) { 80 | this.dispatchEvent({ type: 'progress', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable }); 81 | progressLast = now; 82 | } 83 | // drift = (expected chunk duration) - (actual chunk duration) 84 | const drift = (1000 * chunk.length / this.throttle) - (now - last); 85 | last = now; 86 | if (drift > 0) await new Promise(resolve => setTimeout(resolve, 2 * drift)); 87 | this.loaded += chunk.length; 88 | controller.enqueue(chunk); 89 | } : 90 | (chunk, controller) => { 91 | const now = Date.now(); 92 | if (now - progressLast > this.progressInterval) { 93 | this.dispatchEvent({ type: 'progress', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable }); 94 | progressLast = now; 95 | } 96 | this.loaded += chunk.length; 97 | controller.enqueue(chunk); 98 | }, 99 | 100 | flush: () => { 101 | this.dispatchEvent({ type: 'load', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable }); 102 | }, 103 | }, writableStrategy, readableStrategy); 104 | this.addEventListener('progress', () => this.dispatchEvent({ type: 'loadstart', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable }), { once: true }); 105 | 106 | this.onloadstart = onloadstart; 107 | this.onprogress = onprogress; 108 | this.onabort = onabort; 109 | this.onload = onload; 110 | 111 | this.throttle = throttle; 112 | this.loaded = loaded; 113 | this.total = total; 114 | this.lengthComputable = lengthComputable; 115 | this.progressInterval = progressInterval; 116 | this.controller = _controller!; 117 | 118 | // const pipeTo = this.readable.pipeTo.bind(this.readable); 119 | // this.readable.pipeTo = (...args: any[]) => { 120 | // const ret = pipeTo(...args) as Promise; 121 | // ret.catch(error => this.dispatchEvent({ type: 'error', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable, error })); 122 | // return ret; 123 | // } 124 | } 125 | 126 | abort() { 127 | this.dispatchEvent({ type: 'abort', target: this, loaded: this.loaded, total: this.total, lengthComputable: this.lengthComputable }); 128 | return this.controller.error(new DOMException('This pipeline is aborted by a MonitorStream', 'AbortError')); 129 | } 130 | 131 | getProgressEvent(type: string) { 132 | return { 133 | type, 134 | target: this, 135 | loaded: this.loaded, 136 | total: this.total, 137 | lengthComputable: this.lengthComputable, 138 | } 139 | } 140 | 141 | static get isSupported() { 142 | return typeof TransformStream === 'function'; 143 | } 144 | } 145 | 146 | const _UNIT_TEST = (location = window.location) => { 147 | let reportLast = Date.now(); 148 | let loadedLast = 0; 149 | 150 | let ms = new MonitorStream({ 151 | throttle: 200 * 1024, 152 | onprogress: ({ loaded }) => { 153 | const now = Date.now(); 154 | if (now - reportLast > 1000) { 155 | console.log(`speed: ${((loaded - loadedLast) * 1.024 / (now - reportLast)).toPrecision(2)}KB/s`); 156 | loadedLast = loaded; 157 | reportLast = now; 158 | } 159 | }, 160 | }); 161 | fetch(location.href).then(({ body }) => (body as any).pipeThrough(ms).pipeTo(new WritableStream())); 162 | } 163 | 164 | export { MonitorStream } 165 | export default MonitorStream; 166 | -------------------------------------------------------------------------------- /src/codec/flvparser/flv-stream.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import TwentyFourDataView from '../../util/twenty-four-dataview.js'; 11 | import FLVTag from './flv-tag.js'; 12 | import { Transformer } from '../../util/lib-util-streams/transformstream-types.js'; 13 | 14 | export interface TransformStreamWithHeader { 15 | readable: ReadableStream & { header: Uint8Array | null } 16 | writable: WritableStream 17 | } 18 | 19 | declare const TransformStream: { 20 | prototype: TransformStreamWithHeader 21 | new(transformer?: Transformer, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy): TransformStreamWithHeader 22 | } 23 | 24 | /** 25 | * A streamified flv parser 26 | * IN: whole flv file piped to writeableStream 27 | * OUT: ...tag objects (script tag, audio tag and video tag) as readableStream 28 | */ 29 | class FLVStream extends TransformStream { 30 | buffer: Uint8Array[] 31 | bufferByteLength: number 32 | targetByteLength: number 33 | header: Uint8Array | null 34 | 35 | constructor({ headerByteLength = 9 + 4 } = {}, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy) { 36 | super({ 37 | transform: (chunk, controller) => { 38 | // 1. scan through chunk for tags 39 | let next = chunk; 40 | while (true) { 41 | // 2. find the size of all buffered data 42 | const byteLength = this.bufferByteLength + next.byteLength; 43 | 44 | // 2. size of next tag unknown => try to find it 45 | if (this.targetByteLength < 0) { 46 | // 2.1 probe the incoming chunk for tag size 47 | this.targetByteLength = this.probe(next); 48 | 49 | // 2.2 still unknown => tag header incomplete => buffer it and break 50 | if (this.targetByteLength < 0) { 51 | this.buffer.push(next); 52 | this.bufferByteLength = byteLength; 53 | break; 54 | } 55 | } 56 | 57 | // 3. buffered data < next tag => buffer it and break 58 | if (byteLength < this.targetByteLength) { 59 | this.buffer.push(next); 60 | this.bufferByteLength = byteLength; 61 | break; 62 | } 63 | 64 | // 4. buffered data === next tag => output it and break 65 | else if (byteLength === this.targetByteLength) { 66 | this.buffer.push(next); 67 | this.bufferByteLength = byteLength; 68 | if (this.header) { 69 | controller.enqueue(this.wrapTag(this.concatBuffer().buffer)); 70 | } 71 | else { 72 | this.header = this.concatBuffer(); 73 | this.readable.header = this.header; 74 | } 75 | this.buffer = []; 76 | this.bufferByteLength = 0; 77 | this.targetByteLength = -1; 78 | break; 79 | } 80 | 81 | // 5. buffered data > next tag => output next tag, continue on the rest of chunk 82 | else { 83 | const remainderByteLength = this.targetByteLength - this.bufferByteLength; 84 | this.buffer.push(new Uint8Array(next.buffer, next.byteOffset, remainderByteLength)); 85 | this.bufferByteLength = this.targetByteLength; 86 | if (this.header) { 87 | controller.enqueue(this.wrapTag(this.concatBuffer().buffer)); 88 | } 89 | else { 90 | this.header = this.concatBuffer(); 91 | this.readable.header = this.header; 92 | } 93 | this.buffer = []; 94 | this.bufferByteLength = 0; 95 | this.targetByteLength = -1; 96 | next = new Uint8Array(next.buffer, next.byteOffset + remainderByteLength); 97 | } 98 | } 99 | } 100 | }, writableStrategy, readableStrategy); 101 | this.buffer = []; 102 | this.bufferByteLength = 0; 103 | this.targetByteLength = headerByteLength; 104 | this.header = null; 105 | this.readable.header = null; 106 | } 107 | 108 | probe(next: Uint8Array) { 109 | // 1. flv tag header: 110 | // 0 | 1 2 3 111 | // tagType(uint8) | dataSize(uint24) 112 | // => we need a 4-byte buffer 113 | let probeBuffer = new Uint8Array(4); 114 | let probeBufferByteLength = 0; 115 | 116 | // 2. collect the first 4 bytes from buffer 117 | for (const chunk of this.buffer) { 118 | probeBuffer.set(chunk, probeBufferByteLength); 119 | probeBufferByteLength += chunk.byteLength; 120 | } 121 | 122 | // 3. collect the rest bytes from next chunk 123 | probeBuffer.set(next.slice(0, 4 - probeBufferByteLength), probeBufferByteLength); 124 | probeBufferByteLength += next.byteLength; 125 | 126 | // 4. dataSize received => compute tagSize 127 | if (probeBufferByteLength >= 4) { 128 | // tag = tagHeader(11) + tagData(tagHeader.getUint24(1), see comment 1) + previousSize(4) 129 | return 11 + (new DataView(probeBuffer.buffer).getUint32(0) & 0x00FFFFFF) + 4; 130 | } 131 | 132 | // 5. otherwise => still unknow 133 | else { 134 | return -1; 135 | } 136 | } 137 | 138 | concatBuffer() { 139 | // NOTE: please return a deep-clone for convience of garbage collection 140 | // ArrayBuffer ----+++++--- NOT collectable 141 | // DataView/TypedArray +++++ you cannot perform partial free 142 | if (this.buffer.length === 1) return this.buffer[0].slice(); 143 | if (typeof Buffer !== 'undefined') return Buffer.concat(this.buffer); 144 | const ret = new Uint8Array(this.bufferByteLength); 145 | let byteLength = 0; 146 | for (const chunk of this.buffer) { 147 | ret.set(chunk, byteLength); 148 | byteLength += chunk.byteLength; 149 | } 150 | return ret; 151 | } 152 | 153 | wrapTag(buffer: SharedArrayBuffer | ArrayBuffer) { 154 | // if (ArrayBuffer.isView(buffer)) buffer = buffer.buffer; 155 | return new FLVTag(new TwentyFourDataView(buffer)); 156 | } 157 | } 158 | 159 | export default FLVStream; 160 | -------------------------------------------------------------------------------- /src/util/hooked-function.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | export interface RawFunction { 11 | (...args: ArgType[]): RetType 12 | } 13 | 14 | export interface Context { 15 | args: ArgType[] 16 | raw: RawFunction 17 | ret?: RetType 18 | hookedFunction: HookedFunction 19 | } 20 | 21 | export interface Hook { 22 | (context: Context): void 23 | } 24 | 25 | export interface HookedFunctionInit { 26 | raw: RawFunction 27 | pre?: Hook | Iterable> 28 | post?: Hook | Iterable> 29 | } 30 | 31 | export interface HookedFunctionSpreadableInit { 32 | raw: RawFunction | HookedFunction 33 | pre?: Hook | Iterable> 34 | post?: Hook | Iterable> 35 | } 36 | 37 | const flagSymbol = Symbol('HookedFunctionFlag'); 38 | 39 | /** 40 | * A util to hook a function 41 | */ 42 | class HookedFunction extends Function { 43 | // @ts-ignore: constructor returns explictly 44 | raw: RawFunction 45 | // @ts-ignore: constructor returns explictly 46 | pre: Set> 47 | // @ts-ignore: constructor returns explictly 48 | post: Set> 49 | [flagSymbol]: true 50 | 51 | /** 52 | * Create a hooked function. 53 | * 54 | * @returns the wrapped function 55 | */ 56 | constructor(raw: HookedFunctionInit['raw'], pre?: HookedFunctionInit['pre'], post?: HookedFunctionInit['post']) 57 | constructor({ raw, pre, post }: HookedFunctionInit) 58 | constructor(raw: HookedFunctionInit | HookedFunctionInit['raw'], pre?: HookedFunctionInit['pre'], post?: HookedFunctionInit['post']) { // 1. init parameter 59 | // 1. parameters 60 | if (typeof raw === 'object') { 61 | pre = raw.pre; 62 | post = raw.post; 63 | raw = raw.raw; 64 | } 65 | if (typeof pre === 'function') pre = [pre]; 66 | if (typeof post === 'function') post = [post]; 67 | 68 | // 2. build bundle 69 | const self = function (this: any, ...args: ArgType[]) { 70 | const { raw, pre, post } = self; 71 | const context: Context = { args, raw, ret: undefined, hookedFunction: self }; 72 | for (const hook of pre) { 73 | new Promise(() => hook.call(this, context)); 74 | } 75 | if (context.raw) context.ret = context.raw.apply(this, context.args); 76 | for (const hook of post) { 77 | new Promise(() => hook.call(this, context)); 78 | } 79 | return context.ret; 80 | } as any as HookedFunction; 81 | self.raw = raw; 82 | self.pre = new Set(pre!); 83 | self.post = new Set(post!); 84 | self[flagSymbol] = true; 85 | self.constructor = HookedFunction; 86 | 87 | try { 88 | return self; 89 | } 90 | catch { 91 | super(); 92 | return self; 93 | } 94 | } 95 | 96 | static [Symbol.hasInstance]({ [flagSymbol]: is }: any) { 97 | return is; 98 | } 99 | 100 | /** 101 | * Wrap a function if it has not been, or apply more hooks otherwise 102 | * 103 | * @returns the wrapped function 104 | */ 105 | static hook(raw: HookedFunctionSpreadableInit['raw'], pre?: HookedFunctionSpreadableInit['pre'], post?: HookedFunctionSpreadableInit['post']): HookedFunction 106 | static hook({ raw, pre, post }: HookedFunctionSpreadableInit): HookedFunction 107 | static hook(raw: HookedFunctionSpreadableInit | HookedFunctionSpreadableInit['raw'], pre?: HookedFunctionSpreadableInit['pre'], post?: HookedFunctionSpreadableInit['post']) { 108 | // 1. parameters 109 | if (typeof raw === 'object') { 110 | pre = raw.pre; 111 | post = raw.post; 112 | raw = raw.raw; 113 | } 114 | if (typeof pre === 'function') pre = [pre]; 115 | if (typeof post === 'function') post = [post]; 116 | 117 | // 2 wrap 118 | // 2.1 already wrapped => concat 119 | if (raw instanceof HookedFunction) { 120 | if (pre) { 121 | for (const callback of pre) { 122 | (raw as HookedFunction).pre.add(callback); 123 | } 124 | } 125 | if (post) { 126 | for (const callback of post) { 127 | (raw as HookedFunction).post.add(callback); 128 | } 129 | } 130 | return raw; 131 | } 132 | 133 | // 2.2 otherwise => new 134 | else { 135 | return new HookedFunction(raw as RawFunction, pre, post); 136 | } 137 | } 138 | 139 | static dehook(raw: HookedFunctionSpreadableInit['raw']) { 140 | if (raw instanceof HookedFunction) { 141 | return (raw as HookedFunction).raw 142 | } 143 | else { 144 | return raw as RawFunction; 145 | } 146 | } 147 | 148 | /** 149 | * Add debugger statement hook to a function 150 | * 151 | * @param raw function to debug. 152 | * @param pre (default true) add pre-hook. default true 153 | * @param post (default false) add post-hook. default false 154 | */ 155 | static hookDebugger(raw: HookedFunctionSpreadableInit['raw'], pre = true, post = false) { 156 | const { debuggerFunction } = HookedFunction; 157 | if (raw instanceof HookedFunction) { 158 | (raw as HookedFunction).pre.add(debuggerFunction); 159 | (raw as HookedFunction).post.add(debuggerFunction); 160 | return raw as HookedFunction; 161 | } 162 | else { 163 | return new HookedFunction(raw as RawFunction, pre ? debuggerFunction : undefined, post ? debuggerFunction : undefined, ); 164 | } 165 | } 166 | 167 | static debuggerFunction(ctx: Context) { 168 | debugger; 169 | } 170 | 171 | static flagSymbol = flagSymbol 172 | } 173 | 174 | export default HookedFunction; 175 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/id.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.matroska.org/technical/specs/index.html 3 | */ 4 | export declare const ID: { 5 | EBML: Uint8Array; 6 | EBMLVersion: Uint8Array; 7 | EBMLReadVersion: Uint8Array; 8 | EBMLMaxIDLength: Uint8Array; 9 | EBMLMaxSizeLength: Uint8Array; 10 | DocType: Uint8Array; 11 | DocTypeVersion: Uint8Array; 12 | DocTypeReadVersion: Uint8Array; 13 | Void: Uint8Array; 14 | CRC32: Uint8Array; 15 | Segment: Uint8Array; 16 | SeekHead: Uint8Array; 17 | Seek: Uint8Array; 18 | SeekID: Uint8Array; 19 | SeekPosition: Uint8Array; 20 | Info: Uint8Array; 21 | SegmentUID: Uint8Array; 22 | SegmentFilename: Uint8Array; 23 | PrevUID: Uint8Array; 24 | PrevFilename: Uint8Array; 25 | NextUID: Uint8Array; 26 | NextFilename: Uint8Array; 27 | SegmentFamily: Uint8Array; 28 | ChapterTranslate: Uint8Array; 29 | ChapterTranslateEditionUID: Uint8Array; 30 | ChapterTranslateCodec: Uint8Array; 31 | ChapterTranslateID: Uint8Array; 32 | TimecodeScale: Uint8Array; 33 | Duration: Uint8Array; 34 | DateUTC: Uint8Array; 35 | Title: Uint8Array; 36 | MuxingApp: Uint8Array; 37 | WritingApp: Uint8Array; 38 | Cluster: Uint8Array; 39 | Timecode: Uint8Array; 40 | SilentTracks: Uint8Array; 41 | SilentTrackNumber: Uint8Array; 42 | Position: Uint8Array; 43 | PrevSize: Uint8Array; 44 | SimpleBlock: Uint8Array; 45 | BlockGroup: Uint8Array; 46 | Block: Uint8Array; 47 | BlockAdditions: Uint8Array; 48 | BlockMore: Uint8Array; 49 | BlockAddID: Uint8Array; 50 | BlockAdditional: Uint8Array; 51 | BlockDuration: Uint8Array; 52 | ReferencePriority: Uint8Array; 53 | ReferenceBlock: Uint8Array; 54 | CodecState: Uint8Array; 55 | DiscardPadding: Uint8Array; 56 | Slices: Uint8Array; 57 | TimeSlice: Uint8Array; 58 | LaceNumber: Uint8Array; 59 | Tracks: Uint8Array; 60 | TrackEntry: Uint8Array; 61 | TrackNumber: Uint8Array; 62 | TrackUID: Uint8Array; 63 | TrackType: Uint8Array; 64 | FlagEnabled: Uint8Array; 65 | FlagDefault: Uint8Array; 66 | FlagForced: Uint8Array; 67 | FlagLacing: Uint8Array; 68 | MinCache: Uint8Array; 69 | MaxCache: Uint8Array; 70 | DefaultDuration: Uint8Array; 71 | DefaultDecodedFieldDuration: Uint8Array; 72 | MaxBlockAdditionID: Uint8Array; 73 | Name: Uint8Array; 74 | Language: Uint8Array; 75 | CodecID: Uint8Array; 76 | CodecPrivate: Uint8Array; 77 | CodecName: Uint8Array; 78 | AttachmentLink: Uint8Array; 79 | CodecDecodeAll: Uint8Array; 80 | TrackOverlay: Uint8Array; 81 | CodecDelay: Uint8Array; 82 | SeekPreRoll: Uint8Array; 83 | TrackTranslate: Uint8Array; 84 | TrackTranslateEditionUID: Uint8Array; 85 | TrackTranslateCodec: Uint8Array; 86 | TrackTranslateTrackID: Uint8Array; 87 | Video: Uint8Array; 88 | FlagInterlaced: Uint8Array; 89 | FieldOrder: Uint8Array; 90 | StereoMode: Uint8Array; 91 | AlphaMode: Uint8Array; 92 | PixelWidth: Uint8Array; 93 | PixelHeight: Uint8Array; 94 | PixelCropBottom: Uint8Array; 95 | PixelCropTop: Uint8Array; 96 | PixelCropLeft: Uint8Array; 97 | PixelCropRight: Uint8Array; 98 | DisplayWidth: Uint8Array; 99 | DisplayHeight: Uint8Array; 100 | DisplayUnit: Uint8Array; 101 | AspectRatioType: Uint8Array; 102 | ColourSpace: Uint8Array; 103 | Colour: Uint8Array; 104 | MatrixCoefficients: Uint8Array; 105 | BitsPerChannel: Uint8Array; 106 | ChromaSubsamplingHorz: Uint8Array; 107 | ChromaSubsamplingVert: Uint8Array; 108 | CbSubsamplingHorz: Uint8Array; 109 | CbSubsamplingVert: Uint8Array; 110 | ChromaSitingHorz: Uint8Array; 111 | ChromaSitingVert: Uint8Array; 112 | Range: Uint8Array; 113 | TransferCharacteristics: Uint8Array; 114 | Primaries: Uint8Array; 115 | MaxCLL: Uint8Array; 116 | MaxFALL: Uint8Array; 117 | MasteringMetadata: Uint8Array; 118 | PrimaryRChromaticityX: Uint8Array; 119 | PrimaryRChromaticityY: Uint8Array; 120 | PrimaryGChromaticityX: Uint8Array; 121 | PrimaryGChromaticityY: Uint8Array; 122 | PrimaryBChromaticityX: Uint8Array; 123 | PrimaryBChromaticityY: Uint8Array; 124 | WhitePointChromaticityX: Uint8Array; 125 | WhitePointChromaticityY: Uint8Array; 126 | LuminanceMax: Uint8Array; 127 | LuminanceMin: Uint8Array; 128 | Audio: Uint8Array; 129 | SamplingFrequency: Uint8Array; 130 | OutputSamplingFrequency: Uint8Array; 131 | Channels: Uint8Array; 132 | BitDepth: Uint8Array; 133 | TrackOperation: Uint8Array; 134 | TrackCombinePlanes: Uint8Array; 135 | TrackPlane: Uint8Array; 136 | TrackPlaneUID: Uint8Array; 137 | TrackPlaneType: Uint8Array; 138 | TrackJoinBlocks: Uint8Array; 139 | TrackJoinUID: Uint8Array; 140 | ContentEncodings: Uint8Array; 141 | ContentEncoding: Uint8Array; 142 | ContentEncodingOrder: Uint8Array; 143 | ContentEncodingScope: Uint8Array; 144 | ContentEncodingType: Uint8Array; 145 | ContentCompression: Uint8Array; 146 | ContentCompAlgo: Uint8Array; 147 | ContentCompSettings: Uint8Array; 148 | ContentEncryption: Uint8Array; 149 | ContentEncAlgo: Uint8Array; 150 | ContentEncKeyID: Uint8Array; 151 | ContentSignature: Uint8Array; 152 | ContentSigKeyID: Uint8Array; 153 | ContentSigAlgo: Uint8Array; 154 | ContentSigHashAlgo: Uint8Array; 155 | Cues: Uint8Array; 156 | CuePoint: Uint8Array; 157 | CueTime: Uint8Array; 158 | CueTrackPositions: Uint8Array; 159 | CueTrack: Uint8Array; 160 | CueClusterPosition: Uint8Array; 161 | CueRelativePosition: Uint8Array; 162 | CueDuration: Uint8Array; 163 | CueBlockNumber: Uint8Array; 164 | CueCodecState: Uint8Array; 165 | CueReference: Uint8Array; 166 | CueRefTime: Uint8Array; 167 | Attachments: Uint8Array; 168 | AttachedFile: Uint8Array; 169 | FileDescription: Uint8Array; 170 | FileName: Uint8Array; 171 | FileMimeType: Uint8Array; 172 | FileData: Uint8Array; 173 | FileUID: Uint8Array; 174 | Chapters: Uint8Array; 175 | EditionEntry: Uint8Array; 176 | EditionUID: Uint8Array; 177 | EditionFlagHidden: Uint8Array; 178 | EditionFlagDefault: Uint8Array; 179 | EditionFlagOrdered: Uint8Array; 180 | ChapterAtom: Uint8Array; 181 | ChapterUID: Uint8Array; 182 | ChapterStringUID: Uint8Array; 183 | ChapterTimeStart: Uint8Array; 184 | ChapterTimeEnd: Uint8Array; 185 | ChapterFlagHidden: Uint8Array; 186 | ChapterFlagEnabled: Uint8Array; 187 | ChapterSegmentUID: Uint8Array; 188 | ChapterSegmentEditionUID: Uint8Array; 189 | ChapterPhysicalEquiv: Uint8Array; 190 | ChapterTrack: Uint8Array; 191 | ChapterTrackNumber: Uint8Array; 192 | ChapterDisplay: Uint8Array; 193 | ChapString: Uint8Array; 194 | ChapLanguage: Uint8Array; 195 | ChapCountry: Uint8Array; 196 | ChapProcess: Uint8Array; 197 | ChapProcessCodecID: Uint8Array; 198 | ChapProcessPrivate: Uint8Array; 199 | ChapProcessCommand: Uint8Array; 200 | ChapProcessTime: Uint8Array; 201 | ChapProcessData: Uint8Array; 202 | Tags: Uint8Array; 203 | Tag: Uint8Array; 204 | Targets: Uint8Array; 205 | TargetTypeValue: Uint8Array; 206 | TargetType: Uint8Array; 207 | TagTrackUID: Uint8Array; 208 | TagEditionUID: Uint8Array; 209 | TagChapterUID: Uint8Array; 210 | TagAttachmentUID: Uint8Array; 211 | SimpleTag: Uint8Array; 212 | TagName: Uint8Array; 213 | TagLanguage: Uint8Array; 214 | TagDefault: Uint8Array; 215 | TagString: Uint8Array; 216 | TagBinary: Uint8Array; 217 | }; 218 | -------------------------------------------------------------------------------- /src/util/on-event-target.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { SimpleEvent, SimpleEventListener, SimpleEventMap, CommonEventTargetInterface } from './simple-event-target.js'; 11 | import { SimpleEventTarget, mixinCommonEventTarget, } from './simple-event-target.js'; 12 | import { Constructor } from './common-types.js'; 13 | 14 | export interface OnSimpleEventMap { 15 | [onevent: string]: SimpleEvent 16 | } 17 | 18 | export type OnSimpleEventListenerMap = { 19 | [onevent in keyof OnEventMap]: SimpleEventListener | null 20 | } 21 | 22 | export type OnEventTargetInterface 23 | = SimpleEventTarget & OnSimpleEventListenerMap 24 | 25 | export interface OnEventTargetConstructor extends Constructor> { 26 | readonly eventList: ReadonlyArray 27 | asyncOnce(target: CommonEventTargetInterface, name: string, errorName?: string): Promise 28 | asyncOnce(target: CommonEventTargetInterface, name: string, errorName: string, bind: T): Promise 29 | mixin(this: OnEventTargetConstructor, target: T): OnEventTargetMixinConstructor & T 30 | } 31 | 32 | export type OnEventTargetMixinInterface 33 | = CommonEventTargetInterface & OnSimpleEventListenerMap 34 | 35 | export interface OnEventTargetMixinConstructor extends Constructor> { 36 | asyncOnce(target: CommonEventTargetInterface, name: string, errorName?: string): Promise 37 | asyncOnce(target: CommonEventTargetInterface, name: string, errorName: string, bind: T): Promise 38 | } 39 | 40 | /** 41 | * Promisify a one-time event listener 42 | * 43 | * @param target event target 44 | * @param name name of event 45 | * @param errorName (default 'error') name of error event 46 | * @param bind (default Event) customized resolve value of Promise 47 | * @returns a Promise that resolves when the specific event fires 48 | */ 49 | // typescript: #(duplicate method declaration) = 2 * 2 50 | function asyncOnce(target: CommonEventTargetInterface, name: string, errorName?: string): Promise 51 | function asyncOnce(target: CommonEventTargetInterface, name: string, errorName: string, bind: T): Promise 52 | function asyncOnce(target: CommonEventTargetInterface, name: string, errorName: string = 'error', bind?: T) { 53 | return new Promise((resolve, reject) => { 54 | const once = { once: true }; 55 | const _resolve = (e: SimpleEvent) => { 56 | resolve(bind === undefined ? e : bind); 57 | if (errorName) target.removeEventListener(errorName, _reject); 58 | }; 59 | const _reject = (e: SimpleEvent) => { 60 | reject(e); 61 | target.removeEventListener(name, _resolve); 62 | }; 63 | target.addEventListener(name, resolve, once); 64 | if (errorName) target.addEventListener(errorName, reject, once); 65 | }); 66 | }; 67 | 68 | /** 69 | * mix OnEventTarget features into a target class 70 | * @param target the target class to mix 71 | * @returns the mixed class 72 | */ 73 | // typescript: #(duplicate method declaration) = 1 (renamed to `mixin`) 74 | function mixinOnEventTarget(this: OnEventTargetConstructor, target: T) { 75 | // 1. target does not implement CommonEventTargetInterface => mock interface 76 | if (!(target instanceof EventTarget) 77 | && (typeof (target.prototype as CommonEventTargetInterface).addEventListener !== 'function' 78 | || typeof (target.prototype as CommonEventTargetInterface).removeEventListener !== 'function' 79 | || typeof (target.prototype as CommonEventTargetInterface).dispatchEvent !== 'function')) { 80 | target = mixinCommonEventTarget(target); 81 | } 82 | 83 | // 2. extends target 84 | /** 85 | * A wrapper of the EventTarget interface in browsers. 86 | * 87 | * - create standalone EventTarget with `on[name]` handlers 88 | * - promisify one-time listeners with error propagation 89 | * - mix OnEventTarget functions into another object 90 | */ 91 | class OnEventTarget extends target { 92 | static asyncOnce = asyncOnce 93 | } 94 | 95 | // 3. add `on[name]` handlers 96 | const prototype = Object.getOwnPropertyDescriptors(this.prototype); 97 | (prototype.constructor as any) = {}; 98 | Object.defineProperties(OnEventTarget.prototype, prototype); 99 | 100 | return OnEventTarget as any as OnEventTargetMixinConstructor & T; 101 | } 102 | 103 | /** 104 | * Create an OnEventTarget class with specific `on[name]` handlers 105 | * 106 | * @param init (default undefined) a list of names that 107 | * you want to have `on[name]` handlers, or a falsy value 108 | */ 109 | function OnEventTargetFactory(init?: Iterable) { 110 | // 1. extends EventTarget 111 | /** 112 | * A wrapper of the EventTarget interface in browsers. 113 | * 114 | * - create standalone EventTarget with `on[name]` handlers 115 | * - promisify one-time listeners with error propagation 116 | * - mix OnEventTarget functions into another object 117 | */ 118 | class OnEventTarget extends SimpleEventTarget { 119 | static readonly eventList: ReadonlyArray 120 | static asyncOnce = asyncOnce 121 | static mixin = mixinOnEventTarget 122 | } 123 | 124 | // 2. add `on[name]` handlers 125 | if (!init) { 126 | Object.defineProperty(OnEventTarget, 'eventList', { value: Object.freeze([]) }); 127 | } 128 | else if (init[Symbol.iterator]) { 129 | Object.defineProperty(OnEventTarget, 'eventList', { value: Object.freeze([...init]) }); 130 | for (const name of init) { 131 | const i = Symbol(`on${name}`); 132 | (OnEventTarget.prototype as any)[i] = null; 133 | Object.defineProperty(OnEventTarget.prototype, `on${name}`, { 134 | get() { 135 | return (this as any)[i]; 136 | }, 137 | set(e) { 138 | if (typeof e !== 'function') e = null; 139 | this.removeEventListener(name, (this as any)[i]); 140 | this.addEventListener(name, e); 141 | (this as any)[i] = e; 142 | }, 143 | }); 144 | } 145 | } 146 | else { 147 | throw new TypeError(`OnEventTargetFactory: parameter 0 expect falsy value or Iterable but get ${init}`); 148 | } 149 | 150 | // 3. return new class 151 | return OnEventTarget as any as OnEventTargetConstructor; 152 | } 153 | 154 | /** 155 | * A sample usage of OnEventTarget.mixin 156 | * Creates an array that fires 'push' event when push is called 157 | */ 158 | class PushEventArray extends OnEventTargetFactory<{ push: SimpleEvent }, { onpush: SimpleEvent }>(['push']).mixin(Array) { 159 | /** 160 | * will fire CustomEvent when called 161 | * @param args will be passed to super.push 162 | */ 163 | push(...args: T[]) { 164 | this.dispatchEvent(new CustomEvent('push', { detail: args })); 165 | return super.push(...args); 166 | } 167 | } 168 | 169 | export { asyncOnce, mixinOnEventTarget, OnEventTargetFactory, PushEventArray }; 170 | export default OnEventTargetFactory; 171 | -------------------------------------------------------------------------------- /src/util/simple-event-target.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { Constructor } from './common-types.js'; 11 | 12 | const listenersDictSymbol = Symbol('listenersDict'); 13 | 14 | export interface SimpleBareEvent { 15 | type: string 16 | } 17 | 18 | export interface SimpleEvent extends SimpleBareEvent { 19 | [payload: string]: any 20 | } 21 | 22 | export interface SimpleCustomEvent extends SimpleEvent { 23 | type: string 24 | detail: T 25 | } 26 | 27 | export interface SimpleEventListener { 28 | (event: EventType): void 29 | } 30 | 31 | export interface SimpleEventTargetListenersList extends Set> { 32 | onceListeners: Set> 33 | } 34 | 35 | export interface SimpleEventMap { 36 | [type: string]: SimpleEvent 37 | } 38 | 39 | export interface CommonEventTargetInterface { 40 | addEventListener(type: Type, listener: SimpleEventListener | null, options?: { once?: boolean }): void 41 | removeEventListener(type: Type, listener: SimpleEventListener | null): void 42 | dispatchEvent(event: SimpleEvent): boolean 43 | } 44 | 45 | export interface IndexedEventTargetInterface { 46 | [listenersDictSymbol]: { 47 | [type: string]: SimpleEventTargetListenersList 48 | } 49 | } 50 | 51 | export interface mixinWithEventMap { 52 | (target: T): Constructor> & T 53 | } 54 | 55 | export type SimpleEventTargetConstructor = typeof SimpleEventTarget 56 | 57 | const mixinCommonEventTarget = function mixinCommonEventTarget(target: T) { 58 | const prototype = SimpleEventTarget.prototype; 59 | { 60 | class SimpleEventTarget extends target { 61 | private [listenersDictSymbol]: { 62 | [type: string]: SimpleEventTargetListenersList 63 | } = {} 64 | } 65 | (SimpleEventTarget.prototype as CommonEventTargetInterface).addEventListener = prototype.addEventListener; 66 | (SimpleEventTarget.prototype as CommonEventTargetInterface).removeEventListener = prototype.removeEventListener; 67 | (SimpleEventTarget.prototype as CommonEventTargetInterface).dispatchEvent = prototype.dispatchEvent; 68 | 69 | return SimpleEventTarget as Constructor & T; 70 | } 71 | } as mixinWithEventMap; 72 | 73 | class SimpleEventTarget implements IndexedEventTargetInterface, CommonEventTargetInterface { 74 | [listenersDictSymbol]: { [type: string]: SimpleEventTargetListenersList } = {} 75 | 76 | addEventListener(type: Type, listener: SimpleEventListener | null, options?: { once?: boolean }) { 77 | if (!listener) return; 78 | 79 | // 1. new type of event => create new listeners list 80 | if (!this[listenersDictSymbol][type]) { 81 | this[listenersDictSymbol][type] = new Set() as SimpleEventTargetListenersList; 82 | this[listenersDictSymbol][type].onceListeners = new Set(); 83 | } 84 | 85 | // 2. retreive listeners list of that type 86 | const listenersList = this[listenersDictSymbol][type]; 87 | 88 | // 3. add to store 89 | listenersList.add(listener); 90 | 91 | // 4. once => add to once list store 92 | if (typeof options === 'object' && options.once) { 93 | listenersList.onceListeners.add(listener); 94 | } 95 | } 96 | 97 | removeEventListener(type: Type, listener: SimpleEventListener | null) { 98 | if (!listener) return; 99 | 100 | const listenersList = this[listenersDictSymbol][type]; 101 | 102 | if (listenersList) { 103 | listenersList.delete(listener); 104 | listenersList.onceListeners.delete(listener); 105 | } 106 | } 107 | 108 | dispatchEvent(event: SimpleEvent) { 109 | // 1. retreive listeners set of that type 110 | const { type } = event; 111 | 112 | // 2. retreive listeners list of that type 113 | const listenersList = this[listenersDictSymbol][type]; 114 | 115 | // 3. listeners exist => trigger all listeners 116 | if (listenersList) { 117 | // 3.1 iter through all listeners 118 | for (const listener of listenersList) { 119 | // create a separate error stack 120 | // use Promise instead of try-catch to preserve pause on exception functionality 121 | new Promise(() => listener.call(this, event)); 122 | } 123 | 124 | // 3.2 once listeners exist => remove once listeners 125 | if (listenersList.onceListeners.size) { 126 | // 3.2.1 remove once listeners 127 | for (const listener of listenersList.onceListeners) { 128 | listenersList.delete(listener); 129 | } 130 | 131 | // 3.2.2 empty once listener list 132 | listenersList.onceListeners = new Set(); 133 | } 134 | } 135 | 136 | return true; 137 | } 138 | 139 | getEventListeners(type: string) { 140 | return this[listenersDictSymbol][type]; 141 | } 142 | 143 | copyEventListenersFrom(target: IndexedEventTargetInterface) { 144 | if (typeof target[listenersDictSymbol] !== 'object') { 145 | throw new TypeError('copyEventListenersFrom: target is not compatible'); 146 | } 147 | 148 | for (const type in target[listenersDictSymbol]) { 149 | // 1. new type of event => create new listeners list 150 | if (!this[listenersDictSymbol][type]) { 151 | this[listenersDictSymbol][type] = new Set() as SimpleEventTargetListenersList; 152 | this[listenersDictSymbol][type].onceListeners = new Set(); 153 | } 154 | 155 | // 2. retreive listeners list of that type 156 | const targetListenerList = target[listenersDictSymbol][type]; 157 | const listenersList = this[listenersDictSymbol][type]; 158 | 159 | // 3. add to store 160 | for (const listener of targetListenerList) { 161 | listenersList.add(listener); 162 | } 163 | 164 | // 4. once => add to once list store 165 | for (const listener of targetListenerList.onceListeners) { 166 | listenersList.onceListeners.add(listener); 167 | } 168 | } 169 | } 170 | 171 | pasteEventListenersTo(target: { addEventListener: CommonEventTargetInterface['addEventListener'] }) { 172 | for (const type in this[listenersDictSymbol]) { 173 | // 1. retreive listeners list of that type 174 | const listenersList = this[listenersDictSymbol][type]; 175 | 176 | // 2. once => add once listener 177 | for (const listener of listenersList.onceListeners) { 178 | target.addEventListener(type, listener, { once: true }); 179 | } 180 | 181 | // 3. otherwise => add listener 182 | for (const listener of listenersList) { 183 | if (!listenersList.onceListeners.has(listener)) { 184 | target.addEventListener(type, listener); 185 | } 186 | } 187 | } 188 | } 189 | 190 | removeDuplicatteEventListenersFrom(target: { removeEventListener: CommonEventTargetInterface['removeEventListener'] }) { 191 | for (const type in this[listenersDictSymbol]) { 192 | // 1. retreive listeners list of that type 193 | const listenersList = this[listenersDictSymbol][type]; 194 | 195 | // 2. remove 196 | for (const listener of listenersList) { 197 | target.removeEventListener(type, listener); 198 | } 199 | } 200 | } 201 | 202 | static mixin = mixinCommonEventTarget; 203 | } 204 | 205 | export { SimpleEventTarget, mixinCommonEventTarget, listenersDictSymbol }; 206 | export default SimpleEventTarget; 207 | -------------------------------------------------------------------------------- /src/util/event-duplex.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import { SimpleEventMap, CommonEventTargetInterface } from './simple-event-target.js'; 11 | import { mixinCommonEventTarget } from './simple-event-target.js'; 12 | import SimpleEventTarget from './simple-event-target.js'; 13 | import { OnSimpleEventMap, mixinOnEventTarget, OnEventTargetMixinConstructor, OnEventTargetConstructor, OnEventTargetInterface, OnEventTargetMixinInterface } from './on-event-target.js'; 14 | import OnEventTargetFactory from './on-event-target.js'; 15 | import EventSocket from './event-socket.js'; 16 | import { Constructor, PickStatic, Omit } from './common-types.js'; 17 | 18 | const inputSocketSymbol = Symbol('inputSocket'); 19 | 20 | export interface EventDuplexInterface extends SimpleEventTarget { 21 | [inputSocketSymbol]: EventSocket 22 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T): T 23 | } 24 | 25 | export type EventDuplexConstructor = typeof EventDuplex 26 | 27 | export interface EventDuplexMixinInterface extends CommonEventTargetInterface { 28 | [inputSocketSymbol]: EventSocket 29 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T): T 30 | } 31 | 32 | export interface EventDuplexMixinConstructor extends Constructor> { 33 | pipeEventsThrough(upstream: CommonEventTargetInterface, downstream: T): T 34 | } 35 | 36 | export interface mixinWithEventMap { 37 | (target: T): EventDuplexMixinConstructor & T 38 | } 39 | 40 | export type OnEventDuplexInterface< 41 | InputEventMap extends SimpleEventMap = SimpleEventMap, 42 | OutputEventMap extends SimpleEventMap = SimpleEventMap, 43 | OutOnEventMap extends OnSimpleEventMap = {}> 44 | = OnEventTargetInterface & { 45 | [inputSocketSymbol]: EventSocket 46 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T): T 47 | } 48 | 49 | export interface OnEventDuplexConstructor< 50 | InputEventMap extends SimpleEventMap = SimpleEventMap, 51 | OutputEventMap extends SimpleEventMap = SimpleEventMap, 52 | OutOnEventMap extends OnSimpleEventMap = {}> 53 | extends Constructor>, 54 | Omit, 'mixin' | 'prototype'> { 55 | pipeEventsThrough(upstream: CommonEventTargetInterface, downstream: T): T 56 | mixin(target: T): OnEventDuplexMixinConstructor & T 57 | } 58 | 59 | export type OnEventDuplexMixinInterface< 60 | InputEventMap extends SimpleEventMap = SimpleEventMap, 61 | OutputEventMap extends SimpleEventMap = SimpleEventMap, 62 | OutOnEventMap extends OnSimpleEventMap = {}> 63 | = OnEventTargetMixinInterface & { 64 | [inputSocketSymbol]: EventSocket 65 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T): T 66 | } 67 | 68 | export interface OnEventDuplexMixinConstructor< 69 | InputEventMap extends SimpleEventMap = SimpleEventMap, 70 | OutputEventMap extends SimpleEventMap = SimpleEventMap, 71 | OutOnEventMap extends OnSimpleEventMap = {}> 72 | extends Constructor>, 73 | PickStatic> { 74 | pipeEventsThrough(upstream: CommonEventTargetInterface, downstream: T): T 75 | } 76 | 77 | // typescript: #(duplicate method declaration) = 3 78 | function pipeEventsThrough(upstream: CommonEventTargetInterface, downstream: T) { 79 | downstream[inputSocketSymbol].connect(upstream); 80 | return downstream; 81 | } 82 | 83 | const mixinEventDuplex = function mixinEventDuplex(target: T) { 84 | // 1. target does not implement CommonEventTargetInterface => mock interface 85 | if (!(target instanceof EventTarget) 86 | && (typeof (target.prototype as CommonEventTargetInterface).addEventListener !== 'function' 87 | || typeof (target.prototype as CommonEventTargetInterface).removeEventListener !== 'function' 88 | || typeof (target.prototype as CommonEventTargetInterface).dispatchEvent !== 'function')) { 89 | target = mixinCommonEventTarget(target); 90 | } 91 | 92 | // 2. extends target 93 | const prototype = EventDuplex.prototype; 94 | { 95 | class EventDuplex extends target { 96 | private [inputSocketSymbol] = new EventSocket() 97 | static pipeEventsThrough = pipeEventsThrough 98 | } 99 | (EventDuplex.prototype as EventDuplexInterface).pipeEventsThrough = prototype.pipeEventsThrough; 100 | 101 | return EventDuplex as Constructor & T 102 | } 103 | } as mixinWithEventMap; 104 | 105 | class EventDuplex< 106 | InputEventMap extends SimpleEventMap = SimpleEventMap, 107 | OutputEventMap extends SimpleEventMap = SimpleEventMap 108 | > extends SimpleEventTarget implements EventDuplexInterface { 109 | [inputSocketSymbol] = new EventSocket() 110 | 111 | // typescript: #(duplicate method declaration) = 4 112 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T) { 113 | downstream[inputSocketSymbol].connect(this); 114 | return downstream; 115 | } 116 | 117 | static mixin = mixinEventDuplex 118 | static pipeEventsThrough = pipeEventsThrough 119 | } 120 | 121 | const mixinOnEventDuplex = function mixinOnEventDuplex(this: OnEventTargetConstructor, target: T) { 122 | return mixinEventDuplex(mixinOnEventTarget.call(Object.getPrototypeOf(this), target)); 123 | } 124 | 125 | function OnEventDuplexFactory< 126 | InputEventMap extends SimpleEventMap = SimpleEventMap, 127 | OutputEventMap extends SimpleEventMap = SimpleEventMap, 128 | OutOnEventMap extends OnSimpleEventMap = {} 129 | >(init?: Iterable) { 130 | class OnEventDuplex extends OnEventTargetFactory(init) { 131 | [inputSocketSymbol] = new EventSocket() 132 | 133 | pipeEventsThrough(this: CommonEventTargetInterface, downstream: T) { 134 | downstream[inputSocketSymbol].connect(this); 135 | return downstream; 136 | } 137 | 138 | static mixin = mixinOnEventDuplex 139 | 140 | static pipeEventsThrough = pipeEventsThrough 141 | } 142 | OnEventDuplex.prototype.pipeEventsThrough = EventDuplex.prototype.pipeEventsThrough; 143 | return OnEventDuplex as OnEventDuplexConstructor; 144 | } 145 | 146 | export { inputSocketSymbol, pipeEventsThrough, mixinEventDuplex, EventDuplex, OnEventDuplexFactory }; 147 | export default EventDuplex; 148 | -------------------------------------------------------------------------------- /src/util/lib-cache-db/idb-cache-db.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Qli5. All Rights Reserved. 3 | * 4 | * @author qli5 5 | * 6 | * This Source Code Form is subject to the terms of the Mozilla Public 7 | * License, v. 2.0. If a copy of the MPL was not distributed with this 8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 9 | 10 | import CommonCacheDB, { navigator, FileLike } from './common-cache-db.js'; 11 | import { ForceOverride } from '../common-types.js'; 12 | 13 | export interface ItemChunk { 14 | name: string 15 | numChunks: number 16 | item: Blob 17 | } 18 | 19 | export type CacheDBItemIDBRequest = ForceOverride, ev: Event & { target: typeof this }) => void) | null 21 | result: T 22 | }> 23 | 24 | /** 25 | * A promisified indexedDB with large file(>100MB) support 26 | */ 27 | class IDBCacheDB implements CommonCacheDB { 28 | dbName: string 29 | storeName: string 30 | maxItemSize: number 31 | db: Promise | IDBDatabase | null 32 | 33 | constructor(dbName: string, storeName = 'flv', { maxItemSize = 100 * 1024 * 1024 } = {}) { 34 | // Neither Chrome or Firefox can handle item size > 100M 35 | this.dbName = dbName; 36 | this.storeName = storeName; 37 | this.maxItemSize = maxItemSize; 38 | this.db = null; 39 | } 40 | 41 | async getDB() { 42 | if (this.db) return this.db; 43 | this.db = new Promise((resolve, reject) => { 44 | const req = indexedDB.open(this.dbName); 45 | req.onupgradeneeded = () => { 46 | const db = req.result; 47 | if (!db.objectStoreNames.contains(this.storeName)) { 48 | db.createObjectStore(this.storeName, { keyPath: 'name' }); 49 | } 50 | } 51 | req.onsuccess = () => { 52 | return resolve(this.db = req.result); 53 | } 54 | req.onerror = reject; 55 | }); 56 | return this.db; 57 | } 58 | 59 | async createData(item: Blob, name: string): Promise 60 | async createData(item: FileLike, name = item.name) { 61 | const itemChunks = [] as ItemChunk[]; 62 | const numChunks = Math.ceil(item.size / this.maxItemSize); 63 | for (let i = 0; i < numChunks; i++) { 64 | itemChunks.push({ 65 | name: `${name}.part_${i}`, 66 | numChunks, 67 | item: item.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize) 68 | }); 69 | } 70 | 71 | const db = await this.getDB(); 72 | const reqCascade = new Promise((resolve, reject) => { 73 | const objectStore = db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); 74 | const onsuccess = (e: Event = new Event('zerowrite')) => { 75 | const chunk = itemChunks.pop(); 76 | if (!chunk) return resolve(e); 77 | const req = objectStore.add(chunk); 78 | req.onerror = reject; 79 | req.onsuccess = onsuccess; 80 | }; 81 | onsuccess(); 82 | }); 83 | 84 | return reqCascade; 85 | } 86 | 87 | async setData(item: Blob, name: string): Promise 88 | async setData(item: FileLike, name = item.name) { 89 | const itemChunks = [] as ItemChunk[]; 90 | const numChunks = Math.ceil(item.size / this.maxItemSize); 91 | for (let i = 0; i < numChunks; i++) { 92 | itemChunks.push({ 93 | name: `${name}.part_${i}`, 94 | numChunks, 95 | item: item.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize) 96 | }); 97 | } 98 | 99 | const db = await this.getDB(); 100 | const reqCascade = new Promise((resolve, reject) => { 101 | const objectStore = db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); 102 | const onsuccess = (e: Event = new Event('zerowrite')) => { 103 | const chunk = itemChunks.pop(); 104 | if (!chunk) return resolve(e); 105 | const req = objectStore.put(chunk); 106 | req.onerror = reject; 107 | req.onsuccess = onsuccess; 108 | }; 109 | onsuccess(); 110 | }); 111 | 112 | return reqCascade; 113 | } 114 | 115 | async getData(name: string) { 116 | const db = await this.getDB(); 117 | const reqCascade = new Promise((resolve, reject) => { 118 | const dataChunks = [] as Blob[]; 119 | const objectStore = db.transaction(this.storeName, 'readonly').objectStore(this.storeName); 120 | const probe = objectStore.get(`${name}.part_0`) as CacheDBItemIDBRequest; 121 | probe.onerror = reject; 122 | probe.onsuccess = e => { 123 | // 1. Probe fails => key does not exist 124 | if (!probe.result) return resolve(null); 125 | 126 | // 2. How many chunks to retrieve? 127 | const { numChunks } = probe.result; 128 | 129 | // 3. Cascade on the remaining chunks 130 | const onsuccess = ({ target: { result: { item } } }: { target: CacheDBItemIDBRequest }) => { 131 | dataChunks.push(item); 132 | if (dataChunks.length == numChunks) return resolve(dataChunks); 133 | const req = objectStore.get(`${name}.part_${dataChunks.length}`) as CacheDBItemIDBRequest; 134 | req.onerror = reject; 135 | req.onsuccess = onsuccess; 136 | }; 137 | onsuccess(e as { target: CacheDBItemIDBRequest }); 138 | } 139 | }); 140 | 141 | const dataChunks = await reqCascade; 142 | 143 | return dataChunks ? new File(dataChunks, name) : null; 144 | } 145 | 146 | async hasData(name: string) { 147 | const db = await this.getDB(); 148 | return new Promise((resolve, reject) => { 149 | const objectStore = db.transaction(this.storeName, 'readonly').objectStore(this.storeName); 150 | const probe = objectStore.count(`${name}.part_0`) as CacheDBItemIDBRequest; 151 | probe.onerror = reject; 152 | probe.onsuccess = e => { 153 | resolve(Boolean(probe.result)); 154 | } 155 | }); 156 | } 157 | 158 | async deleteData(name: string) { 159 | const db = await this.getDB(); 160 | const reqCascade = new Promise((resolve, reject) => { 161 | let currentChunkNum = 0; 162 | const objectStore = db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); 163 | const probe = objectStore.get(`${name}.part_0`) as CacheDBItemIDBRequest; 164 | probe.onerror = reject; 165 | probe.onsuccess = e => { 166 | // 1. Probe fails => key does not exist 167 | if (!probe.result) return resolve(null); 168 | 169 | // 2. How many chunks to delete? 170 | const { numChunks } = probe.result; 171 | 172 | // 3. Cascade on the remaining chunks 173 | const onsuccess = (e: Event = new Event('zerowrite')) => { 174 | if (currentChunkNum === numChunks) return resolve(e); 175 | const req = objectStore.delete(`${name}.part_${currentChunkNum}`); 176 | req.onerror = reject; 177 | req.onsuccess = onsuccess; 178 | currentChunkNum++; 179 | }; 180 | onsuccess(); 181 | } 182 | }); 183 | 184 | return reqCascade; 185 | } 186 | 187 | async deleteAllData() { 188 | const db = await this.getDB(); 189 | const objectStore = db.transaction(this.storeName, 'readwrite').objectStore(this.storeName); 190 | const req = objectStore.clear(); 191 | return new Promise((resolve, reject) => { 192 | req.onsuccess = resolve; 193 | req.onerror = reject; 194 | }); 195 | } 196 | 197 | async deleteEntireDB() { 198 | const req = indexedDB.deleteDatabase(this.dbName); 199 | return new Promise((resolve, reject) => { 200 | req.onsuccess = () => resolve(this.db = null); 201 | req.onerror = reject; 202 | }); 203 | } 204 | 205 | static get isSupported() { 206 | return typeof indexedDB == 'object'; 207 | } 208 | 209 | static async quota() { 210 | if (navigator.storage) { 211 | return navigator.storage.estimate(); 212 | } 213 | else if (navigator.webkitTemporaryStorage) { 214 | return new Promise<{ usage: number, quota: number }>(resolve => { 215 | navigator.webkitTemporaryStorage!.queryUsageAndQuota((usage: number, quota: number) => resolve({ usage, quota })); 216 | }) 217 | } 218 | else { 219 | return { usage: -1, quota: -1 }; 220 | } 221 | } 222 | } 223 | 224 | const _UNIT_TEST = async () => { 225 | let db = new IDBCacheDB('test'); 226 | console.warn('Storing 201MB...'); 227 | console.log(await db.setData(new Blob([new ArrayBuffer(201 * 1024 * 1024)]), 'test')); 228 | console.warn('Deleting 201MB...'); 229 | console.log(await db.deleteData('test')); 230 | } 231 | 232 | export default IDBCacheDB; 233 | -------------------------------------------------------------------------------- /src/codec/flvass2mkv/util/simple-ebml-builder/id.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.matroska.org/technical/specs/index.html 3 | */ 4 | export const ID = { 5 | EBML: Uint8Array.of(0x1A, 0x45, 0xDF, 0xA3), 6 | EBMLVersion: Uint8Array.of(0x42, 0x86), 7 | EBMLReadVersion: Uint8Array.of(0x42, 0xF7), 8 | EBMLMaxIDLength: Uint8Array.of(0x42, 0xF2), 9 | EBMLMaxSizeLength: Uint8Array.of(0x42, 0xF3), 10 | DocType: Uint8Array.of(0x42, 0x82), 11 | DocTypeVersion: Uint8Array.of(0x42, 0x87), 12 | DocTypeReadVersion: Uint8Array.of(0x42, 0x85), 13 | Void: Uint8Array.of(0xEC), 14 | CRC32: Uint8Array.of(0xBF), 15 | Segment: Uint8Array.of(0x18, 0x53, 0x80, 0x67), 16 | SeekHead: Uint8Array.of(0x11, 0x4D, 0x9B, 0x74), 17 | Seek: Uint8Array.of(0x4D, 0xBB), 18 | SeekID: Uint8Array.of(0x53, 0xAB), 19 | SeekPosition: Uint8Array.of(0x53, 0xAC), 20 | Info: Uint8Array.of(0x15, 0x49, 0xA9, 0x66), 21 | SegmentUID: Uint8Array.of(0x73, 0xA4), 22 | SegmentFilename: Uint8Array.of(0x73, 0x84), 23 | PrevUID: Uint8Array.of(0x3C, 0xB9, 0x23), 24 | PrevFilename: Uint8Array.of(0x3C, 0x83, 0xAB), 25 | NextUID: Uint8Array.of(0x3E, 0xB9, 0x23), 26 | NextFilename: Uint8Array.of(0x3E, 0x83, 0xBB), 27 | SegmentFamily: Uint8Array.of(0x44, 0x44), 28 | ChapterTranslate: Uint8Array.of(0x69, 0x24), 29 | ChapterTranslateEditionUID: Uint8Array.of(0x69, 0xFC), 30 | ChapterTranslateCodec: Uint8Array.of(0x69, 0xBF), 31 | ChapterTranslateID: Uint8Array.of(0x69, 0xA5), 32 | TimecodeScale: Uint8Array.of(0x2A, 0xD7, 0xB1), 33 | Duration: Uint8Array.of(0x44, 0x89), 34 | DateUTC: Uint8Array.of(0x44, 0x61), 35 | Title: Uint8Array.of(0x7B, 0xA9), 36 | MuxingApp: Uint8Array.of(0x4D, 0x80), 37 | WritingApp: Uint8Array.of(0x57, 0x41), 38 | Cluster: Uint8Array.of(0x1F, 0x43, 0xB6, 0x75), 39 | Timecode: Uint8Array.of(0xE7), 40 | SilentTracks: Uint8Array.of(0x58, 0x54), 41 | SilentTrackNumber: Uint8Array.of(0x58, 0xD7), 42 | Position: Uint8Array.of(0xA7), 43 | PrevSize: Uint8Array.of(0xAB), 44 | SimpleBlock: Uint8Array.of(0xA3), 45 | BlockGroup: Uint8Array.of(0xA0), 46 | Block: Uint8Array.of(0xA1), 47 | BlockAdditions: Uint8Array.of(0x75, 0xA1), 48 | BlockMore: Uint8Array.of(0xA6), 49 | BlockAddID: Uint8Array.of(0xEE), 50 | BlockAdditional: Uint8Array.of(0xA5), 51 | BlockDuration: Uint8Array.of(0x9B), 52 | ReferencePriority: Uint8Array.of(0xFA), 53 | ReferenceBlock: Uint8Array.of(0xFB), 54 | CodecState: Uint8Array.of(0xA4), 55 | DiscardPadding: Uint8Array.of(0x75, 0xA2), 56 | Slices: Uint8Array.of(0x8E), 57 | TimeSlice: Uint8Array.of(0xE8), 58 | LaceNumber: Uint8Array.of(0xCC), 59 | Tracks: Uint8Array.of(0x16, 0x54, 0xAE, 0x6B), 60 | TrackEntry: Uint8Array.of(0xAE), 61 | TrackNumber: Uint8Array.of(0xD7), 62 | TrackUID: Uint8Array.of(0x73, 0xC5), 63 | TrackType: Uint8Array.of(0x83), 64 | FlagEnabled: Uint8Array.of(0xB9), 65 | FlagDefault: Uint8Array.of(0x88), 66 | FlagForced: Uint8Array.of(0x55, 0xAA), 67 | FlagLacing: Uint8Array.of(0x9C), 68 | MinCache: Uint8Array.of(0x6D, 0xE7), 69 | MaxCache: Uint8Array.of(0x6D, 0xF8), 70 | DefaultDuration: Uint8Array.of(0x23, 0xE3, 0x83), 71 | DefaultDecodedFieldDuration: Uint8Array.of(0x23, 0x4E, 0x7A), 72 | MaxBlockAdditionID: Uint8Array.of(0x55, 0xEE), 73 | Name: Uint8Array.of(0x53, 0x6E), 74 | Language: Uint8Array.of(0x22, 0xB5, 0x9C), 75 | CodecID: Uint8Array.of(0x86), 76 | CodecPrivate: Uint8Array.of(0x63, 0xA2), 77 | CodecName: Uint8Array.of(0x25, 0x86, 0x88), 78 | AttachmentLink: Uint8Array.of(0x74, 0x46), 79 | CodecDecodeAll: Uint8Array.of(0xAA), 80 | TrackOverlay: Uint8Array.of(0x6F, 0xAB), 81 | CodecDelay: Uint8Array.of(0x56, 0xAA), 82 | SeekPreRoll: Uint8Array.of(0x56, 0xBB), 83 | TrackTranslate: Uint8Array.of(0x66, 0x24), 84 | TrackTranslateEditionUID: Uint8Array.of(0x66, 0xFC), 85 | TrackTranslateCodec: Uint8Array.of(0x66, 0xBF), 86 | TrackTranslateTrackID: Uint8Array.of(0x66, 0xA5), 87 | Video: Uint8Array.of(0xE0), 88 | FlagInterlaced: Uint8Array.of(0x9A), 89 | FieldOrder: Uint8Array.of(0x9D), 90 | StereoMode: Uint8Array.of(0x53, 0xB8), 91 | AlphaMode: Uint8Array.of(0x53, 0xC0), 92 | PixelWidth: Uint8Array.of(0xB0), 93 | PixelHeight: Uint8Array.of(0xBA), 94 | PixelCropBottom: Uint8Array.of(0x54, 0xAA), 95 | PixelCropTop: Uint8Array.of(0x54, 0xBB), 96 | PixelCropLeft: Uint8Array.of(0x54, 0xCC), 97 | PixelCropRight: Uint8Array.of(0x54, 0xDD), 98 | DisplayWidth: Uint8Array.of(0x54, 0xB0), 99 | DisplayHeight: Uint8Array.of(0x54, 0xBA), 100 | DisplayUnit: Uint8Array.of(0x54, 0xB2), 101 | AspectRatioType: Uint8Array.of(0x54, 0xB3), 102 | ColourSpace: Uint8Array.of(0x2E, 0xB5, 0x24), 103 | Colour: Uint8Array.of(0x55, 0xB0), 104 | MatrixCoefficients: Uint8Array.of(0x55, 0xB1), 105 | BitsPerChannel: Uint8Array.of(0x55, 0xB2), 106 | ChromaSubsamplingHorz: Uint8Array.of(0x55, 0xB3), 107 | ChromaSubsamplingVert: Uint8Array.of(0x55, 0xB4), 108 | CbSubsamplingHorz: Uint8Array.of(0x55, 0xB5), 109 | CbSubsamplingVert: Uint8Array.of(0x55, 0xB6), 110 | ChromaSitingHorz: Uint8Array.of(0x55, 0xB7), 111 | ChromaSitingVert: Uint8Array.of(0x55, 0xB8), 112 | Range: Uint8Array.of(0x55, 0xB9), 113 | TransferCharacteristics: Uint8Array.of(0x55, 0xBA), 114 | Primaries: Uint8Array.of(0x55, 0xBB), 115 | MaxCLL: Uint8Array.of(0x55, 0xBC), 116 | MaxFALL: Uint8Array.of(0x55, 0xBD), 117 | MasteringMetadata: Uint8Array.of(0x55, 0xD0), 118 | PrimaryRChromaticityX: Uint8Array.of(0x55, 0xD1), 119 | PrimaryRChromaticityY: Uint8Array.of(0x55, 0xD2), 120 | PrimaryGChromaticityX: Uint8Array.of(0x55, 0xD3), 121 | PrimaryGChromaticityY: Uint8Array.of(0x55, 0xD4), 122 | PrimaryBChromaticityX: Uint8Array.of(0x55, 0xD5), 123 | PrimaryBChromaticityY: Uint8Array.of(0x55, 0xD6), 124 | WhitePointChromaticityX: Uint8Array.of(0x55, 0xD7), 125 | WhitePointChromaticityY: Uint8Array.of(0x55, 0xD8), 126 | LuminanceMax: Uint8Array.of(0x55, 0xD9), 127 | LuminanceMin: Uint8Array.of(0x55, 0xDA), 128 | Audio: Uint8Array.of(0xE1), 129 | SamplingFrequency: Uint8Array.of(0xB5), 130 | OutputSamplingFrequency: Uint8Array.of(0x78, 0xB5), 131 | Channels: Uint8Array.of(0x9F), 132 | BitDepth: Uint8Array.of(0x62, 0x64), 133 | TrackOperation: Uint8Array.of(0xE2), 134 | TrackCombinePlanes: Uint8Array.of(0xE3), 135 | TrackPlane: Uint8Array.of(0xE4), 136 | TrackPlaneUID: Uint8Array.of(0xE5), 137 | TrackPlaneType: Uint8Array.of(0xE6), 138 | TrackJoinBlocks: Uint8Array.of(0xE9), 139 | TrackJoinUID: Uint8Array.of(0xED), 140 | ContentEncodings: Uint8Array.of(0x6D, 0x80), 141 | ContentEncoding: Uint8Array.of(0x62, 0x40), 142 | ContentEncodingOrder: Uint8Array.of(0x50, 0x31), 143 | ContentEncodingScope: Uint8Array.of(0x50, 0x32), 144 | ContentEncodingType: Uint8Array.of(0x50, 0x33), 145 | ContentCompression: Uint8Array.of(0x50, 0x34), 146 | ContentCompAlgo: Uint8Array.of(0x42, 0x54), 147 | ContentCompSettings: Uint8Array.of(0x42, 0x55), 148 | ContentEncryption: Uint8Array.of(0x50, 0x35), 149 | ContentEncAlgo: Uint8Array.of(0x47, 0xE1), 150 | ContentEncKeyID: Uint8Array.of(0x47, 0xE2), 151 | ContentSignature: Uint8Array.of(0x47, 0xE3), 152 | ContentSigKeyID: Uint8Array.of(0x47, 0xE4), 153 | ContentSigAlgo: Uint8Array.of(0x47, 0xE5), 154 | ContentSigHashAlgo: Uint8Array.of(0x47, 0xE6), 155 | Cues: Uint8Array.of(0x1C, 0x53, 0xBB, 0x6B), 156 | CuePoint: Uint8Array.of(0xBB), 157 | CueTime: Uint8Array.of(0xB3), 158 | CueTrackPositions: Uint8Array.of(0xB7), 159 | CueTrack: Uint8Array.of(0xF7), 160 | CueClusterPosition: Uint8Array.of(0xF1), 161 | CueRelativePosition: Uint8Array.of(0xF0), 162 | CueDuration: Uint8Array.of(0xB2), 163 | CueBlockNumber: Uint8Array.of(0x53, 0x78), 164 | CueCodecState: Uint8Array.of(0xEA), 165 | CueReference: Uint8Array.of(0xDB), 166 | CueRefTime: Uint8Array.of(0x96), 167 | Attachments: Uint8Array.of(0x19, 0x41, 0xA4, 0x69), 168 | AttachedFile: Uint8Array.of(0x61, 0xA7), 169 | FileDescription: Uint8Array.of(0x46, 0x7E), 170 | FileName: Uint8Array.of(0x46, 0x6E), 171 | FileMimeType: Uint8Array.of(0x46, 0x60), 172 | FileData: Uint8Array.of(0x46, 0x5C), 173 | FileUID: Uint8Array.of(0x46, 0xAE), 174 | Chapters: Uint8Array.of(0x10, 0x43, 0xA7, 0x70), 175 | EditionEntry: Uint8Array.of(0x45, 0xB9), 176 | EditionUID: Uint8Array.of(0x45, 0xBC), 177 | EditionFlagHidden: Uint8Array.of(0x45, 0xBD), 178 | EditionFlagDefault: Uint8Array.of(0x45, 0xDB), 179 | EditionFlagOrdered: Uint8Array.of(0x45, 0xDD), 180 | ChapterAtom: Uint8Array.of(0xB6), 181 | ChapterUID: Uint8Array.of(0x73, 0xC4), 182 | ChapterStringUID: Uint8Array.of(0x56, 0x54), 183 | ChapterTimeStart: Uint8Array.of(0x91), 184 | ChapterTimeEnd: Uint8Array.of(0x92), 185 | ChapterFlagHidden: Uint8Array.of(0x98), 186 | ChapterFlagEnabled: Uint8Array.of(0x45, 0x98), 187 | ChapterSegmentUID: Uint8Array.of(0x6E, 0x67), 188 | ChapterSegmentEditionUID: Uint8Array.of(0x6E, 0xBC), 189 | ChapterPhysicalEquiv: Uint8Array.of(0x63, 0xC3), 190 | ChapterTrack: Uint8Array.of(0x8F), 191 | ChapterTrackNumber: Uint8Array.of(0x89), 192 | ChapterDisplay: Uint8Array.of(0x80), 193 | ChapString: Uint8Array.of(0x85), 194 | ChapLanguage: Uint8Array.of(0x43, 0x7C), 195 | ChapCountry: Uint8Array.of(0x43, 0x7E), 196 | ChapProcess: Uint8Array.of(0x69, 0x44), 197 | ChapProcessCodecID: Uint8Array.of(0x69, 0x55), 198 | ChapProcessPrivate: Uint8Array.of(0x45, 0x0D), 199 | ChapProcessCommand: Uint8Array.of(0x69, 0x11), 200 | ChapProcessTime: Uint8Array.of(0x69, 0x22), 201 | ChapProcessData: Uint8Array.of(0x69, 0x33), 202 | Tags: Uint8Array.of(0x12, 0x54, 0xC3, 0x67), 203 | Tag: Uint8Array.of(0x73, 0x73), 204 | Targets: Uint8Array.of(0x63, 0xC0), 205 | TargetTypeValue: Uint8Array.of(0x68, 0xCA), 206 | TargetType: Uint8Array.of(0x63, 0xCA), 207 | TagTrackUID: Uint8Array.of(0x63, 0xC5), 208 | TagEditionUID: Uint8Array.of(0x63, 0xC9), 209 | TagChapterUID: Uint8Array.of(0x63, 0xC4), 210 | TagAttachmentUID: Uint8Array.of(0x63, 0xC6), 211 | SimpleTag: Uint8Array.of(0x67, 0xC8), 212 | TagName: Uint8Array.of(0x45, 0xA3), 213 | TagLanguage: Uint8Array.of(0x44, 0x7A), 214 | TagDefault: Uint8Array.of(0x44, 0x84), 215 | TagString: Uint8Array.of(0x44, 0x87), 216 | TagBinary: Uint8Array.of(0x44, 0x85), 217 | }; 218 | --------------------------------------------------------------------------------