// ==UserScript==
// @name bilibili merged flv+mp4+ass+enhance
// @namespace http://qli5.tk/
// @homepageURL https://github.com/liqi0816/bilitwin/
// @description bilibili/哔哩哔哩:超清FLV下载,FLV合并,原生MP4下载,弹幕ASS下载,MKV打包,播放体验增强,原生appsecret,不借助其他网站
// @match *://www.bilibili.com/video/av*
// @match *://bangumi.bilibili.com/anime/*/play*
// @match *://www.bilibili.com/bangumi/play/ep*
// @match *://www.bilibili.com/bangumi/play/ss*
// @match *://www.bilibili.com/watchlater/
// @version 1.13
// @author qli5
// @copyright qli5, 2014+, 田生, grepmusic, zheng qian, ryiwamoto
// @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
// @grant none
// ==/UserScript==
let debugOption = {
// console会清空,生成 window.m 和 window.p
//debug: 1,
// 别拖啦~
//betabeta: 1,
// UP主不容易,B站也不容易,充电是有益的尝试,我不鼓励跳。
//electricSkippable: 0,
};
/**
* @author qli5 <goodlq11[at](163|gmail).com>
*
* BiliTwin consists of two parts - BiliMonkey and BiliPolyfill.
* They are bundled because I am too lazy to write two user interfaces.
*
* So what is the difference between BiliMonkey and BiliPolyfill?
*
* BiliMonkey deals with network. It is a (naIve) Service Worker.
* This is also why it uses IndexedDB instead of localStorage.
* BiliPolyfill deals with experience. It is more a "user script".
* Everything it can do can be done by hand.
*
* BiliPolyfill will be pointless in the long run - I believe bilibili
* will finally provide these functions themselves.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Covered Software is provided under this License on an “as is” basis,
* without warranty of any kind, either expressed, implied, or statutory,
* including, without limitation, warranties that the Covered Software
* is free of defects, merchantable, fit for a particular purpose or
* non-infringing. The entire risk as to the quality and performance of
* the Covered Software is with You. Should any Covered Software prove
* defective in any respect, You (not any Contributor) assume the cost
* of any necessary servicing, repair, or correction. This disclaimer
* of warranty constitutes an essential part of this License. No use of
* any Covered Software is authorized under this License except under
* this disclaimer.
*
* Under no circumstances and under no legal theory, whether tort
* (including negligence), contract, or otherwise, shall any Contributor,
* or anyone who distributes Covered Software as permitted above, be
* liable to You for any direct, indirect, special, incidental, or
* consequential damages of any character including, without limitation,
* damages for lost profits, loss of goodwill, work stoppage, computer
* failure or malfunction, or any and all other commercial damages or
* losses, even if such party shall have been informed of the possibility
* of such damages. This limitation of liability shall not apply to
* liability for death or personal injury resulting from such party’s
* negligence to the extent applicable law prohibits such limitation.
* Some jurisdictions do not allow the exclusion or limitation of
* incidental or consequential damages, so this exclusion and limitation
* may not apply to You.
*/
/**
* BiliMonkey
* A bilibili user script
* by qli5 goodlq11[at](gmail|163).com
*
* The FLV merge utility is a Javascript translation of
* https://github.com/grepmusic/flvmerge
* by grepmusic
*
* The ASS convert utility is a fork of
* https://tiansh.github.io/us-danmaku/bilibili/
* by tiansh
*
* The FLV demuxer is from
* https://github.com/Bilibili/flv.js/
* by zheng qian
*
* The EMBL builder is from
* <https://www.npmjs.com/package/simple-ebml-builder>
* by ryiwamoto
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/**
* BiliPolyfill
* A bilibili user script
* by qli5 goodlq11[at](gmail|163).com
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
class TwentyFourDataView extends DataView {
constructor(...args) {
if (TwentyFourDataView.es6) {
super(...args);
}
else {
// ES5 polyfill
// It is dirty. Very dirty.
if (TwentyFourDataView.es6 === undefined) {
try {
TwentyFourDataView.es6 = 1;
return super(...args);
}
catch (e) {
if (e.name == 'TypeError') {
TwentyFourDataView.es6 = 0;
let setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
obj.__proto__ = proto;
return obj;
};
setPrototypeOf(TwentyFourDataView, Object);
}
else throw e;
}
}
super();
let _dataView = new DataView(...args);
_dataView.getUint24 = TwentyFourDataView.prototype.getUint24;
_dataView.setUint24 = TwentyFourDataView.prototype.setUint24;
_dataView.indexOf = TwentyFourDataView.prototype.indexOf;
return _dataView;
}
}
getUint24(byteOffset, littleEndian) {
if (littleEndian) throw 'littleEndian int24 not implemented';
return this.getUint32(byteOffset - 1) & 0x00FFFFFF;
}
setUint24(byteOffset, value, littleEndian) {
if (littleEndian) throw 'littleEndian int24 not implemented';
if (value > 0x00FFFFFF) throw 'setUint24: number out of range';
let msb = value >> 16;
let lsb = value & 0xFFFF;
this.setUint8(byteOffset, msb);
this.setUint16(byteOffset + 1, lsb);
}
indexOf(search, startOffset = 0, endOffset = this.byteLength - search.length + 1) {
// I know it is NAIVE
if (search.charCodeAt) {
for (let i = startOffset; i < endOffset; i++) {
if (this.getUint8(i) != search.charCodeAt(0)) continue;
let found = 1;
for (let j = 0; j < search.length; j++) {
if (this.getUint8(i + j) != search.charCodeAt(j)) {
found = 0;
break;
}
}
if (found) return i;
}
return -1;
}
else {
for (let i = startOffset; i < endOffset; i++) {
if (this.getUint8(i) != search[0]) continue;
let found = 1;
for (let j = 0; j < search.length; j++) {
if (this.getUint8(i + j) != search[j]) {
found = 0;
break;
}
}
if (found) return i;
}
return -1;
}
}
}
class FLVTag {
constructor(dataView, currentOffset = 0) {
this.tagHeader = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset, 11);
this.tagData = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11, this.dataSize);
this.previousSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + currentOffset + 11 + this.dataSize, 4);
}
get tagType() {
return this.tagHeader.getUint8(0);
}
get dataSize() {
return this.tagHeader.getUint24(1);
}
get timestamp() {
return this.tagHeader.getUint24(4);
}
get timestampExtension() {
return this.tagHeader.getUint8(7);
}
get streamID() {
return this.tagHeader.getUint24(8);
}
stripKeyframesScriptData() {
let hasKeyframes = 'hasKeyframes\x01';
let keyframes = '\x00\x09keyframs\x03';
if (this.tagType != 0x12) throw 'can not strip non-scriptdata\'s keyframes';
let index;
index = this.tagData.indexOf(hasKeyframes);
if (index != -1) {
//0x0101 => 0x0100
this.tagData.setUint8(index + hasKeyframes.length, 0x00);
}
// Well, I think it is unnecessary
/*index = this.tagData.indexOf(keyframes)
if (index != -1) {
this.dataSize = index;
this.tagHeader.setUint24(1, index);
this.tagData = new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset, index);
}*/
}
getDuration() {
if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
let duration = 'duration\x00';
let index = this.tagData.indexOf(duration);
if (index == -1) throw 'can not get flv meta duration';
index += 9;
return this.tagData.getFloat64(index);
}
getDurationAndView() {
if (this.tagType != 0x12) throw 'can not find non-scriptdata\'s duration';
let duration = 'duration\x00';
let index = this.tagData.indexOf(duration);
if (index == -1) throw 'can not get flv meta duration';
index += 9;
return {
duration: this.tagData.getFloat64(index),
durationDataView: new TwentyFourDataView(this.tagData.buffer, this.tagData.byteOffset + index, 8)
};
}
getCombinedTimestamp() {
return (this.timestampExtension << 24 | this.timestamp);
}
setCombinedTimestamp(timestamp) {
if (timestamp < 0) throw 'timestamp < 0';
this.tagHeader.setUint8(7, timestamp >> 24);
this.tagHeader.setUint24(4, timestamp & 0x00FFFFFF);
}
}
class FLV {
constructor(dataView) {
if (dataView.indexOf('FLV', 0, 1) != 0) throw 'Invalid FLV header';
this.header = new TwentyFourDataView(dataView.buffer, dataView.byteOffset, 9);
this.firstPreviousTagSize = new TwentyFourDataView(dataView.buffer, dataView.byteOffset + 9, 4);
this.tags = [];
let offset = this.headerLength + 4;
while (offset < dataView.byteLength) {
let tag = new FLVTag(dataView, offset);
// debug for scrpit data tag
// if (tag.tagType != 0x08 && tag.tagType != 0x09)
offset += 11 + tag.dataSize + 4;
this.tags.push(tag);
}
if (offset != dataView.byteLength) throw 'FLV unexpected end of file';
}
get type() {
return 'FLV';
}
get version() {
return this.header.getUint8(3);
}
get typeFlag() {
return this.header.getUint8(4);
}
get headerLength() {
return this.header.getUint32(5);
}
static merge(flvs) {
if (flvs.length < 1) throw 'Usage: FLV.merge([flvs])';
let blobParts = [];
let basetimestamp = [0, 0];
let lasttimestamp = [0, 0];
let duration = 0.0;
let durationDataView;
blobParts.push(flvs[0].header);
blobParts.push(flvs[0].firstPreviousTagSize);
for (let flv of flvs) {
let bts = duration * 1000;
basetimestamp[0] = lasttimestamp[0];
basetimestamp[1] = lasttimestamp[1];
bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
let foundDuration = 0;
for (let tag of flv.tags) {
if (tag.tagType == 0x12 && !foundDuration) {
duration += tag.getDuration();
foundDuration = 1;
if (flv == flvs[0]) {
({ duration, durationDataView } = tag.getDurationAndView());
tag.stripKeyframesScriptData();
blobParts.push(tag.tagHeader);
blobParts.push(tag.tagData);
blobParts.push(tag.previousSize);
}
}
else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
blobParts.push(tag.tagHeader);
blobParts.push(tag.tagData);
blobParts.push(tag.previousSize);
}
}
}
durationDataView.setFloat64(0, duration);
return new Blob(blobParts);
}
static async mergeBlobs(blobs) {
// Blobs can be swapped to disk, while Arraybuffers can not.
// This is a RAM saving workaround. Somewhat.
if (blobs.length < 1) throw 'Usage: FLV.mergeBlobs([blobs])';
let ret = [];
let basetimestamp = [0, 0];
let lasttimestamp = [0, 0];
let duration = 0.0;
let durationDataView;
for (let blob of blobs) {
let bts = duration * 1000;
basetimestamp[0] = lasttimestamp[0];
basetimestamp[1] = lasttimestamp[1];
bts = Math.max(bts, basetimestamp[0], basetimestamp[1]);
let foundDuration = 0;
let flv = await new Promise((resolve, reject) => {
let fr = new FileReader();
fr.onload = () => resolve(new FLV(new TwentyFourDataView(fr.result)));
fr.readAsArrayBuffer(blob);
fr.onerror = reject;
});
let modifiedMediaTags = [];
for (let tag of flv.tags) {
if (tag.tagType == 0x12 && !foundDuration) {
duration += tag.getDuration();
foundDuration = 1;
if (blob == blobs[0]) {
ret.push(flv.header, flv.firstPreviousTagSize);
({ duration, durationDataView } = tag.getDurationAndView());
tag.stripKeyframesScriptData();
ret.push(tag.tagHeader);
ret.push(tag.tagData);
ret.push(tag.previousSize);
}
}
else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
modifiedMediaTags.push(tag.tagHeader, tag.tagData, tag.previousSize);
}
}
ret.push(new Blob(modifiedMediaTags));
}
durationDataView.setFloat64(0, duration);
return new Blob(ret);
}
}
class CacheDB {
constructor(dbName = 'biliMonkey', osName = 'flv', keyPath = 'name', maxItemSize = 100 * 1024 * 1024) {
this.dbName = dbName;
this.osName = osName;
this.keyPath = keyPath;
this.maxItemSize = maxItemSize;
this.db = null;
}
async getDB() {
if (this.db) return this.db;
this.db = new Promise((resolve, reject) => {
let openRequest = indexedDB.open(this.dbName);
openRequest.onupgradeneeded = e => {
let db = e.target.result;
if (!db.objectStoreNames.contains(this.osName)) {
db.createObjectStore(this.osName, { keyPath: this.keyPath });
}
}
openRequest.onsuccess = e => {
resolve(this.db = e.target.result);
}
openRequest.onerror = reject;
});
return this.db;
}
async addData(item, name = item.name, data = item.data) {
if (!data instanceof Blob) throw 'CacheDB: data must be a Blob';
let db = await this.getDB();
let itemChunks = [];
let numChunks = Math.ceil(data.size / this.maxItemSize);
for (let i = 0; i < numChunks; i++) {
itemChunks.push({
name: `${name}_part_${i}`,
numChunks,
data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
});
}
let reqArr = [];
for (let chunk of itemChunks) {
reqArr.push(new Promise((resolve, reject) => {
let req = db
.transaction([this.osName], 'readwrite')
.objectStore(this.osName)
.add(chunk);
req.onsuccess = resolve;
req.onerror = reject;
}));
}
return Promise.all(reqArr);
}
async putData(item, name = item.name, data = item.data) {
if (!data instanceof Blob) throw 'CacheDB: data must be a Blob';
let db = await this.getDB();
let itemChunks = [];
let numChunks = Math.ceil(data.size / this.maxItemSize);
for (let i = 0; i < numChunks; i++) {
itemChunks.push({
name: `${name}_part_${i}`,
numChunks,
data: data.slice(i * this.maxItemSize, (i + 1) * this.maxItemSize)
});
}
let reqArr = [];
for (let chunk of itemChunks) {
reqArr.push(new Promise((resolve, reject) => {
let req = db
.transaction([this.osName], 'readwrite')
.objectStore(this.osName)
.put(chunk);
req.onsuccess = resolve;
req.onerror = reject;
}));
}
return Promise.all(reqArr);
}
async getData(index) {
let db = await this.getDB();
let item_0 = await new Promise((resolve, reject) => {
let req = db
.transaction([this.osName])
.objectStore(this.osName)
.get(`${index}_part_0`);
req.onsuccess = () => resolve(req.result);
req.onerror = reject;
});
if (!item_0) return undefined;
let { numChunks, data: data_0 } = item_0;
let reqArr = [Promise.resolve(data_0)];
for (let i = 1; i < numChunks; i++) {
reqArr.push(new Promise((resolve, reject) => {
let req = db
.transaction([this.osName])
.objectStore(this.osName)
.get(`${index}_part_${i}`);
req.onsuccess = () => resolve(req.result.data);
req.onerror = reject;
}));
}
let itemChunks = await Promise.all(reqArr);
return { name: index, data: new Blob(itemChunks) };
}
async deleteData(index) {
let db = await this.getDB();
let item_0 = await new Promise((resolve, reject) => {
let req = db
.transaction([this.osName])
.objectStore(this.osName)
.get(`${index}_part_0`);
req.onsuccess = () => resolve(req.result);
req.onerror = reject;
});
if (!item_0) return undefined;
let numChunks = item_0.numChunks;
let reqArr = [];
for (let i = 0; i < numChunks; i++) {
reqArr.push(new Promise((resolve, reject) => {
let req = db
.transaction([this.osName], 'readwrite')
.objectStore(this.osName)
.delete(`${index}_part_${i}`);
req.onsuccess = resolve;
req.onerror = reject;
}));
}
return Promise.all(reqArr);
}
async deleteEntireDB() {
let req = indexedDB.deleteDatabase(this.dbName);
return new Promise((resolve, reject) => {
req.onsuccess = () => resolve(this.db = null);
req.onerror = reject;
});
}
}
class DetailedFetchBlob {
constructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror, fetch = init.fetch || top.fetch) {
// Fire in the Fox fix
if (this.firefoxConstructor(input, init, onprogress, onabort, onerror)) return;
// Now I know why standardizing cancelable Promise is that difficult
// PLEASE refactor me!
this.onprogress = onprogress;
this.onabort = onabort;
this.onerror = onerror;
this.abort = null;
this.loaded = init.cacheLoaded || 0;
this.total = init.cacheLoaded || 0;
this.lengthComputable = false;
this.buffer = [];
this.blob = null;
this.reader = null;
this.blobPromise = fetch(input, init).then(res => {
if (this.reader == 'abort') return res.body.getReader().cancel().then(() => null);
if (!res.ok) throw `HTTP Error ${res.status}: ${res.statusText}`;
this.lengthComputable = res.headers.has('Content-Length');
this.total += parseInt(res.headers.get('Content-Length')) || Infinity;
if (this.lengthComputable) {
this.reader = res.body.getReader();
return this.blob = this.consume();
}
else {
if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
return this.blob = res.blob();
}
});
this.blobPromise.then(() => this.abort = () => { });
this.blobPromise.catch(e => this.onerror({ target: this, type: e }));
this.promise = Promise.race([
this.blobPromise,
new Promise(resolve => this.abort = () => {
this.onabort({ target: this, type: 'abort' });
resolve('abort');
this.buffer = [];
this.blob = null;
if (this.reader) this.reader.cancel();
else this.reader = 'abort';
})
]).then(s => s == 'abort' ? new Promise(() => { }) : s);
this.then = this.promise.then.bind(this.promise);
this.catch = this.promise.catch.bind(this.promise);
}
getPartialBlob() {
return new Blob(this.buffer);
}
async getBlob() {
return this.promise;
}
async pump() {
while (true) {
let { done, value } = await this.reader.read();
if (done) return this.loaded;
this.loaded += value.byteLength;
this.buffer.push(new Blob([value]));
if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
}
}
async consume() {
await this.pump();
this.blob = new Blob(this.buffer);
this.buffer = null;
return this.blob;
}
firefoxConstructor(input, init = {}, onprogress = init.onprogress, onabort = init.onabort, onerror = init.onerror) {
if (!top.navigator.userAgent.includes('Firefox')) return false;
this.onprogress = onprogress;
this.onabort = onabort;
this.onerror = onerror;
this.abort = null;
this.loaded = init.cacheLoaded || 0;
this.total = init.cacheLoaded || 0;
this.lengthComputable = false;
this.buffer = [];
this.blob = null;
this.reader = undefined;
this.blobPromise = new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.responseType = 'moz-chunked-arraybuffer';
xhr.onload = () => { resolve(this.blob = new Blob(this.buffer)); this.buffer = null; }
let cacheLoaded = this.loaded;
xhr.onprogress = e => {
this.loaded = e.loaded + cacheLoaded;
this.total = e.total + cacheLoaded;
this.lengthComputable = e.lengthComputable;
this.buffer.push(new Blob([xhr.response]));
if (this.onprogress) this.onprogress(this.loaded, this.total, this.lengthComputable);
};
xhr.onabort = e => this.onabort({ target: this, type: 'abort' });
xhr.onerror = e => { this.onerror({ target: this, type: e.type }); reject(e); };
this.abort = xhr.abort.bind(xhr);
xhr.open('get', input);
xhr.send();
});
this.promise = this.blobPromise;
this.then = this.promise.then.bind(this.promise);
this.catch = this.promise.catch.bind(this.promise);
return true;
}
}
class Mutex {
constructor() {
this.queueTail = Promise.resolve();
this.resolveHead = null;
}
async lock() {
let myResolve;
let _queueTail = this.queueTail;
this.queueTail = new Promise(resolve => myResolve = resolve);
await _queueTail;
this.resolveHead = myResolve;
return;
}
unlock() {
this.resolveHead();
return;
}
async lockAndAwait(asyncFunc) {
await this.lock();
let ret;
try {
ret = await asyncFunc();
}
finally {
this.unlock();
}
return ret;
}
static _UNIT_TEST() {
let m = new Mutex();
function sleep(time) {
return new Promise(r => setTimeout(r, time));
}
m.lockAndAwait(() => {
console.warn('Check message timestamps.');
console.warn('Bad:');
console.warn('1 1 1 1 1:5s');
console.warn(' 1 1 1 1 1:10s');
console.warn('Good:');
console.warn('1 1 1 1 1:5s');
console.warn(' 1 1 1 1 1:10s');
});
m.lockAndAwait(async () => {
await sleep(1000);
await sleep(1000);
await sleep(1000);
await sleep(1000);
await sleep(1000);
});
m.lockAndAwait(async () => console.log('5s!'));
m.lockAndAwait(async () => {
await sleep(1000);
await sleep(1000);
await sleep(1000);
await sleep(1000);
await sleep(1000);
});
m.lockAndAwait(async () => console.log('10s!'));
}
}
class AsyncContainer {
// Yes, this is something like cancelable Promise. But I insist they are different.
constructor() {
//this.state = 0; // I do not know why will I need this.
this.resolve = null;
this.reject = null;
this.hang = null;
this.hangReturn = Symbol();
this.primaryPromise = new Promise((s, j) => {
this.resolve = arg => { s(arg); return arg; }
this.reject = arg => { j(arg); return arg; }
});
//this.primaryPromise.then(() => this.state = 1);
//this.primaryPromise.catch(() => this.state = 2);
this.hangPromise = new Promise(s => this.hang = () => s(this.hangReturn));
//this.hangPromise.then(() => this.state = 3);
this.promise = Promise
.race([this.primaryPromise, this.hangPromise])
.then(s => s == this.hangReturn ? new Promise(() => { }) : s);
this.then = this.promise.then.bind(this.promise);
this.catch = this.promise.catch.bind(this.promise);
this.destroiedThen = this.hangPromise.then.bind(this.hangPromise);
}
destroy() {
this.hang();
this.resolve = () => { };
this.reject = this.resolve;
this.hang = this.resolve;
this.primaryPromise = null;
this.hangPromise = null;
this.promise = null;
this.then = this.resolve;
this.catch = this.resolve;
this.destroiedThen = f => f();
// Do NEVER NEVER NEVER dereference hangReturn.
// Mysteriously this tiny symbol will keep you from Memory LEAK.
//this.hangReturn = null;
}
static _UNIT_TEST() {
let containers = [];
async function foo() {
let buf = new ArrayBuffer(600000000);
let ac = new AsyncContainer();
ac.destroiedThen(() => console.log('asyncContainer destroied'))
containers.push(ac);
await ac;
return buf;
}
let foos = [foo(), foo(), foo()];
containers.forEach(e => e.destroy());
console.warn('Check your RAM usage. I allocated 1.8GB in three dead-end promises.')
return [foos, containers];
}
}
class ASSDownloader {
constructor(option) {
({ fetchDanmaku: this.fetchDanmaku, generateASS: this.generateASS, setPosition: this.setPosition } = new Function('option', `
// ==UserScript==
// @name bilibili ASS Danmaku Downloader
// @namespace https://github.com/tiansh
// @description 以 ASS 格式下载 bilibili 的弹幕
// @include http://www.bilibili.com/video/av*
// @include http://bangumi.bilibili.com/movie/*
// @updateURL https://tiansh.github.io/us-danmaku/bilibili/bilibili_ASS_Danmaku_Downloader.meta.js
// @downloadURL https://tiansh.github.io/us-danmaku/bilibili/bilibili_ASS_Danmaku_Downloader.user.js
// @version 1.11
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @run-at document-start
// @author 田生
// @copyright 2014+, 田生
// @license Mozilla Public License 2.0; http://www.mozilla.org/MPL/2.0/
// @license CC Attribution-ShareAlike 4.0 International; http://creativecommons.org/licenses/by-sa/4.0/
// @connect-src comment.bilibili.com
// @connect-src interface.bilibili.com
// ==/UserScript==
/*
* Common
*/
// 设置项
var config = {
'playResX': 560, // 屏幕分辨率宽(像素)
'playResY': 420, // 屏幕分辨率高(像素)
'fontlist': [ // 字形(会自动选择最前面一个可用的)
'SimHei',
'\\'Microsoft JhengHei\\'',
'SimSun',
'NSimSun',
'FangSong',
'\\'Microsoft YaHei\\'',
'\\'Microsoft Yahei UI Light\\'',
'\\'Noto Sans CJK SC Bold\\'',
'\\'Noto Sans CJK SC DemiLight\\'',
'\\'Noto Sans CJK SC Regular\\'',
'Microsoft YaHei UI',
'Microsoft YaHei',
'文泉驿正黑',
'STHeitiSC',
'黑体',
],
'bold': 1, // 加粗(0/1)
'font_size': 1.0, // 字号(比例)
'r2ltime': 8, // 右到左弹幕持续时间(秒)
'fixtime': 4, // 固定弹幕持续时间(秒)
'opacity': 0.6, // 不透明度(比例)
'space': 0, // 弹幕间隔的最小水平距离(像素)
'max_delay': 6, // 最多允许延迟几秒出现弹幕
'bottom': 50, // 底端给字幕保留的空间(像素)
'use_canvas': null, // 是否使用canvas计算文本宽度(布尔值,Linux下的火狐默认否,其他默认是,Firefox bug #561361)
'debug': false, // 打印调试信息
};
if (option instanceof Object) {
for (var prop in config) {
if (prop in option) {
config[prop] = option[prop]
}
}
}
var debug = config.debug ? console.log.bind(console) : function () { };
// 将字典中的值填入字符串
var fillStr = function (str) {
var dict = Array.apply(Array, arguments).slice(1);
return str.replace(/{{([^}]+)}}/g, function (r, o) {
var ret;
dict.some(function (i) { return ret = i[o]; });
return ret || '';
});
};
// 将颜色的数值化为十六进制字符串表示
var RRGGBB = function (color) {
var t = Number(color).toString(16).toUpperCase();
return (Array(7).join('0') + t).slice(-6);
};
// 将可见度转换为透明度
var hexAlpha = function (opacity) {
var alpha = Math.round(0xFF * (1 - opacity)).toString(16).toUpperCase();
return Array(3 - alpha.length).join('0') + alpha;
};
// 字符串
var funStr = function (fun) {
return fun.toString().split(/\\r\\n|\\n|\\r/).slice(1, -1).join('\\n');
};
// 平方和开根
var hypot = Math.hypot ? Math.hypot.bind(Math) : function () {
return Math.sqrt([0].concat(Array.apply(Array, arguments))
.reduce(function (x, y) { return x + y * y; }));
};
// 创建下载
var startDownload = function (data, filename) {
var blob = new Blob([data], { type: 'application/octet-stream' });
var url = window.URL.createObjectURL(blob);
var saveas = document.createElement('a');
saveas.href = url;
saveas.style.display = 'none';
document.body.appendChild(saveas);
saveas.download = filename;
saveas.click();
setTimeout(function () { saveas.parentNode.removeChild(saveas); }, 1000)
document.addEventListener('unload', function () { window.URL.revokeObjectURL(url); });
};
// 计算文字宽度
var calcWidth = (function () {
// 使用Canvas计算
var calcWidthCanvas = function () {
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
return function (fontname, text, fontsize) {
context.font = 'bold ' + fontsize + 'px ' + fontname;
return Math.ceil(context.measureText(text).width + config.space);
};
}
// 使用Div计算
var calcWidthDiv = function () {
var d = document.createElement('div');
d.setAttribute('style', [
'all: unset', 'top: -10000px', 'left: -10000px',
'width: auto', 'height: auto', 'position: absolute',
'',].join(' !important; '));
var ld = function () { document.body.parentNode.appendChild(d); }
if (!document.body) document.addEventListener('DOMContentLoaded', ld);
else ld();
return function (fontname, text, fontsize) {
d.textContent = text;
d.style.font = 'bold ' + fontsize + 'px ' + fontname;
return d.clientWidth + config.space;
};
};
// 检查使用哪个测量文字宽度的方法
if (config.use_canvas === null) {
if (navigator.platform.match(/linux/i) &&
!navigator.userAgent.match(/chrome/i)) config.use_canvas = false;
}
debug('use canvas: %o', config.use_canvas !== false);
if (config.use_canvas === false) return calcWidthDiv();
return calcWidthCanvas();
}());
// 选择合适的字体
var choseFont = function (fontlist) {
// 检查这个字串的宽度来检查字体是否存在
var sampleText =
'The quick brown fox jumps over the lazy dog' +
'7531902468' + ',.!-' + ',。:!' +
'天地玄黄' + '則近道矣';
// 和这些字体进行比较
var sampleFont = [
'monospace', 'sans-serif', 'sans',
'Symbol', 'Arial', 'Comic Sans MS', 'Fixed', 'Terminal',
'Times', 'Times New Roman',
'宋体', '黑体', '文泉驿正黑', 'Microsoft YaHei'
];
// 如果被检查的字体和基准字体可以渲染出不同的宽度
// 那么说明被检查的字体总是存在的
var diffFont = function (base, test) {
var baseSize = calcWidth(base, sampleText, 72);
var testSize = calcWidth(test + ',' + base, sampleText, 72);
return baseSize !== testSize;
};
var validFont = function (test) {
var valid = sampleFont.some(function (base) {
return diffFont(base, test);
});
debug('font %s: %o', test, valid);
return valid;
};
// 找一个能用的字体
var f = fontlist[fontlist.length - 1];
fontlist = fontlist.filter(validFont);
debug('fontlist: %o', fontlist);
return fontlist[0] || f;
};
// 从备选的字体中选择一个机器上提供了的字体
var initFont = (function () {
var done = false;
return function () {
if (done) return; done = true;
calcWidth = calcWidth.bind(window,
config.font = choseFont(config.fontlist)
);
};
}());
var generateASS = function (danmaku, info) {
var assHeader = fillStr(
'[Script Info]\\nTitle: {{title}}\\nOriginal Script: \\u6839\\u636E {{ori}} \\u7684\\u5F39\\u5E55\\u4FE1\\u606F\\uFF0C\\u7531 https://github.com/tiansh/us-danmaku \\u751F\\u6210\\nScriptType: v4.00+\\nCollisions: Normal\\nPlayResX: {{playResX}}\\nPlayResY: {{playResY}}\\nTimer: 10.0000\\n\\n[V4+ Styles]\\nFormat: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\\nStyle: Fix,{{font}},{{font_size}},&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,{{bold}},0,0,0,100,100,0,0,1,2,0,2,20,20,2,0\\nStyle: R2L,{{font}},{{font_size}},&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,{{bold}},0,0,0,100,100,0,0,1,2,0,2,20,20,2,0\\n\\n[Events]\\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\\n',
{'alpha': hexAlpha(config.opacity), 'font_size': 25 * config.font_size }, config, info
);
// 补齐数字开头的0
var paddingNum = function (num, len) {
num = '' + num;
while (num.length < len) num = '0' + num;
return num;
};
// 格式化时间
var formatTime = function (time) {
time = 100 * time ^ 0;
var l = [[100, 2], [60, 2], [60, 2], [Infinity, 0]].map(function (c) {
var r = time % c[0];
time = (time - r) / c[0];
return paddingNum(r, c[1]);
}).reverse();
return l.slice(0, -1).join(':') + '.' + l[3];
};
// 格式化特效
var format = (function () {
// 适用于所有弹幕
var common = function (line) {
var s = '';
var rgb = line.color.split(/(..)/).filter(function (x) { return x; })
.map(function (x) { return parseInt(x, 16); });
// 如果不是白色,要指定弹幕特殊的颜色
if (line.color !== 'FFFFFF') // line.color 是 RRGGBB 格式
s += '\\\\c&H' + line.color.split(/(..)/).reverse().join('');
// 如果弹幕颜色比较深,用白色的外边框
var dark = rgb[0] * 0.299 + rgb[1] * 0.587 + rgb[2] * 0.114 < 0x30;
if (dark) s += '\\\\3c&HFFFFFF';
if (line.size !== 25) s += '\\\\fs' + line.size;
return s;
};
// 适用于从右到左弹幕
var r2l = function (line) {
return '\\\\move(' + [
line.poss.x, line.poss.y, line.posd.x, line.posd.y
].join(',') + ')';
};
// 适用于固定位置弹幕
var fix = function (line) {
return '\\\\pos(' + [
line.poss.x, line.poss.y
].join(',') + ')';
};
var withCommon = function (f) {
return function (line) { return f(line) + common(line); };
};
return {
'R2L': withCommon(r2l),
'Fix': withCommon(fix),
};
}());
// 转义一些字符
var escapeAssText = function (s) {
// "{"、"}"字符libass可以转义,但是VSFilter不可以,所以直接用全角补上
return s.replace(/{/g, '{').replace(/}/g, '}').replace(/\\r|\\n/g, '');
};
// 将一行转换为ASS的事件
var convert2Ass = function (line) {
return 'Dialogue: ' + [
0,
formatTime(line.stime),
formatTime(line.dtime),
line.type,
',20,20,2,,',
].join(',')
+ '{' + format[line.type](line) + '}'
+ escapeAssText(line.text);
};
return assHeader +
danmaku.map(convert2Ass)
.filter(function (x) { return x; })
.join('\\n');
};
/*
下文字母含义:
0 ||----------------------x---------------------->
_____________________c_____________________
= / wc \\ 0
| | |--v--| wv | |--v--|
| d |--v--| d f |--v--|
y |--v--| l f | s _ p
| | VIDEO |--v--| |--v--| _ m
v | AREA (x ^ y) |
v: 弹幕
c: 屏幕
0: 弹幕发送
a: 可行方案
s: 开始出现
f: 出现完全
l: 开始消失
d: 消失完全
p: 上边缘(含)
m: 下边缘(不含)
w: 宽度
h: 高度
b: 底端保留
t: 时间点
u: 时间段
r: 延迟
并规定
ts := t0s + r
tf := wv / (wc + ws) * p + ts
tl := ws / (wc + ws) * p + ts
td := p + ts
*/
// 滚动弹幕
var normalDanmaku = (function (wc, hc, b, u, maxr) {
return function () {
// 初始化屏幕外面是不可用的
var used = [
{ 'p': -Infinity, 'm': 0, 'tf': Infinity, 'td': Infinity, 'b': false },
{ 'p': hc, 'm': Infinity, 'tf': Infinity, 'td': Infinity, 'b': false },
{ 'p': hc - b, 'm': hc, 'tf': Infinity, 'td': Infinity, 'b': true },
];
// 检查一些可用的位置
var available = function (hv, t0s, t0l, b) {
var suggestion = [];
// 这些上边缘总之别的块的下边缘
used.forEach(function (i) {
if (i.m > hc) return;
var p = i.m;
var m = p + hv;
var tas = t0s;
var tal = t0l;
// 这些块的左边缘总是这个区域里面最大的边缘
used.forEach(function (j) {
if (j.p >= m) return;
if (j.m <= p) return;
if (j.b && b) return;
tas = Math.max(tas, j.tf);
tal = Math.max(tal, j.td);
});
// 最后作为一种备选留下来
suggestion.push({
'p': p,
'r': Math.max(tas - t0s, tal - t0l),
});
});
// 根据高度排序
suggestion.sort(function (x, y) { return x.p - y.p; });
var mr = maxr;
// 又靠右又靠下的选择可以忽略,剩下的返回
suggestion = suggestion.filter(function (i) {
if (i.r >= mr) return false;
mr = i.r;
return true;
});
return suggestion;
};
// 添加一个被使用的
var use = function (p, m, tf, td) {
used.push({ 'p': p, 'm': m, 'tf': tf, 'td': td, 'b': false });
};
// 根据时间同步掉无用的
var syn = function (t0s, t0l) {
used = used.filter(function (i) { return i.tf > t0s || i.td > t0l; });
};
// 给所有可能的位置打分,分数是[0, 1)的
var score = function (i) {
if (i.r > maxr) return -Infinity;
return 1 - hypot(i.r / maxr, i.p / hc) * Math.SQRT1_2;
};
// 添加一条
return function (t0s, wv, hv, b) {
var t0l = wc / (wv + wc) * u + t0s;
syn(t0s, t0l);
var al = available(hv, t0s, t0l, b);
if (!al.length) return null;
var scored = al.map(function (i) { return [score(i), i]; });
var best = scored.reduce(function (x, y) {
return x[0] > y[0] ? x : y;
})[1];
var ts = t0s + best.r;
var tf = wv / (wv + wc) * u + ts;
var td = u + ts;
use(best.p, best.p + hv, tf, td);
return {
'top': best.p,
'time': ts,
};
};
};
}(config.playResX, config.playResY, config.bottom, config.r2ltime, config.max_delay));
// 顶部、底部弹幕
var sideDanmaku = (function (hc, b, u, maxr) {
return function () {
var used = [
{ 'p': -Infinity, 'm': 0, 'td': Infinity, 'b': false },
{ 'p': hc, 'm': Infinity, 'td': Infinity, 'b': false },
{ 'p': hc - b, 'm': hc, 'td': Infinity, 'b': true },
];
// 查找可用的位置
var fr = function (p, m, t0s, b) {
var tas = t0s;
used.forEach(function (j) {
if (j.p >= m) return;
if (j.m <= p) return;
if (j.b && b) return;
tas = Math.max(tas, j.td);
});
return { 'r': tas - t0s, 'p': p, 'm': m };
};
// 顶部
var top = function (hv, t0s, b) {
var suggestion = [];
used.forEach(function (i) {
if (i.m > hc) return;
suggestion.push(fr(i.m, i.m + hv, t0s, b));
});
return suggestion;
};
// 底部
var bottom = function (hv, t0s, b) {
var suggestion = [];
used.forEach(function (i) {
if (i.p < 0) return;
suggestion.push(fr(i.p - hv, i.p, t0s, b));
});
return suggestion;
};
var use = function (p, m, td) {
used.push({ 'p': p, 'm': m, 'td': td, 'b': false });
};
var syn = function (t0s) {
used = used.filter(function (i) { return i.td > t0s; });
};
// 挑选最好的方案:延迟小的优先,位置不重要
var score = function (i, is_top) {
if (i.r > maxr) return -Infinity;
var f = function (p) { return is_top ? p : (hc - p); };
return 1 - (i.r / maxr * (31/32) + f(i.p) / hc * (1/32));
};
return function (t0s, hv, is_top, b) {
syn(t0s);
var al = (is_top ? top : bottom)(hv, t0s, b);
if (!al.length) return null;
var scored = al.map(function (i) { return [score(i, is_top), i]; });
var best = scored.reduce(function (x, y) {
return x[0] > y[0] ? x : y;
})[1];
use(best.p, best.m, best.r + t0s + u)
return { 'top': best.p, 'time': best.r + t0s };
};
};
}(config.playResY, config.bottom, config.fixtime, config.max_delay));
// 为每条弹幕安置位置
var setPosition = function (danmaku) {
var normal = normalDanmaku(), side = sideDanmaku();
return danmaku
.sort(function (x, y) { return x.time - y.time; })
.map(function (line) {
var font_size = Math.round(line.size * config.font_size);
var width = calcWidth(line.text, font_size);
switch (line.mode) {
case 'R2L': return (function () {
var pos = normal(line.time, width, font_size, line.bottom);
if (!pos) return null;
line.type = 'R2L';
line.stime = pos.time;
line.poss = {
'x': config.playResX + width / 2,
'y': pos.top + font_size,
};
line.posd = {
'x': -width / 2,
'y': pos.top + font_size,
};
line.dtime = config.r2ltime + line.stime;
return line;
}());
case 'TOP': case 'BOTTOM': return (function (isTop) {
var pos = side(line.time, font_size, isTop, line.bottom);
if (!pos) return null;
line.type = 'Fix';
line.stime = pos.time;
line.posd = line.poss = {
'x': Math.round(config.playResX / 2),
'y': pos.top + font_size,
};
line.dtime = config.fixtime + line.stime;
return line;
}(line.mode === 'TOP'));
default: return null;
};
})
.filter(function (l) { return l; })
.sort(function (x, y) { return x.stime - y.stime; });
};
/*
* bilibili
*/
// 获取xml
var fetchXML = function (cid, callback) {
GM_xmlhttpRequest({
'method': 'GET',
'url': 'http://comment.bilibili.com/{{cid}}.xml'.replace('{{cid}}', cid),
'onload': function (resp) {
var content = resp.responseText.replace(/(?:[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\uFFFE\\uFFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/g, "");
callback(content);
}
});
};
var fetchDanmaku = function (cid, callback) {
fetchXML(cid, function (content) {
callback(parseXML(content));
});
};
var parseXML = function (content) {
var data = (new DOMParser()).parseFromString(content, 'text/xml');
return Array.apply(Array, data.querySelectorAll('d')).map(function (line) {
var info = line.getAttribute('p').split(','), text = line.textContent;
return {
'text': text,
'time': Number(info[0]),
'mode': [undefined, 'R2L', 'R2L', 'R2L', 'BOTTOM', 'TOP'][Number(info[1])],
'size': Number(info[2]),
'color': RRGGBB(parseInt(info[3], 10) & 0xffffff),
'bottom': Number(info[5]) > 0,
// 'create': new Date(Number(info[4])),
// 'pool': Number(info[5]),
// 'sender': String(info[6]),
// 'dmid': Number(info[7]),
};
});
};
fetchXML = function (cid, callback) {
var oReq = new XMLHttpRequest();
oReq.open('GET', 'https://comment.bilibili.com/{{cid}}.xml'.replace('{{cid}}', cid));
oReq.onload = function () {
var content = oReq.responseText.replace(/(?:[\\0-\\x08\\x0B\\f\\x0E-\\x1F\\uFFFE\\uFFFF]|[\\uD800-\\uDBFF](?![\\uDC00-\\uDFFF])|(?:[^\\uD800-\\uDBFF]|^)[\\uDC00-\\uDFFF])/g, "");
callback(content);
};
oReq.send();
};
initFont();
return { fetchDanmaku: fetchDanmaku, generateASS: generateASS, setPosition: setPosition };
`)(option));
}
fetchDanmaku() { }
generateASS() { }
setPosition() { }
}
class MKVTransmuxer {
constructor(option) {
this.playerWin = null;
this.option = option;
}
exec(flv, ass, name) {
// 1. Allocate for a new window
if (!this.playerWin) this.playerWin = top.open('', undefined, ' ');
// 2. Inject scripts
this.playerWin.document.write(`
<p>
加载文件…… loading files...
<progress value="0" max="100" id="fileProgress"></progress>
</p>
<p>
构建mkv…… building mkv...
<progress value="0" max="100" id="mkvProgress"></progress>
</p>
<p>
<a id="a" download="merged.mkv">merged.mkv</a>
</p>
<script>
/**
* FLV + ASS => MKV transmuxer
* Demux FLV into H264 + AAC stream and ASS into line stream; then
* remux them into a MKV file.
*
* @author qli5 <goodlq11[at](163|gmail).com>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* The FLV demuxer is from flv.js <https://github.com/Bilibili/flv.js/>
* by zheng qian <[email protected]>, licensed under Apache 2.0.
*
* The EMBL builder is from simple-ebml-builder
* <https://www.npmjs.com/package/simple-ebml-builder> by ryiwamoto,
* licensed under MIT.
*/
// nodejs polyfill
if (typeof Blob == 'undefined') {
var Blob = class {
constructor(array) {
return Buffer.concat(array.map(Buffer.from.bind(Buffer)));
}
};
}
if (typeof TextEncoder == 'undefined') {
var TextEncoder = class {
/**
*
* @param {string} chunk
* @returns {Uint8Array}
*/
encode(chunk) {
return Buffer.from(chunk, 'utf-8');
}
}
}
if (typeof TextDecoder == 'undefined') {
const StringDecoder = require('string_decoder').StringDecoder;
var TextDecoder = class extends StringDecoder {
/**
*
* @param {ArrayBuffer} chunk
* @returns {string}
*/
decode(chunk) {
return this.end(Buffer.from(chunk));
}
}
}
/**
* The FLV demuxer is from flv.js
*
* Copyright (C) 2016 Bilibili. All Rights Reserved.
*
* @author zheng qian <[email protected]>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const FLVDemuxer = (() => {
// I browserified flv.js manually - so that I can know how it works
if (typeof navigator == 'undefined') navigator = {
userAgent: 'chrome',
}
// import FLVDemuxer from 'flv.js/src/demux/flv-demuxer';
// ..import Log from '../utils/logger.js';
const Log = {
e: console.error.bind(console),
w: console.warn.bind(console),
i: console.log.bind(console),
v: console.log.bind(console),
};
// ..import AMF from './amf-parser.js';
// ....import Log from '../utils/logger.js';
// ....import decodeUTF8 from '../utils/utf8-conv.js';
function checkContinuation(uint8array, start, checkLength) {
let array = uint8array;
if (start + checkLength < array.length) {
while (checkLength--) {
if ((array[++start] & 0xC0) !== 0x80)
return false;
}
return true;
} else {
return false;
}
}
function decodeUTF8(uint8array) {
let out = [];
let input = uint8array;
let i = 0;
let length = uint8array.length;
while (i < length) {
if (input[i] < 0x80) {
out.push(String.fromCharCode(input[i]));
++i;
continue;
} else if (input[i] < 0xC0) {
// fallthrough
} else if (input[i] < 0xE0) {
if (checkContinuation(input, i, 1)) {
let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F);
if (ucs4 >= 0x80) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 2;
continue;
}
}
} else if (input[i] < 0xF0) {
if (checkContinuation(input, i, 2)) {
let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F;
if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) {
out.push(String.fromCharCode(ucs4 & 0xFFFF));
i += 3;
continue;
}
}
} else if (input[i] < 0xF8) {
if (checkContinuation(input, i, 3)) {
let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12
| (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F);
if (ucs4 > 0x10000 && ucs4 < 0x110000) {
ucs4 -= 0x10000;
out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800));
out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00));
i += 4;
continue;
}
}
}
out.push(String.fromCharCode(0xFFFD));
++i;
}
return out.join('');
}
// ....import {IllegalStateException} from '../utils/exception.js';
class IllegalStateException extends Error { }
let le = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
class AMF {
static parseScriptData(arrayBuffer, dataOffset, dataSize) {
let data = {};
try {
let name = AMF.parseValue(arrayBuffer, dataOffset, dataSize);
let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
data[name.data] = value.data;
} catch (e) {
Log.e('AMF', e.toString());
}
return data;
}
static parseObject(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 3) {
throw new IllegalStateException('Data not enough when parse ScriptDataObject');
}
let name = AMF.parseString(arrayBuffer, dataOffset, dataSize);
let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);
let isObjectEnd = value.objectEnd;
return {
data: {
name: name.data,
value: value.data
},
size: name.size + value.size,
objectEnd: isObjectEnd
};
}
static parseVariable(arrayBuffer, dataOffset, dataSize) {
return AMF.parseObject(arrayBuffer, dataOffset, dataSize);
}
static parseString(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 2) {
throw new IllegalStateException('Data not enough when parse String');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let length = v.getUint16(0, !le);
let str;
if (length > 0) {
str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 2, length));
} else {
str = '';
}
return {
data: str,
size: 2 + length
};
}
static parseLongString(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 4) {
throw new IllegalStateException('Data not enough when parse LongString');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let length = v.getUint32(0, !le);
let str;
if (length > 0) {
str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 4, length));
} else {
str = '';
}
return {
data: str,
size: 4 + length
};
}
static parseDate(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 10) {
throw new IllegalStateException('Data size invalid when parse Date');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let timestamp = v.getFloat64(0, !le);
let localTimeOffset = v.getInt16(8, !le);
timestamp += localTimeOffset * 60 * 1000; // get UTC time
return {
data: new Date(timestamp),
size: 8 + 2
};
}
static parseValue(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 1) {
throw new IllegalStateException('Data not enough when parse Value');
}
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let offset = 1;
let type = v.getUint8(0);
let value;
let objectEnd = false;
try {
switch (type) {
case 0: // Number(Double) type
value = v.getFloat64(1, !le);
offset += 8;
break;
case 1: { // Boolean type
let b = v.getUint8(1);
value = b ? true : false;
offset += 1;
break;
}
case 2: { // String type
let amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
value = amfstr.data;
offset += amfstr.size;
break;
}
case 3: { // Object(s) type
value = {};
let terminal = 0; // workaround for malformed Objects which has missing ScriptDataObjectEnd
if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
terminal = 3;
}
while (offset < dataSize - 4) { // 4 === type(UI8) + ScriptDataObjectEnd(UI24)
let amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
if (amfobj.objectEnd)
break;
value[amfobj.data.name] = amfobj.data.value;
offset += amfobj.size;
}
if (offset <= dataSize - 3) {
let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
if (marker === 9) {
offset += 3;
}
}
break;
}
case 8: { // ECMA array type (Mixed array)
value = {};
offset += 4; // ECMAArrayLength(UI32)
let terminal = 0; // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd
if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {
terminal = 3;
}
while (offset < dataSize - 8) { // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24)
let amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);
if (amfvar.objectEnd)
break;
value[amfvar.data.name] = amfvar.data.value;
offset += amfvar.size;
}
if (offset <= dataSize - 3) {
let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;
if (marker === 9) {
offset += 3;
}
}
break;
}
case 9: // ScriptDataObjectEnd
value = undefined;
offset = 1;
objectEnd = true;
break;
case 10: { // Strict array type
// ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf
value = [];
let strictArrayLength = v.getUint32(1, !le);
offset += 4;
for (let i = 0; i < strictArrayLength; i++) {
let val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset);
value.push(val.data);
offset += val.size;
}
break;
}
case 11: { // Date type
let date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1);
value = date.data;
offset += date.size;
break;
}
case 12: { // Long string type
let amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);
value = amfLongStr.data;
offset += amfLongStr.size;
break;
}
default:
// ignore and skip
offset = dataSize;
Log.w('AMF', 'Unsupported AMF value type ' + type);
}
} catch (e) {
Log.e('AMF', e.toString());
}
return {
data: value,
size: offset,
objectEnd: objectEnd
};
}
}
// ..import SPSParser from './sps-parser.js';
// ....import ExpGolomb from './exp-golomb.js';
// ......import {IllegalStateException, InvalidArgumentException} from '../utils/exception.js';
class InvalidArgumentException extends Error { }
class ExpGolomb {
constructor(uint8array) {
this.TAG = 'ExpGolomb';
this._buffer = uint8array;
this._buffer_index = 0;
this._total_bytes = uint8array.byteLength;
this._total_bits = uint8array.byteLength * 8;
this._current_word = 0;
this._current_word_bits_left = 0;
}
destroy() {
this._buffer = null;
}
_fillCurrentWord() {
let buffer_bytes_left = this._total_bytes - this._buffer_index;
if (buffer_bytes_left <= 0)
throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available');
let bytes_read = Math.min(4, buffer_bytes_left);
let word = new Uint8Array(4);
word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));
this._current_word = new DataView(word.buffer).getUint32(0, false);
this._buffer_index += bytes_read;
this._current_word_bits_left = bytes_read * 8;
}
readBits(bits) {
if (bits > 32)
throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!');
if (bits <= this._current_word_bits_left) {
let result = this._current_word >>> (32 - bits);
this._current_word <<= bits;
this._current_word_bits_left -= bits;
return result;
}
let result = this._current_word_bits_left ? this._current_word : 0;
result = result >>> (32 - this._current_word_bits_left);
let bits_need_left = bits - this._current_word_bits_left;
this._fillCurrentWord();
let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);
let result2 = this._current_word >>> (32 - bits_read_next);
this._current_word <<= bits_read_next;
this._current_word_bits_left -= bits_read_next;
result = (result << bits_read_next) | result2;
return result;
}
readBool() {
return this.readBits(1) === 1;
}
readByte() {
return this.readBits(8);
}
_skipLeadingZero() {
let zero_count;
for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {
if (0 !== (this._current_word & (0x80000000 >>> zero_count))) {
this._current_word <<= zero_count;
this._current_word_bits_left -= zero_count;
return zero_count;
}
}
this._fillCurrentWord();
return zero_count + this._skipLeadingZero();
}
readUEG() { // unsigned exponential golomb
let leading_zeros = this._skipLeadingZero();
return this.readBits(leading_zeros + 1) - 1;
}
readSEG() { // signed exponential golomb
let value = this.readUEG();
if (value & 0x01) {
return (value + 1) >>> 1;
} else {
return -1 * (value >>> 1);
}
}
}
class SPSParser {
static _ebsp2rbsp(uint8array) {
let src = uint8array;
let src_length = src.byteLength;
let dst = new Uint8Array(src_length);
let dst_idx = 0;
for (let i = 0; i < src_length; i++) {
if (i >= 2) {
// Unescape: Skip 0x03 after 00 00
if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {
continue;
}
}
dst[dst_idx] = src[i];
dst_idx++;
}
return new Uint8Array(dst.buffer, 0, dst_idx);
}
static parseSPS(uint8array) {
let rbsp = SPSParser._ebsp2rbsp(uint8array);
let gb = new ExpGolomb(rbsp);
gb.readByte();
let profile_idc = gb.readByte(); // profile_idc
gb.readByte(); // constraint_set_flags[5] + reserved_zero[3]
let level_idc = gb.readByte(); // level_idc
gb.readUEG(); // seq_parameter_set_id
let profile_string = SPSParser.getProfileString(profile_idc);
let level_string = SPSParser.getLevelString(level_idc);
let chroma_format_idc = 1;
let chroma_format = 420;
let chroma_format_table = [0, 420, 422, 444];
let bit_depth = 8;
if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 ||
profile_idc === 244 || profile_idc === 44 || profile_idc === 83 ||
profile_idc === 86 || profile_idc === 118 || profile_idc === 128 ||
profile_idc === 138 || profile_idc === 144) {
chroma_format_idc = gb.readUEG();
if (chroma_format_idc === 3) {
gb.readBits(1); // separate_colour_plane_flag
}
if (chroma_format_idc <= 3) {
chroma_format = chroma_format_table[chroma_format_idc];
}
bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8
gb.readUEG(); // bit_depth_chroma_minus8
gb.readBits(1); // qpprime_y_zero_transform_bypass_flag
if (gb.readBool()) { // seq_scaling_matrix_present_flag
let scaling_list_count = (chroma_format_idc !== 3) ? 8 : 12;
for (let i = 0; i < scaling_list_count; i++) {
if (gb.readBool()) { // seq_scaling_list_present_flag
if (i < 6) {
SPSParser._skipScalingList(gb, 16);
} else {
SPSParser._skipScalingList(gb, 64);
}
}
}
}
}
gb.readUEG(); // log2_max_frame_num_minus4
let pic_order_cnt_type = gb.readUEG();
if (pic_order_cnt_type === 0) {
gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4
} else if (pic_order_cnt_type === 1) {
gb.readBits(1); // delta_pic_order_always_zero_flag
gb.readSEG(); // offset_for_non_ref_pic
gb.readSEG(); // offset_for_top_to_bottom_field
let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();
for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
gb.readSEG(); // offset_for_ref_frame
}
}
gb.readUEG(); // max_num_ref_frames
gb.readBits(1); // gaps_in_frame_num_value_allowed_flag
let pic_width_in_mbs_minus1 = gb.readUEG();
let pic_height_in_map_units_minus1 = gb.readUEG();
let frame_mbs_only_flag = gb.readBits(1);
if (frame_mbs_only_flag === 0) {
gb.readBits(1); // mb_adaptive_frame_field_flag
}
gb.readBits(1); // direct_8x8_inference_flag
let frame_crop_left_offset = 0;
let frame_crop_right_offset = 0;
let frame_crop_top_offset = 0;
let frame_crop_bottom_offset = 0;
let frame_cropping_flag = gb.readBool();
if (frame_cropping_flag) {
frame_crop_left_offset = gb.readUEG();
frame_crop_right_offset = gb.readUEG();
frame_crop_top_offset = gb.readUEG();
frame_crop_bottom_offset = gb.readUEG();
}
let sar_width = 1, sar_height = 1;
let fps = 0, fps_fixed = true, fps_num = 0, fps_den = 0;
let vui_parameters_present_flag = gb.readBool();
if (vui_parameters_present_flag) {
if (gb.readBool()) { // aspect_ratio_info_present_flag
let aspect_ratio_idc = gb.readByte();
let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];
let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1];
if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {
sar_width = sar_w_table[aspect_ratio_idc - 1];
sar_height = sar_h_table[aspect_ratio_idc - 1];
} else if (aspect_ratio_idc === 255) {
sar_width = gb.readByte() << 8 | gb.readByte();
sar_height = gb.readByte() << 8 | gb.readByte();
}
}
if (gb.readBool()) { // overscan_info_present_flag
gb.readBool(); // overscan_appropriate_flag
}
if (gb.readBool()) { // video_signal_type_present_flag
gb.readBits(4); // video_format & video_full_range_flag
if (gb.readBool()) { // colour_description_present_flag
gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients
}
}
if (gb.readBool()) { // chroma_loc_info_present_flag
gb.readUEG(); // chroma_sample_loc_type_top_field
gb.readUEG(); // chroma_sample_loc_type_bottom_field
}
if (gb.readBool()) { // timing_info_present_flag
let num_units_in_tick = gb.readBits(32);
let time_scale = gb.readBits(32);
fps_fixed = gb.readBool(); // fixed_frame_rate_flag
fps_num = time_scale;
fps_den = num_units_in_tick * 2;
fps = fps_num / fps_den;
}
}
let sarScale = 1;
if (sar_width !== 1 || sar_height !== 1) {
sarScale = sar_width / sar_height;
}
let crop_unit_x = 0, crop_unit_y = 0;
if (chroma_format_idc === 0) {
crop_unit_x = 1;
crop_unit_y = 2 - frame_mbs_only_flag;
} else {
let sub_wc = (chroma_format_idc === 3) ? 1 : 2;
let sub_hc = (chroma_format_idc === 1) ? 2 : 1;
crop_unit_x = sub_wc;
crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);
}
let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;
let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);
codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;
codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;
let present_width = Math.ceil(codec_width * sarScale);
gb.destroy();
gb = null;
return {
profile_string: profile_string, // baseline, high, high10, ...
level_string: level_string, // 3, 3.1, 4, 4.1, 5, 5.1, ...
bit_depth: bit_depth, // 8bit, 10bit, ...
chroma_format: chroma_format, // 4:2:0, 4:2:2, ...
chroma_format_string: SPSParser.getChromaFormatString(chroma_format),
frame_rate: {
fixed: fps_fixed,
fps: fps,
fps_den: fps_den,
fps_num: fps_num
},
sar_ratio: {
width: sar_width,
height: sar_height
},
codec_size: {
width: codec_width,
height: codec_height
},
present_size: {
width: present_width,
height: codec_height
}
};
}
static _skipScalingList(gb, count) {
let last_scale = 8, next_scale = 8;
let delta_scale = 0;
for (let i = 0; i < count; i++) {
if (next_scale !== 0) {
delta_scale = gb.readSEG();
next_scale = (last_scale + delta_scale + 256) % 256;
}
last_scale = (next_scale === 0) ? last_scale : next_scale;
}
}
static getProfileString(profile_idc) {
switch (profile_idc) {
case 66:
return 'Baseline';
case 77:
return 'Main';
case 88:
return 'Extended';
case 100:
return 'High';
case 110:
return 'High10';
case 122:
return 'High422';
case 244:
return 'High444';
default:
return 'Unknown';
}
}
static getLevelString(level_idc) {
return (level_idc / 10).toFixed(1);
}
static getChromaFormatString(chroma) {
switch (chroma) {
case 420:
return '4:2:0';
case 422:
return '4:2:2';
case 444:
return '4:4:4';
default:
return 'Unknown';
}
}
}
// ..import DemuxErrors from './demux-errors.js';
const DemuxErrors = {
OK: 'OK',
FORMAT_ERROR: 'FormatError',
FORMAT_UNSUPPORTED: 'FormatUnsupported',
CODEC_UNSUPPORTED: 'CodecUnsupported'
};
// ..import MediaInfo from '../core/media-info.js';
class MediaInfo {
constructor() {
this.mimeType = null;
this.duration = null;
this.hasAudio = null;
this.hasVideo = null;
this.audioCodec = null;
this.videoCodec = null;
this.audioDataRate = null;
this.videoDataRate = null;
this.audioSampleRate = null;
this.audioChannelCount = null;
this.width = null;
this.height = null;
this.fps = null;
this.profile = null;
this.level = null;
this.chromaFormat = null;
this.sarNum = null;
this.sarDen = null;
this.metadata = null;
this.segments = null; // MediaInfo[]
this.segmentCount = null;
this.hasKeyframesIndex = null;
this.keyframesIndex = null;
}
isComplete() {
let audioInfoComplete = (this.hasAudio === false) ||
(this.hasAudio === true &&
this.audioCodec != null &&
this.audioSampleRate != null &&
this.audioChannelCount != null);
let videoInfoComplete = (this.hasVideo === false) ||
(this.hasVideo === true &&
this.videoCodec != null &&
this.width != null &&
this.height != null &&
this.fps != null &&
this.profile != null &&
this.level != null &&
this.chromaFormat != null &&
this.sarNum != null &&
this.sarDen != null);
// keyframesIndex may not be present
return this.mimeType != null &&
this.duration != null &&
this.metadata != null &&
this.hasKeyframesIndex != null &&
audioInfoComplete &&
videoInfoComplete;
}
isSeekable() {
return this.hasKeyframesIndex === true;
}
getNearestKeyframe(milliseconds) {
if (this.keyframesIndex == null) {
return null;
}
let table = this.keyframesIndex;
let keyframeIdx = this._search(table.times, milliseconds);
return {
index: keyframeIdx,
milliseconds: table.times[keyframeIdx],
fileposition: table.filepositions[keyframeIdx]
};
}
_search(list, value) {
let idx = 0;
let last = list.length - 1;
let mid = 0;
let lbound = 0;
let ubound = last;
if (value < list[0]) {
idx = 0;
lbound = ubound + 1; // skip search
}
while (lbound <= ubound) {
mid = lbound + Math.floor((ubound - lbound) / 2);
if (mid === last || (value >= list[mid] && value < list[mid + 1])) {
idx = mid;
break;
} else if (list[mid] < value) {
lbound = mid + 1;
} else {
ubound = mid - 1;
}
}
return idx;
}
}
function Swap16(src) {
return (((src >>> 8) & 0xFF) |
((src & 0xFF) << 8));
}
function Swap32(src) {
return (((src & 0xFF000000) >>> 24) |
((src & 0x00FF0000) >>> 8) |
((src & 0x0000FF00) << 8) |
((src & 0x000000FF) << 24));
}
function ReadBig32(array, index) {
return ((array[index] << 24) |
(array[index + 1] << 16) |
(array[index + 2] << 8) |
(array[index + 3]));
}
class FLVDemuxer {
/**
* Create a new FLV demuxer
* @param {Object} probeData
* @param {boolean} probeData.match
* @param {number} probeData.consumed
* @param {number} probeData.dataOffset
* @param {booleam} probeData.hasAudioTrack
* @param {boolean} probeData.hasVideoTrack
* @param {*} config
*/
constructor(probeData, config) {
this.TAG = 'FLVDemuxer';
this._config = config;
this._onError = null;
this._onMediaInfo = null;
this._onTrackMetadata = null;
this._onDataAvailable = null;
this._dataOffset = probeData.dataOffset;
this._firstParse = true;
this._dispatch = false;
this._hasAudio = probeData.hasAudioTrack;
this._hasVideo = probeData.hasVideoTrack;
this._hasAudioFlagOverrided = false;
this._hasVideoFlagOverrided = false;
this._audioInitialMetadataDispatched = false;
this._videoInitialMetadataDispatched = false;
this._mediaInfo = new MediaInfo();
this._mediaInfo.hasAudio = this._hasAudio;
this._mediaInfo.hasVideo = this._hasVideo;
this._metadata = null;
this._audioMetadata = null;
this._videoMetadata = null;
this._naluLengthSize = 4;
this._timestampBase = 0; // int32, in milliseconds
this._timescale = 1000;
this._duration = 0; // int32, in milliseconds
this._durationOverrided = false;
this._referenceFrameRate = {
fixed: true,
fps: 23.976,
fps_num: 23976,
fps_den: 1000
};
this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];
this._mpegSamplingRates = [
96000, 88200, 64000, 48000, 44100, 32000,
24000, 22050, 16000, 12000, 11025, 8000, 7350
];
this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0];
this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0];
this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0];
this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1];
this._mpegAudioL2BitRateTable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1];
this._mpegAudioL3BitRateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1];
this._videoTrack = { type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0 };
this._audioTrack = { type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0 };
this._littleEndian = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
}
destroy() {
this._mediaInfo = null;
this._metadata = null;
this._audioMetadata = null;
this._videoMetadata = null;
this._videoTrack = null;
this._audioTrack = null;
this._onError = null;
this._onMediaInfo = null;
this._onTrackMetadata = null;
this._onDataAvailable = null;
}
/**
* Probe the flv data
* @param {ArrayBuffer} buffer
* @returns {Object} - probeData to be feed into constructor
*/
static probe(buffer) {
let data = new Uint8Array(buffer);
let mismatch = { match: false };
if (data[0] !== 0x46 || data[1] !== 0x4C || data[2] !== 0x56 || data[3] !== 0x01) {
return mismatch;
}
let hasAudio = ((data[4] & 4) >>> 2) !== 0;
let hasVideo = (data[4] & 1) !== 0;
let offset = ReadBig32(data, 5);
if (offset < 9) {
return mismatch;
}
return {
match: true,
consumed: offset,
dataOffset: offset,
hasAudioTrack: hasAudio,
hasVideoTrack: hasVideo
};
}
bindDataSource(loader) {
loader.onDataArrival = this.parseChunks.bind(this);
return this;
}
// prototype: function(type: string, metadata: any): void
get onTrackMetadata() {
return this._onTrackMetadata;
}
set onTrackMetadata(callback) {
this._onTrackMetadata = callback;
}
// prototype: function(mediaInfo: MediaInfo): void
get onMediaInfo() {
return this._onMediaInfo;
}
set onMediaInfo(callback) {
this._onMediaInfo = callback;
}
// prototype: function(type: number, info: string): void
get onError() {
return this._onError;
}
set onError(callback) {
this._onError = callback;
}
// prototype: function(videoTrack: any, audioTrack: any): void
get onDataAvailable() {
return this._onDataAvailable;
}
set onDataAvailable(callback) {
this._onDataAvailable = callback;
}
// timestamp base for output samples, must be in milliseconds
get timestampBase() {
return this._timestampBase;
}
set timestampBase(base) {
this._timestampBase = base;
}
get overridedDuration() {
return this._duration;
}
// Force-override media duration. Must be in milliseconds, int32
set overridedDuration(duration) {
this._durationOverrided = true;
this._duration = duration;
this._mediaInfo.duration = duration;
}
// Force-override audio track present flag, boolean
set overridedHasAudio(hasAudio) {
this._hasAudioFlagOverrided = true;
this._hasAudio = hasAudio;
this._mediaInfo.hasAudio = hasAudio;
}
// Force-override video track present flag, boolean
set overridedHasVideo(hasVideo) {
this._hasVideoFlagOverrided = true;
this._hasVideo = hasVideo;
this._mediaInfo.hasVideo = hasVideo;
}
resetMediaInfo() {
this._mediaInfo = new MediaInfo();
}
_isInitialMetadataDispatched() {
if (this._hasAudio && this._hasVideo) { // both audio & video
return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched;
}
if (this._hasAudio && !this._hasVideo) { // audio only
return this._audioInitialMetadataDispatched;
}
if (!this._hasAudio && this._hasVideo) { // video only
return this._videoInitialMetadataDispatched;
}
return false;
}
// function parseChunks(chunk: ArrayBuffer, byteStart: number): number;
parseChunks(chunk, byteStart) {
if (!this._onError || !this._onMediaInfo || !this._onTrackMetadata || !this._onDataAvailable) {
throw new IllegalStateException('Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified');
}
// qli5: fix nonzero byteStart
let offset = byteStart || 0;
let le = this._littleEndian;
if (byteStart === 0) { // buffer with FLV header
if (chunk.byteLength > 13) {
let probeData = FLVDemuxer.probe(chunk);
offset = probeData.dataOffset;
} else {
return 0;
}
}
if (this._firstParse) { // handle PreviousTagSize0 before Tag1
this._firstParse = false;
if (offset !== this._dataOffset) {
Log.w(this.TAG, 'First time parsing but chunk byteStart invalid!');
}
let v = new DataView(chunk, offset);
let prevTagSize0 = v.getUint32(0, !le);
if (prevTagSize0 !== 0) {
Log.w(this.TAG, 'PrevTagSize0 !== 0 !!!');
}
offset += 4;
}
while (offset < chunk.byteLength) {
this._dispatch = true;
let v = new DataView(chunk, offset);
if (offset + 11 + 4 > chunk.byteLength) {
// data not enough for parsing an flv tag
break;
}
let tagType = v.getUint8(0);
let dataSize = v.getUint32(0, !le) & 0x00FFFFFF;
if (offset + 11 + dataSize + 4 > chunk.byteLength) {
// data not enough for parsing actual data body
break;
}
if (tagType !== 8 && tagType !== 9 && tagType !== 18) {
Log.w(this.TAG, \`Unsupported tag type \${tagType}, skipped\`);
// consume the whole tag (skip it)
offset += 11 + dataSize + 4;
continue;
}
let ts2 = v.getUint8(4);
let ts1 = v.getUint8(5);
let ts0 = v.getUint8(6);
let ts3 = v.getUint8(7);
let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);
let streamId = v.getUint32(7, !le) & 0x00FFFFFF;
if (streamId !== 0) {
Log.w(this.TAG, 'Meet tag which has StreamID != 0!');
}
let dataOffset = offset + 11;
switch (tagType) {
case 8: // Audio
this._parseAudioData(chunk, dataOffset, dataSize, timestamp);
break;
case 9: // Video
this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);
break;
case 18: // ScriptDataObject
this._parseScriptData(chunk, dataOffset, dataSize);
break;
}
let prevTagSize = v.getUint32(11 + dataSize, !le);
if (prevTagSize !== 11 + dataSize) {
Log.w(this.TAG, \`Invalid PrevTagSize \${prevTagSize}\`);
}
offset += 11 + dataSize + 4; // tagBody + dataSize + prevTagSize
}
// dispatch parsed frames to consumer (typically, the remuxer)
if (this._isInitialMetadataDispatched()) {
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
}
return offset; // consumed bytes, just equals latest offset index
}
_parseScriptData(arrayBuffer, dataOffset, dataSize) {
let scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);
if (scriptData.hasOwnProperty('onMetaData')) {
if (scriptData.onMetaData == null || typeof scriptData.onMetaData !== 'object') {
Log.w(this.TAG, 'Invalid onMetaData structure!');
return;
}
if (this._metadata) {
Log.w(this.TAG, 'Found another onMetaData tag!');
}
this._metadata = scriptData;
let onMetaData = this._metadata.onMetaData;
if (typeof onMetaData.hasAudio === 'boolean') { // hasAudio
if (this._hasAudioFlagOverrided === false) {
this._hasAudio = onMetaData.hasAudio;
this._mediaInfo.hasAudio = this._hasAudio;
}
}
if (typeof onMetaData.hasVideo === 'boolean') { // hasVideo
if (this._hasVideoFlagOverrided === false) {
this._hasVideo = onMetaData.hasVideo;
this._mediaInfo.hasVideo = this._hasVideo;
}
}
if (typeof onMetaData.audiodatarate === 'number') { // audiodatarate
this._mediaInfo.audioDataRate = onMetaData.audiodatarate;
}
if (typeof onMetaData.videodatarate === 'number') { // videodatarate
this._mediaInfo.videoDataRate = onMetaData.videodatarate;
}
if (typeof onMetaData.width === 'number') { // width
this._mediaInfo.width = onMetaData.width;
}
if (typeof onMetaData.height === 'number') { // height
this._mediaInfo.height = onMetaData.height;
}
if (typeof onMetaData.duration === 'number') { // duration
if (!this._durationOverrided) {
let duration = Math.floor(onMetaData.duration * this._timescale);
this._duration = duration;
this._mediaInfo.duration = duration;
}
} else {
this._mediaInfo.duration = 0;
}
if (typeof onMetaData.framerate === 'number') { // framerate
let fps_num = Math.floor(onMetaData.framerate * 1000);
if (fps_num > 0) {
let fps = fps_num / 1000;
this._referenceFrameRate.fixed = true;
this._referenceFrameRate.fps = fps;
this._referenceFrameRate.fps_num = fps_num;
this._referenceFrameRate.fps_den = 1000;
this._mediaInfo.fps = fps;
}
}
if (typeof onMetaData.keyframes === 'object') { // keyframes
this._mediaInfo.hasKeyframesIndex = true;
let keyframes = onMetaData.keyframes;
this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes);
onMetaData.keyframes = null; // keyframes has been extracted, remove it
} else {
this._mediaInfo.hasKeyframesIndex = false;
}
this._dispatch = false;
this._mediaInfo.metadata = onMetaData;
Log.v(this.TAG, 'Parsed onMetaData');
if (this._mediaInfo.isComplete()) {
this._onMediaInfo(this._mediaInfo);
}
}
}
_parseKeyframesIndex(keyframes) {
let times = [];
let filepositions = [];
// ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord)
for (let i = 1; i < keyframes.times.length; i++) {
let time = this._timestampBase + Math.floor(keyframes.times[i] * 1000);
times.push(time);
filepositions.push(keyframes.filepositions[i]);
}
return {
times: times,
filepositions: filepositions
};
}
_parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid audio packet, missing SoundData payload!');
return;
}
if (this._hasAudioFlagOverrided === true && this._hasAudio === false) {
// If hasAudio: false indicated explicitly in MediaDataSource,
// Ignore all the audio packets
return;
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let soundSpec = v.getUint8(0);
let soundFormat = soundSpec >>> 4;
if (soundFormat !== 2 && soundFormat !== 10) { // MP3 or AAC
this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat);
return;
}
let soundRate = 0;
let soundRateIndex = (soundSpec & 12) >>> 2;
if (soundRateIndex >= 0 && soundRateIndex <= 4) {
soundRate = this._flvSoundRateTable[soundRateIndex];
} else {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex);
return;
}
let soundSize = (soundSpec & 2) >>> 1; // unused
let soundType = (soundSpec & 1);
let meta = this._audioMetadata;
let track = this._audioTrack;
if (!meta) {
if (this._hasAudio === false && this._hasAudioFlagOverrided === false) {
this._hasAudio = true;
this._mediaInfo.hasAudio = true;
}
// initial metadata
meta = this._audioMetadata = {};
meta.type = 'audio';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
meta.audioSampleRate = soundRate;
meta.channelCount = (soundType === 0 ? 1 : 2);
}
if (soundFormat === 10) { // AAC
let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1);
if (aacData == undefined) {
return;
}
if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig)
if (meta.config) {
Log.w(this.TAG, 'Found another AudioSpecificConfig!');
}
let misc = aacData.data;
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
meta.config = misc.config;
// added by qli5
meta.configRaw = misc.configRaw;
// The decode result of an aac sample is 1024 PCM samples
meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale;
Log.v(this.TAG, 'Parsed AudioSpecificConfig');
if (this._isInitialMetadataDispatched()) {
// Non-initial metadata, force dispatch (or flush) parsed frames to remuxer
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
} else {
this._audioInitialMetadataDispatched = true;
}
// then notify new metadata
this._dispatch = false;
this._onTrackMetadata('audio', meta);
let mi = this._mediaInfo;
mi.audioCodec = meta.originalCodec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
if (mi.hasVideo) {
if (mi.videoCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
} else if (aacData.packetType === 1) { // AAC raw frame data
let dts = this._timestampBase + tagTimestamp;
let aacSample = { unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts };
track.samples.push(aacSample);
track.length += aacData.data.length;
} else {
Log.e(this.TAG, \`Flv: Unsupported AAC data type \${aacData.packetType}\`);
}
} else if (soundFormat === 2) { // MP3
if (!meta.codec) {
// We need metadata for mp3 audio track, extract info from frame header
let misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true);
if (misc == undefined) {
return;
}
meta.audioSampleRate = misc.samplingRate;
meta.channelCount = misc.channelCount;
meta.codec = misc.codec;
meta.originalCodec = misc.originalCodec;
// The decode result of an mp3 sample is 1152 PCM samples
meta.refSampleDuration = 1152 / meta.audioSampleRate * meta.timescale;
Log.v(this.TAG, 'Parsed MPEG Audio Frame Header');
this._audioInitialMetadataDispatched = true;
this._onTrackMetadata('audio', meta);
let mi = this._mediaInfo;
mi.audioCodec = meta.codec;
mi.audioSampleRate = meta.audioSampleRate;
mi.audioChannelCount = meta.channelCount;
mi.audioDataRate = misc.bitRate;
if (mi.hasVideo) {
if (mi.videoCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
}
// This packet is always a valid audio packet, extract it
let data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false);
if (data == undefined) {
return;
}
let dts = this._timestampBase + tagTimestamp;
let mp3Sample = { unit: data, length: data.byteLength, dts: dts, pts: dts };
track.samples.push(mp3Sample);
track.length += data.length;
}
}
_parseAACAudioData(arrayBuffer, dataOffset, dataSize) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!');
return;
}
let result = {};
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
result.packetType = array[0];
if (array[0] === 0) {
result.data = this._parseAACAudioSpecificConfig(arrayBuffer, dataOffset + 1, dataSize - 1);
} else {
result.data = array.subarray(1);
}
return result;
}
_parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let config = null;
/* Audio Object Type:
0: Null
1: AAC Main
2: AAC LC
3: AAC SSR (Scalable Sample Rate)
4: AAC LTP (Long Term Prediction)
5: HE-AAC / SBR (Spectral Band Replication)
6: AAC Scalable
*/
let audioObjectType = 0;
let originalAudioObjectType = 0;
let audioExtensionObjectType = null;
let samplingIndex = 0;
let extensionSamplingIndex = null;
// 5 bits
audioObjectType = originalAudioObjectType = array[0] >>> 3;
// 4 bits
samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);
if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!');
return;
}
let samplingFrequence = this._mpegSamplingRates[samplingIndex];
// 4 bits
let channelConfig = (array[1] & 0x78) >>> 3;
if (channelConfig < 0 || channelConfig >= 8) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid channel configuration');
return;
}
if (audioObjectType === 5) { // HE-AAC?
// 4 bits
extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);
// 5 bits
audioExtensionObjectType = (array[2] & 0x7C) >>> 2;
}
// workarounds for various browsers
let userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf('firefox') !== -1) {
// firefox: use SBR (HE-AAC) if freq less than 24kHz
if (samplingIndex >= 6) {
audioObjectType = 5;
config = new Array(4);
extensionSamplingIndex = samplingIndex - 3;
} else { // use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
} else if (userAgent.indexOf('android') !== -1) {
// android: always use LC-AAC
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
} else {
// for other browsers, e.g. chrome...
// Always use HE-AAC to make it easier to switch aac codec profile
audioObjectType = 5;
extensionSamplingIndex = samplingIndex;
config = new Array(4);
if (samplingIndex >= 6) {
extensionSamplingIndex = samplingIndex - 3;
} else if (channelConfig === 1) { // Mono channel
audioObjectType = 2;
config = new Array(2);
extensionSamplingIndex = samplingIndex;
}
}
config[0] = audioObjectType << 3;
config[0] |= (samplingIndex & 0x0F) >>> 1;
config[1] = (samplingIndex & 0x0F) << 7;
config[1] |= (channelConfig & 0x0F) << 3;
if (audioObjectType === 5) {
config[1] |= ((extensionSamplingIndex & 0x0F) >>> 1);
config[2] = (extensionSamplingIndex & 0x01) << 7;
// extended audio object type: force to 2 (LC-AAC)
config[2] |= (2 << 2);
config[3] = 0;
}
return {
// configRaw: added by qli5
configRaw: array,
config: config,
samplingRate: samplingFrequence,
channelCount: channelConfig,
codec: 'mp4a.40.' + audioObjectType,
originalCodec: 'mp4a.40.' + originalAudioObjectType
};
}
_parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) {
if (dataSize < 4) {
Log.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!');
return;
}
let le = this._littleEndian;
let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);
let result = null;
if (requestHeader) {
if (array[0] !== 0xFF) {
return;
}
let ver = (array[1] >>> 3) & 0x03;
let layer = (array[1] & 0x06) >> 1;
let bitrate_index = (array[2] & 0xF0) >>> 4;
let sampling_freq_index = (array[2] & 0x0C) >>> 2;
let channel_mode = (array[3] >>> 6) & 0x03;
let channel_count = channel_mode !== 3 ? 2 : 1;
let sample_rate = 0;
let bit_rate = 0;
let object_type = 34; // Layer-3, listed in MPEG-4 Audio Object Types
let codec = 'mp3';
switch (ver) {
case 0: // MPEG 2.5
sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index];
break;
case 2: // MPEG 2
sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index];
break;
case 3: // MPEG 1
sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index];
break;
}
switch (layer) {
case 1: // Layer 3
object_type = 34;
if (bitrate_index < this._mpegAudioL3BitRateTable.length) {
bit_rate = this._mpegAudioL3BitRateTable[bitrate_index];
}
break;
case 2: // Layer 2
object_type = 33;
if (bitrate_index < this._mpegAudioL2BitRateTable.length) {
bit_rate = this._mpegAudioL2BitRateTable[bitrate_index];
}
break;
case 3: // Layer 1
object_type = 32;
if (bitrate_index < this._mpegAudioL1BitRateTable.length) {
bit_rate = this._mpegAudioL1BitRateTable[bitrate_index];
}
break;
}
result = {
bitRate: bit_rate,
samplingRate: sample_rate,
channelCount: channel_count,
codec: codec,
originalCodec: codec
};
} else {
result = array;
}
return result;
}
_parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) {
if (dataSize <= 1) {
Log.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!');
return;
}
if (this._hasVideoFlagOverrided === true && this._hasVideo === false) {
// If hasVideo: false indicated explicitly in MediaDataSource,
// Ignore all the video packets
return;
}
let spec = (new Uint8Array(arrayBuffer, dataOffset, dataSize))[0];
let frameType = (spec & 240) >>> 4;
let codecId = spec & 15;
if (codecId !== 7) {
this._onError(DemuxErrors.CODEC_UNSUPPORTED, \`Flv: Unsupported codec in video frame: \${codecId}\`);
return;
}
this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, dataSize - 1, tagTimestamp, tagPosition, frameType);
}
_parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType) {
if (dataSize < 4) {
Log.w(this.TAG, 'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime');
return;
}
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let packetType = v.getUint8(0);
let cts = v.getUint32(0, !le) & 0x00FFFFFF;
if (packetType === 0) { // AVCDecoderConfigurationRecord
this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4);
} else if (packetType === 1) { // One or more Nalus
this._parseAVCVideoData(arrayBuffer, dataOffset + 4, dataSize - 4, tagTimestamp, tagPosition, frameType, cts);
} else if (packetType === 2) {
// empty, AVC end of sequence
} else {
this._onError(DemuxErrors.FORMAT_ERROR, \`Flv: Invalid video packet type \${packetType}\`);
return;
}
}
_parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {
if (dataSize < 7) {
Log.w(this.TAG, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');
return;
}
let meta = this._videoMetadata;
let track = this._videoTrack;
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
if (!meta) {
if (this._hasVideo === false && this._hasVideoFlagOverrided === false) {
this._hasVideo = true;
this._mediaInfo.hasVideo = true;
}
meta = this._videoMetadata = {};
meta.type = 'video';
meta.id = track.id;
meta.timescale = this._timescale;
meta.duration = this._duration;
} else {
if (typeof meta.avcc !== 'undefined') {
Log.w(this.TAG, 'Found another AVCDecoderConfigurationRecord!');
}
}
let version = v.getUint8(0); // configurationVersion
let avcProfile = v.getUint8(1); // avcProfileIndication
let profileCompatibility = v.getUint8(2); // profile_compatibility
let avcLevel = v.getUint8(3); // AVCLevelIndication
if (version !== 1 || avcProfile === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');
return;
}
this._naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne
if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) { // holy shit!!!
this._onError(DemuxErrors.FORMAT_ERROR, \`Flv: Strange NaluLengthSizeMinusOne: \${this._naluLengthSize - 1}\`);
return;
}
let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets
if (spsCount === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');
return;
} else if (spsCount > 1) {
Log.w(this.TAG, \`Flv: Strange AVCDecoderConfigurationRecord: SPS Count = \${spsCount}\`);
}
let offset = 6;
for (let i = 0; i < spsCount; i++) {
let len = v.getUint16(offset, !le); // sequenceParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
// Notice: Nalu without startcode header (00 00 00 01)
let sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);
offset += len;
let config = SPSParser.parseSPS(sps);
if (i !== 0) {
// ignore other sps's config
continue;
}
meta.codecWidth = config.codec_size.width;
meta.codecHeight = config.codec_size.height;
meta.presentWidth = config.present_size.width;
meta.presentHeight = config.present_size.height;
meta.profile = config.profile_string;
meta.level = config.level_string;
meta.bitDepth = config.bit_depth;
meta.chromaFormat = config.chroma_format;
meta.sarRatio = config.sar_ratio;
meta.frameRate = config.frame_rate;
if (config.frame_rate.fixed === false ||
config.frame_rate.fps_num === 0 ||
config.frame_rate.fps_den === 0) {
meta.frameRate = this._referenceFrameRate;
}
let fps_den = meta.frameRate.fps_den;
let fps_num = meta.frameRate.fps_num;
meta.refSampleDuration = meta.timescale * (fps_den / fps_num);
let codecArray = sps.subarray(1, 4);
let codecString = 'avc1.';
for (let j = 0; j < 3; j++) {
let h = codecArray[j].toString(16);
if (h.length < 2) {
h = '0' + h;
}
codecString += h;
}
meta.codec = codecString;
let mi = this._mediaInfo;
mi.width = meta.codecWidth;
mi.height = meta.codecHeight;
mi.fps = meta.frameRate.fps;
mi.profile = meta.profile;
mi.level = meta.level;
mi.chromaFormat = config.chroma_format_string;
mi.sarNum = meta.sarRatio.width;
mi.sarDen = meta.sarRatio.height;
mi.videoCodec = codecString;
if (mi.hasAudio) {
if (mi.audioCodec != null) {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"';
}
} else {
mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + '"';
}
if (mi.isComplete()) {
this._onMediaInfo(mi);
}
}
let ppsCount = v.getUint8(offset); // numOfPictureParameterSets
if (ppsCount === 0) {
this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');
return;
} else if (ppsCount > 1) {
Log.w(this.TAG, \`Flv: Strange AVCDecoderConfigurationRecord: PPS Count = \${ppsCount}\`);
}
offset++;
for (let i = 0; i < ppsCount; i++) {
let len = v.getUint16(offset, !le); // pictureParameterSetLength
offset += 2;
if (len === 0) {
continue;
}
// pps is useless for extracting video information
offset += len;
}
meta.avcc = new Uint8Array(dataSize);
meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0);
Log.v(this.TAG, 'Parsed AVCDecoderConfigurationRecord');
if (this._isInitialMetadataDispatched()) {
// flush parsed frames
if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {
this._onDataAvailable(this._audioTrack, this._videoTrack);
}
} else {
this._videoInitialMetadataDispatched = true;
}
// notify new metadata
this._dispatch = false;
this._onTrackMetadata('video', meta);
}
_parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType, cts) {
let le = this._littleEndian;
let v = new DataView(arrayBuffer, dataOffset, dataSize);
let units = [], length = 0;
let offset = 0;
const lengthSize = this._naluLengthSize;
let dts = this._timestampBase + tagTimestamp;
let keyframe = (frameType === 1); // from FLV Frame Type constants
let refIdc = 1; // added by qli5
while (offset < dataSize) {
if (offset + 4 >= dataSize) {
Log.w(this.TAG, \`Malformed Nalu near timestamp \${dts}, offset = \${offset}, dataSize = \${dataSize}\`);
break; // data not enough for next Nalu
}
// Nalu with length-header (AVC1)
let naluSize = v.getUint32(offset, !le); // Big-Endian read
if (lengthSize === 3) {
naluSize >>>= 8;
}
if (naluSize > dataSize - lengthSize) {
Log.w(this.TAG, \`Malformed Nalus near timestamp \${dts}, NaluSize > DataSize!\`);
return;
}
let unitType = v.getUint8(offset + lengthSize) & 0x1F;
// added by qli5
refIdc = v.getUint8(offset + lengthSize) & 0x60;
if (unitType === 5) { // IDR
keyframe = true;
}
let data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);
let unit = { type: unitType, data: data };
units.push(unit);
length += data.byteLength;
offset += lengthSize + naluSize;
}
if (units.length) {
let track = this._videoTrack;
let avcSample = {
units: units,
length: length,
isKeyframe: keyframe,
refIdc: refIdc,
dts: dts,
cts: cts,
pts: (dts + cts)
};
if (keyframe) {
avcSample.fileposition = tagPosition;
}
track.samples.push(avcSample);
track.length += length;
}
}
}
return FLVDemuxer;
})();
const ASS = class {
/**
* Extract sections from ass string
* @param {string} str
* @returns {Object} - object from sections
*/
static extractSections(str) {
const regex = /^\\[(.*)\\]\$/mg;
let match;
let matchArr = [];
while ((match = regex.exec(str)) !== null) {
matchArr.push({ name: match[1], index: match.index });
}
let ret = {};
matchArr.forEach((match, i) => ret[match.name] = str.slice(match.index, matchArr[i + 1] && matchArr[i + 1].index));
return ret;
}
/**
* Extract subtitle lines from section Events
* @param {string} str
* @returns {Array<Object>} - array of subtitle lines
*/
static extractSubtitleLines(str) {
const lines = str.split('\\n');
if (lines[0] != '[Events]' && lines[0] != '[events]') throw new Error('ASSDemuxer: section is not [Events]');
if (lines[1].indexOf('Format:') != 0 && lines[1].indexOf('format:') != 0) throw new Error('ASSDemuxer: cannot find Format definition in section [Events]');
const format = lines[1].slice(lines[1].indexOf(':') + 1).split(',').map(e => e.trim());
return lines.slice(2).map(e => {
let j = {};
e.replace(/[d|D]ialogue:\\s*/, '')
.match(new RegExp(new Array(format.length - 1).fill('(.*?),').join('') + '(.*)'))
.slice(1)
.forEach((k, index) => j[format[index]] = k)
return j;
});
}
/**
* Create a new ASS Demuxer
*/
constructor() {
this.info = '';
this.styles = '';
this.events = '';
this.eventsHeader = '';
this.pictures = '';
this.fonts = '';
this.lines = '';
}
get header() {
// return this.info + this.styles + this.eventsHeader;
return this.info + this.styles;
}
/**
* Load a file from an arraybuffer of a string
* @param {(ArrayBuffer|string)} chunk
*/
parseFile(chunk) {
const str = typeof chunk == 'string' ? chunk : new TextDecoder('utf-8').decode(chunk);
for (let [i, j] of Object.entries(ASS.extractSections(str))) {
if (i.match(/Script Info(?:mation)?/i)) this.info = j;
else if (i.match(/V4\\+? Styles?/i)) this.styles = j;
else if (i.match(/Events?/i)) this.events = j;
else if (i.match(/Pictures?/i)) this.pictures = j;
else if (i.match(/Fonts?/i)) this.fonts = j;
}
this.eventsHeader = this.events.split('\\n', 2).join('\\n') + '\\n';
this.lines = ASS.extractSubtitleLines(this.events);
return this;
}
};
/**
* The EMBL builder is from simple-ebml-builder
*
* Copyright 2017 ryiwamoto
*
* @author ryiwamoto
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject
* to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
// const EBML = require('./ebml');
const EBML = (function e(t, n, r) { function s(o, u) { if (!n[o]) { if (!t[o]) { var a = typeof require == "function" && require; if (!u && a) return a(o, !0); if (i) return i(o, !0); var f = new Error("Cannot find module '" + o + "'"); throw f.code = "MODULE_NOT_FOUND", f } var l = n[o] = { exports: {} }; t[o][0].call(l.exports, function (e) { var n = t[o][1][e]; return s(n ? n : e) }, l, l.exports, e, t, n, r) } return n[o].exports } var i = typeof require == "function" && require; for (var o = 0; o < r.length; o++)s(r[o]); return s })({
1: [function (require, module, exports) {
let EBML = require('simple-ebml-builder');
EBML.float = num => new EBML.Value(EBML.float32bit(num));
EBML.int16 = num => new EBML.Value(EBML.int16Bit(num));
module.exports = EBML;
}, { "simple-ebml-builder": 5 }], 2: [function (require, module, exports) {
(function (global) {
/**
* lodash (Custom Build) <https://lodash.com/>
* Build: \`lodash modularize exports="npm" -o ./\`
* Copyright jQuery Foundation and other contributors <https://jquery.org/>
* Released under MIT license <https://lodash.com/license>
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
/** Used as the \`TypeError\` message for "Functions" methods. */
var FUNC_ERROR_TEXT = 'Expected a function';
/** Used to stand-in for \`undefined\` hash values. */
var HASH_UNDEFINED = '__lodash_hash_undefined__';
/** \`Object#toString\` result references. */
var funcTag = '[object Function]',
genTag = '[object GeneratorFunction]';
/**
* Used to match \`RegExp\`
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
*/
var reRegExpChar = /[\\\\^\$.*+?()[\\]{}|]/g;
/** Used to detect host constructors (Safari). */
var reIsHostCtor = /^\\[object .+?Constructor\\]\$/;
/** Detect free variable \`global\` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
/** Detect free variable \`self\`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
/**
* Gets the value at \`key\` of \`object\`.
*
* @private
* @param {Object} [object] The object to query.
* @param {string} key The key of the property to get.
* @returns {*} Returns the property value.
*/
function getValue(object, key) {
return object == null ? undefined : object[key];
}
/**
* Checks if \`value\` is a host object in IE < 9.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns \`true\` if \`value\` is a host object, else \`false\`.
*/
function isHostObject(value) {
// Many host objects are \`Object\` objects that can coerce to strings
// despite having improperly defined \`toString\` methods.
var result = false;
if (value != null && typeof value.toString != 'function') {
try {
result = !!(value + '');
} catch (e) { }
}
return result;
}
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
/** Used to detect overreaching core-js shims. */
var coreJsData = root['__core-js_shared__'];
/** Used to detect methods masquerading as native. */
var maskSrcKey = (function () {
var uid = /[^.]+\$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
return uid ? ('Symbol(src)_1.' + uid) : '';
}());
/** Used to resolve the decompiled source of functions. */
var funcToString = funcProto.toString;
/** Used to check objects for own properties. */
var hasOwnProperty = objectProto.hasOwnProperty;
/**
* Used to resolve the
* [\`toStringTag\`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
* of values.
*/
var objectToString = objectProto.toString;
/** Used to detect if a method is native. */
var reIsNative = RegExp('^' +
funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\\\\$&')
.replace(/hasOwnProperty|(function).*?(?=\\\\\\()| for .+?(?=\\\\\\])/g, '\$1.*?') + '\$'
);
/** Built-in value references. */
var splice = arrayProto.splice;
/* Built-in method references that are verified to be native. */
var Map = getNative(root, 'Map'),
nativeCreate = getNative(Object, 'create');
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Hash(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the hash.
*
* @private
* @name clear
* @memberOf Hash
*/
function hashClear() {
this.__data__ = nativeCreate ? nativeCreate(null) : {};
}
/**
* Removes \`key\` and its value from the hash.
*
* @private
* @name delete
* @memberOf Hash
* @param {Object} hash The hash to modify.
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
*/
function hashDelete(key) {
return this.has(key) && delete this.__data__[key];
}
/**
* Gets the hash value for \`key\`.
*
* @private
* @name get
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function hashGet(key) {
var data = this.__data__;
if (nativeCreate) {
var result = data[key];
return result === HASH_UNDEFINED ? undefined : result;
}
return hasOwnProperty.call(data, key) ? data[key] : undefined;
}
/**
* Checks if a hash value for \`key\` exists.
*
* @private
* @name has
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
*/
function hashHas(key) {
var data = this.__data__;
return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
}
/**
* Sets the hash \`key\` to \`value\`.
*
* @private
* @name set
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
function hashSet(key, value) {
var data = this.__data__;
data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
return this;
}
// Add methods to \`Hash\`.
Hash.prototype.clear = hashClear;
Hash.prototype['delete'] = hashDelete;
Hash.prototype.get = hashGet;
Hash.prototype.has = hashHas;
Hash.prototype.set = hashSet;
/**
* Creates an list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
}
/**
* Removes \`key\` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
return true;
}
/**
* Gets the list cache value for \`key\`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/**
* Checks if a list cache value for \`key\` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache \`key\` to \`value\`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to \`ListCache\`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype['delete'] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries ? entries.length : 0;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.__data__ = {
'hash': new Hash,
'map': new (Map || ListCache),
'string': new Hash
};
}
/**
* Removes \`key\` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns \`true\` if the entry was removed, else \`false\`.
*/
function mapCacheDelete(key) {
return getMapData(this, key)['delete'](key);
}
/**
* Gets the map value for \`key\`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for \`key\` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns \`true\` if an entry for \`key\` exists, else \`false\`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map \`key\` to \`value\`.
*
* @private
* @name set
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
function mapCacheSet(key, value) {
getMapData(this, key).set(key, value);
return this;
}
// Add methods to \`MapCache\`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype['delete'] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
/**
* Gets the index at which the \`key\` is found in \`array\` of key-value pairs.
*
* @private
* @param {Array} array The array to inspect.
* @param {*} key The key to search for.
* @returns {number} Returns the index of the matched value, else \`-1\`.
*/
function assocIndexOf(array, key) {
var length = array.length;
while (length--) {
if (eq(array[length][0], key)) {
return length;
}
}
return -1;
}
/**
* The base implementation of \`_.isNative\` without bad shim checks.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns \`true\` if \`value\` is a native function,
* else \`false\`.
*/
function baseIsNative(value) {
if (!isObject(value) || isMasked(value)) {
return false;
}
var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor;
return pattern.test(toSource(value));
}
/**
* Gets the data for \`map\`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData(map, key) {
var data = map.__data__;
return isKeyable(key)
? data[typeof key == 'string' ? 'string' : 'hash']
: data.map;
}
/**
* Gets the native function at \`key\` of \`object\`.
*
* @private
* @param {Object} object The object to query.
* @param {string} key The key of the method to get.
* @returns {*} Returns the function if it's native, else \`undefined\`.
*/
function getNative(object, key) {
var value = getValue(object, key);
return baseIsNative(value) ? value : undefined;
}
/**
* Checks if \`value\` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns \`true\` if \`value\` is suitable, else \`false\`.
*/
function isKeyable(value) {
var type = typeof value;
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
? (value !== '__proto__')
: (value === null);
}
/**
* Checks if \`func\` has its source masked.
*
* @private
* @param {Function} func The function to check.
* @returns {boolean} Returns \`true\` if \`func\` is masked, else \`false\`.
*/
function isMasked(func) {
return !!maskSrcKey && (maskSrcKey in func);
}
/**
* Converts \`func\` to its source code.
*
* @private
* @param {Function} func The function to process.
* @returns {string} Returns the source code.
*/
function toSource(func) {
if (func != null) {
try {
return funcToString.call(func);
} catch (e) { }
try {
return (func + '');
} catch (e) { }
}
return '';
}
/**
* Creates a function that memoizes the result of \`func\`. If \`resolver\` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The \`func\`
* is invoked with the \`this\` binding of the memoized function.
*
* **Note:** The cache is exposed as the \`cache\` property on the memoized
* function. Its creation may be customized by replacing the \`_.memoize.Cache\`
* constructor with one whose instances implement the
* [\`Map\`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
* method interface of \`delete\`, \`get\`, \`has\`, and \`set\`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* var object = { 'a': 1, 'b': 2 };
* var other = { 'c': 3, 'd': 4 };
*
* var values = _.memoize(_.values);
* values(object);
* // => [1, 2]
*
* values(other);
* // => [3, 4]
*
* object.a = 2;
* values(object);
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b']);
* values(object);
* // => ['a', 'b']
*
* // Replace \`_.memoize.Cache\`.
* _.memoize.Cache = WeakMap;
*/
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver && typeof resolver != 'function')) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result);
return result;
};
memoized.cache = new (memoize.Cache || MapCache);
return memoized;
}
// Assign cache to \`_.memoize\`.
memoize.Cache = MapCache;
/**
* Performs a
* [\`SameValueZero\`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* comparison between two values to determine if they are equivalent.
*
* @static
* @memberOf _
* @since 4.0.0
* @category Lang
* @param {*} value The value to compare.
* @param {*} other The other value to compare.
* @returns {boolean} Returns \`true\` if the values are equivalent, else \`false\`.
* @example
*
* var object = { 'a': 1 };
* var other = { 'a': 1 };
*
* _.eq(object, object);
* // => true
*
* _.eq(object, other);
* // => false
*
* _.eq('a', 'a');
* // => true
*
* _.eq('a', Object('a'));
* // => false
*
* _.eq(NaN, NaN);
* // => true
*/
function eq(value, other) {
return value === other || (value !== value && other !== other);
}
/**
* Checks if \`value\` is classified as a \`Function\` object.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns \`true\` if \`value\` is a function, else \`false\`.
* @example
*
* _.isFunction(_);
* // => true
*
* _.isFunction(/abc/);
* // => false
*/
function isFunction(value) {
// The use of \`Object#toString\` avoids issues with the \`typeof\` operator
// in Safari 8-9 which returns 'object' for typed array and other constructors.
var tag = isObject(value) ? objectToString.call(value) : '';
return tag == funcTag || tag == genTag;
}
/**
* Checks if \`value\` is the
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
* of \`Object\`. (e.g. arrays, functions, objects, regexes, \`new Number(0)\`, and \`new String('')\`)
*
* @static
* @memberOf _
* @since 0.1.0
* @category Lang
* @param {*} value The value to check.
* @returns {boolean} Returns \`true\` if \`value\` is an object, else \`false\`.
* @example
*
* _.isObject({});
* // => true
*
* _.isObject([1, 2, 3]);
* // => true
*
* _.isObject(_.noop);
* // => true
*
* _.isObject(null);
* // => false
*/
function isObject(value) {
var type = typeof value;
return !!value && (type == 'object' || type == 'function');
}
module.exports = memoize;
}).call(this, typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
}, {}], 3: [function (require, module, exports) {
"use strict";
var memoize = require("lodash.memoize");
var typedArrayUtils_1 = require("./typedArrayUtils");
var Value = (function () {
function Value(bytes) {
this.bytes = bytes;
}
Value.prototype.write = function (buf, pos) {
buf.set(this.bytes, pos);
return pos + this.bytes.length;
};
Value.prototype.countSize = function () {
return this.bytes.length;
};
return Value;
}());
exports.Value = Value;
var Element = (function () {
function Element(id, children, isSizeUnknown) {
this.id = id;
this.children = children;
var bodySize = this.children.reduce(function (p, c) { return p + c.countSize(); }, 0);
this.sizeMetaData = isSizeUnknown ?
exports.UNKNOWN_SIZE :
exports.vintEncode(typedArrayUtils_1.numberToByteArray(bodySize, exports.getEBMLByteLength(bodySize)));
this.size = this.id.length + this.sizeMetaData.length + bodySize;
}
Element.prototype.write = function (buf, pos) {
buf.set(this.id, pos);
buf.set(this.sizeMetaData, pos + this.id.length);
return this.children.reduce(function (p, c) { return c.write(buf, p); }, pos + this.id.length + this.sizeMetaData.length);
};
Element.prototype.countSize = function () {
return this.size;
};
return Element;
}());
exports.Element = Element;
exports.bytes = memoize(function (data) {
return new Value(data);
});
exports.number = memoize(function (num) {
return exports.bytes(typedArrayUtils_1.numberToByteArray(num));
});
exports.vintEncodedNumber = memoize(function (num) {
return exports.bytes(exports.vintEncode(typedArrayUtils_1.numberToByteArray(num, exports.getEBMLByteLength(num))));
});
exports.string = memoize(function (str) {
return exports.bytes(typedArrayUtils_1.stringToByteArray(str));
});
exports.element = function (id, child) {
return new Element(id, Array.isArray(child) ? child : [child], false);
};
exports.unknownSizeElement = function (id, child) {
return new Element(id, Array.isArray(child) ? child : [child], true);
};
exports.build = function (v) {
var b = new Uint8Array(v.countSize());
v.write(b, 0);
return b;
};
exports.getEBMLByteLength = function (num) {
if (num < 0) {
throw new Error("EBML.getEBMLByteLength: negative number not implemented");
}
else if (num < 0x7f) {
return 1;
}
else if (num < 0x3fff) {
return 2;
}
else if (num < 0x1fffff) {
return 3;
}
else if (num < 0xfffffff) {
return 4;
}
else if (num < 0x7ffffffff) {
return 5;
}
else if (num < 0x3ffffffffff) {
return 6;
}
else if (num < 0x1ffffffffffff) {
return 7;
}
else if (num < 0x20000000000000) {
return 8;
}
else if (num < 0xffffffffffffff) {
throw new Error("EBMLgetEBMLByteLength: number exceeds Number.MAX_SAFE_INTEGER");
}
else {
throw new Error("EBMLgetEBMLByteLength: data size must be less than or equal to " + (Math.pow(2, 56) - 2));
}
};
exports.UNKNOWN_SIZE = new Uint8Array([0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
exports.vintEncode = function (byteArray) {
byteArray[0] = exports.getSizeMask(byteArray.length) | byteArray[0];
return byteArray;
};
exports.getSizeMask = function (byteLength) {
return 0x80 >> (byteLength - 1);
};
}, { "./typedArrayUtils": 6, "lodash.memoize": 2 }], 4: [function (require, module, exports) {
"use strict";
/**
* @see https://www.matroska.org/technical/specs/index.html
*/
exports.ID = {
EBML: Uint8Array.of(0x1A, 0x45, 0xDF, 0xA3),
EBMLVersion: Uint8Array.of(0x42, 0x86),
EBMLReadVersion: Uint8Array.of(0x42, 0xF7),
EBMLMaxIDLength: Uint8Array.of(0x42, 0xF2),
EBMLMaxSizeLength: Uint8Array.of(0x42, 0xF3),
DocType: Uint8Array.of(0x42, 0x82),
DocTypeVersion: Uint8Array.of(0x42, 0x87),
DocTypeReadVersion: Uint8Array.of(0x42, 0x85),
Void: Uint8Array.of(0xEC),
CRC32: Uint8Array.of(0xBF),
Segment: Uint8Array.of(0x18, 0x53, 0x80, 0x67),
SeekHead: Uint8Array.of(0x11, 0x4D, 0x9B, 0x74),
Seek: Uint8Array.of(0x4D, 0xBB),
SeekID: Uint8Array.of(0x53, 0xAB),
SeekPosition: Uint8Array.of(0x53, 0xAC),
Info: Uint8Array.of(0x15, 0x49, 0xA9, 0x66),
SegmentUID: Uint8Array.of(0x73, 0xA4),
SegmentFilename: Uint8Array.of(0x73, 0x84),
PrevUID: Uint8Array.of(0x3C, 0xB9, 0x23),
PrevFilename: Uint8Array.of(0x3C, 0x83, 0xAB),
NextUID: Uint8Array.of(0x3E, 0xB9, 0x23),
NextFilename: Uint8Array.of(0x3E, 0x83, 0xBB),
SegmentFamily: Uint8Array.of(0x44, 0x44),
ChapterTranslate: Uint8Array.of(0x69, 0x24),
ChapterTranslateEditionUID: Uint8Array.of(0x69, 0xFC),
ChapterTranslateCodec: Uint8Array.of(0x69, 0xBF),
ChapterTranslateID: Uint8Array.of(0x69, 0xA5),
TimecodeScale: Uint8Array.of(0x2A, 0xD7, 0xB1),
Duration: Uint8Array.of(0x44, 0x89),
DateUTC: Uint8Array.of(0x44, 0x61),
Title: Uint8Array.of(0x7B, 0xA9),
MuxingApp: Uint8Array.of(0x4D, 0x80),
WritingApp: Uint8Array.of(0x57, 0x41),
Cluster: Uint8Array.of(0x1F, 0x43, 0xB6, 0x75),
Timecode: Uint8Array.of(0xE7),
SilentTracks: Uint8Array.of(0x58, 0x54),
SilentTrackNumber: Uint8Array.of(0x58, 0xD7),
Position: Uint8Array.of(0xA7),
PrevSize: Uint8Array.of(0xAB),
SimpleBlock: Uint8Array.of(0xA3),
BlockGroup: Uint8Array.of(0xA0),
Block: Uint8Array.of(0xA1),
BlockAdditions: Uint8Array.of(0x75, 0xA1),
BlockMore: Uint8Array.of(0xA6),
BlockAddID: Uint8Array.of(0xEE),
BlockAdditional: Uint8Array.of(0xA5),
BlockDuration: Uint8Array.of(0x9B),
ReferencePriority: Uint8Array.of(0xFA),
ReferenceBlock: Uint8Array.of(0xFB),
CodecState: Uint8Array.of(0xA4),
DiscardPadding: Uint8Array.of(0x75, 0xA2),
Slices: Uint8Array.of(0x8E),
TimeSlice: Uint8Array.of(0xE8),
LaceNumber: Uint8Array.of(0xCC),
Tracks: Uint8Array.of(0x16, 0x54, 0xAE, 0x6B),
TrackEntry: Uint8Array.of(0xAE),
TrackNumber: Uint8Array.of(0xD7),
TrackUID: Uint8Array.of(0x73, 0xC5),
TrackType: Uint8Array.of(0x83),
FlagEnabled: Uint8Array.of(0xB9),
FlagDefault: Uint8Array.of(0x88),
FlagForced: Uint8Array.of(0x55, 0xAA),
FlagLacing: Uint8Array.of(0x9C),
MinCache: Uint8Array.of(0x6D, 0xE7),
MaxCache: Uint8Array.of(0x6D, 0xF8),
DefaultDuration: Uint8Array.of(0x23, 0xE3, 0x83),
DefaultDecodedFieldDuration: Uint8Array.of(0x23, 0x4E, 0x7A),
MaxBlockAdditionID: Uint8Array.of(0x55, 0xEE),
Name: Uint8Array.of(0x53, 0x6E),
Language: Uint8Array.of(0x22, 0xB5, 0x9C),
CodecID: Uint8Array.of(0x86),
CodecPrivate: Uint8Array.of(0x63, 0xA2),
CodecName: Uint8Array.of(0x25, 0x86, 0x88),
AttachmentLink: Uint8Array.of(0x74, 0x46),
CodecDecodeAll: Uint8Array.of(0xAA),
TrackOverlay: Uint8Array.of(0x6F, 0xAB),
CodecDelay: Uint8Array.of(0x56, 0xAA),
SeekPreRoll: Uint8Array.of(0x56, 0xBB),
TrackTranslate: Uint8Array.of(0x66, 0x24),
TrackTranslateEditionUID: Uint8Array.of(0x66, 0xFC),
TrackTranslateCodec: Uint8Array.of(0x66, 0xBF),
TrackTranslateTrackID: Uint8Array.of(0x66, 0xA5),
Video: Uint8Array.of(0xE0),
FlagInterlaced: Uint8Array.of(0x9A),
FieldOrder: Uint8Array.of(0x9D),
StereoMode: Uint8Array.of(0x53, 0xB8),
AlphaMode: Uint8Array.of(0x53, 0xC0),
PixelWidth: Uint8Array.of(0xB0),
PixelHeight: Uint8Array.of(0xBA),
PixelCropBottom: Uint8Array.of(0x54, 0xAA),
PixelCropTop: Uint8Array.of(0x54, 0xBB),
PixelCropLeft: Uint8Array.of(0x54, 0xCC),
PixelCropRight: Uint8Array.of(0x54, 0xDD),
DisplayWidth: Uint8Array.of(0x54, 0xB0),
DisplayHeight: Uint8Array.of(0x54, 0xBA),
DisplayUnit: Uint8Array.of(0x54, 0xB2),
AspectRatioType: Uint8Array.of(0x54, 0xB3),
ColourSpace: Uint8Array.of(0x2E, 0xB5, 0x24),
Colour: Uint8Array.of(0x55, 0xB0),
MatrixCoefficients: Uint8Array.of(0x55, 0xB1),
BitsPerChannel: Uint8Array.of(0x55, 0xB2),
ChromaSubsamplingHorz: Uint8Array.of(0x55, 0xB3),
ChromaSubsamplingVert: Uint8Array.of(0x55, 0xB4),
CbSubsamplingHorz: Uint8Array.of(0x55, 0xB5),
CbSubsamplingVert: Uint8Array.of(0x55, 0xB6),
ChromaSitingHorz: Uint8Array.of(0x55, 0xB7),
ChromaSitingVert: Uint8Array.of(0x55, 0xB8),
Range: Uint8Array.of(0x55, 0xB9),
TransferCharacteristics: Uint8Array.of(0x55, 0xBA),
Primaries: Uint8Array.of(0x55, 0xBB),
MaxCLL: Uint8Array.of(0x55, 0xBC),
MaxFALL: Uint8Array.of(0x55, 0xBD),
MasteringMetadata: Uint8Array.of(0x55, 0xD0),
PrimaryRChromaticityX: Uint8Array.of(0x55, 0xD1),
PrimaryRChromaticityY: Uint8Array.of(0x55, 0xD2),
PrimaryGChromaticityX: Uint8Array.of(0x55, 0xD3),
PrimaryGChromaticityY: Uint8Array.of(0x55, 0xD4),
PrimaryBChromaticityX: Uint8Array.of(0x55, 0xD5),
PrimaryBChromaticityY: Uint8Array.of(0x55, 0xD6),
WhitePointChromaticityX: Uint8Array.of(0x55, 0xD7),
WhitePointChromaticityY: Uint8Array.of(0x55, 0xD8),
LuminanceMax: Uint8Array.of(0x55, 0xD9),
LuminanceMin: Uint8Array.of(0x55, 0xDA),
Audio: Uint8Array.of(0xE1),
SamplingFrequency: Uint8Array.of(0xB5),
OutputSamplingFrequency: Uint8Array.of(0x78, 0xB5),
Channels: Uint8Array.of(0x9F),
BitDepth: Uint8Array.of(0x62, 0x64),
TrackOperation: Uint8Array.of(0xE2),
TrackCombinePlanes: Uint8Array.of(0xE3),
TrackPlane: Uint8Array.of(0xE4),
TrackPlaneUID: Uint8Array.of(0xE5),
TrackPlaneType: Uint8Array.of(0xE6),
TrackJoinBlocks: Uint8Array.of(0xE9),
TrackJoinUID: Uint8Array.of(0xED),
ContentEncodings: Uint8Array.of(0x6D, 0x80),
ContentEncoding: Uint8Array.of(0x62, 0x40),
ContentEncodingOrder: Uint8Array.of(0x50, 0x31),
ContentEncodingScope: Uint8Array.of(0x50, 0x32),
ContentEncodingType: Uint8Array.of(0x50, 0x33),
ContentCompression: Uint8Array.of(0x50, 0x34),
ContentCompAlgo: Uint8Array.of(0x42, 0x54),
ContentCompSettings: Uint8Array.of(0x42, 0x55),
ContentEncryption: Uint8Array.of(0x50, 0x35),
ContentEncAlgo: Uint8Array.of(0x47, 0xE1),
ContentEncKeyID: Uint8Array.of(0x47, 0xE2),
ContentSignature: Uint8Array.of(0x47, 0xE3),
ContentSigKeyID: Uint8Array.of(0x47, 0xE4),
ContentSigAlgo: Uint8Array.of(0x47, 0xE5),
ContentSigHashAlgo: Uint8Array.of(0x47, 0xE6),
Cues: Uint8Array.of(0x1C, 0x53, 0xBB, 0x6B),
CuePoint: Uint8Array.of(0xBB),
CueTime: Uint8Array.of(0xB3),
CueTrackPositions: Uint8Array.of(0xB7),
CueTrack: Uint8Array.of(0xF7),
CueClusterPosition: Uint8Array.of(0xF1),
CueRelativePosition: Uint8Array.of(0xF0),
CueDuration: Uint8Array.of(0xB2),
CueBlockNumber: Uint8Array.of(0x53, 0x78),
CueCodecState: Uint8Array.of(0xEA),
CueReference: Uint8Array.of(0xDB),
CueRefTime: Uint8Array.of(0x96),
Attachments: Uint8Array.of(0x19, 0x41, 0xA4, 0x69),
AttachedFile: Uint8Array.of(0x61, 0xA7),
FileDescription: Uint8Array.of(0x46, 0x7E),
FileName: Uint8Array.of(0x46, 0x6E),
FileMimeType: Uint8Array.of(0x46, 0x60),
FileData: Uint8Array.of(0x46, 0x5C),
FileUID: Uint8Array.of(0x46, 0xAE),
Chapters: Uint8Array.of(0x10, 0x43, 0xA7, 0x70),
EditionEntry: Uint8Array.of(0x45, 0xB9),
EditionUID: Uint8Array.of(0x45, 0xBC),
EditionFlagHidden: Uint8Array.of(0x45, 0xBD),
EditionFlagDefault: Uint8Array.of(0x45, 0xDB),
EditionFlagOrdered: Uint8Array.of(0x45, 0xDD),
ChapterAtom: Uint8Array.of(0xB6),
ChapterUID: Uint8Array.of(0x73, 0xC4),
ChapterStringUID: Uint8Array.of(0x56, 0x54),
ChapterTimeStart: Uint8Array.of(0x91),
ChapterTimeEnd: Uint8Array.of(0x92),
ChapterFlagHidden: Uint8Array.of(0x98),
ChapterFlagEnabled: Uint8Array.of(0x45, 0x98),
ChapterSegmentUID: Uint8Array.of(0x6E, 0x67),
ChapterSegmentEditionUID: Uint8Array.of(0x6E, 0xBC),
ChapterPhysicalEquiv: Uint8Array.of(0x63, 0xC3),
ChapterTrack: Uint8Array.of(0x8F),
ChapterTrackNumber: Uint8Array.of(0x89),
ChapterDisplay: Uint8Array.of(0x80),
ChapString: Uint8Array.of(0x85),
ChapLanguage: Uint8Array.of(0x43, 0x7C),
ChapCountry: Uint8Array.of(0x43, 0x7E),
ChapProcess: Uint8Array.of(0x69, 0x44),
ChapProcessCodecID: Uint8Array.of(0x69, 0x55),
ChapProcessPrivate: Uint8Array.of(0x45, 0x0D),
ChapProcessCommand: Uint8Array.of(0x69, 0x11),
ChapProcessTime: Uint8Array.of(0x69, 0x22),
ChapProcessData: Uint8Array.of(0x69, 0x33),
Tags: Uint8Array.of(0x12, 0x54, 0xC3, 0x67),
Tag: Uint8Array.of(0x73, 0x73),
Targets: Uint8Array.of(0x63, 0xC0),
TargetTypeValue: Uint8Array.of(0x68, 0xCA),
TargetType: Uint8Array.of(0x63, 0xCA),
TagTrackUID: Uint8Array.of(0x63, 0xC5),
TagEditionUID: Uint8Array.of(0x63, 0xC9),
TagChapterUID: Uint8Array.of(0x63, 0xC4),
TagAttachmentUID: Uint8Array.of(0x63, 0xC6),
SimpleTag: Uint8Array.of(0x67, 0xC8),
TagName: Uint8Array.of(0x45, 0xA3),
TagLanguage: Uint8Array.of(0x44, 0x7A),
TagDefault: Uint8Array.of(0x44, 0x84),
TagString: Uint8Array.of(0x44, 0x87),
TagBinary: Uint8Array.of(0x44, 0x85),
};
}, {}], 5: [function (require, module, exports) {
"use strict";
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
__export(require("./ebml"));
__export(require("./id"));
__export(require("./typedArrayUtils"));
}, { "./ebml": 3, "./id": 4, "./typedArrayUtils": 6 }], 6: [function (require, module, exports) {
"use strict";
var memoize = require("lodash.memoize");
exports.numberToByteArray = function (num, byteLength) {
if (byteLength === void 0) byteLength = getNumberByteLength(num);
var byteArray;
if (byteLength == 1) {
byteArray = new DataView(new ArrayBuffer(1));
byteArray.setUint8(0, num);
}
else if (byteLength == 2) {
byteArray = new DataView(new ArrayBuffer(2));
byteArray.setUint16(0, num);
}
else if (byteLength == 3) {
byteArray = new DataView(new ArrayBuffer(3));
byteArray.setUint8(0, num >> 16);
byteArray.setUint16(1, num & 0xffff);
}
else if (byteLength == 4) {
byteArray = new DataView(new ArrayBuffer(4));
byteArray.setUint32(0, num);
}
// 4GB (upper limit for int32) should be enough in most cases
else if (/* byteLength == 5 && */num < 0xffffffff) {
byteArray = new DataView(new ArrayBuffer(5));
byteArray.setUint32(1, num);
}
// Naive emulations of int64 bitwise opreators
else if (byteLength == 5) {
byteArray = new DataView(new ArrayBuffer(5));
byteArray.setUint8(0, num / 0x100000000 | 0);
byteArray.setUint32(1, num % 0x100000000);
}
else if (byteLength == 6) {
byteArray = new DataView(new ArrayBuffer(6));
byteArray.setUint16(0, num / 0x100000000 | 0);
byteArray.setUint32(2, num % 0x100000000);
}
else if (byteLength == 7) {
byteArray = new DataView(new ArrayBuffer(7));
byteArray.setUint8(0, num / 0x1000000000000 | 0);
byteArray.setUint16(1, num / 0x100000000 & 0xffff);
byteArray.setUint32(3, num % 0x100000000);
}
else if (byteLength == 8) {
byteArray = new DataView(new ArrayBuffer(8));
byteArray.setUint32(0, num / 0x100000000 | 0);
byteArray.setUint32(4, num % 0x100000000);
}
else {
throw new Error("EBML.typedArrayUtils.numberToByteArray: byte length must be less than or equal to 8");
}
return new Uint8Array(byteArray.buffer)
};
exports.stringToByteArray = memoize(function (str) {
return Uint8Array.from(Array.from(str).map(function (_) { return _.codePointAt(0); }));
});
function getNumberByteLength(num) {
if (num < 0) {
throw new Error("EBML.typedArrayUtils.getNumberByteLength: negative number not implemented");
}
else if (num < 0x100) {
return 1;
}
else if (num < 0x10000) {
return 2;
}
else if (num < 0x1000000) {
return 3;
}
else if (num < 0x100000000) {
return 4;
}
else if (num < 0x10000000000) {
return 5;
}
else if (num < 0x1000000000000) {
return 6;
}
else if (num < 0x20000000000000) {
return 7;
}
else {
throw new Error("EBML.typedArrayUtils.getNumberByteLength: number exceeds Number.MAX_SAFE_INTEGER");
}
}
exports.getNumberByteLength = getNumberByteLength;
exports.int16Bit = memoize(function (num) {
var ab = new ArrayBuffer(2);
new DataView(ab).setInt16(0, num);
return new Uint8Array(ab);
});
exports.float32bit = memoize(function (num) {
var ab = new ArrayBuffer(4);
new DataView(ab).setFloat32(0, num);
return new Uint8Array(ab);
});
exports.dumpBytes = function (b) {
return Array.from(new Uint8Array(b)).map(function (_) { return "0x" + _.toString(16); }).join(", ");
};
}, { "lodash.memoize": 2 }]
}, {}, [])(1);
const MKV = class {
constructor(config) {
this.min = true;
this.onprogress = null;
Object.assign(this, config);
this.segmentUID = MKV.randomBytes(16);
this.trackUIDBase = Math.trunc(Math.random() * 2 ** 16);
this.trackMetadata = { h264: null, aac: null, ass: null };
this.duration = 0;
this.blocks = { h264: [], aac: [], ass: [] };
}
static randomBytes(num) {
return Array.from(new Array(num), () => Math.trunc(Math.random() * 256));
}
static textToMS(str) {
const [, h, mm, ss, ms10] = str.match(/(\\d+):(\\d+):(\\d+).(\\d+)/);
return h * 3600000 + mm * 60000 + ss * 1000 + ms10 * 10;
}
static mimeToCodecID(str) {
if (str.startsWith('avc1')) {
return 'V_MPEG4/ISO/AVC';
}
else if (str.startsWith('mp4a')) {
return 'A_AAC';
}
else {
throw new Error(\`MKVRemuxer: unknown codec \${str}\`);
}
}
static uint8ArrayConcat(...array) {
// if (Array.isArray(array[0])) array = array[0];
if (array.length == 1) return array[0];
if (typeof Buffer != 'undefined') return Buffer.concat(array);
const ret = new Uint8Array(array.reduce((i, j) => i.byteLength + j.byteLength));
let length = 0;
for (let e of array) {
ret.set(e, length);
length += e.byteLength;
}
return ret;
}
addH264Metadata(h264) {
this.trackMetadata.h264 = {
codecId: MKV.mimeToCodecID(h264.codec),
codecPrivate: h264.avcc,
defaultDuration: h264.refSampleDuration * 1000000,
pixelWidth: h264.codecWidth,
pixelHeight: h264.codecHeight,
displayWidth: h264.presentWidth,
displayHeight: h264.presentHeight
};
this.duration = Math.max(this.duration, h264.duration);
}
addAACMetadata(aac) {
this.trackMetadata.aac = {
codecId: MKV.mimeToCodecID(aac.originalCodec),
codecPrivate: aac.configRaw,
defaultDuration: aac.refSampleDuration * 1000000,
samplingFrequence: aac.audioSampleRate,
channels: aac.channelCount
};
this.duration = Math.max(this.duration, aac.duration);
}
addASSMetadata(ass) {
this.trackMetadata.ass = {
codecId: 'S_TEXT/ASS',
codecPrivate: new TextEncoder().encode(ass.header)
};
}
addH264Stream(h264) {
this.blocks.h264 = this.blocks.h264.concat(h264.samples.map(e => ({
track: 1,
frame: MKV.uint8ArrayConcat(...e.units.map(i => i.data)),
isKeyframe: e.isKeyframe,
discardable: Boolean(e.refIdc),
timestamp: e.pts,
simple: true,
})));
}
addAACStream(aac) {
this.blocks.aac = this.blocks.aac.concat(aac.samples.map(e => ({
track: 2,
frame: e.unit,
timestamp: e.pts,
simple: true,
})));
}
addASSStream(ass) {
this.blocks.ass = this.blocks.ass.concat(ass.lines.map((e, i) => ({
track: 3,
frame: new TextEncoder().encode(\`\${i},\${e['Layer'] || ''},\${e['Style'] || ''},\${e['Name'] || ''},\${e['MarginL'] || ''},\${e['MarginR'] || ''},\${e['MarginV'] || ''},\${e['Effect'] || ''},\${e['Text'] || ''}\`),
timestamp: MKV.textToMS(e['Start']),
duration: MKV.textToMS(e['End']) - MKV.textToMS(e['Start']),
})));
}
build() {
return new Blob([
this.buildHeader(),
this.buildBody()
]);
}
buildHeader() {
return new Blob([EBML.build(EBML.element(EBML.ID.EBML, [
EBML.element(EBML.ID.EBMLVersion, EBML.number(1)),
EBML.element(EBML.ID.EBMLReadVersion, EBML.number(1)),
EBML.element(EBML.ID.EBMLMaxIDLength, EBML.number(4)),
EBML.element(EBML.ID.EBMLMaxSizeLength, EBML.number(8)),
EBML.element(EBML.ID.DocType, EBML.string('matroska')),
EBML.element(EBML.ID.DocTypeVersion, EBML.number(4)),
EBML.element(EBML.ID.DocTypeReadVersion, EBML.number(2)),
]))]);
}
buildBody() {
if (this.min) {
return new Blob([EBML.build(EBML.element(EBML.ID.Segment, [
this.getSegmentInfo(),
this.getTracks(),
...this.getClusterArray()
]))]);
}
else {
return new Blob([EBML.build(EBML.element(EBML.ID.Segment, [
this.getSeekHead(),
this.getVoid(4100),
this.getSegmentInfo(),
this.getTracks(),
this.getVoid(1100),
...this.getClusterArray()
]))]);
}
}
getSeekHead() {
return EBML.element(EBML.ID.SeekHead, [
EBML.element(EBML.ID.Seek, [
EBML.element(EBML.ID.SeekID, EBML.bytes(EBML.ID.Info)),
EBML.element(EBML.ID.SeekPosition, EBML.number(4050))
]),
EBML.element(EBML.ID.Seek, [
EBML.element(EBML.ID.SeekID, EBML.bytes(EBML.ID.Tracks)),
EBML.element(EBML.ID.SeekPosition, EBML.number(4200))
]),
]);
}
getVoid(length = 2000) {
return EBML.element(EBML.ID.Void, EBML.bytes(new Uint8Array(length)));
}
getSegmentInfo() {
return EBML.element(EBML.ID.Info, [
EBML.element(EBML.ID.TimecodeScale, EBML.number(1000000)),
EBML.element(EBML.ID.MuxingApp, EBML.string('flv.js + assparser_qli5 -> simple-ebml-builder')),
EBML.element(EBML.ID.WritingApp, EBML.string('flvass2mkv.js by qli5')),
EBML.element(EBML.ID.Duration, EBML.float(this.duration)),
EBML.element(EBML.ID.SegmentUID, EBML.bytes(this.segmentUID)),
]);
}
getTracks() {
return EBML.element(EBML.ID.Tracks, [
this.getVideoTrackEntry(),
this.getAudioTrackEntry(),
this.getSubtitleTrackEntry()
]);
}
getVideoTrackEntry() {
return EBML.element(EBML.ID.TrackEntry, [
EBML.element(EBML.ID.TrackNumber, EBML.number(1)),
EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 1)),
EBML.element(EBML.ID.TrackType, EBML.number(0x01)),
EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.h264.codecId)),
EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.h264.codecPrivate)),
EBML.element(EBML.ID.DefaultDuration, EBML.number(this.trackMetadata.h264.defaultDuration)),
EBML.element(EBML.ID.Language, EBML.string('und')),
EBML.element(EBML.ID.Video, [
EBML.element(EBML.ID.PixelWidth, EBML.number(this.trackMetadata.h264.pixelWidth)),
EBML.element(EBML.ID.PixelHeight, EBML.number(this.trackMetadata.h264.pixelHeight)),
EBML.element(EBML.ID.DisplayWidth, EBML.number(this.trackMetadata.h264.displayWidth)),
EBML.element(EBML.ID.DisplayHeight, EBML.number(this.trackMetadata.h264.displayHeight)),
]),
]);
}
getAudioTrackEntry() {
return EBML.element(EBML.ID.TrackEntry, [
EBML.element(EBML.ID.TrackNumber, EBML.number(2)),
EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 2)),
EBML.element(EBML.ID.TrackType, EBML.number(0x02)),
EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.aac.codecId)),
EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.aac.codecPrivate)),
EBML.element(EBML.ID.DefaultDuration, EBML.number(this.trackMetadata.aac.defaultDuration)),
EBML.element(EBML.ID.Language, EBML.string('und')),
EBML.element(EBML.ID.Audio, [
EBML.element(EBML.ID.SamplingFrequency, EBML.float(this.trackMetadata.aac.samplingFrequence)),
EBML.element(EBML.ID.Channels, EBML.number(this.trackMetadata.aac.channels)),
]),
]);
}
getSubtitleTrackEntry() {
return EBML.element(EBML.ID.TrackEntry, [
EBML.element(EBML.ID.TrackNumber, EBML.number(3)),
EBML.element(EBML.ID.TrackUID, EBML.number(this.trackUIDBase + 3)),
EBML.element(EBML.ID.TrackType, EBML.number(0x11)),
EBML.element(EBML.ID.FlagLacing, EBML.number(0x00)),
EBML.element(EBML.ID.CodecID, EBML.string(this.trackMetadata.ass.codecId)),
EBML.element(EBML.ID.CodecPrivate, EBML.bytes(this.trackMetadata.ass.codecPrivate)),
EBML.element(EBML.ID.Language, EBML.string('und')),
]);
}
getClusterArray() {
// H264 codecState
this.blocks.h264[0].simple = false;
this.blocks.h264[0].codecState = this.trackMetadata.h264.codecPrivate;
let i = 0;
let j = 0;
let k = 0;
let clusterTimeCode = 0;
let clusterContent = [EBML.element(EBML.ID.Timecode, EBML.number(clusterTimeCode))];
let ret = [clusterContent];
const progressThrottler = Math.pow(2, Math.floor(Math.log(this.blocks.h264.length >> 7) / Math.log(2))) - 1;
for (i = 0; i < this.blocks.h264.length; i++) {
const e = this.blocks.h264[i];
for (; j < this.blocks.aac.length; j++) {
if (this.blocks.aac[j].timestamp < e.timestamp) {
clusterContent.push(this.getBlocks(this.blocks.aac[j], clusterTimeCode));
}
else {
break;
}
}
for (; k < this.blocks.ass.length; k++) {
if (this.blocks.ass[k].timestamp < e.timestamp) {
clusterContent.push(this.getBlocks(this.blocks.ass[k], clusterTimeCode));
}
else {
break;
}
}
if (e.isKeyframe/* || clusterContent.length > 72 */) {
// start new cluster
clusterTimeCode = e.timestamp;
clusterContent = [EBML.element(EBML.ID.Timecode, EBML.number(clusterTimeCode))];
ret.push(clusterContent);
}
clusterContent.push(this.getBlocks(e, clusterTimeCode));
if (this.onprogress && !(i & progressThrottler)) this.onprogress({ loaded: i, total: this.blocks.h264.length });
}
for (; j < this.blocks.aac.length; j++) clusterContent.push(this.getBlocks(this.blocks.aac[j], clusterTimeCode));
for (; k < this.blocks.ass.length; k++) clusterContent.push(this.getBlocks(this.blocks.ass[k], clusterTimeCode));
if (this.onprogress) this.onprogress({ loaded: i, total: this.blocks.h264.length });
if (ret[0].length == 1) ret.shift();
ret = ret.map(clusterContent => EBML.element(EBML.ID.Cluster, clusterContent));
return ret;
}
getBlocks(e, clusterTimeCode) {
if (e.simple) {
return EBML.element(EBML.ID.SimpleBlock, [
EBML.vintEncodedNumber(e.track),
EBML.int16(e.timestamp - clusterTimeCode),
EBML.bytes(e.isKeyframe ? [128] : [0]),
EBML.bytes(e.frame)
]);
}
else {
let blockGroupContent = [EBML.element(EBML.ID.Block, [
EBML.vintEncodedNumber(e.track),
EBML.int16(e.timestamp - clusterTimeCode),
EBML.bytes([0]),
EBML.bytes(e.frame)
])];
if (typeof e.duration != 'undefined') {
blockGroupContent.push(EBML.element(EBML.ID.BlockDuration, EBML.number(e.duration)));
}
if (typeof e.codecState != 'undefined') {
blockGroupContent.push(EBML.element(EBML.ID.CodecState, EBML.bytes(e.codecState)));
}
return EBML.element(EBML.ID.BlockGroup, blockGroupContent);
}
}
};
const FLVASS2MKV = class {
constructor(config = {}) {
this.onflvprogress = null;
this.onassprogress = null;
this.onurlrevokesafe = null;
this.onfileload = null;
this.onmkvprogress = null;
this.onload = null;
Object.assign(this, config);
this.mkvConfig = { onprogress: this.onmkvprogress };
Object.assign(this.mkvConfig, config.mkvConfig);
}
/**
* Demux FLV into H264 + AAC stream and ASS into line stream; then
* remux them into a MKV file.
* @param {Blob|string|ArrayBuffer} flv
* @param {Blob|string|ArrayBuffer} ass
*/
async build(flv = './gen_case.flv', ass = './gen_case.ass') {
// load flv and ass as arraybuffer
await Promise.all([
new Promise((r, j) => {
if (flv instanceof Blob) {
const e = new FileReader();
e.onprogress = this.onflvprogress;
e.onload = () => r(flv = e.result);
e.onerror = j;
e.readAsArrayBuffer(flv);
}
else if (typeof flv == 'string') {
const e = new XMLHttpRequest();
e.responseType = 'arraybuffer';
e.onprogress = this.onflvprogress;
e.onload = () => r(flv = e.response);
e.onerror = j;
e.open('get', flv);
e.send();
flv = 2; // onurlrevokesafe
}
else if (flv instanceof ArrayBuffer) {
r(flv);
}
else {
j(new TypeError('flvass2mkv: flv {Blob|string|ArrayBuffer}'));
}
if (typeof ass != 'string' && this.onurlrevokesafe) this.onurlrevokesafe();
}),
new Promise((r, j) => {
if (ass instanceof Blob) {
const e = new FileReader();
e.onprogress = this.onflvprogress;
e.onload = () => r(ass = e.result);
e.onerror = j;
e.readAsArrayBuffer(ass);
}
else if (typeof ass == 'string') {
const e = new XMLHttpRequest();
e.responseType = 'arraybuffer';
e.onprogress = this.onflvprogress;
e.onload = () => r(ass = e.response);
e.onerror = j;
e.open('get', ass);
e.send();
ass = 2; // onurlrevokesafe
}
else if (ass instanceof ArrayBuffer) {
r(ass);
}
else {
j(new TypeError('flvass2mkv: ass {Blob|string|ArrayBuffer}'));
}
if (typeof flv != 'string' && this.onurlrevokesafe) this.onurlrevokesafe();
}),
]);
if (this.onfileload) this.onfileload();
const mkv = new MKV(this.mkvConfig);
const assParser = new ASS();
ass = assParser.parseFile(ass);
mkv.addASSMetadata(ass);
mkv.addASSStream(ass);
const flvProbeData = FLVDemuxer.probe(flv);
const flvDemuxer = new FLVDemuxer(flvProbeData);
let mediaInfo = null;
let h264 = null;
let aac = null;
flvDemuxer.onDataAvailable = (...array) => {
array.forEach(e => {
if (e.type == 'video') h264 = e;
else if (e.type == 'audio') aac = e;
else throw new Error(\`MKVRemuxer: unrecoginzed data type \${e.type}\`);
});
};
flvDemuxer.onMediaInfo = i => mediaInfo = i;
flvDemuxer.onTrackMetadata = (i, e) => {
if (i == 'video') mkv.addH264Metadata(e);
else if (i == 'audio') mkv.addAACMetadata(e);
else throw new Error(\`MKVRemuxer: unrecoginzed metadata type \${i}\`);
};
flvDemuxer.onError = e => { throw new Error(e); };
const finalOffset = flvDemuxer.parseChunks(flv, flvProbeData.dataOffset);
if (finalOffset != flv.byteLength) throw new Error('FLVDemuxer: unexpected EOF');
mkv.addH264Stream(h264);
mkv.addAACStream(aac);
const ret = mkv.build();
if (this.onload) this.onload(ret);
return ret;
}
};
// if nodejs then test
if (typeof require == 'function') {
(async () => {
const fs = require('fs');
const assFile = fs.readFileSync('gen_case.ass').buffer;
const flvFile = fs.readFileSync('large_case.flv').buffer;
fs.writeFileSync('out.mkv', await new FLVASS2MKV({ onmkvprogress: console.log.bind(console) }).build(flvFile, assFile));
})();
}
</script>
<script>
const fileProgress = document.getElementById('fileProgress');
const mkvProgress = document.getElementById('mkvProgress');
const a = document.getElementById('a');
top.exec = async option => {
const defaultOption = {
onflvprogress: ({ loaded, total }) => {
fileProgress.value = loaded;
fileProgress.max = total;
},
onfileload: () => {
console.timeEnd('file');
console.time('flvass2mkv');
},
onmkvprogress: ({ loaded, total }) => {
mkvProgress.value = loaded;
mkvProgress.max = total;
},
name: 'merged.mkv',
};
option = Object.assign(defaultOption, option);
a.download = a.textContent = option.name;
console.time('file');
const mkv = await new FLVASS2MKV(option).build(option.flv, option.ass);
console.timeEnd('flvass2mkv');
a.href = URL.createObjectURL(mkv);
}
</script>
`);
// 3. Invoke exec
if (!(this.option instanceof Object)) this.option = null;
this.playerWin.exec(Object.assign({}, this.option, { flv, ass, name }));
URL.revokeObjectURL(flv);
URL.revokeObjectURL(ass);
// 4. Free parent window
// if (top.confirm('MKV打包中……要关掉这个窗口,释放内存吗?'))
top.location = 'about:blank';
}
}
class BiliMonkey {
constructor(playerWin, option = { cache: null, partial: false, proxy: false, blocker: false, font: false }) {
this.playerWin = playerWin;
this.protocol = playerWin.location.protocol;
this.cid = null;
this.flvs = null;
this.mp4 = null;
this.ass = null;
this.flvFormatName = null;
this.mp4FormatName = null;
this.cidAsyncContainer = new AsyncContainer();
this.cidAsyncContainer.then(cid => { this.cid = cid; this.ass = this.getASS(); });
if (typeof top.cid === 'string') this.cidAsyncContainer.resolve(top.cid);
/* cache + proxy = Service Worker
* Hope bilibili will have a SW as soon as possible.
* partial = Stream
* Hope the fetch API will be stabilized as soon as possible.
* If you are using your grandpa's browser, do not enable these functions.
**/
this.cache = option.cache;
this.partial = option.partial;
this.proxy = option.proxy;
this.blocker = option.blocker;
this.font = option.font;
this.option = option;
if (this.cache && (!(this.cache instanceof CacheDB))) this.cache = new CacheDB('biliMonkey', 'flv', 'name');
this.flvsDetailedFetch = [];
this.flvsBlob = [];
this.defaultFormatPromise = null;
this.queryInfoMutex = new Mutex();
this.queryInfoMutex.lockAndAwait(() => this.getPlayerButtons());
this.queryInfoMutex.lockAndAwait(() => this.getAvailableFormatName());
}
lockFormat(format) {
// null => uninitialized
// async pending => another one is working on it
// async resolve => that guy just finished work
// sync value => someone already finished work
let h = this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-top')[0];
if (h) h.style.visibility = 'hidden';
switch (format) {
// Single writer is not a must.
// Plus, if one writer fail, others should be able to overwrite its garbage.
case 'flv_p60':
case 'flv720_p60':
case 'hdflv2':
case 'flv':
case 'flv720':
case 'flv480':
case 'flv320':
//if (this.flvs) return this.flvs;
return this.flvs = new AsyncContainer();
case 'hdmp4':
case 'mp4':
//if (this.mp4) return this.mp4;
return this.mp4 = new AsyncContainer();
default:
throw `lockFormat error: ${format} is a unrecognizable format`;
}
}
resolveFormat(res, shouldBe) {
let h = this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-top')[0];
if (h) {
h.style.visibility = '';
if (h.children.length) h.children[0].style.visibility = 'hidden';
let i = e => {
if (h.children.length) h.children[0].style.visibility = 'hidden';
e.target.removeEventListener(e.type, i);
};
let j = this.playerWin.document.getElementsByTagName('video')[0];
if (j) j.addEventListener('emptied', i);
}
switch (res.format) {
case 'flv_p60':
case 'flv720_p60':
case 'hdflv2':
case 'flv':
case 'flv720':
case 'flv480':
case 'flv320':
if (shouldBe && shouldBe != res.format) {
this.flvs = null;
throw `URL interface error: response is not ${shouldBe}`;
}
return this.flvs = this.flvs.resolve(res.durl.map(e => e.url.replace('http:', this.protocol)));
case 'hdmp4':
case 'mp4':
if (shouldBe && shouldBe != res.format) {
this.mp4 = null;
throw `URL interface error: response is not ${shouldBe}`;
}
return this.mp4 = this.mp4.resolve(res.durl[0].url.replace('http:', this.protocol));
default:
throw `resolveFormat error: ${res.format} is a unrecognizable format`;
}
}
getAvailableFormatName(accept_quality) {
if (!Array.isArray(accept_quality)) accept_quality = Array.from(this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul').getElementsByTagName('li')).map(e => e.getAttribute('data-value'));
accept_quality = accept_quality.map(e => e.toString());
if (accept_quality.includes('80')) this.flvFormatName = 'flv';
else if (accept_quality.includes('64')) this.flvFormatName = 'flv720';
else if (accept_quality.includes('32')) this.flvFormatName = 'flv480';
else if (accept_quality.includes('15')) this.flvFormatName = 'flv320';
else this.flvFormatName = 'does_not_exist';
if (accept_quality.includes('16')) this.mp4FormatName = 'mp4';
else this.mp4FormatName = 'does_not_exist';
}
async execOptions() {
if (this.cache) await this.cache.getDB();
if (this.option.autoDefault) await this.sniffDefaultFormat();
if (this.option.autoFLV) this.queryInfo('flv');
if (this.option.autoMP4) this.queryInfo('mp4');
}
async sniffDefaultFormat() {
if (this.defaultFormatPromise) return this.defaultFormatPromise;
if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li')) return this.defaultFormatPromise = Promise.resolve();
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
this.defaultFormatPromise = new Promise(resolve => {
let timeout = setTimeout(() => { jq.ajax = _ajax; resolve(); }, 5000);
let self = this;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
clearTimeout(timeout);
self.cidAsyncContainer.resolve(a.url.match(/cid=\d+/)[0].slice(4));
let _success = a.success;
a.success = res => {
let format = res.format;
let accept_format = res.accept_format.split(',');
switch (format) {
case 'flv360':
if (accept_format.includes('flv480')) break;
case 'flv480':
if (accept_format.includes('flv720')) break;
case 'flv720':
if (accept_format.includes('flv')) break;
case 'flv720_p60':
if (accept_format.includes('flv_p60')) break;
case 'flv':
case 'hdflv2':
case 'flv_p60':
self.lockFormat(format);
self.resolveFormat(res, format);
break;
case 'mp4':
if (accept_format.includes('hdmp4')) break;
case 'hdmp4':
self.lockFormat(format);
self.resolveFormat(res, format);
break;
}
if (self.proxy && self.flvs) {
self.setupProxy(res, _success);
}
else {
_success(res);
}
resolve(res);
};
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
});
return this.defaultFormatPromise;
}
async getBackgroundFormat(format) {
if (format == 'hdmp4' || format == 'mp4') {
let src = this.playerWin.document.getElementsByTagName('video')[0].src;
if ((src.includes('hd') || format == 'mp4') && src.includes('.mp4')) {
let pendingFormat = this.lockFormat(format);
this.resolveFormat({ durl: [{ url: src }] }, format);
return pendingFormat;
}
}
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
let pendingFormat = this.lockFormat(format);
let self = this;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
self.cidAsyncContainer.resolve(a.url.match(/cid=\d+/)[0].slice(4));
let _success = a.success;
a.success = res => {
if (format == 'hdmp4') res.durl = [res.durl[0].backup_url.find(e => e.includes('hd') && e.includes('.mp4'))];
if (format == 'mp4') res.durl = [res.durl[0].backup_url.find(e => !e.includes('hd') && e.includes('.mp4'))];
self.resolveFormat(res, format);
};
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
this.playerWin.player.reloadAccess();
return pendingFormat;
}
async getCurrentFormat(format) {
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
const _setItem = this.playerWin.localStorage.setItem;
const siblingFormat = format == this.flvFormatName ? this.mp4FormatName : this.flvFormatName;
const fakedRes = { 'from': 'local', 'result': 'suee', 'format': 'faked_mp4', 'timelength': 10, 'accept_format': 'hdflv2,flv,hdmp4,faked_mp4,mp4', 'accept_quality': [112, 80, 64, 32, 16], 'seek_param': 'start', 'seek_type': 'second', 'durl': [{ 'order': 1, 'length': 1000, 'size': 30000, 'url': 'https://static.hdslb.com/encoding.mp4', 'backup_url': ['https://static.hdslb.com/encoding.mp4'] }] };
let pendingFormat = this.lockFormat(format);
let self = this;
let blockedRequest = await new Promise(resolve => {
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
// Send back a fake response to enable the change-format button.
self.cidAsyncContainer.resolve(a.url.match(/cid=\d+/)[0].slice(4));
a.success(fakedRes);
self.playerWin.document.getElementsByTagName('video')[1].loop = true;
let h = e => { resolve([a, c]); e.target.removeEventListener(e.type, h); };
self.playerWin.document.getElementsByTagName('video')[0].addEventListener('emptied', h);
}
else {
return _ajax.call(jq, a, c);
}
};
this.playerWin.localStorage.setItem = () => this.playerWin.localStorage.setItem = _setItem;
this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div ul li[data-value="${BiliMonkey.formatToValue(siblingFormat)}"]`).click();
});
let siblingOK = siblingFormat == this.flvFormatName ? this.flvs : this.mp4;
if (!siblingOK) {
this.lockFormat(siblingFormat);
blockedRequest[0].success = res => this.resolveFormat(res, siblingFormat);
_ajax.call(jq, ...blockedRequest);
}
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
let _success = a.success;
a.success = res => {
if (self.proxy) {
self.resolveFormat(res, format);
if (self.flvs) self.setupProxy(res, _success);
}
else {
_success(res);
self.resolveFormat(res, format);
}
};
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
this.playerWin.localStorage.setItem = () => this.playerWin.localStorage.setItem = _setItem;
this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div ul li[data-value="${BiliMonkey.formatToValue(format)}"]`).click();
return pendingFormat;
}
async getNonCurrentFormat(format) {
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
const _setItem = this.playerWin.localStorage.setItem;
let pendingFormat = this.lockFormat(format);
let self = this;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
self.cidAsyncContainer.resolve(a.url.match(/cid=\d+/)[0].slice(4));
let _success = a.success;
_success({});
a.success = res => self.resolveFormat(res, format);
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
this.playerWin.localStorage.setItem = () => this.playerWin.localStorage.setItem = _setItem;
this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div ul li[data-value="${BiliMonkey.formatToValue(format)}"]`).click();
return pendingFormat;
}
async getASS(clickableFormat) {
if (this.ass) return this.ass;
this.ass = new Promise(async resolve => {
if (!this.cid) this.cid = await new Promise(resolve => {
if (!clickableFormat) reject('get ASS Error: cid unavailable, nor clickable format given.');
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
const _setItem = this.playerWin.localStorage.setItem;
this.lockFormat(clickableFormat);
let self = this;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
resolve(self.cid = a.url.match(/cid=\d+/)[0].slice(4));
let _success = a.success;
_success({});
a.success = res => self.resolveFormat(res, clickableFormat);
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
this.playerWin.localStorage.setItem = () => this.playerWin.localStorage.setItem = _setItem;
this.playerWin.document.querySelector(`div.bilibili-player-video-btn-quality > div ul li[data-value="${BiliMonkey.formatToValue(clickableFormat)}"]`).click();
});
let bilibili_player_settings = this.playerWin.localStorage.bilibili_player_settings && JSON.parse(this.playerWin.localStorage.bilibili_player_settings);
let option = bilibili_player_settings && this.font && {
'fontlist': bilibili_player_settings.setting_config['fontfamily'] != 'custom' ? bilibili_player_settings.setting_config['fontfamily'].split(/, ?/) : bilibili_player_settings.setting_config['fontfamilycustom'].split(/, ?/),
'font_size': parseFloat(bilibili_player_settings.setting_config['fontsize']),
'opacity': parseFloat(bilibili_player_settings.setting_config['opacity']),
'bold': bilibili_player_settings.setting_config['bold'] ? 1 : 0,
} || undefined;
const { fetchDanmaku, generateASS, setPosition } = new ASSDownloader(option);
fetchDanmaku(this.cid, danmaku => {
if (bilibili_player_settings && this.blocker) {
let regexps = bilibili_player_settings.block.list.map(e => e.v).join('|');
if (regexps) {
regexps = new RegExp(regexps);
danmaku = danmaku.filter(d => !regexps.test(d.text));
}
}
let ass = generateASS(setPosition(danmaku), {
'title': top.document.title,
'ori': top.location.href,
});
// I would assume most users are using Windows
let blob = new Blob(['\ufeff' + ass], { type: 'application/octet-stream' });
resolve(this.ass = top.URL.createObjectURL(blob));
});
});
return this.ass;
}
async queryInfo(format) {
return this.queryInfoMutex.lockAndAwait(async () => {
switch (format) {
case 'flv':
if (this.flvs)
return this.flvs;
else if (this.flvFormatName == 'does_not_exist')
return this.flvFormatName;
else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li[data-selected]').getAttribute('data-value') == BiliMonkey.formatToValue(this.flvFormatName))
return this.getCurrentFormat(this.flvFormatName);
else
return this.getNonCurrentFormat(this.flvFormatName);
case 'mp4':
if (this.mp4)
return this.mp4;
else if (this.mp4FormatName == 'does_not_exist')
return this.mp4FormatName;
else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li[data-selected]').getAttribute('data-value') == BiliMonkey.formatToValue(this.mp4FormatName))
return this.getCurrentFormat(this.mp4FormatName);
else
return this.getNonCurrentFormat(this.mp4FormatName);
case 'ass':
if (this.ass)
return this.ass;
else if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li[data-selected]').getAttribute('data-value') == BiliMonkey.formatToValue(this.flvFormatName))
return this.getASS(this.mp4FormatName);
else
return this.getASS(this.flvFormatName);
default:
throw `Bilimonkey: What is format ${format}?`;
}
});
}
async getPlayerButtons() {
if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li')) {
return this.playerWin;
}
else {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
if (this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li')) {
observer.disconnect();
resolve(this.playerWin);
}
});
observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
});
}
}
async hangPlayer() {
const fakedRes = { 'from': 'local', 'result': 'suee', 'format': 'faked_mp4', 'timelength': 10, 'accept_format': 'hdflv2,flv,hdmp4,faked_mp4,mp4', 'accept_quality': [112, 80, 64, 32, 16], 'seek_param': 'start', 'seek_type': 'second', 'durl': [{ 'order': 1, 'length': 1000, 'size': 30000, 'url': '' }] };
const jq = this.playerWin.jQuery;
const _ajax = jq.ajax;
const _setItem = this.playerWin.localStorage.setItem;
return this.queryInfoMutex.lockAndAwait(() => new Promise(async resolve => {
let blockerTimeout;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
clearTimeout(blockerTimeout);
a.success(fakedRes);
blockerTimeout = setTimeout(() => {
jq.ajax = _ajax;
resolve();
}, 2500);
}
else {
return _ajax.call(jq, a, c);
}
};
this.playerWin.localStorage.setItem = () => this.playerWin.localStorage.setItem = _setItem;
let button = Array.from(this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul').getElementsByTagName('li'))
.find(e => !e.getAttribute('data-selected') && !e.children.length);
button.click();
}));
}
async loadFLVFromCache(index) {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let name = this.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
let item = await this.cache.getData(name);
if (!item) return;
return this.flvsBlob[index] = item.data;
}
async loadPartialFLVFromCache(index) {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let name = this.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
name = 'PC_' + name;
let item = await this.cache.getData(name);
if (!item) return;
return item.data;
}
async loadAllFLVFromCache() {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let promises = [];
for (let i = 0; i < this.flvs.length; i++) promises.push(this.loadFLVFromCache(i));
return Promise.all(promises);
}
async saveFLVToCache(index, blob) {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let name = this.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
return this.cache.addData({ name, data: blob });
}
async savePartialFLVToCache(index, blob) {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let name = this.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
name = 'PC_' + name;
return this.cache.putData({ name, data: blob });
}
async cleanPartialFLVInCache(index) {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let name = this.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
name = 'PC_' + name;
return this.cache.deleteData(name);
}
async getFLV(index, progressHandler) {
if (this.flvsBlob[index]) return this.flvsBlob[index];
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
this.flvsBlob[index] = (async () => {
let cache = await this.loadFLVFromCache(index);
if (cache) return this.flvsBlob[index] = cache;
let partialFLVFromCache = await this.loadPartialFLVFromCache(index);
let burl = this.flvs[index];
if (partialFLVFromCache) burl += `&bstart=${partialFLVFromCache.size}`;
let opt = {
fetch: this.playerWin.fetch,
method: 'GET',
mode: 'cors',
cache: 'default',
referrerPolicy: 'no-referrer-when-downgrade',
cacheLoaded: partialFLVFromCache ? partialFLVFromCache.size : 0,
headers: partialFLVFromCache && (!burl.includes('wsTime')) ? { Range: `bytes=${partialFLVFromCache.size}-` } : undefined
};
opt.onprogress = progressHandler;
opt.onerror = opt.onabort = ({ target, type }) => {
let partialFLV = target.getPartialBlob();
if (partialFLVFromCache) partialFLV = new Blob([partialFLVFromCache, partialFLV]);
this.savePartialFLVToCache(index, partialFLV);
}
let fch = new DetailedFetchBlob(burl, opt);
this.flvsDetailedFetch[index] = fch;
let fullFLV = await fch.getBlob();
this.flvsDetailedFetch[index] = undefined;
if (partialFLVFromCache) {
fullFLV = new Blob([partialFLVFromCache, fullFLV]);
this.cleanPartialFLVInCache(index);
}
this.saveFLVToCache(index, fullFLV);
return (this.flvsBlob[index] = fullFLV);
})();
return this.flvsBlob[index];
}
async abortFLV(index) {
if (this.flvsDetailedFetch[index]) return this.flvsDetailedFetch[index].abort();
}
async getAllFLVs(progressHandler) {
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let promises = [];
for (let i = 0; i < this.flvs.length; i++) promises.push(this.getFLV(i, progressHandler));
return Promise.all(promises);
}
async cleanAllFLVsInCache() {
if (!this.cache) return;
if (!this.flvs) throw 'BiliMonkey: info uninitialized';
let promises = [];
for (let flv of this.flvs) {
let name = flv.match(/\d+-\d+(?:-\d+)?\.flv/)[0];
promises.push(this.cache.deleteData(name));
promises.push(this.cache.deleteData('PC_' + name));
}
return Promise.all(promises);
}
async setupProxy(res, onsuccess) {
(() => {
let _fetch = this.playerWin.fetch;
this.playerWin.fetch = function (input, init) {
if (!input.slice || input.slice(0, 5) != 'blob:') {
return _fetch(input, init);
}
let bstart = input.indexOf('?bstart=');
if (bstart < 0) {
return _fetch(input, init);
}
if (!init.headers instanceof Headers) init.headers = new Headers(init.headers || {});
init.headers.set('Range', `bytes=${input.slice(bstart + 8)}-`);
return _fetch(input.slice(0, bstart), init)
}
})();
await this.loadAllFLVFromCache();
let resProxy = Object.assign({}, res);
for (let i = 0; i < this.flvsBlob.length; i++) {
if (this.flvsBlob[i]) resProxy.durl[i].url = this.playerWin.URL.createObjectURL(this.flvsBlob[i]);
}
return onsuccess(resProxy);
}
static async getAllPageDefaultFormats(playerWin = top) {
const jq = playerWin.jQuery;
const _ajax = jq.ajax;
const queryInfoMutex = new Mutex();
const { fetchDanmaku, generateASS, setPosition } = new ASSDownloader();
const list = await new Promise(resolve => {
const i = setInterval(() => {
const ret = playerWin.player.getPlaylist();
if (ret) {
clearInterval(i);
resolve(ret);
}
}, 500);
});
const index = list.reduce((acc, cur) => { acc[cur.cid] = cur; return acc }, {});
const end = list[list.length - 1].cid.toString();
const ret = [];
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('comment.bilibili.com') || a.url.includes('interface.bilibili.com/player?') || a.url.includes('api.bilibili.com/x/player/playurl/token')) return _ajax.call(jq, a, c);
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
(async () => {
a.success = undefined;
let cid = a.url.match(/cid=\d+/)[0].slice(4);
const [danmuku, res] = await Promise.all([
new Promise(resolve => {
fetchDanmaku(cid, danmaku => {
let ass = generateASS(setPosition(danmaku), {
'title': document.title,
'ori': location.href,
});
// I would assume most users are using Windows
let blob = new Blob(['\ufeff' + ass], { type: 'application/octet-stream' });
resolve(playerWin.URL.createObjectURL(blob));
});
}),
_ajax.call(jq, a, c)
]);
ret.push({
durl: res.durl.map(({ url }) => url.replace('http:', playerWin.location.protocol)),
danmuku,
name: index[cid].part || index[cid].index,
outputName: res.durl[0].url.match(/\d+-\d+(?:-\d+)?(?=\.flv)/) ?
res.durl[0].url.match(/\d+-\d+(?:-\d+)?(?=\.flv)/)[0].replace(/(?<=\d+)-\d+(?=(?:-\d+)?\.flv)/, '')
: res.durl[0].url.match(/\d(?:\d|-|hd)*(?=\.mp4)/) ?
res.durl[0].url.match(/\d(?:\d|-|hd)*(?=\.mp4)/)
: cid,
cid,
res,
});
queryInfoMutex.unlock();
})();
}
return _ajax.call(jq, { url: '//0.0.0.0' });
};
await queryInfoMutex.lock();
playerWin.player.next(1);
while (1) {
await queryInfoMutex.lock();
if (ret[ret.length - 1].cid == end) break;
playerWin.player.next();
}
let table = [`
<style>
table {
width: 100%;
table-layout: fixed;
}
td {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
}
</style>
`,
`
<h1>(测试) 批量抓取</h1>
<ul>
<li>
<p>只抓取默认清晰度</p>
</li>
<li>
<p>复制链接地址无效,请左键单击/右键另存为/右键调用下载工具</p>
<p><em>开发者:需要校验referrer和user agent</em></p>
</li>
<li>
<p>flv合并 <a href='http://www.flvcd.com/teacher2.htm'>硕鼠</a></p>
<p>批量合并对单标签页负荷太大</p>
<p><em>开发者:可以用webworker,但是我没需求,又懒</em></p>
</li>
</ul>
`,
`
<table onclick="function(e) {e.stopPropagation(); }">
<tbody>
<tr>
<th style="width: 10em;">标题</th>
<th>视频(flv/mp4) <a download="bilitwin.ef2" href=${typeof UI == 'function' && typeof UI.exportIDM == 'function' && playerWin.URL.createObjectURL(new Blob([UI.exportIDM([].concat.apply([], ret.map(e => e.durl)), top.location.origin)]))}>批量导出IDM</a></th>
<th>弹幕(ass)</th>
</tr>
`];
for (let i of ret) {
table.push(`
<tr>
<td>
${i.name}
</td>
<td>
<a href="${i.durl[0]}" download referrerpolicy="origin">${i.durl[0]}</a>
</td>
<td>
<a href="${i.danmuku}" download="${i.outputName}.ass" referrerpolicy="origin">${i.danmuku}</a>
</td>
</tr>`);
for (let j of i.durl.slice(1)) {
table.push(`
<tr>
<td>
</td>
<td>
<a href="${j}" download referrerpolicy="origin">${j}</a>
</td>
<td>
</td>
</tr>`);
}
}
table.push(`
</tbody>
</table>
`);
playerWin.document.write(table.join(''));
return ret;
}
static formatToValue(format) {
switch (format) {
case 'flv_p60': return '116';
case 'flv720_p60': return '74';
case 'flv': return '80';
case 'flv720': return '64';
case 'flv480': return '32';
case 'flv320': return '15';
// legacy - late 2017
case 'hdflv2': return '112';
case 'hdmp4': return '64'; // data-value is still '64' instead of '48'. return '48';
case 'mp4': return '16';
default: return null;
}
}
static valueToFormat(value) {
switch (parseInt(value)) {
case 116: return 'flv_p60';
case 74: return 'flv720_p60';
case 80: return 'flv';
case 64: return 'flv720';
case 32: return 'flv480';
case 15: return 'flv320';
// legacy - late 2017
case 112: return 'hdflv2';
case 48: return 'hdmp4';
case 16: return 'mp4';
// legacy - early 2017
case 3: return 'flv';
case 2: return 'hdmp4';
case 1: return 'mp4';
default: return null;
}
}
static _UNIT_TEST() {
(async () => {
let playerWin = await BiliUserJS.getPlayerWin();
window.m = new BiliMonkey(playerWin);
console.warn('sniffDefaultFormat test');
await m.sniffDefaultFormat();
console.log(m);
console.warn('data race test');
m.queryInfo('mp4');
console.log(m.queryInfo('mp4'));
console.warn('getNonCurrentFormat test');
console.log(await m.queryInfo('mp4'));
console.warn('getCurrentFormat test');
console.log(await m.queryInfo('flv'));
//location.reload();
})();
}
}
class BiliPolyfill {
constructor(playerWin,
option = {
setStorage: (n, i) => playerWin.localStorage.setItem(n, i),
getStorage: n => playerWin.localStorage.getItem(n),
badgeWatchLater: true,
dblclick: true,
scroll: true,
recommend: true,
electric: true,
electricSkippable: false,
lift: true,
autoResume: true,
autoPlay: false,
autoWideScreen: false,
autoFullScreen: false,
oped: true,
focus: true,
menuFocus: true,
limitedKeydown: true,
speech: false,
series: true,
}, hintInfo = () => { }) {
this.playerWin = playerWin;
this.video = null;
this.vanillaPlayer = null;
this.option = option;
this.setStorage = option.setStorage;
this.getStorage = option.getStorage;
this.hintInfo = hintInfo;
this.series = [];
this.userdata = { oped: {} };
}
saveUserdata() {
this.setStorage('biliPolyfill', JSON.stringify(this.userdata));
}
retrieveUserdata() {
try {
this.userdata = this.getStorage('biliPolyfill');
if (this.userdata.length > 1073741824) top.alert('BiliPolyfill脚本数据已经快满了,在播放器上右键->BiliPolyfill->片头片尾->检视数据,删掉一些吧。');
this.userdata = JSON.parse(this.userdata);
}
catch (e) { }
finally {
if (!this.userdata) this.userdata = {};
if (!(this.userdata.oped instanceof Object)) this.userdata.oped = {};
}
}
async setFunctions({ videoRefresh = false } = {}) {
// 1. Initialize
this.video = await this.getPlayerVideo();
// 2. If not enabled, run the process without real actions
if (!this.option.betabeta) return this.getPlayerMenu();
// 3. Set up functions that are page static
if (!videoRefresh) {
this.retrieveUserdata();
if (this.option.badgeWatchLater) this.badgeWatchLater();
if (this.option.scroll) this.scrollToPlayer();
if (this.option.recommend) this.showRecommendTab();
if (this.option.autoResume) this.autoResume();
if (this.option.autoPlay) this.autoPlay();
if (this.option.autoWideScreen) this.autoWideScreen();
if (this.option.autoFullScreen) this.autoFullScreen();
if (this.option.focus) this.focusOnPlayer();
if (this.option.limitedKeydown) this.limitedKeydownFullScreenPlay();
if (this.option.series) this.inferNextInSeries();
this.playerWin.addEventListener('beforeunload', () => this.saveUserdata());
}
// 4. Set up functions that are binded to the video DOM
if (this.option.lift) this.liftBottomDanmuku();
if (this.option.dblclick) this.dblclickFullScreen();
if (this.option.electric) this.reallocateElectricPanel();
if (this.option.oped) this.skipOPED();
this.video.addEventListener('emptied', () => this.setFunctions({ videoRefresh: true }));
// 5. Set up functions that require everything to be ready
await this.getPlayerMenu();
if (this.option.menuFocus) this.menuFocusOnPlayer();
// 6. Set up experimental functions
if (this.option.speech) top.document.body.addEventListener('click', e => e.detail > 2 && this.speechRecognition());
}
async inferNextInSeries() {
let title = top.document.getElementsByTagName('h1')[0].textContent.replace(/\(\d+\)$/, '').trim();
// 1. Find series name
let epNumberText = title.match(/\d+/g);
if (!epNumberText) return this.series = [];
epNumberText = epNumberText.pop();
let seriesTitle = title.slice(0, title.lastIndexOf(epNumberText)).trim();
// 2. Substitude ep number
let ep = parseInt(epNumberText);
if (epNumberText === '09') ep = [`08`, `10`];
else if (epNumberText[0] === '0') ep = [`0${ep - 1}`, `0${ep + 1}`];
else ep = [`${ep - 1}`, `${ep + 1}`];
ep = [...ep.map(e => seriesTitle + e), ...ep];
let mid = top.document.getElementById('r-info-rank') || top.document.querySelector('.user');
if (!mid) return this.series = [];
mid = mid.children[0].href.match(/\d+/)[0];
let vlist = await Promise.all([title, ...ep].map(keyword => new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.onload = () => resolve((req.response.status && req.response.data.vlist) || []);
req.onerror = reject;
req.open('get', `https://space.bilibili.com/ajax/member/getSubmitVideos?mid=${mid}&keyword=${keyword}`);
req.responseType = 'json';
req.send();
})));
vlist[0] = [vlist[0].find(e => e.title == title)];
if (!vlist[0][0]) { console && console.warn('BiliPolyfill: inferNextInSeries: cannot find current video in mid space'); return this.series = []; }
this.series = [vlist[1].find(e => e.created < vlist[0][0].created), vlist[2].reverse().find(e => e.created > vlist[0][0].created)];
if (!this.series[0]) this.series[0] = vlist[3].find(e => e.created < vlist[0][0].created) || null;
if (!this.series[1]) this.series[1] = vlist[4].reverse().find(e => e.created > vlist[0][0].created) || null;
return this.series;
}
badgeWatchLater() {
let li = top.document.getElementById('i_menu_watchLater_btn') || top.document.getElementById('i_menu_later_btn');
if (!li || !li.children[1]) return;
li.children[1].style.visibility = 'hidden';
li.dispatchEvent(new Event('mouseover'));
let observer = new MutationObserver(() => {
if (li.children[1].children[0].children[0].className == 'm-w-loading') return;
observer.disconnect();
li.dispatchEvent(new Event('mouseout'));
setTimeout(() => li.children[1].style.visibility = '', 700);
if (li.children[1].children[0].children[0].className == 'no-data') return;
let div = top.document.createElement('div');
div.className = 'num';
div.style.display = 'block';
div.style.left = 'initial';
div.style.right = '-6px';
if (li.children[1].children[0].children.length > 5) {
div.textContent = '5+';
}
else {
div.textContent = li.children[1].children[0].children.length;
}
li.appendChild(div);
});
observer.observe(li.children[1].children[0], { childList: true });
}
dblclickFullScreen() {
this.video.addEventListener('dblclick', () =>
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click()
);
}
scrollToPlayer() {
if (top.scrollY < 200) top.document.getElementById('bofqi').scrollIntoView();
}
showRecommendTab() {
let h = this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-filter-btn-recommend');
if (h) h.click();
}
getCoverImage() {
let ret = top.document.querySelector('.cover_image')
|| top.document.querySelector('div.info-cover > a > img')
|| top.document.querySelector('[data-state-play="true"] img')
|| top.document.querySelector('script[type="application/ld+json"]');
if (!ret) return null;
ret = ret.src || JSON.parse(ret.textContent).images[0];
let i;
i = ret.indexOf('.jpg');
if (i != -1) ret = ret.slice(0, i + 4);
i = ret.indexOf('.png');
if (i != -1) ret = ret.slice(0, i + 4);
return ret;
}
reallocateElectricPanel() {
if (!this.playerWin.localStorage.bilibili_player_settings) return;
if (!this.playerWin.localStorage.bilibili_player_settings.includes('"autopart":1') && !this.option.electricSkippable) return;
this.video.addEventListener('ended', () => {
setTimeout(() => {
let i = this.playerWin.document.getElementsByClassName('bilibili-player-electric-panel')[0];
if (!i) return;
i.children[2].click();
i.style.display = 'block';
i.style.zIndex = 233;
let j = 5;
let h = setInterval(() => {
if (this.playerWin.document.getElementsByClassName('bilibili-player-video-toast-item-jump')[0]) i.style.zIndex = '';
if (j > 0) {
i.children[2].children[0].textContent = `0${j}`;
j--;
}
else {
clearInterval(h);
i.remove();
}
}, 1000);
}, 0);
});
}
liftBottomDanmuku() {
// MUST initialize setting panel before click
this.playerWin.document.getElementsByName('ctlbar_danmuku_close')[0].dispatchEvent(new Event('mouseover'));
this.playerWin.document.getElementsByName('ctlbar_danmuku_close')[0].dispatchEvent(new Event('mouseout'));
if (!this.playerWin.document.getElementsByName('ctlbar_danmuku_prevent')[0].nextSibling.className.includes('bpui-state-active'))
this.playerWin.document.getElementsByName('ctlbar_danmuku_prevent')[0].click();
}
loadOffineSubtitles() {
// NO. NOBODY WILL NEED THIS。
// Hint: https://github.com/jamiees2/ass-to-vtt
throw 'Not implemented';
}
autoResume() {
let h = () => {
let span = this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-text span:nth-child(2)');
if (!span) return;
let [min, sec] = span.textContent.split(':');
if (!min || !sec) return;
let time = parseInt(min) * 60 + parseInt(sec);
if (time < this.video.duration - 10) {
if (!this.video.paused || this.video.autoplay) {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-jump').click();
}
else {
let play = this.video.play;
this.video.play = () => setTimeout(() => {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
this.video.play = play;
}, 0);
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-jump').click();
}
}
else {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom div.bilibili-player-video-toast-item-close').click();
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-toast-bottom').children[0].style.visibility = 'hidden';
}
};
this.video.addEventListener('canplay', h);
setTimeout(() => this.video && this.video.removeEventListener && this.video.removeEventListener('canplay', h), 3000);
}
autoPlay() {
this.video.autoplay = true;
setTimeout(() => {
if (this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click()
}, 0);
}
autoWideScreen() {
if (this.playerWin.document.querySelector('#bilibiliPlayer i.icon-24wideoff'))
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-widescreen').click();
}
autoFullScreen() {
if (this.playerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off'))
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
}
getCollectionId() {
return (top.location.pathname.match(/av\d+/) || top.location.hash.match(/av\d+/) || top.document.querySelector('div.bangumi-info a').href).toString();
}
markOPEDPosition(index) {
let collectionId = this.getCollectionId();
if (!Array.isArray(this.userdata.oped[collectionId])) this.userdata.oped[collectionId] = [];
this.userdata.oped[collectionId][index] = this.video.currentTime;
}
clearOPEDPosition() {
let collectionId = this.getCollectionId();
this.userdata.oped[collectionId] = undefined;
}
skipOPED() {
let collectionId = this.getCollectionId();
if (!Array.isArray(this.userdata.oped[collectionId]) || !this.userdata.oped[collectionId].length) return;
/**
* structure:
* listen for time update -> || <- skip -> || <- remove event listenner
*/
if (!this.userdata.oped[collectionId][0] && this.userdata.oped[collectionId][1]) {
let h = () => {
if (this.video.currentTime >= this.userdata.oped[collectionId][1] - 1) {
this.video.removeEventListener('timeupdate', h);
}
else {
this.video.currentTime = this.userdata.oped[collectionId][1];
this.hintInfo('BiliPolyfill: 已跳过片头');
}
}
this.video.addEventListener('timeupdate', h);
}
if (this.userdata.oped[collectionId][0] && this.userdata.oped[collectionId][1]) {
let h = () => {
if (this.video.currentTime >= this.userdata.oped[collectionId][1] - 1) {
this.video.removeEventListener('timeupdate', h);
}
else if (this.video.currentTime > this.userdata.oped[collectionId][0]) {
this.video.currentTime = this.userdata.oped[collectionId][1];
this.hintInfo('BiliPolyfill: 已跳过片头');
}
}
this.video.addEventListener('timeupdate', h);
}
if (this.userdata.oped[collectionId][2] && !this.userdata.oped[collectionId][3]) {
let h = () => {
if (this.video.currentTime >= this.video.duration - 1) {
this.video.removeEventListener('timeupdate', h);
}
else if (this.video.currentTime > this.userdata.oped[collectionId][2]) {
this.video.currentTime = this.video.duration;
this.hintInfo('BiliPolyfill: 已跳过片尾');
}
}
this.video.addEventListener('timeupdate', h);
}
if (this.userdata.oped[collectionId][2] && this.userdata.oped[collectionId][3]) {
let h = () => {
if (this.video.currentTime >= this.userdata.oped[collectionId][3] - 1) {
this.video.removeEventListener('timeupdate', h);
}
else if (this.video.currentTime > this.userdata.oped[collectionId][2]) {
this.video.currentTime = this.userdata.oped[collectionId][3];
this.hintInfo('BiliPolyfill: 已跳过片尾');
}
}
this.video.addEventListener('timeupdate', h);
}
}
setVideoSpeed(speed) {
if (speed < 0 || speed > 10) return;
this.video.playbackRate = speed;
}
focusOnPlayer() {
this.playerWin.document.getElementsByClassName('bilibili-player-video-progress')[0].click();
}
menuFocusOnPlayer() {
this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0].addEventListener('click', () => setTimeout(() => this.focusOnPlayer(), 0));
}
limitedKeydownFullScreenPlay() {
let h = e => {
if (!e.isTrusted) return;
if (e.key == 'Enter') {
if (this.playerWin.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off')) {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
}
if (this.video.paused) {
if (this.video.readyState) {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
}
else {
let i = () => {
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
this.video.removeEventListener('canplay', i);
}
this.video.addEventListener('canplay', i);
}
}
}
top.document.removeEventListener('keydown', h);
top.document.removeEventListener('click', h);
};
top.document.addEventListener('keydown', h);
top.document.addEventListener('click', h);
}
speechRecognition() {
const SpeechRecognition = top.SpeechRecognition || top.webkitSpeechRecognition;
const SpeechGrammarList = top.SpeechGrammarList || top.webkitSpeechGrammarList;
alert('Yahaha! You found me!\nBiliTwin支持的语音命令: 播放 暂停 全屏 关闭 加速 减速 下一集\nChrome may support Cantonese or Hakka as well. See BiliPolyfill::speechRecognition.');
if (!SpeechRecognition || !SpeechGrammarList) alert('浏览器太旧啦~彩蛋没法运行~');
let player = ['播放', '暂停', '全屏', '关闭', '加速', '减速', '下一集'];
let grammar = '#JSGF V1.0; grammar player; public <player> = ' + player.join(' | ') + ' ;';
let recognition = new SpeechRecognition();
let speechRecognitionList = new SpeechGrammarList();
speechRecognitionList.addFromString(grammar, 1);
recognition.grammars = speechRecognitionList;
// cmn: Mandarin(Putonghua), yue: Cantonese, hak: Hakka
// See https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
recognition.lang = 'cmn';
recognition.continuous = true;
recognition.interimResults = false;
recognition.maxAlternatives = 1;
recognition.start();
recognition.onresult = e => {
let last = e.results.length - 1;
let transcript = e.results[last][0].transcript;
switch (transcript) {
case '播放':
if (this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
this.hintInfo(`BiliPolyfill: 语音:播放`);
break;
case '暂停':
if (!this.video.paused) this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-start').click();
this.hintInfo(`BiliPolyfill: 语音:暂停`);
break;
case '全屏':
this.playerWin.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
this.hintInfo(`BiliPolyfill: 语音:全屏`);
break;
case '关闭':
top.close();
break;
case '加速':
this.setVideoSpeed(2);
this.hintInfo(`BiliPolyfill: 语音:加速`);
break;
case '减速':
this.setVideoSpeed(0.5);
this.hintInfo(`BiliPolyfill: 语音:减速`);
break;
case '下一集':
this.video.dispatchEvent(new Event('ended'));
default:
this.hintInfo(`BiliPolyfill: 语音:"${transcript}"?`);
break;
}
typeof console == "object" && console.log(e.results);
typeof console == "object" && console.log(`transcript:${transcript} confidence:${e.results[0][0].confidence}`);
};
}
substitudeFullscreenPlayer(option) {
if (!option) throw 'usage: substitudeFullscreenPlayer({cid, aid[, p][, ...otherOptions]})';
if (!option.cid) throw 'player init: cid missing';
if (!option.aid) throw 'player init: aid missing';
let h = this.playerWin.document;
let i = [h.webkitExitFullscreen, h.mozExitFullScreen, h.msExitFullscreen, h.exitFullscreen];
h.webkitExitFullscreen = h.mozExitFullScreen = h.msExitFullscreen = h.exitFullscreen = () => { };
this.playerWin.player.destroy();
this.playerWin.player = new bilibiliPlayer(option);
if (option.p) this.playerWin.callAppointPart(option.p);
[h.webkitExitFullscreen, h.mozExitFullScreen, h.msExitFullscreen, h.exitFullscreen] = i;
}
async getPlayerVideo() {
if (this.playerWin.document.getElementsByTagName('video').length) {
return this.video = this.playerWin.document.getElementsByTagName('video')[0];
}
else {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
if (this.playerWin.document.getElementsByTagName('video').length) {
observer.disconnect();
resolve(this.video = this.playerWin.document.getElementsByTagName('video')[0]);
}
});
observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
});
}
}
async getPlayerMenu() {
if (this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black').length) {
return this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0];
}
else {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
if (this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black').length) {
observer.disconnect();
resolve(this.playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0]);
}
});
observer.observe(this.playerWin.document.getElementById('bilibiliPlayer'), { childList: true });
});
}
}
static async openMinimizedPlayer(option = { cid: top.cid, aid: top.aid, playerWin: top }) {
if (!option) throw 'usage: openMinimizedPlayer({cid[, aid]})';
if (!option.cid) throw 'player init: cid missing';
if (!option.aid) option.aid = top.aid;
if (!option.playerWin) option.playerWin = top;
let h = top.open(`//www.bilibili.com/blackboard/html5player.html?cid=${option.cid}&aid=${option.aid}&crossDomain=${top.document.domain != 'www.bilibili.com' ? 'true' : ''}`, undefined, ' ');
let res = top.location.href.includes('bangumi') && await new Promise(resolve => {
const jq = option.playerWin.jQuery;
const _ajax = jq.ajax;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
a.success = resolve;
jq.ajax = _ajax;
}
return _ajax.call(jq, a, c);
};
option.playerWin.player.reloadAccess();
});
await new Promise(resolve => {
let i = setInterval(() => h.document.getElementById('bilibiliPlayer') && resolve(), 500);
h.addEventListener('load', resolve);
setTimeout(() => {
clearInterval(i);
h.removeEventListener('load', resolve);
resolve();
}, 6000);
});
let div = h.document.getElementById('bilibiliPlayer');
if (!div) { console.warn('openMinimizedPlayer: document load timeout'); return; }
if (res) {
await new Promise(resolve => {
const jq = h.jQuery;
const _ajax = jq.ajax;
jq.ajax = function (a, c) {
if (typeof c === 'object') { if (typeof a === 'string') c.url = a; a = c; c = undefined };
if (a.url.includes('interface.bilibili.com/v2/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
a.success(res)
jq.ajax = _ajax;
resolve();
}
else {
return _ajax.call(jq, a, c);
}
};
h.player = new h.bilibiliPlayer({ cid: option.cid, aid: option.aid });
// h.eval(`player = new bilibiliPlayer({ cid: ${option.cid}, aid: ${option.aid} })`);
// console.log(`player = new bilibiliPlayer({ cid: ${option.cid}, aid: ${option.aid} })`);
})
}
await new Promise(resolve => {
if (h.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen')) resolve();
else {
let observer = new MutationObserver(() => {
if (h.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen')) {
observer.disconnect();
resolve();
}
});
observer.observe(h.document.getElementById('bilibiliPlayer'), { childList: true });
}
});
let i = [div.webkitRequestFullscreen, div.mozRequestFullScreen, div.msRequestFullscreen, div.requestFullscreen];
div.webkitRequestFullscreen = div.mozRequestFullScreen = div.msRequestFullscreen = div.requestFullscreen = () => { };
if (h.document.querySelector('#bilibiliPlayer div.video-state-fullscreen-off'))
h.document.querySelector('#bilibiliPlayer div.bilibili-player-video-btn-fullscreen').click();
[div.webkitRequestFullscreen, div.mozRequestFullScreen, div.msRequestFullscreen, div.requestFullscreen] = i;
}
static parseHref(href = top.location.href) {
if (href.includes('bangumi')) {
let anime, play;
anime = (anime = /anime\/\d+/.exec(href)) ? anime[0].slice(6) : null;
play = (play = /play#\d+/.exec(href)) ? play[0].slice(5) : null;
if (!anime || !play) return null;
return `bangumi.bilibili.com/anime/${anime}/play#${play}`;
}
else {
let aid, pid;
aid = (aid = /av\d+/.exec(href)) ? aid[0].slice(2) : null;
if (!aid) return null;
pid = (pid = /page=\d+/.exec(href)) ? pid[0].slice(5) : (pid = /index_\d+.html/.exec(href)) ? pid[0].slice(6, -5) : null;
if (!pid) return `www.bilibili.com/video/av${aid}`;
return `www.bilibili.com/video/av${aid}/index_${pid}.html`;
}
}
static secondToReadable(s) {
if (s > 60) return `${parseInt(s / 60)}分${parseInt(s % 60)}秒`;
else return `${parseInt(s % 60)}秒`;
}
static clearAllUserdata(playerWin = top) {
if (playerWin.GM_setValue) return GM_setValue('biliPolyfill', '');
playerWin.localStorage.removeItem('biliPolyfill');
}
static _UNIT_TEST() {
console.warn('This test is impossible.');
console.warn('You need to close the tab, reopen it, etc.');
console.warn('Maybe you also want to test between bideo parts, etc.');
console.warn('I am too lazy to find workarounds.');
}
}
class BiliUserJS {
static async getIframeWin() {
if (document.querySelector('#bofqi > iframe').contentDocument.getElementById('bilibiliPlayer')) {
return document.querySelector('#bofqi > iframe').contentWindow;
}
else {
return new Promise(resolve => {
document.querySelector('#bofqi > iframe').addEventListener('load', () => {
resolve(document.querySelector('#bofqi > iframe').contentWindow);
});
});
}
}
static async getPlayerWin() {
if (location.href.includes('/watchlater/#/list')) {
await new Promise(resolve => {
let h = () => {
resolve(location.href);
window.removeEventListener('hashchange', h);
};
window.addEventListener('hashchange', h)
});
}
if (location.href.includes('/watchlater/#/')) {
if (!document.getElementById('bofqi')) {
await new Promise(resolve => {
let observer = new MutationObserver(() => {
if (document.getElementById('bofqi')) {
resolve(document.getElementById('bofqi'));
observer.disconnect();
}
});
observer.observe(document, { childList: true, subtree: true });
});
}
}
if (document.getElementById('bilibiliPlayer')) {
return window;
}
else if (document.querySelector('#bofqi > iframe')) {
return BiliUserJS.getIframeWin();
}
else if (document.querySelector('#bofqi > object')) {
throw 'Need H5 Player';
}
else {
return new Promise(resolve => {
let observer = new MutationObserver(() => {
if (document.getElementById('bilibiliPlayer')) {
observer.disconnect();
resolve(window);
}
else if (document.querySelector('#bofqi > iframe')) {
observer.disconnect();
resolve(BiliUserJS.getIframeWin());
}
else if (document.querySelector('#bofqi > object')) {
observer.disconnect();
throw 'Need H5 Player';
}
});
observer.observe(document.getElementById('bofqi'), { childList: true });
})
}
}
}
class UI extends BiliUserJS {
// Title Append
static titleAppend(monkey) {
const tminfo = document.querySelector('div.tminfo') || document.querySelector('div.info-second');
let div = document.createElement('div');
let flvA = document.createElement('a');
let mp4A = document.createElement('a');
let assA = document.createElement('a');
flvA.textContent = '超清FLV';
mp4A.textContent = '原生MP4';
assA.textContent = '弹幕ASS';
flvA.onmouseover = async () => {
flvA.textContent = '正在FLV';
flvA.onmouseover = null;
let href = await monkey.queryInfo('flv');
if (href == 'does_not_exist') return flvA.textContent = '没有FLV';
flvA.textContent = '超清FLV';
let flvDiv = UI.genFLVDiv(monkey);
document.body.appendChild(flvDiv);
flvA.onclick = () => flvDiv.style.display = 'block';
};
mp4A.onmouseover = async () => {
mp4A.textContent = '正在MP4';
mp4A.onmouseover = null;
let href = await monkey.queryInfo('mp4');
if (href == 'does_not_exist') return mp4A.textContent = '没有MP4';
mp4A.href = href;
mp4A.textContent = '原生MP4';
mp4A.download = '';
mp4A.referrerPolicy = 'origin';
};
assA.onmouseover = async () => {
assA.textContent = '正在ASS';
assA.onmouseover = null;
assA.href = await monkey.queryInfo('ass');
assA.textContent = '弹幕ASS';
if (monkey.mp4 && monkey.mp4.match) assA.download = monkey.mp4.match(/\d(?:\d|-|hd)*(?=\.mp4)/)[0] + '.ass';
else assA.download = monkey.cid + '.ass';
};
div.addEventListener('click', e => e.stopPropagation());
flvA.style.fontSize = mp4A.style.fontSize = assA.style.fontSize = '15px';
div.appendChild(flvA);
div.appendChild(document.createTextNode(' '));
div.appendChild(mp4A);
div.appendChild(document.createTextNode(' '));
div.appendChild(assA);
div.className = 'bilitwin';
div.style.float = 'left';
tminfo.style.float = 'none';
tminfo.style.marginLeft = '185px';
tminfo.parentElement.insertBefore(div, tminfo);
return { flvA, mp4A, assA };
}
static genFLVDiv(monkey, flvs = monkey.flvs, cache = monkey.cache) {
let div = UI.genDiv();
let table = document.createElement('table');
table.style.width = '100%';
table.style.lineHeight = '2em';
for (let i = 0; i < flvs.length; i++) {
let tr = table.insertRow(-1);
tr.insertCell(0).innerHTML = `<a href="${flvs[i]}">FLV分段 ${i + 1}</a>`;
tr.insertCell(1).innerHTML = '<a>缓存本段</a>';
tr.insertCell(2).innerHTML = '<progress value="0" max="100">进度条</progress>';
tr.children[1].children[0].onclick = () => {
UI.downloadFLV(tr.children[1].children[0], monkey, i, tr.children[2].children[0]);
}
}
let tr = table.insertRow(-1);
tr.insertCell(0).innerHTML = '<a>全部复制到剪贴板</a>';
tr.insertCell(1).innerHTML = '<a>缓存全部+自动合并</a>';
tr.insertCell(2).innerHTML = `<progress value="0" max="${flvs.length + 1}">进度条</progress>`;
if (top.location.href.includes('bangumi')) {
tr.children[0].children[0].onclick = () => UI.copyToClipboard(flvs.join('\n'));
}
else {
tr.children[0].innerHTML = '<a download="biliTwin.ef2">IDM导出</a>';
tr.children[0].children[0].href = URL.createObjectURL(new Blob([UI.exportIDM(flvs, top.location.origin)]));
}
tr.children[1].children[0].onclick = () => UI.downloadAllFLVs(tr.children[1].children[0], monkey, table);
table.insertRow(-1).innerHTML = '<td colspan="3">合并功能推荐配置:至少8G RAM。把自己下载的分段FLV拖动到这里,也可以合并哦~</td>';
table.insertRow(-1).innerHTML = cache ? '<td colspan="3">下载的缓存分段会暂时停留在电脑里,过一段时间会自动消失。建议只开一个标签页。</td>' : '<td colspan="3">建议只开一个标签页。关掉标签页后,缓存就会被清理。别忘了另存为!</td>';
UI.displayQuota(table.insertRow(-1));
div.appendChild(table);
div.ondragenter = div.ondragover = e => UI.allowDrag(e);
div.ondrop = async e => {
UI.allowDrag(e);
let files = Array.from(e.dataTransfer.files);
if (files.every(e => e.name.search(/\d+-\d+(?:-\d+)?\.flv/) != -1)) {
files.sort((a, b) => a.name.match(/\d+-(\d+)(?:-\d+)?\.flv/)[1] - b.name.match(/\d+-(\d+)(?:-\d+)?\.flv/)[1]);
}
for (let file of files) {
table.insertRow(-1).innerHTML = `<td colspan="3">${file.name}</td>`;
}
let outputName = files[0].name.match(/\d+-\d+(?:-\d+)?\.flv/);
if (outputName) outputName = outputName[0].replace(/-\d/, "");
else outputName = 'merge_' + files[0].name;
let url = await UI.mergeFLVFiles(files);
table.insertRow(-1).innerHTML = `<td colspan="3"><a href="${url}" download="${outputName}">${outputName}</a></td>`;
}
let buttons = [];
for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
buttons.forEach(btn => btn.style.padding = '0.5em');
buttons.forEach(btn => btn.style.margin = '0.2em');
buttons[0].textContent = '关闭';
buttons[0].onclick = () => {
div.style.display = 'none';
}
buttons[1].textContent = '清空这个视频的缓存';
buttons[1].onclick = () => {
monkey.cleanAllFLVsInCache();
}
buttons[2].textContent = '清空所有视频的缓存';
buttons[2].onclick = () => {
UI.clearCacheDB(cache);
}
buttons.forEach(btn => div.appendChild(btn));
return div;
}
static async downloadAllFLVs(a, monkey, table) {
if (table.rows[0].cells.length < 3) return;
monkey.hangPlayer();
table.insertRow(-1).innerHTML = '<td colspan="3">已屏蔽网页播放器的网络链接。切换清晰度可重新激活播放器。</td>';
for (let i = 0; i < monkey.flvs.length; i++) {
if (table.rows[i].cells[1].children[0].textContent == '缓存本段')
table.rows[i].cells[1].children[0].click();
}
let bar = a.parentNode.nextSibling.children[0];
bar.max = monkey.flvs.length + 1;
bar.value = 0;
for (let i = 0; i < monkey.flvs.length; i++) monkey.getFLV(i).then(e => bar.value++);
let blobs;
blobs = await monkey.getAllFLVs();
let mergedFLV = await FLV.mergeBlobs(blobs);
let ass = await monkey.ass;
let url = URL.createObjectURL(mergedFLV);
let outputName = top.document.getElementsByTagName('h1')[0].textContent.trim();
bar.value++;
table.insertRow(0).innerHTML = `
<td colspan="3" style="border: 1px solid black">
<a href="${url}" download="${outputName}.flv">保存合并后FLV</a>
<a href="${ass}" download="${outputName}.ass">弹幕ASS</a>
<a>打包MKV(软字幕封装)</a>
记得清理分段缓存哦~
</td>
`;
table.rows[0].cells[0].children[2].onclick = () => new MKVTransmuxer().exec(url, ass, `${outputName}.mkv`);
return url;
}
static async downloadFLV(a, monkey, index, bar = {}) {
let handler = e => UI.beforeUnloadHandler(e);
window.addEventListener('beforeunload', handler);
a.textContent = '取消';
a.onclick = () => {
a.onclick = null;
window.removeEventListener('beforeunload', handler);
a.textContent = '已取消';
monkey.abortFLV(index);
};
let url;
try {
url = await monkey.getFLV(index, (loaded, total) => {
bar.value = loaded;
bar.max = total;
});
url = URL.createObjectURL(url);
if (bar.value == 0) bar.value = bar.max = 1;
} catch (e) {
a.onclick = null;
window.removeEventListener('beforeunload', handler);
a.textContent = '错误';
throw e;
}
a.onclick = null;
window.removeEventListener('beforeunload', handler);
a.textContent = '另存为';
a.download = monkey.flvs[index].match(/\d+-\d+(?:-\d+)?\.flv/)[0];
a.href = url;
return url;
}
static async mergeFLVFiles(files) {
let merged = await FLV.mergeBlobs(files)
return URL.createObjectURL(merged);
}
static async clearCacheDB(cache) {
if (cache) return cache.deleteEntireDB();
}
static async displayQuota(tr) {
return new Promise(resolve => {
let temporaryStorage = window.navigator.temporaryStorage
|| window.navigator.webkitTemporaryStorage
|| window.navigator.mozTemporaryStorage
|| window.navigator.msTemporaryStorage;
if (!temporaryStorage) return resolve(tr.innerHTML = `<td colspan="3">这个浏览器不支持缓存呢~关掉标签页后,缓存马上就会消失哦</td>`);
temporaryStorage.queryUsageAndQuota((usage, quota) =>
resolve(tr.innerHTML = `<td colspan="3">缓存已用空间:${Math.round(usage / 1048576)}MB / ${Math.round(quota / 1048576)}MB 也包括了B站本来的缓存</td>`)
);
});
}
// Menu Append
static menuAppend(playerWin, { monkey, monkeyTitle, polyfill, displayPolyfillDataDiv, optionDiv }) {
let monkeyMenu = UI.genMonkeyMenu(playerWin, { monkey, monkeyTitle, optionDiv });
let polyfillMenu = UI.genPolyfillMenu(playerWin, { polyfill, displayPolyfillDataDiv, optionDiv });
let div = playerWin.document.getElementsByClassName('bilibili-player-context-menu-container black')[0];
let ul = playerWin.document.createElement('ul');
ul.className = 'bilitwin';
ul.style.borderBottom = '1px solid rgba(255,255,255,.12)';
div.insertBefore(ul, div.children[0]);
ul.appendChild(monkeyMenu);
ul.appendChild(polyfillMenu);
}
static genMonkeyMenu(playerWin, { monkey, monkeyTitle, optionDiv }) {
let li = playerWin.document.createElement('li');
li.className = 'context-menu-menu bilitwin';
li.innerHTML = `
<a class="context-menu-a">
BiliMonkey
<span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
</a>
<ul>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 下载FLV
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 下载MP4
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 下载ASS
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 设置/帮助/关于
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)批量下载
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)载入缓存FLV
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)强制刷新
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)重启脚本
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)销毁播放器
</a>
</li>
</ul>
`;
li.onclick = () => playerWin.document.getElementById('bilibiliPlayer').click();
let ul = li.children[1];
ul.children[0].onclick = async () => { if (monkeyTitle.flvA.onmouseover) await monkeyTitle.flvA.onmouseover(); monkeyTitle.flvA.click(); };
ul.children[1].onclick = async () => { if (monkeyTitle.mp4A.onmouseover) await monkeyTitle.mp4A.onmouseover(); monkeyTitle.mp4A.click(); };
ul.children[2].onclick = async () => { if (monkeyTitle.assA.onmouseover) await monkeyTitle.assA.onmouseover(); monkeyTitle.assA.click(); };
ul.children[3].onclick = () => { optionDiv.style.display = 'block'; };
ul.children[4].onclick = async () => { await BiliMonkey.getAllPageDefaultFormats(playerWin) };
ul.children[5].onclick = async () => {
monkey.proxy = true;
monkey.flvs = null;
UI.hintInfo('请稍候,可能需要10秒时间……', playerWin);
// Yes, I AM lazy.
playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul li[data-value="80"]').click();
await new Promise(r => playerWin.document.getElementsByTagName('video')[0].addEventListener('emptied', r));
return monkey.queryInfo('flv');
};
ul.children[6].onclick = () => { top.location.reload(true); };
ul.children[7].onclick = () => { playerWin.dispatchEvent(new Event('unload')); };
ul.children[8].onclick = () => { playerWin.player && playerWin.player.destroy() };
return li;
}
static genPolyfillMenu(playerWin, { polyfill, displayPolyfillDataDiv, optionDiv }) {
let li = playerWin.document.createElement('li');
li.className = 'context-menu-menu bilitwin';
li.innerHTML = `
<a class="context-menu-a">
BiliPolyfill
<span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
</a>
<ul>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 获取封面
</a>
</li>
<li class="context-menu-menu">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 更多播放速度
<span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
</a>
<ul>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 0.1
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 3
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 点击确认
<input type="text" style="width: 35px; height: 70%">
</a>
</li>
</ul>
</li>
<li class="context-menu-menu">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 片头片尾
<span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
</a>
<ul>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 片头开始:<span></span>
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 片头结束:<span></span>
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 片尾开始:<span></span>
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 片尾结束:<span></span>
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 取消标记
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 检视数据/说明
</a>
</li>
</ul>
</li>
<li class="context-menu-menu">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 找上下集
<span class="bpui-icon bpui-icon-arrow-down" style="transform:rotate(-90deg);margin-top:3px;"></span>
</a>
<ul>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> <span></span>
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> <span></span>
</a>
</li>
</ul>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 小窗播放
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> 设置/帮助/关于
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)立即保存数据
</a>
</li>
<li class="context-menu-function">
<a class="context-menu-a">
<span class="video-contextmenu-icon"></span> (测)强制清空数据
</a>
</li>
</ul>
`;
li.onclick = () => playerWin.document.getElementById('bilibiliPlayer').click();
if (!polyfill.option.betabeta) li.children[0].childNodes[0].textContent += '(到设置开启)';
let ul = li.children[1];
ul.children[0].onclick = () => { top.window.open(polyfill.getCoverImage(), '_blank'); };
ul.children[1].children[1].children[0].onclick = () => { polyfill.setVideoSpeed(0.1); };
ul.children[1].children[1].children[1].onclick = () => { polyfill.setVideoSpeed(3); };
ul.children[1].children[1].children[2].onclick = e => { polyfill.setVideoSpeed(e.target.getElementsByTagName('input')[0].value); };
ul.children[1].children[1].children[2].getElementsByTagName('input')[0].onclick = e => e.stopPropagation();
ul.children[2].children[1].children[0].onclick = () => { polyfill.markOPEDPosition(0); };
ul.children[2].children[1].children[1].onclick = () => { polyfill.markOPEDPosition(1); };
ul.children[2].children[1].children[2].onclick = () => { polyfill.markOPEDPosition(2); };
ul.children[2].children[1].children[3].onclick = () => { polyfill.markOPEDPosition(3); };
ul.children[2].children[1].children[4].onclick = () => { polyfill.clearOPEDPosition(); };
ul.children[2].children[1].children[5].onclick = () => { displayPolyfillDataDiv(polyfill); };
ul.children[3].children[1].children[0].getElementsByTagName('a')[0].style.width = 'initial';
ul.children[3].children[1].children[1].getElementsByTagName('a')[0].style.width = 'initial';
ul.children[4].onclick = () => { BiliPolyfill.openMinimizedPlayer(); };
ul.children[5].onclick = () => { optionDiv.style.display = 'block'; };
ul.children[6].onclick = () => { polyfill.saveUserdata() };
ul.children[7].onclick = () => {
BiliPolyfill.clearAllUserdata(playerWin);
polyfill.retrieveUserdata();
};
li.onmouseenter = () => {
let ul = li.children[1];
ul.children[1].children[1].children[2].getElementsByTagName('input')[0].value = polyfill.video.playbackRate;
let oped = polyfill.userdata.oped[polyfill.getCollectionId()] || [];
ul.children[2].children[1].children[0].getElementsByTagName('span')[1].textContent = oped[0] ? BiliPolyfill.secondToReadable(oped[0]) : '无';
ul.children[2].children[1].children[1].getElementsByTagName('span')[1].textContent = oped[1] ? BiliPolyfill.secondToReadable(oped[1]) : '无';
ul.children[2].children[1].children[2].getElementsByTagName('span')[1].textContent = oped[2] ? BiliPolyfill.secondToReadable(oped[2]) : '无';
ul.children[2].children[1].children[3].getElementsByTagName('span')[1].textContent = oped[3] ? BiliPolyfill.secondToReadable(oped[3]) : '无';
ul.children[3].children[1].children[0].onclick = () => { if (polyfill.series[0]) top.window.open(`https://www.bilibili.com/video/av${polyfill.series[0].aid}`, '_blank'); };
ul.children[3].children[1].children[1].onclick = () => { if (polyfill.series[1]) top.window.open(`https://www.bilibili.com/video/av${polyfill.series[1].aid}`, '_blank'); };
ul.children[3].children[1].children[0].getElementsByTagName('span')[1].textContent = polyfill.series[0] ? polyfill.series[0].title : '找不到';
ul.children[3].children[1].children[1].getElementsByTagName('span')[1].textContent = polyfill.series[1] ? polyfill.series[1].title : '找不到';
}
return li;
}
static genOptionDiv(option) {
let div = UI.genDiv();
div.appendChild(UI.genMonkeyOptionTable(option));
div.appendChild(UI.genPolyfillOptionTable(option));
let table = document.createElement('table');
table.style = 'width: 100%; line-height: 2em;';
table.insertRow(-1).innerHTML = '<td>设置自动保存,刷新后生效。</td>';
table.insertRow(-1).innerHTML = '<td>视频下载组件的缓存功能只在Windows+Chrome测试过,如果出现问题,请关闭缓存。</td>';
table.insertRow(-1).innerHTML = '<td>功能增强组件尽量保证了兼容性。但如果有同功能脚本/插件,请关闭本插件的对应功能。</td>';
table.insertRow(-1).innerHTML = '<td>这个脚本乃“按原样”提供,不附带任何明示,暗示或法定的保证,包括但不限于其没有缺陷,适合特定目的或非侵权。</td>';
table.insertRow(-1).innerHTML = '<td><a href="http://greasyfork.icu/zh-CN/scripts/27819" target="_blank">更新/讨论</a> <a href="https://github.com/liqi0816/bilitwin/" target="_blank">GitHub</a> Author: qli5. Copyright: qli5, 2014+, 田生, grepmusic</td>';
div.appendChild(table);
let buttons = [];
for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
buttons.forEach(btn => btn.style.padding = '0.5em');
buttons.forEach(btn => btn.style.margin = '0.2em');
buttons[0].textContent = '保存并关闭';
buttons[0].onclick = () => {
div.style.display = 'none';;
}
buttons[1].textContent = '保存并刷新';
buttons[1].onclick = () => {
top.location.reload();
}
buttons[2].textContent = '重置并刷新';
buttons[2].onclick = () => {
UI.saveOption({ setStorage: option.setStorage });
top.location.reload();
}
buttons.forEach(btn => div.appendChild(btn));
return div;
}
static genMonkeyOptionTable(option = {}) {
const description = [
['autoDefault', '尝试自动抓取:不会拖慢页面,抓取默认清晰度,但可能抓不到。'],
['autoFLV', '强制自动抓取FLV:会拖慢页面,如果默认清晰度也是超清会更慢,但保证抓到。'],
['autoMP4', '强制自动抓取MP4:会拖慢页面,如果默认清晰度也是高清会更慢,但保证抓到。'],
['cache', '关标签页不清缓存:保留完全下载好的分段到缓存,忘记另存为也没关系。'],
['partial', '断点续传:点击“取消”保留部分下载的分段到缓存,忘记点击会弹窗确认。'],
['proxy', '用缓存加速播放器:如果缓存里有完全下载好的分段,直接喂给网页播放器,不重新访问网络。小水管利器,播放只需500k流量。如果实在搞不清怎么播放ASS弹幕,也可以就这样用。'],
['blocker', '弹幕过滤:在网页播放器里设置的屏蔽词也对下载的弹幕生效。'],
['font', '自定义字体:在网页播放器里设置的字体、大小、加粗、透明度也对下载的弹幕生效。']
];
let table = document.createElement('table');
table.style.width = '100%';
table.style.lineHeight = '2em';
table.insertRow(-1).innerHTML = '<td style="text-align:center">BiliMonkey(视频抓取组件)</td>';
table.insertRow(-1).innerHTML = '<td style="text-align:center">因为作者偷懒了,缓存的三个选项最好要么全开,要么全关。最好。</td>';
for (let d of description) {
let checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = option[d[0]];
checkbox.onchange = () => { option[d[0]] = checkbox.checked; UI.saveOption(option); };
let td = table.insertRow(-1).insertCell(0);
let label = document.createElement('label');
label.appendChild(checkbox);
label.appendChild(document.createTextNode(d[1]));
td.appendChild(label);
}
return table;
}
static genPolyfillOptionTable(option = {}) {
const description = [
['betabeta', '增强组件总开关 <---------更加懒得测试了,反正以后B站也会自己提供这些功能。也许吧。'], //betabeta
['badgeWatchLater', '稍后再看添加数字角标'],
['dblclick', '双击全屏'],
['scroll', '自动滚动到播放器'],
['recommend', '弹幕列表换成相关视频'],
['electric', '整合充电榜与换P倒计时'],
['electricSkippable', '跳过充电榜', 'disabled'],
['lift', '自动防挡字幕'],
['autoResume', '自动跳转上次看到'],
['autoPlay', '自动播放'],
['autoWideScreen', '自动宽屏'],
['autoFullScreen', '自动全屏'],
['oped', '标记后自动跳OP/ED'],
['focus', '自动聚焦到播放器'],
['menuFocus', '关闭菜单后聚焦到播放器'],
['limitedKeydown', '首次回车键可全屏自动播放'],
['series', '尝试自动找上下集'],
['speech', '(测)(需墙外)任意三击鼠标左键开启语音识别'],
];
let table = document.createElement('table');
table.style.width = '100%';
table.style.lineHeight = '2em';
table.insertRow(-1).innerHTML = '<td style="text-align:center">BiliPolyfill(功能增强组件)</td>';
table.insertRow(-1).innerHTML = '<td style="text-align:center">懒鬼作者还在测试的时候,B站已经上线了原生的稍后再看(๑•̀ㅂ•́)و✧</td>';
for (let d of description) {
let checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = option[d[0]];
checkbox.onchange = () => { option[d[0]] = checkbox.checked; UI.saveOption(option); };
let td = table.insertRow(-1).insertCell(0);
let label = document.createElement('label');
label.appendChild(checkbox);
label.appendChild(document.createTextNode(d[1]));
if (d[2] == 'disabled') {
checkbox.disabled = true;
label.style.textDecoration = 'line-through';
}
td.appendChild(label);
}
return table;
}
static displayPolyfillDataDiv(polyfill) {
let div = UI.genDiv();
let p = document.createElement('p');
p.textContent = '这里是脚本储存的数据。所有数据都只存在浏览器里,别人不知道,B站也不知道,脚本作者更不知道(这个家伙连服务器都租不起 摔';
p.style.margin = '0.3em';
div.appendChild(p);
let textareas = [];
for (let i = 0; i < 2; i++) textareas.push(document.createElement('textarea'));
textareas.forEach(ta => ta.style = 'resize:vertical; width: 100%; height: 200px');
p = document.createElement('p');
p.textContent = 'B站已上线原生的稍后观看功能。';
p.style.margin = '0.3em';
div.appendChild(p);
//textareas[0].textContent = JSON.stringify(polyfill.userdata.watchLater).replace(/\[/, '[\n').replace(/\]/, '\n]').replace(/,/g, ',\n');
//div.appendChild(textareas[0]);
p = document.createElement('p');
p.textContent = '这里是片头片尾。格式是,av号或番剧号:[片头开始(默认=0),片头结束(默认=不跳),片尾开始(默认=不跳),片尾结束(默认=无穷大)]。可以任意填写null,脚本会自动采用默认值。';
p.style.margin = '0.3em';
div.appendChild(p);
textareas[1].textContent = JSON.stringify(polyfill.userdata.oped).replace(/{/, '{\n').replace(/}/, '\n}').replace(/],/g, '],\n');
div.appendChild(textareas[1]);
p = document.createElement('p');
p.textContent = '当然可以直接清空啦。只删除其中的一些行的话,一定要记得删掉多余的逗号。';
p.style.margin = '0.3em';
div.appendChild(p);
let buttons = [];
for (let i = 0; i < 3; i++) buttons.push(document.createElement('button'));
buttons.forEach(btn => btn.style.padding = '0.5em');
buttons.forEach(btn => btn.style.margin = '0.2em');
buttons[0].textContent = '关闭';
buttons[0].onclick = () => {
div.remove();
}
buttons[1].textContent = '验证格式';
buttons[1].onclick = () => {
if (!textareas[0].value) textareas[0].value = '{\n\n}';
textareas[0].value = textareas[0].value.replace(/,(\s|\n)*}/, '\n}').replace(/,(\s|\n),/g, ',\n');
if (!textareas[1].value) textareas[1].value = '{\n\n}';
textareas[1].value = textareas[1].value.replace(/,(\s|\n)*}/, '\n}').replace(/,(\s|\n),/g, ',\n').replace(/,(\s|\n)*]/g, ']');
let userdata = {};
try {
//userdata.watchLater = JSON.parse(textareas[0].value);
} catch (e) { alert('稍后观看列表: ' + e); throw e; }
try {
userdata.oped = JSON.parse(textareas[1].value);
} catch (e) { alert('片头片尾: ' + e); throw e; }
buttons[1].textContent = ('格式没有问题!');
return userdata;
}
buttons[2].textContent = '尝试保存';
buttons[2].onclick = () => {
polyfill.userdata = buttons[1].onclick();
polyfill.saveUserdata();
buttons[2].textContent = ('保存成功');
}
buttons.forEach(btn => div.appendChild(btn));
document.body.appendChild(div);
div.style.display = 'block';
}
// Common
static genDiv() {
let div = document.createElement('div');
div.style.position = 'fixed';
div.style.zIndex = '10036';
div.style.top = '50%';
div.style.marginTop = '-200px';
div.style.left = '50%';
div.style.marginLeft = '-320px';
div.style.width = '540px';
div.style.maxHeight = '400px';
div.style.overflowY = 'auto';
div.style.padding = '30px 50px';
div.style.backgroundColor = 'white';
div.style.borderRadius = '6px';
div.style.boxShadow = 'rgba(0, 0, 0, 0.6) 1px 1px 40px 0px';
div.style.display = 'none';
div.className = 'bilitwin';
div.addEventListener('click', e => e.stopPropagation());
return div;
}
static requestH5Player() {
let h = document.querySelector('div.tminfo');
h.insertBefore(document.createTextNode('[[脚本需要HTML5播放器(弹幕列表右上角三个点的按钮切换)]] '), h.firstChild);
}
static copyToClipboard(text) {
let textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = text;
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
static exportIDM(url, referrer) {
return url.map(e => `<\r\n${e}\r\nreferer: ${referrer}\r\n>\r\n`).join('');
}
static allowDrag(e) {
e.stopPropagation();
e.preventDefault();
}
static beforeUnloadHandler(e) {
return e.returnValue = '脚本还没做完工作,真的要退出吗?';
}
static hintInfo(text, playerWin) {
let infoDiv = playerWin.document.createElement('div');
infoDiv.className = 'bilibili-player-video-toast-bottom';
infoDiv.innerHTML = `
<div class="bilibili-player-video-toast-item">
<div class="bilibili-player-video-toast-item-text">
<span>${text}</span>
</div>
</div>
`;
playerWin.document.getElementsByClassName('bilibili-player-video-toast-wrp')[0].appendChild(infoDiv);
setTimeout(() => infoDiv.remove(), 3000);
}
static getOption(playerWin) {
let rawOption = null;
try {
rawOption = JSON.parse(playerWin.localStorage.getItem('BiliTwin'));
}
catch (e) { }
finally {
if (!rawOption) rawOption = {};
rawOption.setStorage = (n, i) => playerWin.localStorage.setItem(n, i);
rawOption.getStorage = n => playerWin.localStorage.getItem(n);
const defaultOption = {
autoDefault: true,
autoFLV: false,
autoMP4: false,
cache: true,
partial: true,
proxy: true,
blocker: true,
font: true,
badgeWatchLater: true,
dblclick: true,
scroll: true,
recommend: true,
electric: true,
electricSkippable: false,
lift: true,
autoResume: true,
autoPlay: false,
autoWideScreen: false,
autoFullScreen: false,
oped: true,
focus: true,
menuFocus: true,
limitedKeydown: true,
speech: false,
series: true,
betabeta: false
};
return Object.assign({}, defaultOption, rawOption, debugOption);
}
}
static saveOption(option) {
return option.setStorage('BiliTwin', JSON.stringify(option));
}
static outdatedEngineClearance() {
if (!Promise || !MutationObserver) {
alert('这个浏览器实在太老了,脚本决定罢工。');
throw 'BiliTwin: browser outdated: Promise or MutationObserver unsupported';
}
}
static firefoxClearance() {
if (navigator.userAgent.includes('Firefox')) {
debugOption.proxy = false;
if (!window.navigator.temporaryStorage && !window.navigator.mozTemporaryStorage) window.navigator.temporaryStorage = { queryUsageAndQuota: func => func(-1048576, 10484711424) };
}
}
static xpcWrapperClearance() {
if (top.unsafeWindow) {
Object.defineProperty(window, 'cid', {
configurable: true,
get: () => String(unsafeWindow.cid)
});
Object.defineProperty(window, 'player', {
configurable: true,
get: () => ({ destroy: unsafeWindow.player.destroy, reloadAccess: unsafeWindow.player.reloadAccess })
});
Object.defineProperty(window, 'jQuery', {
configurable: true,
get: () => unsafeWindow.jQuery,
});
Object.defineProperty(window, 'fetch', {
configurable: true,
get: () => unsafeWindow.fetch.bind(unsafeWindow),
set: _fetch => unsafeWindow.fetch = _fetch.bind(unsafeWindow)
});
}
}
static styleClearance() {
let ret = `
.bilibili-player-context-menu-container.black ul.bilitwin li.context-menu-function > a:hover {
background: rgba(255,255,255,.12);
transition: all .3s ease-in-out;
cursor: pointer;
}
`;
if (top.getComputedStyle(top.document.body).color != 'rgb(34, 34, 34)') ret += `
.bilitwin a {
cursor: pointer;
color: #00a1d6;
}
.bilitwin a:hover {
color: #f25d8e;
}
.bilitwin button {
color: #fff;
cursor: pointer;
text-align: center;
border-radius: 4px;
background-color: #00a1d6;
vertical-align: middle;
border: 1px solid #00a1d6;
transition: .1s;
transition-property: background-color,border,color;
user-select: none;
}
.bilitwin button:hover {
background-color: #00b5e5;
border-color: #00b5e5;
}
.bilitwin progress {
-webkit-appearance: progress;
}
`;
let style = document.createElement('style');
style.type = 'text/css';
style.rel = 'stylesheet';
style.textContent = ret;
document.head.appendChild(style);
}
static cleanUp() {
Array.from(document.getElementsByClassName('bilitwin'))
.filter(e => e.textContent.includes('FLV分段'))
.forEach(e => Array.from(e.getElementsByTagName('a')).forEach(
e => e.textContent == '取消' && e.click()
));
Array.from(document.getElementsByClassName('bilitwin')).forEach(e => e.remove());
}
static async start() {
let cidRefresh = new AsyncContainer();
let href = location.href;
// 1. playerWin and option
let playerWin;
try {
playerWin = await UI.getPlayerWin();
} catch (e) {
if (e == 'Need H5 Player') UI.requestH5Player();
throw e;
}
let option = UI.getOption(playerWin);
let optionDiv = UI.genOptionDiv(option);
document.body.appendChild(optionDiv);
// 2. monkey and polyfill
let monkeyTitle;
let displayPolyfillDataDiv = polyfill => UI.displayPolyfillDataDiv(polyfill);
let [monkey, polyfill] = await Promise.all([
(async () => {
let monkey = new BiliMonkey(playerWin, option);
await monkey.execOptions();
monkeyTitle = UI.titleAppend(monkey);
return monkey;
})(),
(async () => {
let polyfill = new BiliPolyfill(playerWin, option, t => UI.hintInfo(t, playerWin));
await polyfill.setFunctions();
return polyfill;
})()
]);
if (href != location.href) return UI.cleanUp();
// 3. menu
UI.menuAppend(playerWin, { monkey, monkeyTitle, polyfill, displayPolyfillDataDiv, optionDiv });
// 4. refresh
let h = () => {
let video = playerWin.document.getElementsByTagName('video')[0];
if (video) video.addEventListener('emptied', h);
else setTimeout(() => cidRefresh.resolve(), 0);
}
playerWin.document.getElementsByTagName('video')[0].addEventListener('emptied', h);
playerWin.addEventListener('unload', () => setTimeout(() => cidRefresh.resolve(), 0));
// 5. debug
if (debugOption.debug && top.console) top.console.clear();
if (debugOption.debug) ([(top.unsafeWindow || top).m, (top.unsafeWindow || top).p] = [monkey, polyfill]);
await cidRefresh;
UI.cleanUp();
}
static async init() {
if (!document.body) return;
UI.outdatedEngineClearance();
UI.firefoxClearance();
UI.styleClearance();
while (1) {
await UI.start();
}
}
}
UI.init();