bilibili merged flv+mp4+ass+enhance

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

As of 2018-01-12. See the latest version.

// ==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.11
// @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 resultParts = [];
        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;
            });

            for (let tag of flv.tags) {
                if (tag.tagType == 0x12 && !foundDuration) {
                    duration += tag.getDuration();
                    foundDuration = 1;
                    if (blob == blobs[0]) {
                        resultParts.push(new Blob([flv.header, flv.firstPreviousTagSize]));
                        ({ duration, durationDataView } = tag.getDurationAndView());
                        tag.stripKeyframesScriptData();
                        resultParts.push(new Blob([tag.tagHeader]));
                        resultParts.push(tag.tagData);
                        resultParts.push(new Blob([tag.previousSize]));
                    }
                }
                else if (tag.tagType == 0x08 || tag.tagType == 0x09) {
                    lasttimestamp[tag.tagType - 0x08] = bts + tag.getCombinedTimestamp();
                    tag.setCombinedTimestamp(lasttimestamp[tag.tagType - 0x08]);
                    resultParts.push(new Blob([tag.tagHeader, tag.tagData, tag.previousSize]));
                }
            }
        }
        durationDataView.setFloat64(0, duration);

        return new Blob(resultParts);
    }
}

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': [              // 字形(会自动选择最前面一个可用的)
            'Microsoft YaHei UI',
            'Microsoft YaHei',
            '文泉驿正黑',
            'STHeitiSC',
            '黑体',
          ],
          '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);
          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}},25,&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,0,0,0,100,100,0,0,1,2,0,2,20,20,2,0\\nStyle: R2L,{{font}},25,&H{{alpha}}FFFFFF,&H{{alpha}}FFFFFF,&H{{alpha}}000000,&H{{alpha}}000000,1,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',
            config, info, {'alpha': hexAlpha(config.opacity) }
          );
          // 补齐数字开头的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 <xqq@xqq.im>, 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 <xqq@xqq.im>
         *
         * 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 = /\\[(.*)\\]/g;
                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) {
                switch (str) {
                    case 'avc1.640029':
                        return 'V_MPEG4/ISO/AVC';
                    case 'mp4a.40.2':
                        return 'A_AAC';
                    default:
                        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 }) {
        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.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':
            case 'hdflv2':
            case 'flv720':
            case 'flv480':
                //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);
        }
        if (shouldBe && shouldBe != res.format) {
            switch (shouldBe) {
                case 'flv': case 'hdflv2': case 'flv720': case 'flv480': this.flvs = null; break;
                case 'hdmp4': case 'mp4': this.mp4 = null; break;
            }
            throw `URL interface error: response is not ${shouldBe}`;
        }
        switch (res.format) {
            case 'flv':
            case 'hdflv2':
            case 'flv720':
            case 'flv480':
                return this.flvs = this.flvs.resolve(res.durl.map(e => e.url.replace('http:', this.protocol)));
            case 'hdmp4':
            case 'mp4':
                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 (!(accept_quality instanceof Array)) accept_quality = Array.from(this.playerWin.document.querySelector('div.bilibili-player-video-btn-quality > div ul').getElementsByTagName('li')).map(e => e.getAttribute('data-value'));
        this.flvFormatName = accept_quality.includes('80') ? 'flv' : accept_quality.includes('64') ? 'flv720' : 'flv480';
        this.mp4FormatName = 'mp4';
    }

    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/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 'flv480':
                                if (accept_format.includes('flv720')) break;
                            case 'flv720':
                                if (accept_format.includes('flv')) break;
                            case 'flv':
                            case 'hdflv2':
                                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;
                        }
                        _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/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/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/playurl?') || a.url.includes('bangumi.bilibili.com/player/web_api/v2/playurl?')) {
                let _success = a.success;
                a.success = res => {
                    if (self.proxy && res.format == 'flv') {
                        self.resolveFormat(res, format);
                        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/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/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();
            });
            const { fetchDanmaku, generateASS, setPosition } = new ASSDownloader();

            fetchDanmaku(this.cid, danmaku => {
                if (this.blocker) {
                    if (this.playerWin.localStorage.bilibili_player_settings) {
                        let regexps = JSON.parse(this.playerWin.localStorage.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': document.title,
                    'ori': 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.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.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/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 partialCache = await this.loadPartialFLVFromCache(index);

            let burl = this.flvs[index];
            if (partialCache) burl += `&bstart=${partialCache.size}`;
            let opt = {
                fetch: this.playerWin.fetch,
                method: 'GET',
                mode: 'cors',
                cache: 'default',
                referrerPolicy: 'no-referrer-when-downgrade',
                cacheLoaded: partialCache ? partialCache.size : 0,
                headers: partialCache && (!burl.includes('wsTime')) ? { Range: `bytes=${partialCache.size}-` } : undefined
            };
            opt.onprogress = progressHandler;
            opt.onerror = opt.onabort = ({ target, type }) => {
                let pBlob = target.getPartialBlob();
                if (partialCache) pBlob = new Blob([partialCache, pBlob]);
                this.savePartialFLVToCache(index, pBlob);
            }

            let fch = new DetailedFetchBlob(burl, opt);
            this.flvsDetailedFetch[index] = fch;
            let fullResponse = await fch.getBlob();
            this.flvsDetailedFetch[index] = undefined;
            if (partialCache) {
                fullResponse = new Blob([partialCache, fullResponse]);
                this.cleanPartialFLVInCache(index);
            }
            this.saveFLVToCache(index, fullResponse);
            return (this.flvsBlob[index] = fullResponse);
        })();
        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 formatToValue(format) {
        switch (format) {
            case 'hdflv2': return '112';
            case 'flv': return '80';
            case 'flv720': return '64';
            case 'hdmp4': return '64'; // data-value is still '64' instead of '48'. return '48';
            case 'flv480': return '32';
            case 'mp4': return '16';
            default: return null;
        }
    }

    static valueToFormat(value) {
        switch (parseInt(value)) {
            case 112: return 'hdflv2';
            case 80: return 'flv';
            case 64: return 'flv720';
            case 48: return 'hdmp4';
            case 32: return 'flv480';
            case 16: return 'mp4';
            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.getElementsByClassName('v-title')[0] || top.document.getElementsByClassName('header-info')[0] || top.document.getElementsByClassName('video-info-module')[0]).children[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');
        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');
        if (!ret) return null;

        ret = ret.src;
        ret = ret.slice(0, ret.indexOf('.jpg') + 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();
    }

    markOPPosition() {
        let collectionId = this.getCollectionId();
        if (!(this.userdata.oped[collectionId] instanceof Array)) this.userdata.oped[collectionId] = [];
        this.userdata.oped[collectionId][0] = this.video.currentTime;
    }

    markEDPostion() {
        let collectionId = this.getCollectionId();
        if (!(this.userdata.oped[collectionId] instanceof Array)) this.userdata.oped[collectionId] = [];
        this.userdata.oped[collectionId][1] = (this.video.currentTime);
    }

    clearOPEDPosition() {
        let collectionId = this.getCollectionId();
        this.userdata.oped[collectionId] = undefined;
    }

    skipOPED() {
        let collectionId = this.getCollectionId();
        if (!(this.userdata.oped[collectionId] instanceof Array)) return;
        if (this.userdata.oped[collectionId][0]) {
            if (this.video.currentTime < this.userdata.oped[collectionId][0]) {
                this.video.currentTime = this.userdata.oped[collectionId][0];
                this.hintInfo('BiliPolyfill: 已跳过片头');
            }
        }
        if (this.userdata.oped[collectionId][1]) {
            let edHandler = v => {
                if (v.target.currentTime > this.userdata.oped[collectionId][1]) {
                    v.target.removeEventListener('timeupdate', edHandler);
                    v.target.dispatchEvent(new Event('ended'));
                }
            }
            this.video.addEventListener('timeupdate', edHandler);
        }
    }

    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/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/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) {
        let h = document.querySelector('div.viewbox div.info') || document.querySelector('div.bangumi-header div.header-info') || document.querySelector('div.video-info-module');
        let 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;
            await monkey.queryInfo('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;
            mp4A.href = await monkey.queryInfo('mp4');
            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';
        };

        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';
        h.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.getElementsByClassName('v-title')[0] || top.document.getElementsByClassName('header-info')[0] || top.document.getElementsByClassName('video-info-module')[0]).children[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> (测)载入缓存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 () => {
            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[5].onclick = () => { top.location.reload(true); };
        ul.children[6].onclick = () => { playerWin.dispatchEvent(new Event('unload')); };
        ul.children[7].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> 取消标记
                            </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.markOPPosition(); };
        ul.children[2].children[1].children[1].onclick = () => { polyfill.markEDPostion(3); };
        ul.children[2].children[1].children[2].onclick = () => { polyfill.clearOPEDPosition(); };
        ul.children[2].children[1].children[3].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[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="https://greasyfork.org/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', '弹幕过滤:在网页播放器里设置的屏蔽词也对下载的弹幕生效。'],
        ];

        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);
            td.appendChild(checkbox);
            td.appendChild(document.createTextNode(d[1]));
        }

        return table;
    }

    static genPolyfillOptionTable(option = {}) {
        const description = [
            ['betabeta', '增强组件总开关 <---------更加懒得测试了,反正以后B站也会自己提供这些功能。也许吧。'], //betabeta
            ['badgeWatchLater', '稍后再看添加数字角标'],
            ['dblclick', '双击全屏'],
            ['scroll', '自动滚动到播放器'],
            ['recommend', '弹幕列表换成相关视频'],
            ['electric', '整合充电榜与换P倒计时'],
            //['electricSkippable', '跳过充电榜'],
            ['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);
            td.appendChild(checkbox);
            td.appendChild(document.createTextNode(d[1]));
        }

        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号或番剧号:[片头,片尾]。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';
        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,
                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.location.href.includes('www.bilibili.com/video/av')) 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();