!(function () {
    /**
     * @param opt
     *        container: DOM 容器
     *        contextOptions：
     *        videoBuffer：
     *        forceNoGL：
     *        isNotMute：
     *        decoder：
     * @constructor
     */
    function Jessibuca(opt) {
        this._opt = opt;

        if (typeof opt.container === "string") {
            this._opt.container = document.getElementById(opt.container);
        }
        if (!this._opt.container) {
            throw new Error('Jessibuca need container option');
            return;
        }

        this._canvasElement = document.createElement("canvas");
        this._canvasElement.style.position = "absolute";
        this._canvasElement.style.top = 0;
        this._canvasElement.style.left = 0;
        this._opt.container.appendChild(this._canvasElement);
        this._container = this._opt.container;
        this._container.style.overflow = "hidden";
        this._containerOldPostion = {
            position: this._container.style.position,
            top: this._container.style.top,
            left: this._container.style.left,
            width: this._container.style.width,
            height: this._container.style.height
        }
        if (this._containerOldPostion.position != "absolute") {
            this._container.style.position = "relative"
        }
        this._opt.videoBuffer = opt.videoBuffer || 0;
        this._opt.text = opt.text || '';
        //
        this._opt.isResize = opt.isResize === false ? opt.isResize : true;
        this._opt.isFullResize = opt.isFullResize === true ? opt.isFullResize : false;
        this._opt.isDebug = opt.debug === true;
        this._opt.timeout = typeof opt.timeout === 'number' ? opt.timeout : 30;
        this._opt.supportDblclickFullscreen = opt.supportDblclickFullscreen === true;
        this._opt.showBandwidth = opt.showBandwidth === true;
        this._opt.operateBtns = Object.assign({
            fullscreen: false,
            screenshot: false,
            play: false,
            audio: false
        }, opt.operateBtns || {});
        this._opt.keepScreenOn = opt.keepScreenOn === true;

        if (!opt.forceNoGL) this._initContextGL();
        this._audioContext = new (window.AudioContext || window.webkitAudioContext)();
        this._audioEnabled(true);
        if (!opt.isNotMute) this._audioEnabled(false);
        if (this._contextGL) {
            this._initProgram();
            this._initBuffers();
            this._initTextures();
        }
        this._onresize = () => this.resize();
        this._onfullscreenchange = () => this._fullscreenchange();
        window.addEventListener("resize", this._onresize);
        document.addEventListener('fullscreenchange', this._onfullscreenchange);
        this._decoderWorker = new Worker(opt.decoder || 'ff.js')
        var _this = this;
        this._hasLoaded = false;
        this._stats = {
            buf: 0,
            fps: 0,
            abps: '',
            vbps: '',
            ts: ''
        };

        if (this._opt.supportDblclickFullscreen) {
            this._canvasElement.addEventListener('dblclick', function () {
                _this.fullscreen = !_this.fullscreen;
            }, false);
        }
        this.onPlay = noop;
        this.onPause = noop;
        this.onRecord = noop;
        this.onFullscreen = noop;
        this.onMute = noop;
        this.onLoad = noop;
        this.onLog = noop;
        this.onError = noop;
        this.onTimeUpdate = noop;
        this.onInitSize = noop;
        this._onMessage();
        this._initDom();
        this._initStatus();
        this._initEventListener();
        this._hideBtns();
        //
        this._initWakeLock();
        this._enableWakeLock();
    };

    function noop() {

    }

    Jessibuca.prototype._initDom = function () {
        var playBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAARVJREFUSMe9laEOglAUhs+5k9lJFpsJ5QWMJoNGbEY0mEy+gr6GNo0a3SiQCegMRILzGdw4hl+Cd27KxPuXb2zA/91z2YXoGRERkX4fvN3A2QxUiv4dFM3n8jZRBLbbVfd+ubJuF4xjiCyXkksueb1uSKCIZYGLBTEx8ekEoV7PkICeVgs8HiGyXoO2bUigCDM4HoPnM7bI8wwJ6Gk0sEXbLSay30Oo2TQkoGcwgFCSQMhxDAvoETEscDiQkJC4LjMz8+XyZ4HrFYWjEQqHQ1asWGWZfmdFAsVINxuw00HhbvfpydpvxWkKTqdYaRCUfUPJCdzv4Gr1uqfli0tOIAzByUT/iCrL6+84y3Bw+D6ui5Ou+jwA8FnIO++FACgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEl0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvYm9mYW5nLnN2Z8fICi0AAAAASUVORK5CYII=';
        var pauseBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAHVJREFUSMftkCESwCAMBEOnCtdXVMKHeC7oInkEeQJXkRoEZWraipxZc8lsQqQZBACAlIS1oqGhhTCdu3oyxyyMcdRf79c5J7SWDBky+z4173rbJvR+VF/e/qwKqIAKqMBDgZyFzAQCoZTpxq7HLDyOrw/9b07l3z4dDnI2IAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMIo/vCcAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDD7YgSbAAAASnRFWHRzdmc6YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9nZjcwMHM3YjNmdy96YW50aW5nLnN2ZxqNZJkAAAAASUVORK5CYII=';
        var screenshotBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAaxJREFUSMfNlLFOAkEQhmevAZMjR6OGRBJKsFBzdkYNpYSaWkopIOFRCBWh1ieA+ALGRgutjK0HzV2H5SX7W/zsmY3cnTEhcZovOzcz9+/s7Ir8d4OGht7fBwAgjvEri2OTl1ffSf0xAMBxRIkS1e3Se3+vcszEMe/6OqmT/aN2m1wsNu/o5YVsNHI7BgA4PCRfXzfXCwKy1RLbcXZG9nrkzc12jvT8nPU/PtatOThgAx8fuS4WyZ0de2e+T87n5OcnuVqRsxl5cpImQDnKUc7DA1fVqpimZCu+vCSjiNH9PlmpJNTQ0INBErfeafZRAakC6FWKfH9nwU7H/l6rGdqCOx3y7c3U+aOARsMMp+1vNskwTLjulB23XJL1epqA9OshIiKeJxAIoug7UyA4OuLi6Ynr52deu+NjOy4MSc9Ln8rMDpTLybBpaOjdXbJUIqdTm8a/t2fn/RSQewR24HicTLmGhnbdzcPquvYtGY3+PIR24UKBUXd35v6Sk4lN47+9NXm/FBAEedfGTjw9JYdDm76fm6+hoS8ujGAxT6L9Im7bTKeurvIEb92+AES1b6x283XSAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABJdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2NhbWVyYS5zdmeyubWEAAAAAElFTkSuQmCC';
        var fullscreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAALZJREFUSMftVbsORUAQVSj8DomChvh3lU5CoSVCQq2RObeYu8XG3deVoHCak81kds7Oaz3vxRcAAMwztOg6vX9d6/3XFQQC+b7iAoFhYE7Tvx9EIFAcy/ftO3MQGAQkCfM4MmeZWyajiLnvmYuCeduMAuSzvRBVYNluFHCssSgFp7Sq9ALKkjnPf9ubRtkDL27HNT3QtsY9cAjsNAVheHIKBOwD2wpxFHDbJpwmaHH2L1iWx+2BDy8RbXXtqbRBAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABTdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3F1YW5waW5nenVpZGFodWEuc3ZnTBoI7AAAAABJRU5ErkJggg==';
        var minScreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAYJJREFUSMfdVbGKwkAQnQn+geAfWBixUTsVgp3YGKxSWflVNmIjARULwc5KO40ipNHWRgs/wGLniucKa+Jd5ODuuGle5u3szGRmd5bor4iIiMhuB3Sc+HXXBdp2/Lpta7v4dccRJUrUdhtNQIkSVa3C8HwG1uumg34f2OnEB+h0tF1Sv5b+YIsttpZLEhKSdhvscPi8IXFF74GJiYnHY7Cex8zMvFgkbInjmJnv98kqoO30vmhLtaRMB60WtEbDNDudgMUiKiQSzfjOMzFxoQAyCPSfw7/nQZ/PUYnpNGV6OR6BmYzJbzYIoBQCzGaRBDQvJCTdLnTLolg5HN5t6f8V1h/oUT4PrVKJWBotmEzQw+vV3J9Ow851P2/BaoX9Yfh0BrJZYKlk8uUyHOpDeLuBHwzMBJtN2PV6IPUhXK9Nf5cLMAxfluanrmGkRBggtRo03wfq66P/6CsJAnOg+f6rgfZI4BGYiYlHIx048eR6krcnq34kkj1GuVz8+jceo9+SD5A8yGh8CTq7AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABNdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3p1aXhpYW9odWEuc3ZnoCFr0AAAAABJRU5ErkJggg==';
        var quietBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAR9JREFUSMfVlD0LglAYhe9VkwgNihpsjbYQf4JTS7+iuaGxpcGfJjS0NFRLk2NDi6MogafhJGRIX9yEzvJwrx/nvPd9VYh/F3LkyBuN2g3J1QoAgCQhPe/Hxq5Lo+0WlfJ9dYYAgGaTDAIyy/BUnwcwWJlhcLnZkN2ugIBAuy2kkEL2ep8F73S4kjfFcfn6cMj9KLodrWVBiXyf75tMyOOR+4MBOZ8XLXzorboA5UpnM/J0Ivd7+vX7xX2asqGpVKtFXi5sqWmypXefrfIWAACmU/JwKCoun8hu9zA0uk6u13wgirg+n7+bAcsibbt6SB3n9TQXPxwAwHJJpum7M6BcDDQa0SgMaw9QPkJNIxcLMo4ZcDz+eYDqQFLWbqxKV57EtW1WtMbmAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABKdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2ppbmd5aW4uc3ZnIlMYaQAAAABJRU5ErkJggg==';
        var playAudioBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAU5JREFUSMftkzGKwlAURf9PULBQwULSCKK1bZAgNuoaFFyAC3AdZg0uQCwshWzAShEEO7Gy0soUCu9Occ3An5nMGCfdzGsO7+Xy3/03iVL/lbAAACiVIBCI77O37Vi9QCDZbEqLm03ycEBUAoHk818v7nYpul5Jz4tf8HBKYa1mcjwmbzd8rG8NFIsU7ffk8UjmcjE3XK+RtB4G2PT75GbDeblMttumfjSKMRCGLxsQCKTReE9KIJDJxDw/SmKxiOZWWh+ntrSlre2WXRAorbTSrZapip7X66kbMKtQUFBQCENznsmQ93vqBhh5r8fO85jAcsnIrcce1yV3uxgD8zl5uZgU+dGBVlrp6GbTKRPwffaDAek45Gz2/M0AAJ0OeTol+w0rFYrOZ3K1MhNJEjEAwHF4cBA8Z8B1zcXV6msv+JMR2yaHQ1LrXx/8Z+sNRxsWcwZeb6UAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEt0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvc2hlbmd5aW4uc3ZnFog1MQAAAABJRU5ErkJggg==';
        var recordBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAPRJREFUSMflVDEOwjAQO0e8gr2sZYVunREbD6ISfAgmkBjpC/hBEQ+AtTWD6QAI0gBlqRfLp+TiXC5n1nXgMUCS5HBoNBqj6IOMMFwuEpsNAABl6d3HihWrOJaBsuRPkGW+c929HAxuYefb6L+R0ZgkMrJYiItCnCT1sl5Y1jwXj0bNniJNJWqujfX7LyrwJh8AYDxWgulU0dPp20IFlxoODm61kpE4VnS9/puBXyPYgH7LbKY3PhwUnUw+NdC4CdW9+71UgyZspwIBB9No3O0klktxUahyx+Pz+lYG0Xzu84lXRqTqwRQAGAzns8R223gUdxZXGcAK5Hp0ClIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvbHV6aGlzaGlwaW4uc3Zn5Zd7GQAAAABJRU5ErkJggg==';
        var recordingBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAahJREFUSMdjYBjpgBFd4NZK+f+soQYG//T+yzFuUFUl2cApjEWM/758UZvysPDn3127GBkZGBgY/v4l6ICb9xTWsRbp6/9f9W8N44Jz5xgCGI4wfGFiIttrR/5n/3/U3KyR8rj8t0RdHS5lcAv+//yXzzhZTY1ii2FAmsGZocna+maD3GnWY62tNzbJBbDOffLkxie5eJYwa2uYMhaigzb2/zyGguPH/y9mTGKYYGlJUIMiYxDjHCen/4oMDAxznJzg4k8Z/jP+l5LCCAFCQP30Y5dfXVZWDI7/zzIs8PNjNGJ4/7/r+XNKA4rkoNZ4/lj0V9TmzUxJv0J+F+jrM3YyvPq/acsWujmA2oBkB9y4LifLxhoa+teAzYFtwtWr/8sZxBj9fHxo7oCbprJ72MqOHWNgZGBkYFy1isGGoZahTFSU0hAgOhcQnfph4P7/df9T9u1jPMn4nyHmxIn/bAzLGe7GxTHsZyj+f+zpUwYGBmmG6bQsiMr+L/v/rqlJY9Njm9889fW4lGEUxXCHwAomUgH3vxBG8c+f1WWf9P98sns3oaJ4FAAAbtWqHTT84QYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6MzU6MjMrMDg6MDBLHbvEAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjM1OjIzKzA4OjAwOkADeAAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fcTM1YTFhNHBtY2MvbHV6aGlzaGlwaW4uc3Zn6xlv1QAAAABJRU5ErkJggg==';
        var gifBase64 = 'data:image/gif;base64, ';
        var playBigBase64 = 'data:image/png;base64,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';

        function _setStyle(dom, cssObj) {
            Object.keys(cssObj).forEach(function (key) {
                dom.style[key] = cssObj[key];
            })
        }

        var doms = {};

        var fragment = document.createDocumentFragment();
        var btnWrap = document.createElement('div');
        var control1 = document.createElement('div');
        var control2 = document.createElement('div');
        var textDom = document.createElement('div');
        var speedDom = document.createElement('div');
        var playDom = document.createElement('div');
        var playBigDom = document.createElement('div');
        var pauseDom = document.createElement('div');
        var screenshotsDom = document.createElement('div');
        var fullscreenDom = document.createElement('div');
        var minScreenDom = document.createElement('div');
        var loadingDom = document.createElement('div');
        var loadingTextDom = document.createElement('div');
        var quietAudioDom = document.createElement('div');
        var playAudioDom = document.createElement('div');
        var recordDom = document.createElement('div');
        var recordingDom = document.createElement('div');
        var bgDom = document.createElement('div');

        loadingTextDom.innerText = this._opt.loadingText || '';
        textDom.innerText = this._opt.text || '';
        speedDom.innerText = '';
        playDom.title = '播放';
        pauseDom.title = '暂停';
        screenshotsDom.title = '截屏';
        fullscreenDom.title = '全屏';
        minScreenDom.title = '退出全屏';
        quietAudioDom.title = '静音';
        playAudioDom.title = '取消静音';
        recordDom.title = '录制';
        recordingDom.title = '取消录制';

        var wrapStyle = {
            height: '38px',
            zIndex: 11,
            position: 'absolute',
            left: 0,
            bottom: 0,
            width: '100%',
            background: 'rgba(0,0,0)'
        };

        var bgStyle = {
            position: 'absolute',
            width: '100%',
            height: '100%',
        };

        if (this._opt.background) {
            bgStyle = Object.assign({}, bgStyle, {
                backgroundRepeat: "no-repeat",
                backgroundPosition: "center",
                backgroundSize: '100%',
                backgroundImage: "url('" + this._opt.background + "')"
            })
        }

        //
        var loadingStyle = {
            position: 'absolute',
            width: '100%',
            height: '100%',
            textAlign: 'center',
            color: "#fff",
            display: 'none',
            backgroundImage: "url('" + gifBase64 + "')",
            backgroundRepeat: "no-repeat",
            backgroundPosition: "center",
            backgroundSize: "40px 40px",
        };

        var playBigStyle = {
            position: 'absolute',
            width: '100%',
            height: '100%',
            display: 'none',
            background: 'rgba(0,0,0,0.4)',
            backgroundImage: "url('" + playBigBase64 + "')",
            backgroundRepeat: "no-repeat",
            backgroundPosition: "center",
            backgroundSize: "48px 48px",
            cursor: "pointer"
        };

        var loadingTextStyle = {
            position: 'absolute',
            width: "100%",
            top: '60%',
            textAlign: 'center',
        }
        var controlStyle = {
            position: 'absolute',
            top: 0,
            height: '100%',
            display: 'flex',
            alignItems: 'center',
        };
        var styleObj = {
            display: 'none',
            position: 'relative',
            fontSize: '13px',
            color: '#fff',
            lineHeight: '20px',
            marginLeft: '5px',
            marginRight: '5px',
            userSelect: 'none'
        };
        var styleObj2 = {
            display: 'none',
            position: 'relative',
            width: '16px',
            height: '16px',
            marginLeft: '8px',
            marginRight: '8px',
            backgroundRepeat: "no-repeat",
            backgroundPosition: "center",
            backgroundSize: '100%',
            cursor: 'pointer',
        };
        _setStyle(bgDom, bgStyle);
        _setStyle(btnWrap, wrapStyle);
        _setStyle(loadingDom, loadingStyle);
        _setStyle(playBigDom, playBigStyle);
        _setStyle(loadingTextDom, loadingTextStyle);
        _setStyle(control1, Object.assign({}, controlStyle, {
            left: 0
        }));
        _setStyle(control2, Object.assign({}, controlStyle, {
            right: 0
        }));
        _setStyle(textDom, styleObj);
        _setStyle(speedDom, styleObj);
        _setStyle(playDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + playBase64 + "')",
        }));

        _setStyle(pauseDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + pauseBase64 + "')"
        }));

        _setStyle(screenshotsDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + screenshotBase64 + "')"
        }));

        _setStyle(fullscreenDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + fullscreenBase64 + "')"
        }));

        _setStyle(minScreenDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + minScreenBase64 + "')"
        }));

        _setStyle(quietAudioDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + quietBase64 + "')"
        }));

        _setStyle(playAudioDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + playAudioBase64 + "')"
        }));

        _setStyle(recordDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + recordBase64 + "')"
        }));

        _setStyle(recordingDom, Object.assign({}, styleObj2, {
            backgroundImage: "url('" + recordingBase64 + "')"
        }));

        loadingDom.appendChild(loadingTextDom);
        if (this._opt.text) {
            control1.appendChild(textDom);
            doms.textDom = textDom;
        }
        if (this._opt.showBandwidth) {
            control1.appendChild(speedDom);
            doms.speedDom = speedDom;
        }

        // record
        //control2.appendChild(recordingDom);
        //control2.appendChild(recordDom);

        // screenshots
        if (this._opt.operateBtns.screenshot) {
            control2.appendChild(screenshotsDom);
            doms.screenshotsDom = screenshotsDom;
        }

        // play stop
        if (this._opt.operateBtns.play) {
            control2.appendChild(playDom);
            control2.appendChild(pauseDom);
            doms.playDom = playDom;
            doms.pauseDom = pauseDom;
        }

        // audio
        if (this._opt.operateBtns.audio) {
            control2.appendChild(playAudioDom);
            control2.appendChild(quietAudioDom);
            doms.playAudioDom = playAudioDom;
            doms.quietAudioDom = quietAudioDom;
        }

        // fullscreen
        if (this._opt.operateBtns.fullscreen) {
            control2.appendChild(fullscreenDom);
            control2.appendChild(minScreenDom);
            doms.fullscreenDom = fullscreenDom;
            doms.minScreenDom = minScreenDom;
        }

        btnWrap.appendChild(control1);
        btnWrap.appendChild(control2);

        fragment.appendChild(bgDom);
        doms.bgDom = bgDom;
        fragment.appendChild(loadingDom);
        doms.loadingDom = loadingDom;
        if (this._showControl()) {
            fragment.appendChild(btnWrap);
        }
        if (this._opt.operateBtns.play) {
            fragment.appendChild(playBigDom);
            doms.playBigDom = playBigDom;
        }
        this._container.appendChild(fragment);
        this._doms = doms;
    };

    Jessibuca.prototype._initWakeLock = function () {
        this._wakeLock = null;
        var _this = this;
        var handleWakeLock = () => {
            if (this._wakeLock !== null && "visible" === document.visibilityState) {
                _this._enableWakeLock();
            }
        };

        document.addEventListener('visibilitychange', handleWakeLock);
        document.addEventListener('fullscreenchange', handleWakeLock);
    };

    Jessibuca.prototype._enableWakeLock = function () {
        if (this._opt.keepScreenOn) {
            if ("wakeLock" in navigator) {
                var _this = this;
                navigator.wakeLock.request("screen").then((lock) => {
                    _this._wakeLock = lock;
                    _this._wakeLock.addEventListener('release', function () {
                    });
                })
            }
        }
    };


    Jessibuca.prototype._initGainNode = function () {
        var gainNode = this._audioContext.createGain();
        var _this = this;
        var source;
        if (!navigator.mediaDevices.getUserMedia) {
            console.log('getUserMedia not supported on your browser!');
            return;
        }

        navigator.mediaDevices.getUserMedia(
            // constraints - only audio needed for this app
            {
                audio: true
            },

            // Success callback
            function (stream) {
                source = _this._audioContext.createMediaStreamSource(stream);
                source.connect(gainNode);
                gainNode.connect(_this._audioContext.destination);
                _this._gainNode = gainNode;
            },

            // Error callback
            function (err) {
                console.log('The following gUM error occurred: ' + err);
            }
        );
    };

    Jessibuca.prototype._showControl = function () {
        var result = false;

        var hasBtnShow = false;
        Object.keys(this._opt.operateBtns).forEach((key) => {
            if (this._opt.operateBtns[key]) {
                hasBtnShow = true;
            }
        });

        if (this._opt.showBandwidth || this._opt.text || hasBtnShow) {
            result = true;
        }

        return result;
    };

    Jessibuca.prototype._onMessage = function () {
        var _this = this;
        this._decoderWorker.onmessage = function (event) {
            var msg = event.data;
            switch (msg.cmd) {
                case "init":
                    _this._opt.isDebug && console.log("decoder worker init")
                    _this.setBufferTime(_this._opt.videoBuffer);
                    if (!_this._hasLoaded) {
                        _this._opt.isDebug && console.log("has loaded");
                        _this._hasLoaded = true;
                        _this.onLoad();
                        _this._trigger('load');
                    }
                    break
                case "initSize":
                    _this._canvasElement.width = msg.w;
                    _this._canvasElement.height = msg.h;
                    _this.onInitSize();
                    _this.resize();
                    _this._trigger('videoInfo', {w: msg.w, h: msg.h});
                    if (_this.isWebGL()) {

                    } else {
                        _this._initRGB(msg.w, msg.h)
                    }
                    break
                case "render":
                    if (_this._contextGL) {
                        _this._drawNextOutputPictureGL(msg.output);
                    } else {
                        _this._drawNextOutputPictureRGBA(msg.buffer);
                    }
                    if (_this.loading) {
                        _this.loading = false;
                        _this.playing = true;
                        _this._opt.isDebug && console.log("clear check loading timeout");
                        _this._clearCheckLoading();
                    }
                    _this._trigger('timeUpdate', msg.ts);
                    _this.onTimeUpdate(msg.ts);
                    _this._updateStats({bps: msg.bps, ts: msg.ts});
                    _this._checkHeart();
                    break
                case "initAudio":
                    _this._initAudioPlay(msg.frameCount, msg.samplerate, msg.channels)
                    _this._trigger('audioInfo', {
                        numOfChannels: msg.channels, // 声频通道
                        length: msg.frameCount, // 帧数
                        sampleRate: msg.samplerate // 采样率
                    });
                    break
                case "playAudio":
                    _this._playAudio(msg.buffer)
                    break
                case "print":
                    _this.onLog(msg.text)
                    this._trigger('log', msg.text);
                    _this._opt.isDebug && console.log(msg.text);
                    break
                case "printErr":
                    _this.onLog(msg.text);
                    this._trigger('log', msg.text);
                    _this.onError(msg.text);
                    this._trigger('error', msg.text);
                    _this._opt.isDebug && console.error(msg.text);
                    break;
                case "initAudioPlanar":
                    _this._initAudioPlanar(msg);
                    _this._trigger('audioInfo', {
                        numOfChannels: msg.channels, // 声频通道
                        length: undefined, // 帧数
                        sampleRate: msg.samplerate // 采样率
                    });
                    break;
                default:
                    _this._opt.isDebug && console.log(msg);
                    _this[msg.cmd](msg)
            }
        };
    };

    Jessibuca.prototype._initEventListener = function () {
        var _this = this;

        this._doms.playDom && this._doms.playDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.play();
        }, false);

        this._doms.playBigDom && this._doms.playBigDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.play();
        }, false);

        this._doms.pauseDom && this._doms.pauseDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.pause();
        }, false);

        // screenshots
        this._doms.screenshotsDom && this._doms.screenshotsDom.addEventListener('click', function (e) {
            e.stopPropagation();
            var filename = _this._opt.text + '' + _now();
            _this._screenshot(filename);
        }, false);
        //
        this._doms.fullscreenDom && this._doms.fullscreenDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.fullscreen = true;
        }, false);
        //
        this._doms.minScreenDom && this._doms.minScreenDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.fullscreen = false;
        }, false);
        //
        this._doms.recordDom && this._doms.recordDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.recording = true;
        }, false);
        //
        this._doms.recordingDom && this._doms.recordingDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.recording = false;
        }, false);

        this._doms.quietAudioDom && this._doms.quietAudioDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.cancelMute();
        }, false);

        this._doms.playAudioDom && this._doms.playAudioDom.addEventListener('click', function (e) {
            e.stopPropagation();
            _this.mute();
        }, false);
    };
    /**
     * set debug
     * @param flag
     */
    Jessibuca.prototype.setDebug = function (flag) {
        this._opt.isDebug = !!flag;
    };
    /**
     * mute
     */
    Jessibuca.prototype.mute = function () {
        this._audioEnabled(false);
        this.quieting = true;
    };

    /**
     * cancel mute
     */
    Jessibuca.prototype.cancelMute = function () {
        this._audioEnabled(true);
        this.quieting = false;
    };

    /**
     * 设置旋转角度
     */
    Jessibuca.prototype.setRotate = function (deg) {

    };

    Jessibuca.prototype._initStatus = function () {
        this._loading = true;
        this.loading = true;
        this._recording = false;
        this.recording = false;
        this._playing = false;
        this.playing = false;
        this._quieting = this._opt.isNotMute ? false : true;
        this.quieting = this._opt.isNotMute ? false : true;
        this._fullscreen = false;
        this.fullscreen = false;
    }

    Jessibuca.prototype._initBtns = function () {
        // show
        _domToggle(this._doms.pauseDom, true);
        _domToggle(this._doms.screenshotsDom, true);
        _domToggle(this._doms.fullscreenDom, true);
        _domToggle(this._doms.quietAudioDom, true);
        _domToggle(this._doms.textDom, true);
        _domToggle(this._doms.speedDom, true);
        _domToggle(this._doms.recordDom, true);
        // hide
        _domToggle(this._doms.loadingDom, false);
        _domToggle(this._doms.playDom, false);
        _domToggle(this._doms.playBigDom, false);
        _domToggle(this._doms.bgDom, false);
    };

    Jessibuca.prototype._hideBtns = function () {
        var _this = this;
        Object.keys(this._doms).forEach(function (dom) {
            if (dom !== 'bgDom') {
                _domToggle(_this._doms[dom], false);
            }
        })
    };

    function _checkFull() {
        var isFull = document.fullscreenElement || window.webkitFullscreenElement || document.msFullscreenElement;
        if (isFull === undefined) isFull = false;
        return !!isFull;
    }

    Jessibuca.prototype._updateStats = function (options) {
        options = options || {};

        if (!this._startBpsTime) {
            this._startBpsTime = _now();
        }
        var _nowTime = _now();
        var timestamp = _nowTime - this._startBpsTime;

        if (timestamp < 1 * 1000) {
            this._bps += (options.bps || 0);
            this._stats.fps += 1;
            this._stats.vbps += parseInt((options.bps || 0));
            return;
        }
        this._stats.ts = options.ts;
        this._doms.speedDom && (this._doms.speedDom.innerText = _bpsSize(this._bps));
        this._trigger('bps', this._bps);
        this._trigger('stats', this._stats);
        this._trigger('performance', _fpsStatus(this._stats.fps));
        this._bps = 0;
        this._stats.fps = 0;
        this._stats.vbps = 0;
        this._startBpsTime = _nowTime;
    };


    Jessibuca.prototype._checkHeart = function () {
        if (this._checkHeartTimeout) {
            clearTimeout(this._checkHeartTimeout);
            this._checkHeartTimeout = null;
        }
        var _this = this;
        this._checkHeartTimeout = setTimeout(function () {
            _this._opt.isDebug && console.log('check heart timeout');
            _this._trigger('timeout');
            _this.recording = false;
            _this.playing = false;
            _this._close();
        }, this._opt.timeout * 1000);
    };

    Jessibuca.prototype._checkLoading = function () {
        if (this._checkLoadingTimeout) {
            clearTimeout(this._checkLoadingTimeout);
            this._checkLoadingTimeout = null;
        }
        var _this = this;
        this._checkLoadingTimeout = setTimeout(function () {
            _this._opt.isDebug && console.log('check loading timeout');
            _this._trigger('timeout');
            _this.playing = false;
            _this._close();
            _domToggle(_this._doms.loadingDom, false);
        }, this._opt.timeout * 1000);
    };

    Jessibuca.prototype._clearCheckLoading = function () {
        if (this._checkLoadingTimeout) {
            clearTimeout(this._checkLoadingTimeout);
            this._checkLoadingTimeout = null;
        }
    };

    Jessibuca.prototype._initCheckVariable = function () {
        this._startBpsTime = '';
        this._bps = 0;
        if (this._checkHeartTimeout) {
            clearTimeout(this._checkHeartTimeout);
            this._checkHeartTimeout = null;
        }
    }
    //
    Jessibuca.prototype._initAudioPlanar = function (msg) {
        var channels = msg.channels
        var samplerate = msg.samplerate
        var context = this._audioContext;
        var isPlaying = false;
        var audioBuffers = [];
        if (!context) return false;
        var _this = this
        this._playAudio = function (buffer) {
            var frameCount = buffer[0][0].length
            var audioBuffer = context.createBuffer(channels, frameCount * buffer.length, samplerate);
            var copyToCtxBuffer = function (fromBuffer) {
                for (var channel = 0; channel < channels; channel++) {
                    var nowBuffering = audioBuffer.getChannelData(channel);
                    for (var j = 0; j < buffer.length; j++) {
                        for (var i = 0; i < frameCount; i++) {
                            nowBuffering[i + j * frameCount] = fromBuffer[j][channel][i]
                        }
                        //postMessage({ cmd: "setBufferA", buffer: fromBuffer[j] }, '*', fromBuffer[j].map(x => x.buffer))
                    }
                }
            }
            var playNextBuffer = function () {
                isPlaying = false;
                //console.log("~", audioBuffers.length)
                if (audioBuffers.length) {
                    playAudio(audioBuffers.shift());
                }
                //if (audioBuffers.length > 1) audioBuffers.shift();
            };
            var playAudio = function (fromBuffer) {
                if (!fromBuffer) return
                if (isPlaying) {
                    audioBuffers.push(fromBuffer);
                    //console.log(audioBuffers.length)
                    return;
                }
                isPlaying = true;
                copyToCtxBuffer(fromBuffer);
                var source = context.createBufferSource();
                source.buffer = audioBuffer;
                source.connect(context.destination);
                // source.onended = playNextBuffer;
                source.start();
            };
            _this._playAudio = playAudio
            _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
            playAudio(buffer)
        };
    }

    function _unlock(context) {
        context.resume();
        var source = context.createBufferSource();
        source.buffer = context.createBuffer(1, 1, 22050);
        source.connect(context.destination);
        if (source.noteOn)
            source.noteOn(0);
        else
            source.start(0);
    }

    function _domToggle(dom, toggle) {
        if (dom) {
            dom.style.display = toggle ? 'block' : "none";
        }
    }

    function _dataURLToFile(dataURL) {
        const arr = dataURL.split(",");
        const bstr = atob(arr[1]);
        const type = arr[0].replace("data:", "").replace(";base64", "")
        let n = bstr.length, u8arr = new Uint8Array(n);
        while (n--) {
            u8arr[n] = bstr.charCodeAt(n);
        }
        return new File([u8arr], 'file', {type});
    }

    function _downloadImg(content, fileName) {
        const aLink = document.createElement("a");
        aLink.download = fileName;
        aLink.href = URL.createObjectURL(content);
        aLink.click();
        URL.revokeObjectURL(content);
    }

    function _bpsSize(value) {
        if (null == value || value === '') {
            return "0 KB/S";
        }
        var srcsize = parseFloat(value);
        var size = srcsize / 1024;
        size = size.toFixed(2);
        return size + 'KB/S';
    }

    function _fpsStatus(fps) {
        var result = 0;
        if (fps >= 24) {
            result = 2;
        } else if (fps >= 15) {
            result = 1;
        }

        return result;
    }

    /**
     * set audio
     * @param flag
     */
    Jessibuca.prototype._audioEnabled = function (flag) {
        if (flag) {
            _unlock(this._audioContext)
            this._audioEnabled = function (flag) {
                if (flag) {
                    // 恢复
                    this._audioContext.resume();

                } else {
                    // 暂停
                    this._audioContext.suspend();
                }
            }
        } else {
            this._audioContext.suspend();
        }
    }

    Jessibuca.prototype._playAudio = function (data) {
        var context = this._audioContext;
        var isPlaying = false;
        var isDecoding = false;
        if (!context) return false;
        var audioBuffers = [];
        var decodeQueue = []
        var _this = this
        var playNextBuffer = function (e) {
            if (audioBuffers.length) {
                playBuffer(audioBuffers.shift())
            }
        };
        var playBuffer = function (buffer) {
            isPlaying = true;
            var audioBufferSouceNode = context.createBufferSource();
            audioBufferSouceNode.buffer = buffer;
            audioBufferSouceNode.connect(context.destination);
            // audioBufferSouceNode.onended = playNextBuffer;
            audioBufferSouceNode.start();
            if (!_this.audioInterval) {
                _this.audioInterval = setInterval(playNextBuffer, buffer.duration * 1000 - 1);
            }
        }
        var decodeAudio = function () {
            if (decodeQueue.length) {
                context.decodeAudioData(decodeQueue.shift(), tryPlay, decodeAudio);
            } else {
                isDecoding = false
            }
        }
        var tryPlay = function (buffer) {
            decodeAudio()
            if (isPlaying) {
                audioBuffers.push(buffer);
            } else {
                playBuffer(buffer)
            }
        }
        var playAudio = function (data) {
            decodeQueue.push(...data)
            if (!isDecoding) {
                isDecoding = true
                decodeAudio()
            }
        }
        this._playAudio = playAudio
        playAudio(data)
    }
    Jessibuca.prototype._initAudioPlay = function (frameCount, samplerate, channels) {
        var context = this._audioContext;
        var isPlaying = false;
        var audioBuffers = [];
        if (!context) return false;
        var _this = this
        var resampled = samplerate < 22050;
        if (resampled) {
            _this._opt.isDebug && console.log("resampled!")
        }
        var audioBuffer = resampled ? context.createBuffer(channels, frameCount << 1, samplerate << 1) : context.createBuffer(channels, frameCount, samplerate);
        var playNextBuffer = function () {
            isPlaying = false;
            //console.log("~", audioBuffers.length)
            if (audioBuffers.length) {
                playAudio(audioBuffers.shift());
            }
        };

        var copyToCtxBuffer = channels > 1 ? function (fromBuffer) {
            for (var channel = 0; channel < channels; channel++) {
                var nowBuffering = audioBuffer.getChannelData(channel);
                if (resampled) {
                    for (var i = 0; i < frameCount; i++) {
                        nowBuffering[i * 2] = nowBuffering[i * 2 + 1] = fromBuffer[i * (channel + 1)] / 32768;
                    }
                } else
                    for (var i = 0; i < frameCount; i++) {
                        nowBuffering[i] = fromBuffer[i * (channel + 1)] / 32768;
                    }

            }
        } : function (fromBuffer) {
            var nowBuffering = audioBuffer.getChannelData(0);
            for (var i = 0; i < nowBuffering.length; i++) {
                nowBuffering[i] = fromBuffer[i] / 32768;
            }
        };
        var playAudio = function (fromBuffer) {
            if (isPlaying) {
                audioBuffers.push(fromBuffer);
                return;
            }
            isPlaying = true;
            copyToCtxBuffer(fromBuffer);
            var source = context.createBufferSource();
            source.buffer = audioBuffer;
            source.connect(context.destination);
            if (!_this.audioInterval) {
                _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
            }
            source.start();
        };
        this._playAudio = playAudio;
    }
    /**
     * Returns true if the canvas supports WebGL
     */
    Jessibuca.prototype.isWebGL = function () {
        return !!this._contextGL;
    };
    /**
     * set timeout
     * @param time
     */
    Jessibuca.prototype.setTimeout = function (time) {
        if (typeof time === 'number') {
            this._opt.timeout = Number(time);
        }
    };

    /**
     * @desc 视频缩放模式, 当视频分辨率比例与canvas显示区域比例不同时,缩放效果不同:
     0 视频画面完全填充canvas区域,画面会被拉伸
     1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边(默认)
     2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
     * @param type
     *
     */
    Jessibuca.prototype.setScaleMode = function (type) {
        if (type === 0) {
            this._opt.isFullResize = false;
            this._opt.isResize = false;
        } else if (type === 1) {
            this._opt.isFullResize = false;
            this._opt.isResize = true;
        } else if (type === 2) {
            this._opt.isFullResize = true;
        }
        this.resize();
    };

    /**
     * Create the GL context from the canvas element
     */
    Jessibuca.prototype._initContextGL = function () {
        var canvas = this._canvasElement;
        var gl = null;

        var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
        var nameIndex = 0;

        while (!gl && nameIndex < validContextNames.length) {
            var contextName = validContextNames[nameIndex];

            try {
                var contextOptions = {preserveDrawingBuffer: true};
                if (this._opt.contextOptions) {
                    contextOptions = Object.assign(contextOptions, this._opt.contextOptions);
                }

                gl = canvas.getContext(contextName, contextOptions);
            } catch (e) {
                gl = null;
            }

            if (!gl || typeof gl.getParameter !== "function") {
                gl = null;
            }

            ++nameIndex;
        }
        ;

        this._contextGL = gl;
    };

    /**
     * Initialize GL shader program
     */
    Jessibuca.prototype._initProgram = function () {
        var gl = this._contextGL;

        var vertexShaderScript = [
            'attribute vec4 vertexPos;',
            'attribute vec4 texturePos;',
            'varying vec2 textureCoord;',

            'void main()',
            '{',
            'gl_Position = vertexPos;',
            'textureCoord = texturePos.xy;',
            '}'
        ].join('\n');

        var fragmentShaderScript = [
            'precision highp float;',
            'varying highp vec2 textureCoord;',
            'uniform sampler2D ySampler;',
            'uniform sampler2D uSampler;',
            'uniform sampler2D vSampler;',
            'const mat4 YUV2RGB = mat4',
            '(',
            '1.1643828125, 0, 1.59602734375, -.87078515625,',
            '1.1643828125, -.39176171875, -.81296875, .52959375,',
            '1.1643828125, 2.017234375, 0, -1.081390625,',
            '0, 0, 0, 1',
            ');',

            'void main(void) {',
            'highp float y = texture2D(ySampler,  textureCoord).r;',
            'highp float u = texture2D(uSampler,  textureCoord).r;',
            'highp float v = texture2D(vSampler,  textureCoord).r;',
            'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;',
            '}'
        ].join('\n');

        var vertexShader = gl.createShader(gl.VERTEX_SHADER);
        gl.shaderSource(vertexShader, vertexShaderScript);
        gl.compileShader(vertexShader);
        if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
            this._opt.isDebug && console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
        }

        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
        gl.shaderSource(fragmentShader, fragmentShaderScript);
        gl.compileShader(fragmentShader);
        if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
            this._opt.isDebug && console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
        }

        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
            this._opt.isDebug && console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
        }

        gl.useProgram(program);

        this._shaderProgram = program;
    };

    /**
     * Initialize vertex buffers and attach to shader program
     */
    Jessibuca.prototype._initBuffers = function () {
        var gl = this._contextGL;
        var program = this._shaderProgram;

        var vertexPosBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);

        var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
        gl.enableVertexAttribArray(vertexPosRef);
        gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);

        var texturePosBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);

        var texturePosRef = gl.getAttribLocation(program, 'texturePos');
        gl.enableVertexAttribArray(texturePosRef);
        gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);

        this._texturePosBuffer = texturePosBuffer;
    };

    /**
     * Initialize GL textures and attach to shader program
     */
    Jessibuca.prototype._initTextures = function () {
        var gl = this._contextGL;
        var program = this._shaderProgram;

        var yTextureRef = this._initTexture();
        var ySamplerRef = gl.getUniformLocation(program, 'ySampler');
        gl.uniform1i(ySamplerRef, 0);
        this._yTextureRef = yTextureRef;

        var uTextureRef = this._initTexture();
        var uSamplerRef = gl.getUniformLocation(program, 'uSampler');
        gl.uniform1i(uSamplerRef, 1);
        this._uTextureRef = uTextureRef;

        var vTextureRef = this._initTexture();
        var vSamplerRef = gl.getUniformLocation(program, 'vSampler');
        gl.uniform1i(vSamplerRef, 2);
        this._vTextureRef = vTextureRef;
    };

    /**
     * Create and configure a single texture
     */
    Jessibuca.prototype._initTexture = function () {
        var gl = this._contextGL;

        var textureRef = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, textureRef);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.bindTexture(gl.TEXTURE_2D, null);

        return textureRef;
    };

    /**
     * Draw picture data to the canvas.
     * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer,
     * Otherwise, data must be an RGBA formatted ArrayBuffer.
     */
    Jessibuca.prototype._drawNextOutputPicture = function (data) {
        if (this._contextGL) {
            this._drawNextOutputPictureGL(data);
        } else {
            this._drawNextOutputPictureRGBA(data);
        }
    };

    /**
     * Draw the next output picture using WebGL
     */
    Jessibuca.prototype._drawNextOutputPictureGL = function (data) {
        var gl = this._contextGL;
        var texturePosBuffer = this._texturePosBuffer;
        var yTextureRef = this._yTextureRef;
        var uTextureRef = this._uTextureRef;
        var vTextureRef = this._vTextureRef;
        var croppingParams = this.croppingParams
        var width = this._canvasElement.width
        var height = this._canvasElement.height
        if (croppingParams) {
            gl.viewport(0, 0, croppingParams.width, croppingParams.height);
            var tTop = croppingParams.top / height;
            var tLeft = croppingParams.left / width;
            var tBottom = croppingParams.height / height;
            var tRight = croppingParams.width / width;
            var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]);

            gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
            gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW);
        } else {
            gl.viewport(0, 0, this._canvasElement.width, this._canvasElement.height);
        }
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[0]);

        gl.activeTexture(gl.TEXTURE1);
        gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[1]);

        gl.activeTexture(gl.TEXTURE2);
        gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[2]);

        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    };

    /**
     * Draw next output picture using ARGB data on a 2d canvas.
     */
    Jessibuca.prototype._drawNextOutputPictureRGBA = function (data) {
        this.imageData.data.set(data);
        var croppingParams = this.croppingParams
        if (!croppingParams) {
            this.ctx2d.putImageData(this.imageData, 0, 0);
        } else {
            this.ctx2d.putImageData(this.imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height);
        }
    };
    Jessibuca.prototype.ctx2d = null;
    Jessibuca.prototype.imageData = null;
    Jessibuca.prototype._initRGB = function (width, height) {
        this.ctx2d = this._canvasElement.getContext('2d');
        this.imageData = this.ctx2d.getImageData(0, 0, width, height);
        this.clear = function () {
            this.ctx2d.clearRect(0, 0, width, height)
        };
    };

    Jessibuca.prototype.pause = function () {
        this._close();
        if (this.loading) {
            _domToggle(this._doms.loadingDom, false);
        }
        this.recording = false;
        this.playing = false;
    };

    Jessibuca.prototype._close = function () {
        if (this.audioInterval) {
            clearInterval(this.audioInterval)
        }
        delete this._playAudio
        this._decoderWorker.postMessage({cmd: "close"})

        if (this._wakeLock) {
            this._wakeLock.release();
            this._wakeLock = null;
        }

        // this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
        this._initCheckVariable();
    }
    /**
     * destroy
     * @desc delete worker,
     */
    Jessibuca.prototype.destroy = function () {
        // destroy
        this._decoderWorker.terminate()
        window.removeEventListener("resize", this._onresize);
        window.removeEventListener('fullscreenchange', this._onfullscreenchange);
        this._initCheckVariable();
        this._clearCheckLoading();
        this._off();
        this._hasLoaded = false;
        // remove dom
        while (this._container.firstChild) {
            this._container.removeChild(this._container.firstChild);
        }
        if (this._wakeLock) {
            this._wakeLock.release();
        }
    }

    /**
     * 清理画布为黑色背景
     * 用于canvas重用进行多个流切换播放时，将上一个画面清理
     * 避免后一个视频播放之前出现前一个视频最后一个画面
     */
    Jessibuca.prototype.clearView = function () {
        this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
    };
    /**
     * play
     * @param url
     */
    Jessibuca.prototype.play = function (url) {
        if (!this.playUrl && !url) {
            return;
        }
        var needDelay = false;
        if (url) {
            if (this.playUrl) {
                this._close();
                needDelay = true;
                this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
            }
            this.loading = true;
            _domToggle(this._doms.bgDom, false);
            this._checkLoading();
            this.playUrl = url;
        } else if (this.playUrl) {
            // retry
            if (this.loading) {
                this._hideBtns();
                _domToggle(this._doms.fullscreenDom, true);
                _domToggle(this._doms.pauseDom, true);
                _domToggle(this._doms.loadingDom, true);
                this._checkLoading();
            } else {
                this.playing = true;
            }
        }
        this._initCheckVariable();

        if (needDelay) {
            var _this = this;
            setTimeout(function () {
                _this._decoderWorker.postMessage({cmd: "play", url: _this.playUrl, isWebGL: _this.isWebGL()})
            }, 300);
        } else {
            this._decoderWorker.postMessage({cmd: "play", url: this.playUrl, isWebGL: this.isWebGL()})
        }
    };
    /**
     * has loaded
     * @returns {boolean}
     */
    Jessibuca.prototype.hasLoaded = function () {
        return this._hasLoaded;
    };

    Object.defineProperty(Jessibuca.prototype, "fullscreen", {
        set(value) {
            if (value) {
                if (!_checkFull()) {
                    this._container.requestFullscreen();
                }
                _domToggle(this._doms.minScreenDom, true);
                _domToggle(this._doms.fullscreenDom, false);
            } else {
                if (_checkFull()) {
                    document.exitFullscreen();
                }
                _domToggle(this._doms.minScreenDom, false);
                _domToggle(this._doms.fullscreenDom, true);
            }

            if (this._fullscreen !== value) {
                this.onFullscreen(value);
                this._trigger('fullscreen', value);
            }
            this._fullscreen = value;
        },
        get() {
            return this._fullscreen;
        }
    });

    Object.defineProperty(Jessibuca.prototype, 'playing', {
        set(value) {
            if (value) {
                _domToggle(this._doms.playBigDom, false);
                _domToggle(this._doms.playDom, false);
                _domToggle(this._doms.pauseDom, true);

                _domToggle(this._doms.screenshotsDom, true);
                _domToggle(this._doms.recordDom, true);
                if (this._quieting) {
                    _domToggle(this._doms.quietAudioDom, true);
                    _domToggle(this._doms.playAudioDom, false);
                } else {
                    _domToggle(this._doms.quietAudioDom, false);
                    _domToggle(this._doms.playAudioDom, true);
                }
            } else {
                this._doms.speedDom && (this._doms.speedDom.innerText = '');
                if (this.playUrl) {
                    _domToggle(this._doms.playDom, true);
                    _domToggle(this._doms.playBigDom, true);
                    _domToggle(this._doms.pauseDom, false);
                }

                // 在停止状态下录像，截屏，音量是非激活，只有播放,最大化时可点击
                _domToggle(this._doms.recordDom, false);
                _domToggle(this._doms.recordingDom, false);
                _domToggle(this._doms.screenshotsDom, false);
                _domToggle(this._doms.quietAudioDom, false);
                _domToggle(this._doms.playAudioDom, false);
            }

            if (this._playing !== value) {
                if (value) {
                    this.onPlay();
                    this._trigger('play');
                } else {
                    this.onPause();
                    this._trigger('pause');
                }
            }
            this._playing = value;
        },
        get() {
            return this._playing;
        }
    });

    Object.defineProperty(Jessibuca.prototype, 'recording', {
        set(value) {
            if (value) {
                _domToggle(this._doms.recordDom, false);
                _domToggle(this._doms.recordingDom, true);
            } else {
                _domToggle(this._doms.recordDom, true);
                _domToggle(this._doms.recordingDom, false);

            }
            if (this._recording !== value) {
                this.onRecord(value);
                this._trigger('record', value);
                this._recording = value;
            }
        },
        get() {
            return this._recording;
        }
    });

    Object.defineProperty(Jessibuca.prototype, 'quieting', {
        set(value) {
            if (value) {
                _domToggle(this._doms.quietAudioDom, true);
                _domToggle(this._doms.playAudioDom, false);
            } else {
                _domToggle(this._doms.quietAudioDom, false);
                _domToggle(this._doms.playAudioDom, true);
            }
            if (this._quieting !== value) {
                this.onMute(value);
                this._trigger('mute', value);
            }
            this._quieting = value;
        },
        get() {
            return this._quieting;
        }
    });

    Object.defineProperty(Jessibuca.prototype, 'loading', {
        set(value) {
            if (value) {
                this._hideBtns();
                _domToggle(this._doms.fullscreenDom, true);
                _domToggle(this._doms.pauseDom, true);
                _domToggle(this._doms.loadingDom, true);
            } else {
                this._initBtns();
            }
            this._loading = value;
        },
        get() {
            return this._loading;
        }
    });

    /**
     * resize
     */
    Jessibuca.prototype.resize = function () {
        var width = this._container.clientWidth;
        var height = this._container.clientHeight;
        if (this._showControl()) {
            height -= 38;
        }
        var resizeWidth = this._canvasElement.width;
        var resizeHeight = this._canvasElement.height;
        var wScale = width / resizeWidth;
        var hScale = height / resizeHeight;
        var scale = wScale > hScale ? hScale : wScale;
        if (!this._opt.isResize) {
            if (wScale !== hScale) {
                scale = wScale + ',' + hScale;
            }
        }
        //
        if (this._opt.isFullResize) {
            scale = wScale > hScale ? wScale : hScale;
        }

        this._opt.isDebug && console.log('wScale', wScale, 'hScale', hScale, 'scale', scale);
        this._canvasElement.style.transform = "scale(" + scale + ")"
        this._canvasElement.style.left = ((width - resizeWidth) / 2) + "px"
        this._canvasElement.style.top = ((height - resizeHeight) / 2) + "px"
    }

    Jessibuca.prototype._fullscreenchange = function () {
        this.fullscreen = _checkFull();
    }

    /**
     * change buffer
     * @param buffer
     */
    Jessibuca.prototype.changeBuffer = function (buffer) {
        this._stats.buf = Number(buffer) * 1000;
        this._decoderWorker.postMessage({cmd: "setVideoBuffer", time: Number(buffer)});
    };
    /**
     * 设置最大缓冲时长，单位秒，播放器会自动消除延迟。
     * @param buffer
     */
    Jessibuca.prototype.setBufferTime = function (buffer) {
        this.changeBuffer(buffer);
    };

    /**
     * 设置音量大小，取值0.0 — 1.0
     * 当为0.0时，完全无声
     * 当为1.0时，最大音量，默认值
     * @param volume
     */
    Jessibuca.prototype.setVolume = function (volume) {
        if (this._gainNode) {
            this._gainNode.gain.setValueAtTime(volume, this._audioContext.currentTime);
        }
    };

    /**
     * 开启屏幕常亮, 在play前调用
     * 在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
     * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
     * 其余平台为模拟实现，此时为兼容实现，并不保证所有浏览器都支持
     */
    Jessibuca.prototype.setKeepScreenOn = function () {
        this._opt.keepScreenOn = true;
    };


    /**
     * set fullscreen
     * @param flag
     */
    Jessibuca.prototype.setFullscreen = function (flag) {
        var fullscreen = !!flag;
        if (this.fullscreen !== fullscreen) {
            this.fullscreen = fullscreen;
        }
    };

    function _now() {
        return new Date().getTime();
    }

    Jessibuca.prototype._screenshot = function (filename, format, quality) {
        filename = filename || _now();
        var formatType = {
            png: 'image/png',
            jpeg: 'image/jpeg',
            webp: 'image/webp'
        };
        var encoderOptions = 0.92;

        if (typeof quality !== 'undefined') {
            encoderOptions = Number(quality);
        }

        var dataURL = this._canvasElement.toDataURL(formatType[format] || formatType.png, encoderOptions);
        _downloadImg(_dataURLToFile(dataURL), filename);
    }

    /**
     * 截图，调用后弹出下载框保存截图
     * @param filename  保存的文件名 默认时间戳
     * @param format 截图的格式，可选png或jpeg或者webp
     * @param quality 可选参数，当格式是jpeg或者webp时，压缩质量，取值0.0 ~ 1.0
     */
    Jessibuca.prototype.screenshot = function (filename, format, quality) {
        this._screenshot(filename, format, quality);
    };


    var eventSplitter = /\s+/;

    // Execute callbacks
    function _callEach(list, args, context) {
        if (list) {
            for (var i = 0, len = list.length; i < len; i += 1) {
                list[i].apply(context, args);
            }
        }
    }

    /**
     *
     * @param events
     * @param callback
     * @returns {Jessibuca}
     */
    Jessibuca.prototype.on = function (events, callback) {
        var cache, event, list;
        if (!callback) return this;
        cache = this.__events || (this.__events = {});
        events = events.split(eventSplitter);
        while (event = events.shift()) {
            list = cache[event] || (cache[event] = []);
            list.push(callback);
        }
        return this;
    };
    /**
     *
     * @param events
     * @param callback
     * @returns {Jessibuca}
     * @private
     */
    Jessibuca.prototype._off = function () {
        var cache;
        if (!(cache = this.__events)) return this;
        delete this.__events;
        return this;
    };

    /**
     *
     * @param events
     * @returns {Jessibuca}
     * @private
     */
    Jessibuca.prototype._trigger = function (events) {
        var cache, event, all, list, i, len, rest = [], args;
        if (!(cache = this.__events)) return this;
        events = events.split(eventSplitter);
        // Fill up `rest` with the callback arguments.  Since we're only copying
        // the tail of `arguments`, a loop is much faster than Array#slice.
        for (i = 1, len = arguments.length; i < len; i++) {
            rest[i - 1] = arguments[i];
        }
        // For each event, walk through the list of callbacks twice, first to
        // trigger the event, then to trigger any `"all"` callbacks.
        while (event = events.shift()) {
            if (list = cache[event]) list = list.slice();
            // Execute event callbacks.
            _callEach(list, rest, this);
        }
        return this;
    }

    if (typeof define === 'function') {
        define(function () {
            return Jessibuca;
        });
    } else if (typeof exports !== 'undefined') {
        module.exports = Jessibuca;
    } else {
        window.Jessibuca = Jessibuca;
    }
})();
