[
  {
    "path": ".eslintignore",
    "content": "dist/\nnode_modules/"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n        \"ecmaVersion\": 6,\n        \"sourceType\": \"module\",\n        \"ecmaFeatures\": {\n            \"impliedStrict\": true\n        }\n    },\n    \"env\": {\n        \"es6\": true,\n        \"worker\": true,\n        \"node\": true,\n        \"browser\": true\n    },\n    \"rules\": {\n        \"keyword-spacing\": [2, {\n            \"overrides\": {\n                \"if\": {\"after\": true},\n                \"for\": {\"after\": true},\n                \"while\": {\"after\": true},\n                \"switch\": {\"after\": true},\n                \"catch\": {\"after\": true}\n            }\n        }],\n        \"key-spacing\": [2, {\"beforeColon\": false, \"afterColon\": true, \"mode\": \"strict\"}],\n        \"arrow-spacing\": 2,\n        \"comma-spacing\": 2,\n        \"comma-style\": 2,\n        \"indent\": [2, 4, {\"SwitchCase\": 1}],\n        \"no-var\": 2,\n        \"no-bitwise\": 0,\n        \"no-alert\": 2,\n        \"no-console\": 0,\n        \"no-debugger\": 1,\n        \"no-unused-vars\": 0,\n        \"no-mixed-spaces-and-tabs\": 2,\n        \"quotes\": [2, \"single\", \"avoid-escape\"],\n        \"semi\": 2,\n        \"semi-spacing\": 2,\n        \"space-before-blocks\": 2,\n        \"space-before-function-paren\": [2, {\"anonymous\": \"always\", \"named\": \"never\"}],\n        \"space-in-parens\": [2, \"never\"],\n        \"space-infix-ops\": 2\n    }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "#################\n## Node.js\n#################\n\nnode_modules\nnpm-debug.log\n\n#################\n## Grunt\n#################\n\n.grunt\n_SpecRunner.html\nreports\njsdoc\ndist\nbuild/temp\ncoverage/\n\n#################\n## Eclipse\n#################\n\n*.pydevproject\n.project\n.metadata\nbin/\ntmp/\n.idea/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.classpath\n.settings/\n.loadpath\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# CDT-specific\n.cproject\n\n# PDT-specific\n.buildpath\n\n\n#################\n## Visual Studio\n#################\n\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.sln.docstates\n\n# Build results\n[Dd]ebug/\n[Rr]elease/\n*_i.c\n*_p.c\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.vspscc\n.builds\n*.dotCover\n\n## TODO: If you have NuGet Package Restore enabled, uncomment this\n#packages/\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opensdf\n*.sdf\n\n# Visual Studio profiler\n*.psess\n*.vsp\n\n# ReSharper is a .NET coding add-in\n_ReSharper*\n\n# Installshield output folder\n[Ee]xpress\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish\n\n# Others\n[Bb]in\n[Oo]bj\nsql\nTestResults\n*.Cache\nClientBin\nstylecop.*\n~$*\n*.dbmdl\nGenerated_Code #added for RIA/Silverlight projects\n\n# Backup & report files from converting an old project file to a newer\n# Visual Studio version. Backup files are not needed, because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\n\n\n\n############\n## Windows\n############\n\n# Windows image file caches\nThumbs.db\n\n# Folder config file\nDesktop.ini\n\n\n#############\n## Python\n#############\n\n*.py[co]\n\n# Packages\n*.egg\n*.egg-info\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\n\n#Translations\n*.mo\n\n#Mr Developer\n.mr.developer.cfg\n\n# Mac crap\n.DS_Store\n\n# Visual Studio Code\n.vscode/\nbrowse.VC.db\n"
  },
  {
    "path": ".npmignore",
    "content": "## This file is modified from .gitignore from same folder\n## Which allows dist folder and ignores unnecessary folders\n\ndemo\ndocs\n\n#################\n## Node.js\n#################\n\nnode_modules\nnpm-debug.log\n\n#################\n## Grunt\n#################\n\n.grunt\n_SpecRunner.html\nreports\njsdoc\nbuild/temp\ncoverage/\n\n#################\n## Eclipse\n#################\n\n*.pydevproject\n.project\n.metadata\nbin/\ntmp/\n.idea/\n*.tmp\n*.bak\n*.swp\n*~.nib\nlocal.properties\n.classpath\n.settings/\n.loadpath\n\n# External tool builders\n.externalToolBuilders/\n\n# Locally stored \"Eclipse launch configurations\"\n*.launch\n\n# CDT-specific\n.cproject\n\n# PDT-specific\n.buildpath\n\n\n#################\n## Visual Studio\n#################\n\n## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n\n# User-specific files\n*.suo\n*.user\n*.sln.docstates\n\n# Build results\n[Dd]ebug/\n[Rr]elease/\n*_i.c\n*_p.c\n*.ilk\n*.meta\n*.obj\n*.pch\n*.pdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.vspscc\n.builds\n*.dotCover\n\n## TODO: If you have NuGet Package Restore enabled, uncomment this\n#packages/\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opensdf\n*.sdf\n\n# Visual Studio profiler\n*.psess\n*.vsp\n\n# ReSharper is a .NET coding add-in\n_ReSharper*\n\n# Installshield output folder\n[Ee]xpress\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish\n\n# Others\n[Bb]in\n[Oo]bj\nsql\nTestResults\n*.Cache\nClientBin\nstylecop.*\n~$*\n*.dbmdl\nGenerated_Code #added for RIA/Silverlight projects\n\n# Backup & report files from converting an old project file to a newer\n# Visual Studio version. Backup files are not needed, because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\n\n\n\n############\n## Windows\n############\n\n# Windows image file caches\nThumbs.db\n\n# Folder config file\nDesktop.ini\n\n\n#############\n## Python\n#############\n\n*.py[co]\n\n# Packages\n*.egg\n*.egg-info\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\n\n#Translations\n*.mo\n\n#Mr Developer\n.mr.developer.cfg\n\n# Mac crap\n.DS_Store\n\n# Visual Studio Code\n.vscode/\nbrowse.VC.db\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "README.md",
    "content": "\nflv.js  [![npm](https://img.shields.io/npm/v/flv.js.svg?style=flat)](https://www.npmjs.com/package/flv.js)\n======\nAn HTML5 Flash Video (FLV) Player written in pure JavaScript without Flash. LONG LIVE FLV!\n\nThis project relies on [Media Source Extensions][] to work.\n\n**For FLV live stream playback, please consider [mpegts.js][] which is under active development.**\n\n**This project will become rarely maintained.**\n\n[mpegts.js]: https://github.com/xqq/mpegts.js\n## Overview\nflv.js works by transmuxing FLV file stream into ISO BMFF (Fragmented MP4) segments, followed by feeding mp4 segments into an HTML5 `<video>` element through [Media Source Extensions][] API.\n\n[Media Source Extensions]: https://w3c.github.io/media-source/\n\n## Demo\n[http://bilibili.github.io/flv.js/demo/](http://bilibili.github.io/flv.js/demo/)\n\n## Features\n- FLV container with H.264 + AAC / MP3 codec playback\n- Multipart segmented video playback\n- HTTP FLV low latency live stream playback\n- FLV over WebSocket live stream playback\n- Compatible with Chrome, FireFox, Safari 10, IE11 and Edge\n- Extremely low overhead, and hardware accelerated by your browser!\n\n## Installation\n```bash\nnpm install --save flv.js\n```\n\n## Build\n```bash\nnpm ci                 # install dependencies / dev-dependences\nnpm run build:debug    # debug version flv.js will be emitted to /dist\nnpm run build          # minimized release version flv.min.js will be emitted to /dist\n```\n\n[cnpm](https://github.com/cnpm/cnpm) mirror is recommended if you are in Mainland China.\n\n## CORS\nIf you use standalone video server for FLV stream, `Access-Control-Allow-Origin` header must be configured correctly on video server for cross-origin resource fetching.\n\nSee [cors.md](docs/cors.md) for more details.\n\n## Getting Started\n```html\n<script src=\"flv.min.js\"></script>\n<video id=\"videoElement\"></video>\n<script>\n    if (flvjs.isSupported()) {\n        var videoElement = document.getElementById('videoElement');\n        var flvPlayer = flvjs.createPlayer({\n            type: 'flv',\n            url: 'http://example.com/flv/video.flv'\n        });\n        flvPlayer.attachMediaElement(videoElement);\n        flvPlayer.load();\n        flvPlayer.play();\n    }\n</script>\n```\n\n## Limitations\n- MP3 audio codec is currently not working on IE11 / Edge\n- HTTP FLV live stream is not currently working on all browsers, see [livestream.md](docs/livestream.md)\n\n## Multipart playback\nYou only have to provide a playlist for `MediaDataSource`. See [multipart.md](docs/multipart.md)\n\n## Livestream playback\nSee [livestream.md](docs/livestream.md)\n\n## API and Configuration\nSee [api.md](docs/api.md)\n\n## Debug\n```bash\nnpm ci         # install dependencies / dev-dependences\nnpm run dev    # watch file changes and build debug version on the fly\n```\n\n## Design\nSee [design.md](docs/design.md)\n\n## License\n```\nCopyright (C) 2016 Bilibili. All Rights Reserved.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n```\n\n"
  },
  {
    "path": "d.ts/flv.d.ts",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// flv.js TypeScript definition file\n\ndeclare namespace FlvJs {\n    interface MediaSegment {\n        duration: number;\n        filesize?: number;\n        url: string;\n    }\n\n    interface MediaDataSource {\n        type: string;\n        isLive?: boolean;\n        cors?: boolean;\n        withCredentials?: boolean;\n\n        hasAudio?: boolean;\n        hasVideo?: boolean;\n\n        duration?: number;\n        filesize?: number;\n        url?: string;\n\n        segments?: MediaSegment[];\n    }\n\n    interface Config {\n        /**\n         * @desc Enable separated thread for transmuxing (unstable for now)\n         * @defaultvalue false\n         */\n        enableWorker?: boolean;\n        /**\n         * @desc Enable IO stash buffer. Set to false if you need realtime (minimal latency) for live stream\n         *          playback, but may stalled if there's network jittering.\n         * @defaultvalue true\n         */\n        enableStashBuffer?: boolean;\n        /**\n         * @desc Indicates IO stash buffer initial size. Default is `384KB`. Indicate a suitable size can\n         *          improve video load/seek time.\n         */\n        stashInitialSize?: number;\n\n        /**\n         * @desc Same to `isLive` in **MediaDataSource**, ignored if has been set in MediaDataSource structure.\n         * @defaultvalue false\n         */\n        isLive?: boolean;\n\n        /**\n         * @desc Abort the http connection if there's enough data for playback.\n         * @defaultvalue true\n         */\n        lazyLoad?: boolean;\n        /**\n         * @desc Indicates how many seconds of data to be kept for `lazyLoad`.\n         * @defaultvalue 3 * 60\n         */\n        lazyLoadMaxDuration?: number;\n        /**\n         * @desc Indicates the `lazyLoad` recover time boundary in seconds.\n         * @defaultvalue 30\n         */\n        lazyLoadRecoverDuration?: number;\n        /**\n         * @desc Do load after MediaSource `sourceopen` event triggered. On Chrome, tabs which\n         *          be opened in background may not trigger `sourceopen` event until switched to that tab.\n         * @defaultvalue true\n         */\n        deferLoadAfterSourceOpen?: boolean;\n\n        /**\n         * @desc Do auto cleanup for SourceBuffer\n         * @defaultvalue false (from docs)\n         */\n        autoCleanupSourceBuffer?: boolean;\n        /**\n         * @desc When backward buffer duration exceeded this value (in seconds), do auto cleanup for SourceBuffer\n         * @defaultvalue 3 * 60\n         */\n        autoCleanupMaxBackwardDuration?: number;\n        /**\n         * @desc Indicates the duration in seconds to reserve for backward buffer when doing auto cleanup.\n         * @defaultvalue 2 * 60\n         */\n        autoCleanupMinBackwardDuration?: number;\n\n        /**\n         * @defaultvalue 600\n         */\n        statisticsInfoReportInterval?: number;\n\n        /**\n         * @desc Fill silent audio frames to avoid a/v unsync when detect large audio timestamp gap.\n         * @defaultvalue true\n         */\n        fixAudioTimestampGap?: boolean;\n\n        /**\n         * @desc Accurate seek to any frame, not limited to video IDR frame, but may a bit slower.\n         *          Available on Chrome > 50, FireFox and Safari.\n         * @defaultvalue false\n         */\n        accurateSeek?: boolean;\n        /**\n         * @desc 'range' use range request to seek, or 'param' add params into url to indicate request range.\n         * @defaultvalue 'range'\n         */\n        seekType?: 'range' | 'param' | 'custom';\n        /**\n         * @desc Indicates seek start parameter name for seekType = 'param'\n         * @defaultvalue 'bstart'\n         */\n        seekParamStart?: string;\n        /**\n         * @desc Indicates seek end parameter name for seekType = 'param'\n         * @defaultvalue 'bend'\n         */\n        seekParamEnd?: string;\n        /**\n         * @desc Send Range: bytes=0- for first time load if use Range seek\n         * @defaultvalue false\n         */\n        rangeLoadZeroStart?: boolean;\n        /**\n         * @desc Indicates a custom seek handler\n         * @desc Should implement `SeekHandler` interface\n         */\n        customSeekHandler?: CustomSeekHandlerConstructor;\n        /**\n         * @desc Reuse 301/302 redirected url for subsequence request like seek, reconnect, etc.\n         * @defaultvalue false\n         */\n        reuseRedirectedURL?: boolean;\n        /**\n         * @desc Indicates the Referrer Policy when using FetchStreamLoader\n         * @defaultvalue 'no-referrer-when-downgrade' (from docs)\n         */\n        referrerPolicy?: ReferrerPolicy;\n        /**\n         * @desc Indicates additional headers that will be added to request\n         */\n        headers?: {\n            [k: string]: string\n        }\n        /**\n         * @desc Should implement `BaseLoader` interface\n         */\n        customLoader?: CustomLoaderConstructor;\n    }\n\n    interface CustomSeekHandlerConstructor {\n        new(): SeekHandler;\n    }\n\n    interface SeekHandler {\n        getConfig(sourceURL: string, range: Range): SeekConfig;\n        removeURLParameters(url: string): string;\n    }\n\n    interface SeekConfig {\n        url: string;\n        headers: Headers | object;\n    }\n\n    interface BaseLoaderConstructor {\n        new(typeName: string): BaseLoader;\n    }\n\n    interface BaseLoader {\n        _status: number;\n        _needStash: boolean;\n\n        destroy(): void;\n        isWorking(): boolean;\n        readonly type: string;\n        readonly status: number;\n        readonly needStashBuffer: boolean;\n        onContentLengthKnown: (contentLength: number) => void;\n        onURLRedirect: (redirectedURL: string) => void;\n        onDataArrival: (chunk: ArrayBuffer, byteStart: number, receivedLength?: number) => void;\n        onError: (errorType: LoaderErrors, errorInfo: LoaderErrorMessage) => void;\n        onComplete: (rangeFrom: number, rangeTo: number) => void;\n        open(dataSource: MediaSegment, range: Range): void;\n        abort(): void;\n    }\n\n    interface CustomLoaderConstructor {\n        new(seekHandler: SeekHandler, config: Config): BaseLoader;\n    }\n\n    interface Range {\n        from: number;\n        to: number;\n    }\n\n    interface LoaderStatus {\n        readonly kIdle: 0;\n        readonly kConnecting: 1;\n        readonly kBuffering: 2;\n        readonly kError: 3;\n        readonly kComplete: 4;\n    }\n\n    interface LoaderErrors {\n        readonly OK: 'OK';\n        readonly EXCEPTION: 'Exception';\n        readonly HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid';\n        readonly CONNECTING_TIMEOUT: 'ConnectingTimeout';\n        readonly EARLY_EOF: 'EarlyEof';\n        readonly UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof';\n    }\n\n    interface LoaderErrorMessage {\n        code: number;\n        msg: string;\n    }\n\n    interface FeatureList {\n        mseFlvPlayback: boolean;\n        mseLiveFlvPlayback: boolean;\n        networkStreamIO: boolean;\n        networkLoaderName: string;\n        nativeMP4H264Playback: boolean;\n        nativeWebmVP8Playback: boolean;\n        nativeWebmVP9Playback: boolean;\n    }\n\n    interface PlayerConstructor {\n        new (mediaDataSource: MediaDataSource, config?: Config): Player;\n    }\n\n    interface Player {\n        destroy(): void;\n        on(event: string, listener: (...args: any[]) => void): void;\n        off(event: string, listener: (...args: any[]) => void): void;\n        attachMediaElement(mediaElement: HTMLMediaElement): void;\n        detachMediaElement(): void;\n        load(): void;\n        unload(): void;\n        play(): Promise<void> | void;\n        pause(): void;\n        type: string;\n        buffered: TimeRanges;\n        duration: number;\n        volume: number;\n        muted: boolean;\n        currentTime: number;\n        /**\n         * @deprecated FlvPlayer/NativePlayer have its own `mediaInfo` field.\n         * @desc Keep it for backwards compatibility\n         * @since 1.4\n         */\n        mediaInfo: NativePlayerMediaInfo | FlvPlayerMediaInfo;\n        /**\n         * @deprecated FlvPlayer/NativePlayer have its own `statisticsInfo` field.\n         * @desc Keep it for backwards compatibility\n         * @since 1.4\n         */\n        statisticsInfo: NativePlayerStatisticsInfo | FlvPlayerStatisticsInfo;\n    }\n\n    interface NativePlayerStatisticsInfo {\n        playerType: 'NativePlayer';\n        url: string;\n        decodedFrames?: number;\n        droppedFrames?: number;\n    }\n\n    interface FlvPlayerReportStatisticsInfo {\n        url: string;\n        hasRedirect: boolean;\n        redirectedURL?: string;\n        speed: number; // KB/s\n        loaderType: string;\n        currentSegmentIndex: number;\n        totalSegmentCount: number;\n    }\n\n    interface FlvPlayerStatisticsInfo extends Partial<FlvPlayerReportStatisticsInfo> {\n        playerType: 'FlvPlayer';\n        decodedFrames?: number;\n        droppedFrames?: number;\n    }\n\n    interface NativePlayerMediaInfo {\n        mimeType: string;\n        duration?: number;\n        width?: number;\n        height?: number;\n    }\n\n    interface FlvPlayerMediaInfo extends NativePlayerMediaInfo {\n        audioCodec?: string;\n        videoCodec?: string;\n        audioDataRate?: number;\n        videoDataRate?: number;\n        hasAudio?: boolean;\n        hasVideo?: boolean;\n        chromaFormat?: string;\n        fps?: number;\n\n        [k: string]: any;\n    }\n\n    interface FlvPlayer extends Player {\n        mediaInfo: FlvPlayerMediaInfo;\n        statisticsInfo: FlvPlayerStatisticsInfo;\n    }\n\n    interface NativePlayer extends Player {\n        mediaInfo: NativePlayerMediaInfo;\n        statisticsInfo: NativePlayerStatisticsInfo;\n    }\n\n    interface LoggingControlConfig {\n        forceGlobalTag: boolean;\n        globalTag: string;\n        enableAll: boolean;\n        enableDebug: boolean;\n        enableVerbose: boolean;\n        enableInfo: boolean;\n        enableWarn: boolean;\n        enableError: boolean;\n    }\n\n    interface LoggingControl extends LoggingControlConfig {\n        getConfig(): LoggingControlConfig;\n        applyConfig(config: Partial<LoggingControlConfig>): void;\n        addLogListener(listener: (...args: any[]) => void): void;\n        removeLogListener(listener: (...args: any[]) => void): void;\n    }\n\n    interface Events {\n        ERROR: string;\n        LOADING_COMPLETE: string;\n        RECOVERED_EARLY_EOF: string;\n        MEDIA_INFO: string;\n        METADATA_ARRIVED: string;\n        SCRIPTDATA_ARRIVED: string;\n        STATISTICS_INFO: string;\n    }\n\n    interface ErrorTypes {\n        NETWORK_ERROR: string;\n        MEDIA_ERROR: string;\n        OTHER_ERROR: string;\n    }\n\n    interface ErrorDetails {\n        NETWORK_EXCEPTION: string;\n        NETWORK_STATUS_CODE_INVALID: string;\n        NETWORK_TIMEOUT: string;\n        NETWORK_UNRECOVERABLE_EARLY_EOF: string;\n\n        MEDIA_MSE_ERROR: string;\n\n        MEDIA_FORMAT_ERROR: string;\n        MEDIA_FORMAT_UNSUPPORTED: string;\n        MEDIA_CODEC_UNSUPPORTED: string;\n    }\n}\n\ndeclare var FlvJs: {\n    createPlayer(mediaDataSource: FlvJs.MediaDataSource, config?: FlvJs.Config): FlvJs.Player;\n    isSupported(): boolean;\n    getFeatureList(): FlvJs.FeatureList;\n\n    /**\n     * @deprecated Use `FlvJs.BaseLoaderConstructor` instead.\n     *              Because it's not available on `flvjs` variable.\n     * @desc implement interface `BaseLoader`\n     * @since 1.4\n     */\n    BaseLoader: FlvJs.BaseLoaderConstructor;\n    /**\n     * @deprecated Use `FlvJs.BaseLoaderConstructor` instead.\n     *              Because it's not available on `flvjs` variable.\n     * @since 1.4\n     */\n    LoaderStatus: FlvJs.LoaderStatus;\n    /**\n     * @deprecated Use `FlvJs.BaseLoaderConstructor` instead.\n     *              Because it's not available on `flvjs` variable.\n     * @since 1.4\n     */\n    LoaderErrors: FlvJs.LoaderErrors;\n\n    readonly version: string;\n\n    readonly Events: Readonly<FlvJs.Events>;\n    readonly ErrorTypes: Readonly<FlvJs.ErrorTypes>;\n    readonly ErrorDetails: Readonly<FlvJs.ErrorDetails>;\n\n    readonly FlvPlayer: FlvJs.PlayerConstructor;\n    readonly NativePlayer: FlvJs.PlayerConstructor;\n    readonly LoggingControl: FlvJs.LoggingControl;\n};\n\nexport default FlvJs;\n"
  },
  {
    "path": "demo/demo.css",
    "content": ".mainContainer {\n    display: block;\n    width: 100%;\n    margin-left: auto;\n    margin-right: auto;\n}\n@media screen and (min-width: 1152px) {\n    .mainContainer {\n        display: block;\n        width: 1152px;\n        margin-left: auto;\n        margin-right: auto;\n    }\n}\n\n.video-container {\n    position: relative;\n    margin-top: 8px;\n}\n\n.video-container:before {\n    display: block;\n    content: \"\";\n    width: 100%;\n    padding-bottom: 56.25%;\n}\n\n.video-container > div {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n}\n\n.video-container video {\n    width: 100%;\n    height: 100%;\n}\n\n.urlInput {\n    display: block;\n    width: 100%;\n    margin-left: auto;\n    margin-right: auto;\n    margin-top: 8px;\n    margin-bottom: 8px;\n}\n\n.centeredVideo {\n    display: block;\n    width: 100%;\n    height: 100%;\n    margin-left: auto;\n    margin-right: auto;\n    margin-bottom: auto;\n}\n\n.controls {\n    display: block;\n    width: 100%;\n    text-align: left;\n    margin-left: auto;\n    margin-right: auto;\n    margin-top: 8px;\n    margin-bottom: 10px;\n}\n\n.logcatBox {\n    border-color: #CCCCCC;\n    font-size: 11px;\n    font-family: Menlo, Consolas, monospace;\n    display: block;\n    width: 100%;\n    text-align: left;\n    margin-left: auto;\n    margin-right: auto;\n}\n\n.url-input , .options {\n    font-size: 13px;\n}\n\n.url-input {\n    display: flex;\n}\n\n.url-input label {\n    flex: initial;\n}\n\n.url-input input {\n    flex: auto;\n    margin-left: 8px;\n}\n\n.url-input button {\n    flex: initial;\n    margin-left: 8px;\n}\n\n.options {\n    margin-top: 5px;\n}\n\n.hidden {\n    display: none;\n}\n"
  },
  {
    "path": "demo/index.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n    <meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n    <title>flv.js demo</title>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"demo.css\" />\n</head>\n\n<body>\n    \n    <div class=\"mainContainer\">\n        <div>\n            <div id=\"streamURL\">\n                <div class=\"url-input\">\n                    <label for=\"sURL\">Stream URL:</label>\n                    <input id=\"sURL\" type=\"text\" value=\"http://127.0.0.1/flv/7182741-1.flv\" />\n                    <button onclick=\"switch_mds()\">Switch to MediaDataSource</button>\n                </div>\n                <div class=\"options\">\n                    <input type=\"checkbox\" id=\"isLive\" onchange=\"saveSettings()\" />\n                    <label for=\"isLive\">isLive</label>\n                    <input type=\"checkbox\" id=\"withCredentials\" onchange=\"saveSettings()\" />\n                    <label for=\"withCredentials\">withCredentials</label>\n                    <input type=\"checkbox\" id=\"hasAudio\" onchange=\"saveSettings()\" checked />\n                    <label for=\"hasAudio\">hasAudio</label>\n                    <input type=\"checkbox\" id=\"hasVideo\" onchange=\"saveSettings()\" checked />\n                    <label for=\"hasVideo\">hasVideo</label>\n                </div>\n            </div>\n            <div id=\"mediaSourceURL\" class=\"hidden\">\n                <div class=\"url-input\">\n                    <label for=\"msURL\">MediaDataSource JsonURL:</label>\n                    <input id=\"msURL\" type=\"text\" value=\"http://127.0.0.1/flv/7182741.json\" />\n                    <button onclick=\"switch_url()\">Switch to URL</button>\n                </div>\n            </div>\n        </div>\n        <div class=\"video-container\">\n            <div>\n                <video name=\"videoElement\" class=\"centeredVideo\" controls autoplay>\n                    Your browser is too old which doesn't support HTML5 video.\n                </video>\n            </div>\n        </div>\n        <div class=\"controls\">\n            <button onclick=\"flv_load()\">Load</button>\n            <button onclick=\"flv_start()\">Start</button>\n            <button onclick=\"flv_pause()\">Pause</button>\n            <button onclick=\"flv_destroy()\">Destroy</button>\n            <input style=\"width:100px\" type=\"text\" name=\"seekpoint\"/>\n            <button onclick=\"flv_seekto()\">SeekTo</button>\n        </div>\n        <textarea name=\"logcatbox\" class=\"logcatBox\" rows=\"10\" readonly></textarea>\n    </div>\n\n    <script src=\"../dist/flv.js\"></script>\n    \n    <script>\n        var checkBoxFields = ['isLive', 'withCredentials', 'hasAudio', 'hasVideo'];\n        var streamURL, mediaSourceURL;\n\n        function flv_load() {\n            console.log('isSupported: ' + flvjs.isSupported());\n            if (mediaSourceURL.className === '') {\n                var url = document.getElementById('msURL').value;\n    \n                var xhr = new XMLHttpRequest();\n                xhr.open('GET', url, true);\n                xhr.onload = function (e) {\n                    var mediaDataSource = JSON.parse(xhr.response);\n                    flv_load_mds(mediaDataSource);\n                }\n                xhr.send();\n            } else {\n                var i;\n                var mediaDataSource = {\n                    type: 'flv'\n                };\n                for (i = 0; i < checkBoxFields.length; i++) {\n                    var field = checkBoxFields[i];\n                    /** @type {HTMLInputElement} */\n                    var checkbox = document.getElementById(field);\n                    mediaDataSource[field] = checkbox.checked;\n                }\n                mediaDataSource['url'] = document.getElementById('sURL').value;\n                console.log('MediaDataSource', mediaDataSource);\n                flv_load_mds(mediaDataSource);\n            }\n        }\n\n        function flv_load_mds(mediaDataSource) {\n            var element = document.getElementsByName('videoElement')[0];\n            if (typeof player !== \"undefined\") {\n                if (player != null) {\n                    player.unload();\n                    player.detachMediaElement();\n                    player.destroy();\n                    player = null;\n                }\n            }\n            player = flvjs.createPlayer(mediaDataSource, {\n                enableWorker: false,\n                lazyLoadMaxDuration: 3 * 60,\n                seekType: 'range',\n            });\n            player.attachMediaElement(element);\n            player.load();\n        }\n\n        function flv_start() {\n            player.play();\n        }\n\n        function flv_pause() {\n            player.pause();\n        }\n\n        function flv_destroy() {\n            player.pause();\n            player.unload();\n            player.detachMediaElement();\n            player.destroy();\n            player = null;\n        }\n\n        function flv_seekto() {\n            var input = document.getElementsByName('seekpoint')[0];\n            player.currentTime = parseFloat(input.value);\n        }\n\n        function switch_url() {\n            streamURL.className = '';\n            mediaSourceURL.className = 'hidden';\n            saveSettings();\n        }\n\n        function switch_mds() {\n            streamURL.className = 'hidden';\n            mediaSourceURL.className = '';\n            saveSettings();\n        }\n\n        function ls_get(key, def) {\n            try {\n                var ret = localStorage.getItem('flvjs_demo.' + key);\n                if (ret === null) {\n                    ret = def;\n                }\n                return ret;\n            } catch (e) {}\n            return def;\n        }\n\n        function ls_set(key, value) {\n            try {\n                localStorage.setItem('flvjs_demo.' + key, value);\n            } catch (e) {}\n        }\n\n        function saveSettings() {\n            if (mediaSourceURL.className === '') {\n                ls_set('inputMode', 'MediaDataSource');\n            } else {\n                ls_set('inputMode', 'StreamURL');\n            }\n            var i;\n            for (i = 0; i < checkBoxFields.length; i++) {\n                var field = checkBoxFields[i];\n                /** @type {HTMLInputElement} */\n                var checkbox = document.getElementById(field);\n                ls_set(field, checkbox.checked ? '1' : '0');\n            }\n            var msURL = document.getElementById('msURL');\n            var sURL = document.getElementById('sURL');\n            ls_set('msURL', msURL.value);\n            ls_set('sURL', sURL.value);\n            console.log('save');\n        }\n\n        function loadSettings() {\n            var i;\n            for (i = 0; i < checkBoxFields.length; i++) {\n                var field = checkBoxFields[i];\n                /** @type {HTMLInputElement} */\n                var checkbox = document.getElementById(field);\n                var c = ls_get(field, checkbox.checked ? '1' : '0');\n                checkbox.checked = c === '1' ? true : false;\n            }\n\n            var msURL = document.getElementById('msURL');\n            var sURL = document.getElementById('sURL');\n            msURL.value = ls_get('msURL', msURL.value);\n            sURL.value = ls_get('sURL', sURL.value);\n            if (ls_get('inputMode', 'StreamURL') === 'StreamURL') {\n                switch_url();\n            } else {\n                switch_mds();\n            }\n        }\n\n        function showVersion() {\n            var version = flvjs.version;\n            document.title = document.title + \" (v\" + version + \")\";\n        }\n\n        var logcatbox = document.getElementsByName('logcatbox')[0];\n        flvjs.LoggingControl.addLogListener(function(type, str) {\n            logcatbox.value = logcatbox.value + str + '\\n';\n            logcatbox.scrollTop = logcatbox.scrollHeight;\n        });\n\n        document.addEventListener('DOMContentLoaded', function () {\n            streamURL = document.getElementById('streamURL');\n            mediaSourceURL = document.getElementById('mediaSourceURL');\n            loadSettings();\n            showVersion();\n            flv_load();\n        });\n    </script>\n    \n</body>\n\n</html>"
  },
  {
    "path": "docs/api.md",
    "content": "\nflv.js API\n==========\nThis document use TypeScript-like definitions to describe interfaces.\n\n## Interfaces\n\nflv.js exports all the interfaces through `flvjs` object which exposed in global context `window`.\n\n`flvjs` object can also be accessed by require or ES6 import.\n\n\nFunctions:\n- [flvjs.createPlayer()](#flvjscreateplayer)\n- [flvjs.isSupported()](#flvjsissupported)\n- [flvjs.getFeatureList()](#flvjsgetfeaturelist)\n\nClasses:\n- [flvjs.FlvPlayer](#flvjsflvplayer)\n- [flvjs.NativePlayer](#flvjsnativeplayer)\n- [flvjs.LoggingControl](#flvjsloggingcontrol)\n\nEnums:\n- [flvjs.Events](#flvjsevents)\n- [flvjs.ErrorTypes](#flvjserrortypes)\n- [flvjs.ErrorDetails](#flvjserrordetails)\n\n\n\n\n### flvjs.createPlayer()\n```js\nfunction createPlayer(mediaDataSource: MediaDataSource, config?: Config): Player;\n```\n\nCreate a player instance according to `type` field indicated in `mediaDataSource`, with optional `config`.\n\n\n### MediaDataSource\n\n| Field              | Type                  | Description                              |\n| ------------------ | --------------------- | ---------------------------------------- |\n| `type`             | `string`              | Indicates media type, `'flv'` or `'mp4'` |\n| `isLive?`          | `boolean`             | Indicates whether the data source is a **live stream** |\n| `cors?`            | `boolean`             | Indicates whether to enable CORS for http fetching |\n| `withCredentials?` | `boolean`             | Indicates whether to do http fetching with cookies |\n| `hasAudio?`        | `boolean`             | Indicates whether the stream has audio track |\n| `hasVideo?`        | `boolean`             | Indicates whether the stream has video track |\n| `duration?`        | `number`              | Indicates total media duration, in **milliseconds** |\n| `filesize?`        | `number`              | Indicates total file size of media file, in bytes |\n| `url?`             | `string`              | Indicates media URL, can be starts with `'https(s)'` or `'ws(s)'` (WebSocket) |\n| `segments?`        | `Array<MediaSegment>` | Optional field for multipart playback, see **MediaSegment** |\n\nIf `segments` field exists, transmuxer will treat this `MediaDataSource` as a **multipart** source.\n\nIn multipart mode, `duration` `filesize` `url` field in `MediaDataSource` structure will be ignored.\n\n### MediaSegment\n\n| Field       | Type     | Description                              |\n| ----------- | -------- | ---------------------------------------- |\n| `duration`  | `number` | Required field, indicates segment duration in **milliseconds** |\n| `filesize?` | `number` | Optional field, indicates segment file size in bytes |\n| `url`       | `string` | Required field, indicates segment file URL |\n\n\n### Config\n\n| Field                            | Type      | Default                      | Description                              |\n| -------------------------------- | --------- | ---------------------------- | ---------------------------------------- |\n| `enableWorker?`                  | `boolean` | `false`                      | Enable separated thread for transmuxing (unstable for now) |\n| `enableStashBuffer?`             | `boolean` | `true`                       | Enable IO stash buffer. Set to false if you need realtime (minimal latency) for live stream playback, but may stalled if there's network jittering. |\n| `stashInitialSize?`              | `number`  | `384KB`                      | Indicates IO stash buffer initial size. Default is `384KB`. Indicate a suitable size can improve video load/seek time. |\n| `isLive?`                        | `boolean` | `false`                      | Same to `isLive` in **MediaDataSource**, ignored if has been set in MediaDataSource structure. |\n| `lazyLoad?`                      | `boolean` | `true`                       | Abort the http connection if there's enough data for playback. |\n| `lazyLoadMaxDuration?`           | `number`  | `3 * 60`                     | Indicates how many seconds of data to be kept for `lazyLoad`. |\n| `lazyLoadRecoverDuration?`       | `number`  | `30`                         | Indicates the `lazyLoad` recover time boundary in seconds. |\n| `deferLoadAfterSourceOpen?`      | `boolean` | `true`                       | Do load after MediaSource `sourceopen` event triggered. On Chrome, tabs which be opened in background may not trigger `sourceopen` event until switched to that tab. |\n| `autoCleanupSourceBuffer`        | `boolean` | `false`                      | Do auto cleanup for SourceBuffer         |\n| `autoCleanupMaxBackwardDuration` | `number`  | `3 * 60`                     | When backward buffer duration exceeded this value (in seconds), do auto cleanup for SourceBuffer |\n| `autoCleanupMinBackwardDuration` | `number`  | `2 * 60`                     | Indicates the duration in seconds to reserve for backward buffer when doing auto cleanup. |\n| `fixAudioTimestampGap`           | `boolean` | `true`                       | Fill silent audio frames to avoid a/v unsync when detect large audio timestamp gap. |\n| `accurateSeek?`                  | `boolean` | `false`                      | Accurate seek to any frame, not limited to video IDR frame, but may a bit slower. Available on `Chrome > 50`, `FireFox` and `Safari`. |\n| `seekType?`                      | `string`  | `'range'`                    | `'range'` use range request to seek, or `'param'` add params into url to indicate request range. |\n| `seekParamStart?`                | `string`  | `'bstart'`                   | Indicates seek start parameter name for `seekType = 'param'` |\n| `seekParamEnd?`                  | `string`  | `'bend'`                     | Indicates seek end parameter name for `seekType = 'param'` |\n| `rangeLoadZeroStart?`            | `boolean` | `false`                      | Send `Range: bytes=0-` for first time load if use Range seek |\n| `customSeekHandler?`             | `object`  | `undefined`                  | Indicates a custom seek handler          |\n| `reuseRedirectedURL?`            | `boolean` | `false`                      | Reuse 301/302 redirected url for subsequence request like seek, reconnect, etc. |\n| `referrerPolicy?`                | `string`  | `no-referrer-when-downgrade` | Indicates the [Referrer Policy][] when using FetchStreamLoader |\n| `headers?`                       | `object`  | `undefined`                  | Indicates additional headers that will be added to request |\n\n\n[Referrer Policy]: https://w3c.github.io/webappsec-referrer-policy/#referrer-policy\n\n### flvjs.isSupported()\n```js\nfunction isSupported(): boolean;\n```\nReturn `true` if basic playback can works on your browser.\n\n\n\n### flvjs.getFeatureList()\n```js\nfunction getFeatureList(): FeatureList;\n```\nReturn a `FeatureList` object which has following details:\n#### FeatureList\n| Field                   | Type      | Description                              |\n| ----------------------- | --------- | ---------------------------------------- |\n| `mseFlvPlayback`        | `boolean` | Same to `flvjs.isSupported()`, indicates whether basic playback works on your browser. |\n| `mseLiveFlvPlayback`    | `boolean` | Indicates whether HTTP FLV live stream can works on your browser. |\n| `networkStreamIO`       | `boolean` | Indicates whether the network loader is streaming. |\n| `networkLoaderName`     | `string`  | Indicates the network loader type name.  |\n| `nativeMP4H264Playback` | `boolean` | Indicates whether your browser support H.264 MP4 video file natively. |\n| `nativeWebmVP8Playback` | `boolean` | Indicates whether your browser support WebM VP8 video file natively. |\n| `nativeWebmVP9Playback` | `boolean` | Indicates whether your browser support WebM VP9 video file natively. |\n\n\n\n### flvjs.FlvPlayer\n```typescript\ninterface FlvPlayer extends Player {}\n```\n\nFLV player which implements the `Player` interface. Can be created by `new` operator directly.\n\n### flvjs.NativePlayer\n\n```typescript\ninterface NativePlayer extends Player {}\n```\n\nPlayer wrapper for browser's native player (HTMLVideoElement) without MediaSource src, which implements the `Player` interface. Useful for singlepart **MP4** file playback.\n\n### interface Player (abstract)\n\n```typescript\ninterface Player {\n    constructor(mediaDataSource: MediaDataSource, config?: Config): Player;\n    destroy(): void;\n    on(event: string, listener: Function): void;\n    off(event: string, listener: Function): void;\n    attachMediaElement(mediaElement: HTMLMediaElement): void;\n    detachMediaElement(): void;\n    load(): void;\n    unload(): void;\n    play(): Promise<void>;\n    pause(): void;\n    type: string;\n    buffered: TimeRanges;\n    duration: number;\n    volume: number;\n    muted: boolean;\n    currentTime: number;\n    mediaInfo: Object;\n    statisticsInfo: Object;\n}\n```\n\n### flvjs.LoggingControl\n\nA global interface which include several static getter/setter to set flv.js logcat verbose level.\n\n```typescript\ninterface LoggingControl {\n    forceGlobalTag: boolean;\n    globalTag: string;\n    enableAll: boolean;\n    enableDebug: boolean;\n    enableVerbose: boolean;\n    enableInfo: boolean;\n    enableWarn: boolean;\n    enableError: boolean;\n    getConfig(): Object;\n    applyConfig(config: Object): void;\n    addLogListener(listener: Function): void;\n    removeLogListener(listener: Function): void;\n}\n```\n\n### flvjs.Events\n\nA series of constants that can be used with `Player.on()` / `Player.off()`. They require the prefix `flvjs.Events`.\n\n| Event               | Description                              |\n| ------------------- | ---------------------------------------- |\n| ERROR               | An error occurred by any cause during the playback |\n| LOADING_COMPLETE    | The input MediaDataSource has been completely buffered to end |\n| RECOVERED_EARLY_EOF | An unexpected network EOF occurred during buffering but automatically recovered |\n| MEDIA_INFO          | Provides technical information of the media like video/audio codec, bitrate, etc. |\n| METADATA_ARRIVED    | Provides metadata which FLV file(stream) can contain with an \"onMetaData\" marker.  |\n| SCRIPTDATA_ARRIVED  | Provides scriptdata (OnCuePoint / OnTextData) which FLV file(stream) can contain. |\n| STATISTICS_INFO     | Provides playback statistics information like dropped frames, current speed, etc. |\n\n### flvjs.ErrorTypes\n\nThe possible errors that can come up during playback. They require the prefix `flvjs.ErrorTypes`.\n\n| Error         | Description                              |\n| ------------- | ---------------------------------------- |\n| NETWORK_ERROR | Errors related to the network            |\n| MEDIA_ERROR   | Errors related to the media (format error, decode issue, etc) |\n| OTHER_ERROR   | Any other unspecified error              |\n\n\n### flvjs.ErrorDetails\n\nProvide more verbose explanation for Network and Media errors. They require the prefix `flvjs.ErrorDetails`.\n\n| Error                           | Description                              |\n| ------------------------------- | ---------------------------------------- |\n| NETWORK_EXCEPTION               | Related to any other issues with the network; contains a `message` |\n| NETWORK_STATUS_CODE_INVALID     | Related to an invalid HTTP status code, such as 403, 404, etc. |\n| NETWORK_TIMEOUT                 | Related to timeout request issues        |\n| NETWORK_UNRECOVERABLE_EARLY_EOF | Related to unexpected network EOF which cannot be recovered |\n| MEDIA_MSE_ERROR                 | Related to MediaSource's error such as decode issue |\n| MEDIA_FORMAT_ERROR              | Related to any invalid parameters in the media stream |\n| MEDIA_FORMAT_UNSUPPORTED        | The input MediaDataSource format is not supported by flv.js |\n| MEDIA_CODEC_UNSUPPORTED         | The media stream contains video/audio codec which is not supported |\n"
  },
  {
    "path": "docs/cors.md",
    "content": "\nCORS Configuration\n==================\nAnytime you want to play an FLV stream from another `Origin`, the server must response with a CORS header:\n\n```\nAccess-Control-Allow-Origin: <your-origin> | *\n```\n\nFor example, if an html on your site `http://flvplayback.com` want's to play an FLV from another `Origin` like `http://cdn.flvplayback.com`, the video server must response with the following CORS header:\n\n```\nAccess-Control-Allow-Origin: http://flvplayback.com\n```\n\nOr a wildcard value `*` to allow any request origin:\n\n```\nAccess-Control-Allow-Origin: *\n```\n\n## Static FLV file playback\nFor static FLV file playback, we recommend you to add:\n\n```\nAccess-Control-Expose-Headers: Content-Length\n```\n\nOr you should provide accurate filesize in **MediaDataSource** object.\n\n## CORS with 301/302 redirect\nIf your video server response with a 3xx redirection, the redirection's response headers **must** contains `Access-Control-Allow-Origin`;\n\nObviously the redirect target server should also response with CORS headers, but pay attention that the browser will send `Origin: null` in redirected request according to current CORS policy.\n\nIt means that your actual edge server should response with:\n\n```\nAccess-Control-Allow-Origin: null | *\n```\n\nOr you can determine by request header `Origin` dynamically.\n\n## Preflight OPTIONS for Range seek\nWhen use Range seek for cross-origin FLV file, `Range` header added by flv.js will cause a [Preflight OPTIONS][] request by the browser.\n\nThe browser will send an `OPTIONS` request before actual `GET` request, with following additional headers according to CORS policy:\n\n```\nAccess-Control-Request-Headers: range\nAccess-Control-Request-Method: GET\n```\n\nThis means your video server must response to OPTIONS request with following additional CORS headers:\n\n```\nAccess-Control-Allow-Origin: <your-origin> | *\nAccess-Control-Allow-Methods: GET, OPTIONS\nAccess-Control-Allow-Headers: range\n```\n\n[Preflight OPTIONS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests\n\n## Reference\nWe strongly advise you to read [HTTP access control (CORS)][] and [CORS spec][] document carefully.\n\n[HTTP access control (CORS)]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS\n[CORS spec]: https://www.w3.org/TR/cors/\n"
  },
  {
    "path": "docs/design.md",
    "content": "\nflv.js design\n======\n\nArchitecture overview:\n\n![arch](architecture.png)\n"
  },
  {
    "path": "docs/livestream.md",
    "content": "\nLivestream playback\n===================\nYou need to provide a livestream URL in `MediaDataSource` and indicates `isLive: true`.\n\nSample HTTP FLV source:\n\n```js\n{\n    // HTTP FLV\n    \"type\": \"flv\",\n    \"isLive\": true,\n    \"url\": \"http://127.0.0.1:8080/live/livestream.flv\"\n}\n```\n\nOr a WebSocket source:\n\n```js\n{\n    // FLV over WebSocket\n    \"type\": \"flv\",\n    \"isLive\": true,\n    \"url\": \"ws://127.0.0.1:9090/live/livestream.flv\"\n}\n```\n\n## HTTP FLV live stream\n\n### CORS\nYou must configure `Access-Control-Allow-Origin` header correctly on your stream server.\n\nSee [cors.md](../docs/cors.md) for details.\n\n### Compatibility\nDue to IO restrictions, flv.js can support HTTP FLV live stream on `Chrome 43+`, `FireFox 42+`, `Edge 15.15048+` and `Safari 10.1+` for now.\n\nHTTP FLV live stream relies on stream IO, which has been introduced in [fetch][] and [stream][] spec. Now `FetchStreamLoader` works well on most of the modern browsers:\n\n- Chrome: `FetchStreamLoader` works well on Chrome 43+\n- FireFox: FireFox has `fetch` support but `stream` is missing, `moz-chunked-arraybuffer` xhr extension is used\n- Edge: `fetch + stream` is broken on old version of Microsoft Edge, see [Fetch API with ReadableStream has bug with data pumping][]. Got fixed in Creator Update (RS2).\n- Safari: `FetchStreamLoader` works well since Safari 10.1 (macOS 10.12.4)\n\n[fetch]: https://fetch.spec.whatwg.org/\n[stream]: https://streams.spec.whatwg.org/\n[Fetch API with ReadableStream has bug with data pumping]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/\n[Safari Technology Preview]: https://developer.apple.com/safari/technology-preview/\n"
  },
  {
    "path": "docs/multipart.md",
    "content": "\nMultipart playback\n==================\nWhen you create FlvPlayer instance, the `MediaDataSource` structure is passing through the constructor.\n\nYou need to provide a playlist for `MediaDataSource` in following format:\n\n```js\n{\n    // Required\n    \"type\": \"flv\",  // Only flv type supports multipart playback\n\n    // Optional\n    \"duration\": 12345678,  // total duration, in milliseconds\n    \"cors\": true,\n    \"withCredentials\": false,\n\n    // Optional\n    // true by default, do not indicate unless you have to deal with audio-only or video-only stream\n    \"hasAudio\": true,\n    \"hasVideo\": true,\n\n    // Required\n    \"segments\": [\n        {\n            \"duration\": 1234,  // in milliseconds\n            \"filesize\": 5678,  // in bytes\n            \"url\": \"http://cdn.flvplayback.com/segments-1.flv\"\n        },\n        {\n            \"duration\": 2345,\n            \"filesize\": 6789,\n            \"url\": \"http://cdn.flvplayback.com/segments-2.flv\"\n        },\n        {\n            \"duration\": 4567,\n            \"filesize\": 7890,\n            \"url\": \"http://cdn.flvplayback.com/segments-3.flv\"\n        }\n        // more segments...\n    ]\n}\n```\n\nYou must provide **accurate** duration for each segment.\n\n## Sample input\n```json\n{\n    \"type\": \"flv\",\n    \"duration\": 1373161,\n    \"segments\": [\n        {\n            \"duration\": 333438,\n            \"filesize\": 60369190,\n            \"url\": \"http://127.0.0.1/flv/7182741-1.flv\"\n        },{\n            \"duration\": 390828,\n            \"filesize\": 75726439,\n            \"url\": \"http://127.0.0.1/flv/7182741-2.flv\"\n        },{\n            \"duration\": 434453,\n            \"filesize\": 103453988,\n            \"url\": \"http://127.0.0.1/flv/7182741-3.flv\"\n        },{\n            \"duration\": 214442,\n            \"filesize\": 44189200,\n            \"url\": \"http://127.0.0.1/flv/7182741-4.flv\"\n        }\n    ]\n}\n```\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"flv.js\",\n  \"version\": \"1.6.2\",\n  \"description\": \"HTML5 FLV Player\",\n  \"main\": \"./dist/flv.js\",\n  \"types\": \"./d.ts/flv.d.ts\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/Bilibili/flv.js\"\n  },\n  \"keywords\": [\n    \"html5\",\n    \"flv\",\n    \"mse\",\n    \"javascript\"\n  ],\n  \"scripts\": {\n    \"build\": \"webpack --mode=production --progress\",\n    \"build:debug\": \"webpack --mode=development --progress\",\n    \"dev\": \"webpack --mode=development --progress --watch\",\n    \"dtslint\": \"dtslint types\"\n  },\n  \"dependencies\": {\n    \"es6-promise\": \"^4.2.8\",\n    \"webworkify-webpack\": \"^2.1.5\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^16.3.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^4.28.3\",\n    \"@typescript-eslint/parser\": \"^4.28.3\",\n    \"browser-sync\": \"^2.27.4\",\n    \"eslint\": \"^7.30.0\",\n    \"exports-loader\": \"^3.0.0\",\n    \"source-map-loader\": \"^3.0.0\",\n    \"terser-webpack-plugin\": \"^5.1.4\",\n    \"ts-loader\": \"^9.2.3\",\n    \"typescript\": \"^4.3.5\",\n    \"webpack\": \"^5.45.1\",\n    \"webpack-cli\": \"^4.7.2\"\n  },\n  \"author\": \"zheng qian <xqq@xqq.im>\",\n  \"license\": \"Apache-2.0\"\n}\n"
  },
  {
    "path": "src/config.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport const defaultConfig = {\n    enableWorker: false,\n    enableStashBuffer: true,\n    stashInitialSize: undefined,\n\n    isLive: false,\n\n    lazyLoad: true,\n    lazyLoadMaxDuration: 3 * 60,\n    lazyLoadRecoverDuration: 30,\n    deferLoadAfterSourceOpen: true,\n\n    // autoCleanupSourceBuffer: default as false, leave unspecified\n    autoCleanupMaxBackwardDuration: 3 * 60,\n    autoCleanupMinBackwardDuration: 2 * 60,\n\n    statisticsInfoReportInterval: 600,\n\n    fixAudioTimestampGap: true,\n\n    accurateSeek: false,\n    seekType: 'range',  // [range, param, custom]\n    seekParamStart: 'bstart',\n    seekParamEnd: 'bend',\n    rangeLoadZeroStart: false,\n    customSeekHandler: undefined,\n    reuseRedirectedURL: false,\n    // referrerPolicy: leave as unspecified\n\n    headers: undefined,\n    customLoader: undefined\n};\n\nexport function createDefaultConfig() {\n    return Object.assign({}, defaultConfig);\n}"
  },
  {
    "path": "src/core/features.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport IOController from '../io/io-controller.js';\nimport {createDefaultConfig} from '../config.js';\n\nclass Features {\n\n    static supportMSEH264Playback() {\n        return window.MediaSource &&\n               window.MediaSource.isTypeSupported('video/mp4; codecs=\"avc1.42E01E,mp4a.40.2\"');\n    }\n\n    static supportNetworkStreamIO() {\n        let ioctl = new IOController({}, createDefaultConfig());\n        let loaderType = ioctl.loaderType;\n        ioctl.destroy();\n        return loaderType == 'fetch-stream-loader' || loaderType == 'xhr-moz-chunked-loader';\n    }\n\n    static getNetworkLoaderTypeName() {\n        let ioctl = new IOController({}, createDefaultConfig());\n        let loaderType = ioctl.loaderType;\n        ioctl.destroy();\n        return loaderType;\n    }\n\n    static supportNativeMediaPlayback(mimeType) {\n        if (Features.videoElement == undefined) {\n            Features.videoElement = window.document.createElement('video');\n        }\n        let canPlay = Features.videoElement.canPlayType(mimeType);\n        return canPlay === 'probably' || canPlay == 'maybe';\n    }\n\n    static getFeatureList() {\n        let features = {\n            mseFlvPlayback: false,\n            mseLiveFlvPlayback: false,\n            networkStreamIO: false,\n            networkLoaderName: '',\n            nativeMP4H264Playback: false,\n            nativeWebmVP8Playback: false,\n            nativeWebmVP9Playback: false\n        };\n\n        features.mseFlvPlayback = Features.supportMSEH264Playback();\n        features.networkStreamIO = Features.supportNetworkStreamIO();\n        features.networkLoaderName = Features.getNetworkLoaderTypeName();\n        features.mseLiveFlvPlayback = features.mseFlvPlayback && features.networkStreamIO;\n        features.nativeMP4H264Playback = Features.supportNativeMediaPlayback('video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"');\n        features.nativeWebmVP8Playback = Features.supportNativeMediaPlayback('video/webm; codecs=\"vp8.0, vorbis\"');\n        features.nativeWebmVP9Playback = Features.supportNativeMediaPlayback('video/webm; codecs=\"vp9\"');\n\n        return features;\n    }\n\n}\n\nexport default Features;"
  },
  {
    "path": "src/core/media-info.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nclass MediaInfo {\n\n    constructor() {\n        this.mimeType = null;\n        this.duration = null;\n\n        this.hasAudio = null;\n        this.hasVideo = null;\n        this.audioCodec = null;\n        this.videoCodec = null;\n        this.audioDataRate = null;\n        this.videoDataRate = null;\n\n        this.audioSampleRate = null;\n        this.audioChannelCount = null;\n\n        this.width = null;\n        this.height = null;\n        this.fps = null;\n        this.profile = null;\n        this.level = null;\n        this.refFrames = null;\n        this.chromaFormat = null;\n        this.sarNum = null;\n        this.sarDen = null;\n\n        this.metadata = null;\n        this.segments = null;  // MediaInfo[]\n        this.segmentCount = null;\n        this.hasKeyframesIndex = null;\n        this.keyframesIndex = null;\n    }\n\n    isComplete() {\n        let audioInfoComplete = (this.hasAudio === false) ||\n                                (this.hasAudio === true &&\n                                 this.audioCodec != null &&\n                                 this.audioSampleRate != null &&\n                                 this.audioChannelCount != null);\n\n        let videoInfoComplete = (this.hasVideo === false) ||\n                                (this.hasVideo === true &&\n                                 this.videoCodec != null &&\n                                 this.width != null &&\n                                 this.height != null &&\n                                 this.fps != null &&\n                                 this.profile != null &&\n                                 this.level != null &&\n                                 this.refFrames != null &&\n                                 this.chromaFormat != null &&\n                                 this.sarNum != null &&\n                                 this.sarDen != null);\n\n        // keyframesIndex may not be present\n        return this.mimeType != null &&\n               this.duration != null &&\n               this.metadata != null &&\n               this.hasKeyframesIndex != null &&\n               audioInfoComplete &&\n               videoInfoComplete;\n    }\n\n    isSeekable() {\n        return this.hasKeyframesIndex === true;\n    }\n\n    getNearestKeyframe(milliseconds) {\n        if (this.keyframesIndex == null) {\n            return null;\n        }\n\n        let table = this.keyframesIndex;\n        let keyframeIdx = this._search(table.times, milliseconds);\n\n        return {\n            index: keyframeIdx,\n            milliseconds: table.times[keyframeIdx],\n            fileposition: table.filepositions[keyframeIdx]\n        };\n    }\n\n    _search(list, value) {\n        let idx = 0;\n\n        let last = list.length - 1;\n        let mid = 0;\n        let lbound = 0;\n        let ubound = last;\n\n        if (value < list[0]) {\n            idx = 0;\n            lbound = ubound + 1;  // skip search\n        }\n\n        while (lbound <= ubound) {\n            mid = lbound + Math.floor((ubound - lbound) / 2);\n            if (mid === last || (value >= list[mid] && value < list[mid + 1])) {\n                idx = mid;\n                break;\n            } else if (list[mid] < value) {\n                lbound = mid + 1;\n            } else {\n                ubound = mid - 1;\n            }\n        }\n\n        return idx;\n    }\n\n}\n\nexport default MediaInfo;"
  },
  {
    "path": "src/core/media-segment-info.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Represents an media sample (audio / video)\nexport class SampleInfo {\n\n    constructor(dts, pts, duration, originalDts, isSync) {\n        this.dts = dts;\n        this.pts = pts;\n        this.duration = duration;\n        this.originalDts = originalDts;\n        this.isSyncPoint = isSync;\n        this.fileposition = null;\n    }\n\n}\n\n// Media Segment concept is defined in Media Source Extensions spec.\n// Particularly in ISO BMFF format, an Media Segment contains a moof box followed by a mdat box.\nexport class MediaSegmentInfo {\n\n    constructor() {\n        this.beginDts = 0;\n        this.endDts = 0;\n        this.beginPts = 0;\n        this.endPts = 0;\n        this.originalBeginDts = 0;\n        this.originalEndDts = 0;\n        this.syncPoints = [];     // SampleInfo[n], for video IDR frames only\n        this.firstSample = null;  // SampleInfo\n        this.lastSample = null;   // SampleInfo\n    }\n\n    appendSyncPoint(sampleInfo) {  // also called Random Access Point\n        sampleInfo.isSyncPoint = true;\n        this.syncPoints.push(sampleInfo);\n    }\n\n}\n\n// Ordered list for recording video IDR frames, sorted by originalDts\nexport class IDRSampleList {\n\n    constructor() {\n        this._list = [];\n    }\n\n    clear() {\n        this._list = [];\n    }\n\n    appendArray(syncPoints) {\n        let list = this._list;\n\n        if (syncPoints.length === 0) {\n            return;\n        }\n\n        if (list.length > 0 && syncPoints[0].originalDts < list[list.length - 1].originalDts) {\n            this.clear();\n        }\n\n        Array.prototype.push.apply(list, syncPoints);\n    }\n\n    getLastSyncPointBeforeDts(dts) {\n        if (this._list.length == 0) {\n            return null;\n        }\n\n        let list = this._list;\n        let idx = 0;\n        let last = list.length - 1;\n        let mid = 0;\n        let lbound = 0;\n        let ubound = last;\n\n        if (dts < list[0].dts) {\n            idx = 0;\n            lbound = ubound + 1;\n        }\n\n        while (lbound <= ubound) {\n            mid = lbound + Math.floor((ubound - lbound) / 2);\n            if (mid === last || (dts >= list[mid].dts && dts < list[mid + 1].dts)) {\n                idx = mid;\n                break;\n            } else if (list[mid].dts < dts) {\n                lbound = mid + 1;\n            } else {\n                ubound = mid - 1;\n            }\n        }\n        return this._list[idx];\n    }\n\n}\n\n// Data structure for recording information of media segments in single track.\nexport class MediaSegmentInfoList {\n\n    constructor(type) {\n        this._type = type;\n        this._list = [];\n        this._lastAppendLocation = -1;  // cached last insert location\n    }\n\n    get type() {\n        return this._type;\n    }\n\n    get length() {\n        return this._list.length;\n    }\n\n    isEmpty() {\n        return this._list.length === 0;\n    }\n\n    clear() {\n        this._list = [];\n        this._lastAppendLocation = -1;\n    }\n\n    _searchNearestSegmentBefore(originalBeginDts) {\n        let list = this._list;\n        if (list.length === 0) {\n            return -2;\n        }\n        let last = list.length - 1;\n        let mid = 0;\n        let lbound = 0;\n        let ubound = last;\n\n        let idx = 0;\n\n        if (originalBeginDts < list[0].originalBeginDts) {\n            idx = -1;\n            return idx;\n        }\n\n        while (lbound <= ubound) {\n            mid = lbound + Math.floor((ubound - lbound) / 2);\n            if (mid === last || (originalBeginDts > list[mid].lastSample.originalDts &&\n                                (originalBeginDts < list[mid + 1].originalBeginDts))) {\n                idx = mid;\n                break;\n            } else if (list[mid].originalBeginDts < originalBeginDts) {\n                lbound = mid + 1;\n            } else {\n                ubound = mid - 1;\n            }\n        }\n        return idx;\n    }\n\n    _searchNearestSegmentAfter(originalBeginDts) {\n        return this._searchNearestSegmentBefore(originalBeginDts) + 1;\n    }\n\n    append(mediaSegmentInfo) {\n        let list = this._list;\n        let msi = mediaSegmentInfo;\n        let lastAppendIdx = this._lastAppendLocation;\n        let insertIdx = 0;\n\n        if (lastAppendIdx !== -1 && lastAppendIdx < list.length &&\n                                    msi.originalBeginDts >= list[lastAppendIdx].lastSample.originalDts &&\n                                    ((lastAppendIdx === list.length - 1) ||\n                                    (lastAppendIdx < list.length - 1 &&\n                                    msi.originalBeginDts < list[lastAppendIdx + 1].originalBeginDts))) {\n            insertIdx = lastAppendIdx + 1;  // use cached location idx\n        } else {\n            if (list.length > 0) {\n                insertIdx = this._searchNearestSegmentBefore(msi.originalBeginDts) + 1;\n            }\n        }\n\n        this._lastAppendLocation = insertIdx;\n        this._list.splice(insertIdx, 0, msi);\n    }\n\n    getLastSegmentBefore(originalBeginDts) {\n        let idx = this._searchNearestSegmentBefore(originalBeginDts);\n        if (idx >= 0) {\n            return this._list[idx];\n        } else {  // -1\n            return null;\n        }\n    }\n\n    getLastSampleBefore(originalBeginDts) {\n        let segment = this.getLastSegmentBefore(originalBeginDts);\n        if (segment != null) {\n            return segment.lastSample;\n        } else {\n            return null;\n        }\n    }\n\n    getLastSyncPointBefore(originalBeginDts) {\n        let segmentIdx = this._searchNearestSegmentBefore(originalBeginDts);\n        let syncPoints = this._list[segmentIdx].syncPoints;\n        while (syncPoints.length === 0 && segmentIdx > 0) {\n            segmentIdx--;\n            syncPoints = this._list[segmentIdx].syncPoints;\n        }\n        if (syncPoints.length > 0) {\n            return syncPoints[syncPoints.length - 1];\n        } else {\n            return null;\n        }\n    }\n\n}"
  },
  {
    "path": "src/core/mse-controller.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport Log from '../utils/logger.js';\nimport Browser from '../utils/browser.js';\nimport MSEEvents from './mse-events.js';\nimport {SampleInfo, IDRSampleList} from './media-segment-info.js';\nimport {IllegalStateException} from '../utils/exception.js';\n\n// Media Source Extensions controller\nclass MSEController {\n\n    constructor(config) {\n        this.TAG = 'MSEController';\n\n        this._config = config;\n        this._emitter = new EventEmitter();\n\n        if (this._config.isLive && this._config.autoCleanupSourceBuffer == undefined) {\n            // For live stream, do auto cleanup by default\n            this._config.autoCleanupSourceBuffer = true;\n        }\n\n        this.e = {\n            onSourceOpen: this._onSourceOpen.bind(this),\n            onSourceEnded: this._onSourceEnded.bind(this),\n            onSourceClose: this._onSourceClose.bind(this),\n            onSourceBufferError: this._onSourceBufferError.bind(this),\n            onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)\n        };\n\n        this._mediaSource = null;\n        this._mediaSourceObjectURL = null;\n        this._mediaElement = null;\n\n        this._isBufferFull = false;\n        this._hasPendingEos = false;\n\n        this._requireSetMediaDuration = false;\n        this._pendingMediaDuration = 0;\n\n        this._pendingSourceBufferInit = [];\n        this._mimeTypes = {\n            video: null,\n            audio: null\n        };\n        this._sourceBuffers = {\n            video: null,\n            audio: null\n        };\n        this._lastInitSegments = {\n            video: null,\n            audio: null\n        };\n        this._pendingSegments = {\n            video: [],\n            audio: []\n        };\n        this._pendingRemoveRanges = {\n            video: [],\n            audio: []\n        };\n        this._idrList = new IDRSampleList();\n    }\n\n    destroy() {\n        if (this._mediaElement || this._mediaSource) {\n            this.detachMediaElement();\n        }\n        this.e = null;\n        this._emitter.removeAllListeners();\n        this._emitter = null;\n    }\n\n    on(event, listener) {\n        this._emitter.addListener(event, listener);\n    }\n\n    off(event, listener) {\n        this._emitter.removeListener(event, listener);\n    }\n\n    attachMediaElement(mediaElement) {\n        if (this._mediaSource) {\n            throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');\n        }\n        let ms = this._mediaSource = new window.MediaSource();\n        ms.addEventListener('sourceopen', this.e.onSourceOpen);\n        ms.addEventListener('sourceended', this.e.onSourceEnded);\n        ms.addEventListener('sourceclose', this.e.onSourceClose);\n\n        this._mediaElement = mediaElement;\n        this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource);\n        mediaElement.src = this._mediaSourceObjectURL;\n    }\n\n    detachMediaElement() {\n        if (this._mediaSource) {\n            let ms = this._mediaSource;\n            for (let type in this._sourceBuffers) {\n                // pending segments should be discard\n                let ps = this._pendingSegments[type];\n                ps.splice(0, ps.length);\n                this._pendingSegments[type] = null;\n                this._pendingRemoveRanges[type] = null;\n                this._lastInitSegments[type] = null;\n\n                // remove all sourcebuffers\n                let sb = this._sourceBuffers[type];\n                if (sb) {\n                    if (ms.readyState !== 'closed') {\n                        // ms edge can throw an error: Unexpected call to method or property access\n                        try {\n                            ms.removeSourceBuffer(sb);\n                        } catch (error) {\n                            Log.e(this.TAG, error.message);\n                        }\n                        sb.removeEventListener('error', this.e.onSourceBufferError);\n                        sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd);\n                    }\n                    this._mimeTypes[type] = null;\n                    this._sourceBuffers[type] = null;\n                }\n            }\n            if (ms.readyState === 'open') {\n                try {\n                    ms.endOfStream();\n                } catch (error) {\n                    Log.e(this.TAG, error.message);\n                }\n            }\n            ms.removeEventListener('sourceopen', this.e.onSourceOpen);\n            ms.removeEventListener('sourceended', this.e.onSourceEnded);\n            ms.removeEventListener('sourceclose', this.e.onSourceClose);\n            this._pendingSourceBufferInit = [];\n            this._isBufferFull = false;\n            this._idrList.clear();\n            this._mediaSource = null;\n        }\n\n        if (this._mediaElement) {\n            this._mediaElement.src = '';\n            this._mediaElement.removeAttribute('src');\n            this._mediaElement = null;\n        }\n        if (this._mediaSourceObjectURL) {\n            window.URL.revokeObjectURL(this._mediaSourceObjectURL);\n            this._mediaSourceObjectURL = null;\n        }\n    }\n\n    appendInitSegment(initSegment, deferred) {\n        if (!this._mediaSource || this._mediaSource.readyState !== 'open') {\n            // sourcebuffer creation requires mediaSource.readyState === 'open'\n            // so we defer the sourcebuffer creation, until sourceopen event triggered\n            this._pendingSourceBufferInit.push(initSegment);\n            // make sure that this InitSegment is in the front of pending segments queue\n            this._pendingSegments[initSegment.type].push(initSegment);\n            return;\n        }\n\n        let is = initSegment;\n        let mimeType = `${is.container}`;\n        if (is.codec && is.codec.length > 0) {\n            mimeType += `;codecs=${is.codec}`;\n        }\n\n        let firstInitSegment = false;\n\n        Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);\n        this._lastInitSegments[is.type] = is;\n\n        if (mimeType !== this._mimeTypes[is.type]) {\n            if (!this._mimeTypes[is.type]) {  // empty, first chance create sourcebuffer\n                firstInitSegment = true;\n                try {\n                    let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);\n                    sb.addEventListener('error', this.e.onSourceBufferError);\n                    sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);\n                } catch (error) {\n                    Log.e(this.TAG, error.message);\n                    this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});\n                    return;\n                }\n            } else {\n                Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);\n            }\n            this._mimeTypes[is.type] = mimeType;\n        }\n\n        if (!deferred) {\n            // deferred means this InitSegment has been pushed to pendingSegments queue\n            this._pendingSegments[is.type].push(is);\n        }\n        if (!firstInitSegment) {  // append immediately only if init segment in subsequence\n            if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {\n                this._doAppendSegments();\n            }\n        }\n        if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) {\n            // 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN\n            // Manually correct MediaSource.duration to make progress bar seekable, and report right duration\n            this._requireSetMediaDuration = true;\n            this._pendingMediaDuration = is.mediaDuration / 1000;  // in seconds\n            this._updateMediaSourceDuration();\n        }\n    }\n\n    appendMediaSegment(mediaSegment) {\n        let ms = mediaSegment;\n        this._pendingSegments[ms.type].push(ms);\n\n        if (this._config.autoCleanupSourceBuffer && this._needCleanupSourceBuffer()) {\n            this._doCleanupSourceBuffer();\n        }\n\n        let sb = this._sourceBuffers[ms.type];\n        if (sb && !sb.updating && !this._hasPendingRemoveRanges()) {\n            this._doAppendSegments();\n        }\n    }\n\n    seek(seconds) {\n        // remove all appended buffers\n        for (let type in this._sourceBuffers) {\n            if (!this._sourceBuffers[type]) {\n                continue;\n            }\n\n            // abort current buffer append algorithm\n            let sb = this._sourceBuffers[type];\n            if (this._mediaSource.readyState === 'open') {\n                try {\n                    // If range removal algorithm is running, InvalidStateError will be throwed\n                    // Ignore it.\n                    sb.abort();\n                } catch (error) {\n                    Log.e(this.TAG, error.message);\n                }\n            }\n\n            // IDRList should be clear\n            this._idrList.clear();\n\n            // pending segments should be discard\n            let ps = this._pendingSegments[type];\n            ps.splice(0, ps.length);\n\n            if (this._mediaSource.readyState === 'closed') {\n                // Parent MediaSource object has been detached from HTMLMediaElement\n                continue;\n            }\n\n            // record ranges to be remove from SourceBuffer\n            for (let i = 0; i < sb.buffered.length; i++) {\n                let start = sb.buffered.start(i);\n                let end = sb.buffered.end(i);\n                this._pendingRemoveRanges[type].push({start, end});\n            }\n\n            // if sb is not updating, let's remove ranges now!\n            if (!sb.updating) {\n                this._doRemoveRanges();\n            }\n\n            // Safari 10 may get InvalidStateError in the later appendBuffer() after SourceBuffer.remove() call\n            // Internal parser's state may be invalid at this time. Re-append last InitSegment to workaround.\n            // Related issue: https://bugs.webkit.org/show_bug.cgi?id=159230\n            if (Browser.safari) {\n                let lastInitSegment = this._lastInitSegments[type];\n                if (lastInitSegment) {\n                    this._pendingSegments[type].push(lastInitSegment);\n                    if (!sb.updating) {\n                        this._doAppendSegments();\n                    }\n                }\n            }\n        }\n    }\n\n    endOfStream() {\n        let ms = this._mediaSource;\n        let sb = this._sourceBuffers;\n        if (!ms || ms.readyState !== 'open') {\n            if (ms && ms.readyState === 'closed' && this._hasPendingSegments()) {\n                // If MediaSource hasn't turned into open state, and there're pending segments\n                // Mark pending endOfStream, defer call until all pending segments appended complete\n                this._hasPendingEos = true;\n            }\n            return;\n        }\n        if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {\n            // If any sourcebuffer is updating, defer endOfStream operation\n            // See _onSourceBufferUpdateEnd()\n            this._hasPendingEos = true;\n        } else {\n            this._hasPendingEos = false;\n            // Notify media data loading complete\n            // This is helpful for correcting total duration to match last media segment\n            // Otherwise MediaElement's ended event may not be triggered\n            ms.endOfStream();\n        }\n    }\n\n    getNearestKeyframe(dts) {\n        return this._idrList.getLastSyncPointBeforeDts(dts);\n    }\n\n    _needCleanupSourceBuffer() {\n        if (!this._config.autoCleanupSourceBuffer) {\n            return false;\n        }\n\n        let currentTime = this._mediaElement.currentTime;\n\n        for (let type in this._sourceBuffers) {\n            let sb = this._sourceBuffers[type];\n            if (sb) {\n                let buffered = sb.buffered;\n                if (buffered.length >= 1) {\n                    if (currentTime - buffered.start(0) >= this._config.autoCleanupMaxBackwardDuration) {\n                        return true;\n                    }\n                }\n            }\n        }\n\n        return false;\n    }\n\n    _doCleanupSourceBuffer() {\n        let currentTime = this._mediaElement.currentTime;\n\n        for (let type in this._sourceBuffers) {\n            let sb = this._sourceBuffers[type];\n            if (sb) {\n                let buffered = sb.buffered;\n                let doRemove = false;\n\n                for (let i = 0; i < buffered.length; i++) {\n                    let start = buffered.start(i);\n                    let end = buffered.end(i);\n\n                    if (start <= currentTime && currentTime < end + 3) {  // padding 3 seconds\n                        if (currentTime - start >= this._config.autoCleanupMaxBackwardDuration) {\n                            doRemove = true;\n                            let removeEnd = currentTime - this._config.autoCleanupMinBackwardDuration;\n                            this._pendingRemoveRanges[type].push({start: start, end: removeEnd});\n                        }\n                    } else if (end < currentTime) {\n                        doRemove = true;\n                        this._pendingRemoveRanges[type].push({start: start, end: end});\n                    }\n                }\n\n                if (doRemove && !sb.updating) {\n                    this._doRemoveRanges();\n                }\n            }\n        }\n    }\n\n    _updateMediaSourceDuration() {\n        let sb = this._sourceBuffers;\n        if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') {\n            return;\n        }\n        if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) {\n            return;\n        }\n\n        let current = this._mediaSource.duration;\n        let target = this._pendingMediaDuration;\n\n        if (target > 0 && (isNaN(current) || target > current)) {\n            Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`);\n            this._mediaSource.duration = target;\n        }\n\n        this._requireSetMediaDuration = false;\n        this._pendingMediaDuration = 0;\n    }\n\n    _doRemoveRanges() {\n        for (let type in this._pendingRemoveRanges) {\n            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {\n                continue;\n            }\n            let sb = this._sourceBuffers[type];\n            let ranges = this._pendingRemoveRanges[type];\n            while (ranges.length && !sb.updating) {\n                let range = ranges.shift();\n                sb.remove(range.start, range.end);\n            }\n        }\n    }\n\n    _doAppendSegments() {\n        let pendingSegments = this._pendingSegments;\n\n        for (let type in pendingSegments) {\n            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {\n                continue;\n            }\n\n            if (pendingSegments[type].length > 0) {\n                let segment = pendingSegments[type].shift();\n\n                if (segment.timestampOffset) {\n                    // For MPEG audio stream in MSE, if unbuffered-seeking occurred\n                    // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.\n                    let currentOffset = this._sourceBuffers[type].timestampOffset;\n                    let targetOffset = segment.timestampOffset / 1000;  // in seconds\n\n                    let delta = Math.abs(currentOffset - targetOffset);\n                    if (delta > 0.1) {  // If time delta > 100ms\n                        Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`);\n                        this._sourceBuffers[type].timestampOffset = targetOffset;\n                    }\n                    delete segment.timestampOffset;\n                }\n\n                if (!segment.data || segment.data.byteLength === 0) {\n                    // Ignore empty buffer\n                    continue;\n                }\n\n                try {\n                    this._sourceBuffers[type].appendBuffer(segment.data);\n                    this._isBufferFull = false;\n                    if (type === 'video' && segment.hasOwnProperty('info')) {\n                        this._idrList.appendArray(segment.info.syncPoints);\n                    }\n                } catch (error) {\n                    this._pendingSegments[type].unshift(segment);\n                    if (error.code === 22) {  // QuotaExceededError\n                        /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full\n                         * Currently we can only do lazy-load to avoid SourceBuffer become scattered.\n                         * SourceBuffer eviction policy may be changed in future version of FireFox.\n                         *\n                         * Related issues:\n                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885\n                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023\n                         */\n\n                        // report buffer full, abort network IO\n                        if (!this._isBufferFull) {\n                            this._emitter.emit(MSEEvents.BUFFER_FULL);\n                        }\n                        this._isBufferFull = true;\n                    } else {\n                        Log.e(this.TAG, error.message);\n                        this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});\n                    }\n                }\n            }\n        }\n    }\n\n    _onSourceOpen() {\n        Log.v(this.TAG, 'MediaSource onSourceOpen');\n        this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);\n        // deferred sourcebuffer creation / initialization\n        if (this._pendingSourceBufferInit.length > 0) {\n            let pendings = this._pendingSourceBufferInit;\n            while (pendings.length) {\n                let segment = pendings.shift();\n                this.appendInitSegment(segment, true);\n            }\n        }\n        // there may be some pending media segments, append them\n        if (this._hasPendingSegments()) {\n            this._doAppendSegments();\n        }\n        this._emitter.emit(MSEEvents.SOURCE_OPEN);\n    }\n\n    _onSourceEnded() {\n        // fired on endOfStream\n        Log.v(this.TAG, 'MediaSource onSourceEnded');\n    }\n\n    _onSourceClose() {\n        // fired on detaching from media element\n        Log.v(this.TAG, 'MediaSource onSourceClose');\n        if (this._mediaSource && this.e != null) {\n            this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);\n            this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);\n            this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);\n        }\n    }\n\n    _hasPendingSegments() {\n        let ps = this._pendingSegments;\n        return ps.video.length > 0 || ps.audio.length > 0;\n    }\n\n    _hasPendingRemoveRanges() {\n        let prr = this._pendingRemoveRanges;\n        return prr.video.length > 0 || prr.audio.length > 0;\n    }\n\n    _onSourceBufferUpdateEnd() {\n        if (this._requireSetMediaDuration) {\n            this._updateMediaSourceDuration();\n        } else if (this._hasPendingRemoveRanges()) {\n            this._doRemoveRanges();\n        } else if (this._hasPendingSegments()) {\n            this._doAppendSegments();\n        } else if (this._hasPendingEos) {\n            this.endOfStream();\n        }\n        this._emitter.emit(MSEEvents.UPDATE_END);\n    }\n\n    _onSourceBufferError(e) {\n        Log.e(this.TAG, `SourceBuffer Error: ${e}`);\n        // this error might not always be fatal, just ignore it\n    }\n\n}\n\nexport default MSEController;"
  },
  {
    "path": "src/core/mse-events.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst MSEEvents = {\n    ERROR: 'error',\n    SOURCE_OPEN: 'source_open',\n    UPDATE_END: 'update_end',\n    BUFFER_FULL: 'buffer_full'\n};\n\nexport default MSEEvents;"
  },
  {
    "path": "src/core/transmuxer.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport work from 'webworkify-webpack';\nimport Log from '../utils/logger.js';\nimport LoggingControl from '../utils/logging-control.js';\nimport TransmuxingController from './transmuxing-controller.js';\nimport TransmuxingEvents from './transmuxing-events.js';\nimport TransmuxingWorker from './transmuxing-worker.js';\nimport MediaInfo from './media-info.js';\n\nclass Transmuxer {\n\n    constructor(mediaDataSource, config) {\n        this.TAG = 'Transmuxer';\n        this._emitter = new EventEmitter();\n\n        if (config.enableWorker && typeof (Worker) !== 'undefined') {\n            try {\n                this._worker = work(require.resolve('./transmuxing-worker'));\n                this._workerDestroying = false;\n                this._worker.addEventListener('message', this._onWorkerMessage.bind(this));\n                this._worker.postMessage({cmd: 'init', param: [mediaDataSource, config]});\n                this.e = {\n                    onLoggingConfigChanged: this._onLoggingConfigChanged.bind(this)\n                };\n                LoggingControl.registerListener(this.e.onLoggingConfigChanged);\n                this._worker.postMessage({cmd: 'logging_config', param: LoggingControl.getConfig()});\n            } catch (error) {\n                Log.e(this.TAG, 'Error while initialize transmuxing worker, fallback to inline transmuxing');\n                this._worker = null;\n                this._controller = new TransmuxingController(mediaDataSource, config);\n            }\n        } else {\n            this._controller = new TransmuxingController(mediaDataSource, config);\n        }\n\n        if (this._controller) {\n            let ctl = this._controller;\n            ctl.on(TransmuxingEvents.IO_ERROR, this._onIOError.bind(this));\n            ctl.on(TransmuxingEvents.DEMUX_ERROR, this._onDemuxError.bind(this));\n            ctl.on(TransmuxingEvents.INIT_SEGMENT, this._onInitSegment.bind(this));\n            ctl.on(TransmuxingEvents.MEDIA_SEGMENT, this._onMediaSegment.bind(this));\n            ctl.on(TransmuxingEvents.LOADING_COMPLETE, this._onLoadingComplete.bind(this));\n            ctl.on(TransmuxingEvents.RECOVERED_EARLY_EOF, this._onRecoveredEarlyEof.bind(this));\n            ctl.on(TransmuxingEvents.MEDIA_INFO, this._onMediaInfo.bind(this));\n            ctl.on(TransmuxingEvents.METADATA_ARRIVED, this._onMetaDataArrived.bind(this));\n            ctl.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, this._onScriptDataArrived.bind(this));\n            ctl.on(TransmuxingEvents.STATISTICS_INFO, this._onStatisticsInfo.bind(this));\n            ctl.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, this._onRecommendSeekpoint.bind(this));\n        }\n    }\n\n    destroy() {\n        if (this._worker) {\n            if (!this._workerDestroying) {\n                this._workerDestroying = true;\n                this._worker.postMessage({cmd: 'destroy'});\n                LoggingControl.removeListener(this.e.onLoggingConfigChanged);\n                this.e = null;\n            }\n        } else {\n            this._controller.destroy();\n            this._controller = null;\n        }\n        this._emitter.removeAllListeners();\n        this._emitter = null;\n    }\n\n    on(event, listener) {\n        this._emitter.addListener(event, listener);\n    }\n\n    off(event, listener) {\n        this._emitter.removeListener(event, listener);\n    }\n\n    hasWorker() {\n        return this._worker != null;\n    }\n\n    open() {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'start'});\n        } else {\n            this._controller.start();\n        }\n    }\n\n    close() {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'stop'});\n        } else {\n            this._controller.stop();\n        }\n    }\n\n    seek(milliseconds) {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'seek', param: milliseconds});\n        } else {\n            this._controller.seek(milliseconds);\n        }\n    }\n\n    pause() {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'pause'});\n        } else {\n            this._controller.pause();\n        }\n    }\n\n    resume() {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'resume'});\n        } else {\n            this._controller.resume();\n        }\n    }\n\n    _onInitSegment(type, initSegment) {\n        // do async invoke\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);\n        });\n    }\n\n    _onMediaSegment(type, mediaSegment) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);\n        });\n    }\n\n    _onLoadingComplete() {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);\n        });\n    }\n\n    _onRecoveredEarlyEof() {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);\n        });\n    }\n\n    _onMediaInfo(mediaInfo) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.MEDIA_INFO, mediaInfo);\n        });\n    }\n\n    _onMetaDataArrived(metadata) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata);\n        });\n    }\n\n    _onScriptDataArrived(data) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data);\n        });\n    }\n\n    _onStatisticsInfo(statisticsInfo) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, statisticsInfo);\n        });\n    }\n\n    _onIOError(type, info) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);\n        });\n    }\n\n    _onDemuxError(type, info) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);\n        });\n    }\n\n    _onRecommendSeekpoint(milliseconds) {\n        Promise.resolve().then(() => {\n            this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, milliseconds);\n        });\n    }\n\n    _onLoggingConfigChanged(config) {\n        if (this._worker) {\n            this._worker.postMessage({cmd: 'logging_config', param: config});\n        }\n    }\n\n    _onWorkerMessage(e) {\n        let message = e.data;\n        let data = message.data;\n\n        if (message.msg === 'destroyed' || this._workerDestroying) {\n            this._workerDestroying = false;\n            this._worker.terminate();\n            this._worker = null;\n            return;\n        }\n\n        switch (message.msg) {\n            case TransmuxingEvents.INIT_SEGMENT:\n            case TransmuxingEvents.MEDIA_SEGMENT:\n                this._emitter.emit(message.msg, data.type, data.data);\n                break;\n            case TransmuxingEvents.LOADING_COMPLETE:\n            case TransmuxingEvents.RECOVERED_EARLY_EOF:\n                this._emitter.emit(message.msg);\n                break;\n            case TransmuxingEvents.MEDIA_INFO:\n                Object.setPrototypeOf(data, MediaInfo.prototype);\n                this._emitter.emit(message.msg, data);\n                break;\n            case TransmuxingEvents.METADATA_ARRIVED:\n            case TransmuxingEvents.SCRIPTDATA_ARRIVED:\n            case TransmuxingEvents.STATISTICS_INFO:\n                this._emitter.emit(message.msg, data);\n                break;\n            case TransmuxingEvents.IO_ERROR:\n            case TransmuxingEvents.DEMUX_ERROR:\n                this._emitter.emit(message.msg, data.type, data.info);\n                break;\n            case TransmuxingEvents.RECOMMEND_SEEKPOINT:\n                this._emitter.emit(message.msg, data);\n                break;\n            case 'logcat_callback':\n                Log.emitter.emit('log', data.type, data.logcat);\n                break;\n            default:\n                break;\n        }\n    }\n\n}\n\nexport default Transmuxer;"
  },
  {
    "path": "src/core/transmuxing-controller.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport Log from '../utils/logger.js';\nimport Browser from '../utils/browser.js';\nimport MediaInfo from './media-info.js';\nimport FLVDemuxer from '../demux/flv-demuxer.js';\nimport MP4Remuxer from '../remux/mp4-remuxer.js';\nimport DemuxErrors from '../demux/demux-errors.js';\nimport IOController from '../io/io-controller.js';\nimport TransmuxingEvents from './transmuxing-events.js';\nimport {LoaderStatus, LoaderErrors} from '../io/loader.js';\n\n// Transmuxing (IO, Demuxing, Remuxing) controller, with multipart support\nclass TransmuxingController {\n\n    constructor(mediaDataSource, config) {\n        this.TAG = 'TransmuxingController';\n        this._emitter = new EventEmitter();\n\n        this._config = config;\n\n        // treat single part media as multipart media, which has only one segment\n        if (!mediaDataSource.segments) {\n            mediaDataSource.segments = [{\n                duration: mediaDataSource.duration,\n                filesize: mediaDataSource.filesize,\n                url: mediaDataSource.url\n            }];\n        }\n\n        // fill in default IO params if not exists\n        if (typeof mediaDataSource.cors !== 'boolean') {\n            mediaDataSource.cors = true;\n        }\n        if (typeof mediaDataSource.withCredentials !== 'boolean') {\n            mediaDataSource.withCredentials = false;\n        }\n\n        this._mediaDataSource = mediaDataSource;\n        this._currentSegmentIndex = 0;\n        let totalDuration = 0;\n\n        this._mediaDataSource.segments.forEach((segment) => {\n            // timestampBase for each segment, and calculate total duration\n            segment.timestampBase = totalDuration;\n            totalDuration += segment.duration;\n            // params needed by IOController\n            segment.cors = mediaDataSource.cors;\n            segment.withCredentials = mediaDataSource.withCredentials;\n            // referrer policy control, if exist\n            if (config.referrerPolicy) {\n                segment.referrerPolicy = config.referrerPolicy;\n            }\n        });\n\n        if (!isNaN(totalDuration) && this._mediaDataSource.duration !== totalDuration) {\n            this._mediaDataSource.duration = totalDuration;\n        }\n\n        this._mediaInfo = null;\n        this._demuxer = null;\n        this._remuxer = null;\n        this._ioctl = null;\n\n        this._pendingSeekTime = null;\n        this._pendingResolveSeekPoint = null;\n\n        this._statisticsReporter = null;\n    }\n\n    destroy() {\n        this._mediaInfo = null;\n        this._mediaDataSource = null;\n\n        if (this._statisticsReporter) {\n            this._disableStatisticsReporter();\n        }\n        if (this._ioctl) {\n            this._ioctl.destroy();\n            this._ioctl = null;\n        }\n        if (this._demuxer) {\n            this._demuxer.destroy();\n            this._demuxer = null;\n        }\n        if (this._remuxer) {\n            this._remuxer.destroy();\n            this._remuxer = null;\n        }\n\n        this._emitter.removeAllListeners();\n        this._emitter = null;\n    }\n\n    on(event, listener) {\n        this._emitter.addListener(event, listener);\n    }\n\n    off(event, listener) {\n        this._emitter.removeListener(event, listener);\n    }\n\n    start() {\n        this._loadSegment(0);\n        this._enableStatisticsReporter();\n    }\n\n    _loadSegment(segmentIndex, optionalFrom) {\n        this._currentSegmentIndex = segmentIndex;\n        let dataSource = this._mediaDataSource.segments[segmentIndex];\n\n        let ioctl = this._ioctl = new IOController(dataSource, this._config, segmentIndex);\n        ioctl.onError = this._onIOException.bind(this);\n        ioctl.onSeeked = this._onIOSeeked.bind(this);\n        ioctl.onComplete = this._onIOComplete.bind(this);\n        ioctl.onRedirect = this._onIORedirect.bind(this);\n        ioctl.onRecoveredEarlyEof = this._onIORecoveredEarlyEof.bind(this);\n\n        if (optionalFrom) {\n            this._demuxer.bindDataSource(this._ioctl);\n        } else {\n            ioctl.onDataArrival = this._onInitChunkArrival.bind(this);\n        }\n\n        ioctl.open(optionalFrom);\n    }\n\n    stop() {\n        this._internalAbort();\n        this._disableStatisticsReporter();\n    }\n\n    _internalAbort() {\n        if (this._ioctl) {\n            this._ioctl.destroy();\n            this._ioctl = null;\n        }\n    }\n\n    pause() {  // take a rest\n        if (this._ioctl && this._ioctl.isWorking()) {\n            this._ioctl.pause();\n            this._disableStatisticsReporter();\n        }\n    }\n\n    resume() {\n        if (this._ioctl && this._ioctl.isPaused()) {\n            this._ioctl.resume();\n            this._enableStatisticsReporter();\n        }\n    }\n\n    seek(milliseconds) {\n        if (this._mediaInfo == null || !this._mediaInfo.isSeekable()) {\n            return;\n        }\n\n        let targetSegmentIndex = this._searchSegmentIndexContains(milliseconds);\n\n        if (targetSegmentIndex === this._currentSegmentIndex) {\n            // intra-segment seeking\n            let segmentInfo = this._mediaInfo.segments[targetSegmentIndex];\n\n            if (segmentInfo == undefined) {\n                // current segment loading started, but mediainfo hasn't received yet\n                // wait for the metadata loaded, then seek to expected position\n                this._pendingSeekTime = milliseconds;\n            } else {\n                let keyframe = segmentInfo.getNearestKeyframe(milliseconds);\n                this._remuxer.seek(keyframe.milliseconds);\n                this._ioctl.seek(keyframe.fileposition);\n                // Will be resolved in _onRemuxerMediaSegmentArrival()\n                this._pendingResolveSeekPoint = keyframe.milliseconds;\n            }\n        } else {\n            // cross-segment seeking\n            let targetSegmentInfo = this._mediaInfo.segments[targetSegmentIndex];\n\n            if (targetSegmentInfo == undefined) {\n                // target segment hasn't been loaded. We need metadata then seek to expected time\n                this._pendingSeekTime = milliseconds;\n                this._internalAbort();\n                this._remuxer.seek();\n                this._remuxer.insertDiscontinuity();\n                this._loadSegment(targetSegmentIndex);\n                // Here we wait for the metadata loaded, then seek to expected position\n            } else {\n                // We have target segment's metadata, direct seek to target position\n                let keyframe = targetSegmentInfo.getNearestKeyframe(milliseconds);\n                this._internalAbort();\n                this._remuxer.seek(milliseconds);\n                this._remuxer.insertDiscontinuity();\n                this._demuxer.resetMediaInfo();\n                this._demuxer.timestampBase = this._mediaDataSource.segments[targetSegmentIndex].timestampBase;\n                this._loadSegment(targetSegmentIndex, keyframe.fileposition);\n                this._pendingResolveSeekPoint = keyframe.milliseconds;\n                this._reportSegmentMediaInfo(targetSegmentIndex);\n            }\n        }\n\n        this._enableStatisticsReporter();\n    }\n\n    _searchSegmentIndexContains(milliseconds) {\n        let segments = this._mediaDataSource.segments;\n        let idx = segments.length - 1;\n\n        for (let i = 0; i < segments.length; i++) {\n            if (milliseconds < segments[i].timestampBase) {\n                idx = i - 1;\n                break;\n            }\n        }\n        return idx;\n    }\n\n    _onInitChunkArrival(data, byteStart) {\n        let probeData = null;\n        let consumed = 0;\n\n        if (byteStart > 0) {\n            // IOController seeked immediately after opened, byteStart > 0 callback may received\n            this._demuxer.bindDataSource(this._ioctl);\n            this._demuxer.timestampBase = this._mediaDataSource.segments[this._currentSegmentIndex].timestampBase;\n\n            consumed = this._demuxer.parseChunks(data, byteStart);\n        } else if ((probeData = FLVDemuxer.probe(data)).match) {\n            // Always create new FLVDemuxer\n            this._demuxer = new FLVDemuxer(probeData, this._config);\n\n            if (!this._remuxer) {\n                this._remuxer = new MP4Remuxer(this._config);\n            }\n\n            let mds = this._mediaDataSource;\n            if (mds.duration != undefined && !isNaN(mds.duration)) {\n                this._demuxer.overridedDuration = mds.duration;\n            }\n            if (typeof mds.hasAudio === 'boolean') {\n                this._demuxer.overridedHasAudio = mds.hasAudio;\n            }\n            if (typeof mds.hasVideo === 'boolean') {\n                this._demuxer.overridedHasVideo = mds.hasVideo;\n            }\n\n            this._demuxer.timestampBase = mds.segments[this._currentSegmentIndex].timestampBase;\n\n            this._demuxer.onError = this._onDemuxException.bind(this);\n            this._demuxer.onMediaInfo = this._onMediaInfo.bind(this);\n            this._demuxer.onMetaDataArrived = this._onMetaDataArrived.bind(this);\n            this._demuxer.onScriptDataArrived = this._onScriptDataArrived.bind(this);\n\n            this._remuxer.bindDataSource(this._demuxer\n                         .bindDataSource(this._ioctl\n            ));\n\n            this._remuxer.onInitSegment = this._onRemuxerInitSegmentArrival.bind(this);\n            this._remuxer.onMediaSegment = this._onRemuxerMediaSegmentArrival.bind(this);\n\n            consumed = this._demuxer.parseChunks(data, byteStart);\n        } else {\n            probeData = null;\n            Log.e(this.TAG, 'Non-FLV, Unsupported media type!');\n            Promise.resolve().then(() => {\n                this._internalAbort();\n            });\n            this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, DemuxErrors.FORMAT_UNSUPPORTED, 'Non-FLV, Unsupported media type');\n\n            consumed = 0;\n        }\n\n        return consumed;\n    }\n\n    _onMediaInfo(mediaInfo) {\n        if (this._mediaInfo == null) {\n            // Store first segment's mediainfo as global mediaInfo\n            this._mediaInfo = Object.assign({}, mediaInfo);\n            this._mediaInfo.keyframesIndex = null;\n            this._mediaInfo.segments = [];\n            this._mediaInfo.segmentCount = this._mediaDataSource.segments.length;\n            Object.setPrototypeOf(this._mediaInfo, MediaInfo.prototype);\n        }\n\n        let segmentInfo = Object.assign({}, mediaInfo);\n        Object.setPrototypeOf(segmentInfo, MediaInfo.prototype);\n        this._mediaInfo.segments[this._currentSegmentIndex] = segmentInfo;\n\n        // notify mediaInfo update\n        this._reportSegmentMediaInfo(this._currentSegmentIndex);\n\n        if (this._pendingSeekTime != null) {\n            Promise.resolve().then(() => {\n                let target = this._pendingSeekTime;\n                this._pendingSeekTime = null;\n                this.seek(target);\n            });\n        }\n    }\n\n    _onMetaDataArrived(metadata) {\n        this._emitter.emit(TransmuxingEvents.METADATA_ARRIVED, metadata);\n    }\n\n    _onScriptDataArrived(data) {\n        this._emitter.emit(TransmuxingEvents.SCRIPTDATA_ARRIVED, data);\n    }\n\n    _onIOSeeked() {\n        this._remuxer.insertDiscontinuity();\n    }\n\n    _onIOComplete(extraData) {\n        let segmentIndex = extraData;\n        let nextSegmentIndex = segmentIndex + 1;\n\n        if (nextSegmentIndex < this._mediaDataSource.segments.length) {\n            this._internalAbort();\n            this._remuxer.flushStashedSamples();\n            this._loadSegment(nextSegmentIndex);\n        } else {\n            this._remuxer.flushStashedSamples();\n            this._emitter.emit(TransmuxingEvents.LOADING_COMPLETE);\n            this._disableStatisticsReporter();\n        }\n    }\n\n    _onIORedirect(redirectedURL) {\n        let segmentIndex = this._ioctl.extraData;\n        this._mediaDataSource.segments[segmentIndex].redirectedURL = redirectedURL;\n    }\n\n    _onIORecoveredEarlyEof() {\n        this._emitter.emit(TransmuxingEvents.RECOVERED_EARLY_EOF);\n    }\n\n    _onIOException(type, info) {\n        Log.e(this.TAG, `IOException: type = ${type}, code = ${info.code}, msg = ${info.msg}`);\n        this._emitter.emit(TransmuxingEvents.IO_ERROR, type, info);\n        this._disableStatisticsReporter();\n    }\n\n    _onDemuxException(type, info) {\n        Log.e(this.TAG, `DemuxException: type = ${type}, info = ${info}`);\n        this._emitter.emit(TransmuxingEvents.DEMUX_ERROR, type, info);\n    }\n\n    _onRemuxerInitSegmentArrival(type, initSegment) {\n        this._emitter.emit(TransmuxingEvents.INIT_SEGMENT, type, initSegment);\n    }\n\n    _onRemuxerMediaSegmentArrival(type, mediaSegment) {\n        if (this._pendingSeekTime != null) {\n            // Media segments after new-segment cross-seeking should be dropped.\n            return;\n        }\n        this._emitter.emit(TransmuxingEvents.MEDIA_SEGMENT, type, mediaSegment);\n\n        // Resolve pending seekPoint\n        if (this._pendingResolveSeekPoint != null && type === 'video') {\n            let syncPoints = mediaSegment.info.syncPoints;\n            let seekpoint = this._pendingResolveSeekPoint;\n            this._pendingResolveSeekPoint = null;\n\n            // Safari: Pass PTS for recommend_seekpoint\n            if (Browser.safari && syncPoints.length > 0 && syncPoints[0].originalDts === seekpoint) {\n                seekpoint = syncPoints[0].pts;\n            }\n            // else: use original DTS (keyframe.milliseconds)\n\n            this._emitter.emit(TransmuxingEvents.RECOMMEND_SEEKPOINT, seekpoint);\n        }\n    }\n\n    _enableStatisticsReporter() {\n        if (this._statisticsReporter == null) {\n            this._statisticsReporter = self.setInterval(\n                this._reportStatisticsInfo.bind(this),\n            this._config.statisticsInfoReportInterval);\n        }\n    }\n\n    _disableStatisticsReporter() {\n        if (this._statisticsReporter) {\n            self.clearInterval(this._statisticsReporter);\n            this._statisticsReporter = null;\n        }\n    }\n\n    _reportSegmentMediaInfo(segmentIndex) {\n        let segmentInfo = this._mediaInfo.segments[segmentIndex];\n        let exportInfo = Object.assign({}, segmentInfo);\n\n        exportInfo.duration = this._mediaInfo.duration;\n        exportInfo.segmentCount = this._mediaInfo.segmentCount;\n        delete exportInfo.segments;\n        delete exportInfo.keyframesIndex;\n\n        this._emitter.emit(TransmuxingEvents.MEDIA_INFO, exportInfo);\n    }\n\n    _reportStatisticsInfo() {\n        let info = {};\n\n        info.url = this._ioctl.currentURL;\n        info.hasRedirect = this._ioctl.hasRedirect;\n        if (info.hasRedirect) {\n            info.redirectedURL = this._ioctl.currentRedirectedURL;\n        }\n\n        info.speed = this._ioctl.currentSpeed;\n        info.loaderType = this._ioctl.loaderType;\n        info.currentSegmentIndex = this._currentSegmentIndex;\n        info.totalSegmentCount = this._mediaDataSource.segments.length;\n\n        this._emitter.emit(TransmuxingEvents.STATISTICS_INFO, info);\n    }\n\n}\n\nexport default TransmuxingController;"
  },
  {
    "path": "src/core/transmuxing-events.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst TransmuxingEvents = {\n    IO_ERROR: 'io_error',\n    DEMUX_ERROR: 'demux_error',\n    INIT_SEGMENT: 'init_segment',\n    MEDIA_SEGMENT: 'media_segment',\n    LOADING_COMPLETE: 'loading_complete',\n    RECOVERED_EARLY_EOF: 'recovered_early_eof',\n    MEDIA_INFO: 'media_info',\n    METADATA_ARRIVED: 'metadata_arrived',\n    SCRIPTDATA_ARRIVED: 'scriptdata_arrived',\n    STATISTICS_INFO: 'statistics_info',\n    RECOMMEND_SEEKPOINT: 'recommend_seekpoint'\n};\n\nexport default TransmuxingEvents;"
  },
  {
    "path": "src/core/transmuxing-worker.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport LoggingControl from '../utils/logging-control.js';\nimport Polyfill from '../utils/polyfill.js';\nimport TransmuxingController from './transmuxing-controller.js';\nimport TransmuxingEvents from './transmuxing-events.js';\n\n/* post message to worker:\n   data: {\n       cmd: string\n       param: any\n   }\n\n   receive message from worker:\n   data: {\n       msg: string,\n       data: any\n   }\n */\n\nlet TransmuxingWorker = function (self) {\n\n    let TAG = 'TransmuxingWorker';\n    let controller = null;\n    let logcatListener = onLogcatCallback.bind(this);\n\n    Polyfill.install();\n\n    self.addEventListener('message', function (e) {\n        switch (e.data.cmd) {\n            case 'init':\n                controller = new TransmuxingController(e.data.param[0], e.data.param[1]);\n                controller.on(TransmuxingEvents.IO_ERROR, onIOError.bind(this));\n                controller.on(TransmuxingEvents.DEMUX_ERROR, onDemuxError.bind(this));\n                controller.on(TransmuxingEvents.INIT_SEGMENT, onInitSegment.bind(this));\n                controller.on(TransmuxingEvents.MEDIA_SEGMENT, onMediaSegment.bind(this));\n                controller.on(TransmuxingEvents.LOADING_COMPLETE, onLoadingComplete.bind(this));\n                controller.on(TransmuxingEvents.RECOVERED_EARLY_EOF, onRecoveredEarlyEof.bind(this));\n                controller.on(TransmuxingEvents.MEDIA_INFO, onMediaInfo.bind(this));\n                controller.on(TransmuxingEvents.METADATA_ARRIVED, onMetaDataArrived.bind(this));\n                controller.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, onScriptDataArrived.bind(this));\n                controller.on(TransmuxingEvents.STATISTICS_INFO, onStatisticsInfo.bind(this));\n                controller.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, onRecommendSeekpoint.bind(this));\n                break;\n            case 'destroy':\n                if (controller) {\n                    controller.destroy();\n                    controller = null;\n                }\n                self.postMessage({msg: 'destroyed'});\n                break;\n            case 'start':\n                controller.start();\n                break;\n            case 'stop':\n                controller.stop();\n                break;\n            case 'seek':\n                controller.seek(e.data.param);\n                break;\n            case 'pause':\n                controller.pause();\n                break;\n            case 'resume':\n                controller.resume();\n                break;\n            case 'logging_config': {\n                let config = e.data.param;\n                LoggingControl.applyConfig(config);\n\n                if (config.enableCallback === true) {\n                    LoggingControl.addLogListener(logcatListener);\n                } else {\n                    LoggingControl.removeLogListener(logcatListener);\n                }\n                break;\n            }\n        }\n    });\n\n    function onInitSegment(type, initSegment) {\n        let obj = {\n            msg: TransmuxingEvents.INIT_SEGMENT,\n            data: {\n                type: type,\n                data: initSegment\n            }\n        };\n        self.postMessage(obj, [initSegment.data]);  // data: ArrayBuffer\n    }\n\n    function onMediaSegment(type, mediaSegment) {\n        let obj = {\n            msg: TransmuxingEvents.MEDIA_SEGMENT,\n            data: {\n                type: type,\n                data: mediaSegment\n            }\n        };\n        self.postMessage(obj, [mediaSegment.data]);  // data: ArrayBuffer\n    }\n\n    function onLoadingComplete() {\n        let obj = {\n            msg: TransmuxingEvents.LOADING_COMPLETE\n        };\n        self.postMessage(obj);\n    }\n\n    function onRecoveredEarlyEof() {\n        let obj = {\n            msg: TransmuxingEvents.RECOVERED_EARLY_EOF\n        };\n        self.postMessage(obj);\n    }\n\n    function onMediaInfo(mediaInfo) {\n        let obj = {\n            msg: TransmuxingEvents.MEDIA_INFO,\n            data: mediaInfo\n        };\n        self.postMessage(obj);\n    }\n\n    function onMetaDataArrived(metadata) {\n        let obj = {\n            msg: TransmuxingEvents.METADATA_ARRIVED,\n            data: metadata\n        };\n        self.postMessage(obj);\n    }\n\n    function onScriptDataArrived(data) {\n        let obj = {\n            msg: TransmuxingEvents.SCRIPTDATA_ARRIVED,\n            data: data\n        };\n        self.postMessage(obj);\n    }\n\n    function onStatisticsInfo(statInfo) {\n        let obj = {\n            msg: TransmuxingEvents.STATISTICS_INFO,\n            data: statInfo\n        };\n        self.postMessage(obj);\n    }\n\n    function onIOError(type, info) {\n        self.postMessage({\n            msg: TransmuxingEvents.IO_ERROR,\n            data: {\n                type: type,\n                info: info\n            }\n        });\n    }\n\n    function onDemuxError(type, info) {\n        self.postMessage({\n            msg: TransmuxingEvents.DEMUX_ERROR,\n            data: {\n                type: type,\n                info: info\n            }\n        });\n    }\n\n    function onRecommendSeekpoint(milliseconds) {\n        self.postMessage({\n            msg: TransmuxingEvents.RECOMMEND_SEEKPOINT,\n            data: milliseconds\n        });\n    }\n\n    function onLogcatCallback(type, str) {\n        self.postMessage({\n            msg: 'logcat_callback',\n            data: {\n                type: type,\n                logcat: str\n            }\n        });\n    }\n\n};\n\nexport default TransmuxingWorker;"
  },
  {
    "path": "src/demux/amf-parser.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport decodeUTF8 from '../utils/utf8-conv.js';\nimport {IllegalStateException} from '../utils/exception.js';\n\nlet le = (function () {\n    let buf = new ArrayBuffer(2);\n    (new DataView(buf)).setInt16(0, 256, true);  // little-endian write\n    return (new Int16Array(buf))[0] === 256;  // platform-spec read, if equal then LE\n})();\n\nclass AMF {\n\n    static parseScriptData(arrayBuffer, dataOffset, dataSize) {\n        let data = {};\n\n        try {\n            let name = AMF.parseValue(arrayBuffer, dataOffset, dataSize);\n            let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);\n\n            data[name.data] = value.data;\n        } catch (e) {\n            Log.e('AMF', e.toString());\n        }\n\n        return data;\n    }\n\n    static parseObject(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 3) {\n            throw new IllegalStateException('Data not enough when parse ScriptDataObject');\n        }\n        let name = AMF.parseString(arrayBuffer, dataOffset, dataSize);\n        let value = AMF.parseValue(arrayBuffer, dataOffset + name.size, dataSize - name.size);\n        let isObjectEnd = value.objectEnd;\n\n        return {\n            data: {\n                name: name.data,\n                value: value.data\n            },\n            size: name.size + value.size,\n            objectEnd: isObjectEnd\n        };\n    }\n\n    static parseVariable(arrayBuffer, dataOffset, dataSize) {\n        return AMF.parseObject(arrayBuffer, dataOffset, dataSize);\n    }\n\n    static parseString(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 2) {\n            throw new IllegalStateException('Data not enough when parse String');\n        }\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n        let length = v.getUint16(0, !le);\n\n        let str;\n        if (length > 0) {\n            str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 2, length));\n        } else {\n            str = '';\n        }\n\n        return {\n            data: str,\n            size: 2 + length\n        };\n    }\n\n    static parseLongString(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 4) {\n            throw new IllegalStateException('Data not enough when parse LongString');\n        }\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n        let length = v.getUint32(0, !le);\n\n        let str;\n        if (length > 0) {\n            str = decodeUTF8(new Uint8Array(arrayBuffer, dataOffset + 4, length));\n        } else {\n            str = '';\n        }\n\n        return {\n            data: str,\n            size: 4 + length\n        };\n    }\n\n    static parseDate(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 10) {\n            throw new IllegalStateException('Data size invalid when parse Date');\n        }\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n        let timestamp = v.getFloat64(0, !le);\n        let localTimeOffset = v.getInt16(8, !le);\n        timestamp += localTimeOffset * 60 * 1000;  // get UTC time\n\n        return {\n            data: new Date(timestamp),\n            size: 8 + 2\n        };\n    }\n\n    static parseValue(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 1) {\n            throw new IllegalStateException('Data not enough when parse Value');\n        }\n\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n\n        let offset = 1;\n        let type = v.getUint8(0);\n        let value;\n        let objectEnd = false;\n\n        try {\n            switch (type) {\n                case 0:  // Number(Double) type\n                    value = v.getFloat64(1, !le);\n                    offset += 8;\n                    break;\n                case 1: {  // Boolean type\n                    let b = v.getUint8(1);\n                    value = b ? true : false;\n                    offset += 1;\n                    break;\n                }\n                case 2: {  // String type\n                    let amfstr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);\n                    value = amfstr.data;\n                    offset += amfstr.size;\n                    break;\n                }\n                case 3: { // Object(s) type\n                    value = {};\n                    let terminal = 0;  // workaround for malformed Objects which has missing ScriptDataObjectEnd\n                    if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {\n                        terminal = 3;\n                    }\n                    while (offset < dataSize - 4) {  // 4 === type(UI8) + ScriptDataObjectEnd(UI24)\n                        let amfobj = AMF.parseObject(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);\n                        if (amfobj.objectEnd)\n                            break;\n                        value[amfobj.data.name] = amfobj.data.value;\n                        offset += amfobj.size;\n                    }\n                    if (offset <= dataSize - 3) {\n                        let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;\n                        if (marker === 9) {\n                            offset += 3;\n                        }\n                    }\n                    break;\n                }\n                case 8: { // ECMA array type (Mixed array)\n                    value = {};\n                    offset += 4;  // ECMAArrayLength(UI32)\n                    let terminal = 0;  // workaround for malformed MixedArrays which has missing ScriptDataObjectEnd\n                    if ((v.getUint32(dataSize - 4, !le) & 0x00FFFFFF) === 9) {\n                        terminal = 3;\n                    }\n                    while (offset < dataSize - 8) {  // 8 === type(UI8) + ECMAArrayLength(UI32) + ScriptDataVariableEnd(UI24)\n                        let amfvar = AMF.parseVariable(arrayBuffer, dataOffset + offset, dataSize - offset - terminal);\n                        if (amfvar.objectEnd)\n                            break;\n                        value[amfvar.data.name] = amfvar.data.value;\n                        offset += amfvar.size;\n                    }\n                    if (offset <= dataSize - 3) {\n                        let marker = v.getUint32(offset - 1, !le) & 0x00FFFFFF;\n                        if (marker === 9) {\n                            offset += 3;\n                        }\n                    }\n                    break;\n                }\n                case 9:  // ScriptDataObjectEnd\n                    value = undefined;\n                    offset = 1;\n                    objectEnd = true;\n                    break;\n                case 10: {  // Strict array type\n                    // ScriptDataValue[n]. NOTE: according to video_file_format_spec_v10_1.pdf\n                    value = [];\n                    let strictArrayLength = v.getUint32(1, !le);\n                    offset += 4;\n                    for (let i = 0; i < strictArrayLength; i++) {\n                        let val = AMF.parseValue(arrayBuffer, dataOffset + offset, dataSize - offset);\n                        value.push(val.data);\n                        offset += val.size;\n                    }\n                    break;\n                }\n                case 11: {  // Date type\n                    let date = AMF.parseDate(arrayBuffer, dataOffset + 1, dataSize - 1);\n                    value = date.data;\n                    offset += date.size;\n                    break;\n                }\n                case 12: {  // Long string type\n                    let amfLongStr = AMF.parseString(arrayBuffer, dataOffset + 1, dataSize - 1);\n                    value = amfLongStr.data;\n                    offset += amfLongStr.size;\n                    break;\n                }\n                default:\n                    // ignore and skip\n                    offset = dataSize;\n                    Log.w('AMF', 'Unsupported AMF value type ' + type);\n            }\n        } catch (e) {\n            Log.e('AMF', e.toString());\n        }\n\n        return {\n            data: value,\n            size: offset,\n            objectEnd: objectEnd\n        };\n    }\n\n}\n\nexport default AMF;"
  },
  {
    "path": "src/demux/demux-errors.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst DemuxErrors = {\n    OK: 'OK',\n    FORMAT_ERROR: 'FormatError',\n    FORMAT_UNSUPPORTED: 'FormatUnsupported',\n    CODEC_UNSUPPORTED: 'CodecUnsupported'\n};\n\nexport default DemuxErrors;"
  },
  {
    "path": "src/demux/exp-golomb.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {IllegalStateException, InvalidArgumentException} from '../utils/exception.js';\n\n// Exponential-Golomb buffer decoder\nclass ExpGolomb {\n\n    constructor(uint8array) {\n        this.TAG = 'ExpGolomb';\n\n        this._buffer = uint8array;\n        this._buffer_index = 0;\n        this._total_bytes = uint8array.byteLength;\n        this._total_bits = uint8array.byteLength * 8;\n        this._current_word = 0;\n        this._current_word_bits_left = 0;\n    }\n\n    destroy() {\n        this._buffer = null;\n    }\n\n    _fillCurrentWord() {\n        let buffer_bytes_left = this._total_bytes - this._buffer_index;\n        if (buffer_bytes_left <= 0)\n            throw new IllegalStateException('ExpGolomb: _fillCurrentWord() but no bytes available');\n\n        let bytes_read = Math.min(4, buffer_bytes_left);\n        let word = new Uint8Array(4);\n        word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read));\n        this._current_word = new DataView(word.buffer).getUint32(0, false);\n\n        this._buffer_index += bytes_read;\n        this._current_word_bits_left = bytes_read * 8;\n    }\n\n    readBits(bits) {\n        if (bits > 32)\n            throw new InvalidArgumentException('ExpGolomb: readBits() bits exceeded max 32bits!');\n\n        if (bits <= this._current_word_bits_left) {\n            let result = this._current_word >>> (32 - bits);\n            this._current_word <<= bits;\n            this._current_word_bits_left -= bits;\n            return result;\n        }\n\n        let result = this._current_word_bits_left ? this._current_word : 0;\n        result = result >>> (32 - this._current_word_bits_left);\n        let bits_need_left = bits - this._current_word_bits_left;\n\n        this._fillCurrentWord();\n        let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left);\n\n        let result2 = this._current_word >>> (32 - bits_read_next);\n        this._current_word <<= bits_read_next;\n        this._current_word_bits_left -= bits_read_next;\n\n        result = (result << bits_read_next) | result2;\n        return result;\n    }\n\n    readBool() {\n        return this.readBits(1) === 1;\n    }\n\n    readByte() {\n        return this.readBits(8);\n    }\n\n    _skipLeadingZero() {\n        let zero_count;\n        for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) {\n            if (0 !== (this._current_word & (0x80000000 >>> zero_count))) {\n                this._current_word <<= zero_count;\n                this._current_word_bits_left -= zero_count;\n                return zero_count;\n            }\n        }\n        this._fillCurrentWord();\n        return zero_count + this._skipLeadingZero();\n    }\n\n    readUEG() {  // unsigned exponential golomb\n        let leading_zeros = this._skipLeadingZero();\n        return this.readBits(leading_zeros + 1) - 1;\n    }\n\n    readSEG() {  // signed exponential golomb\n        let value = this.readUEG();\n        if (value & 0x01) {\n            return (value + 1) >>> 1;\n        } else {\n            return -1 * (value >>> 1);\n        }\n    }\n\n}\n\nexport default ExpGolomb;"
  },
  {
    "path": "src/demux/flv-demuxer.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport AMF from './amf-parser.js';\nimport SPSParser from './sps-parser.js';\nimport DemuxErrors from './demux-errors.js';\nimport MediaInfo from '../core/media-info.js';\nimport {IllegalStateException} from '../utils/exception.js';\n\nfunction Swap16(src) {\n    return (((src >>> 8) & 0xFF) |\n            ((src & 0xFF) << 8));\n}\n\nfunction Swap32(src) {\n    return (((src & 0xFF000000) >>> 24) |\n            ((src & 0x00FF0000) >>> 8)  |\n            ((src & 0x0000FF00) << 8)   |\n            ((src & 0x000000FF) << 24));\n}\n\nfunction ReadBig32(array, index) {\n    return ((array[index] << 24)     |\n            (array[index + 1] << 16) |\n            (array[index + 2] << 8)  |\n            (array[index + 3]));\n}\n\n\nclass FLVDemuxer {\n\n    constructor(probeData, config) {\n        this.TAG = 'FLVDemuxer';\n\n        this._config = config;\n\n        this._onError = null;\n        this._onMediaInfo = null;\n        this._onMetaDataArrived = null;\n        this._onScriptDataArrived = null;\n        this._onTrackMetadata = null;\n        this._onDataAvailable = null;\n\n        this._dataOffset = probeData.dataOffset;\n        this._firstParse = true;\n        this._dispatch = false;\n\n        this._hasAudio = probeData.hasAudioTrack;\n        this._hasVideo = probeData.hasVideoTrack;\n\n        this._hasAudioFlagOverrided = false;\n        this._hasVideoFlagOverrided = false;\n\n        this._audioInitialMetadataDispatched = false;\n        this._videoInitialMetadataDispatched = false;\n\n        this._mediaInfo = new MediaInfo();\n        this._mediaInfo.hasAudio = this._hasAudio;\n        this._mediaInfo.hasVideo = this._hasVideo;\n        this._metadata = null;\n        this._audioMetadata = null;\n        this._videoMetadata = null;\n\n        this._naluLengthSize = 4;\n        this._timestampBase = 0;  // int32, in milliseconds\n        this._timescale = 1000;\n        this._duration = 0;  // int32, in milliseconds\n        this._durationOverrided = false;\n        this._referenceFrameRate = {\n            fixed: true,\n            fps: 23.976,\n            fps_num: 23976,\n            fps_den: 1000\n        };\n\n        this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000];\n\n        this._mpegSamplingRates = [\n            96000, 88200, 64000, 48000, 44100, 32000,\n            24000, 22050, 16000, 12000, 11025, 8000, 7350\n        ];\n\n        this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0];\n        this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0];\n        this._mpegAudioV25SampleRateTable = [11025, 12000, 8000,  0];\n\n        this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1];\n        this._mpegAudioL2BitRateTable = [0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 384, -1];\n        this._mpegAudioL3BitRateTable = [0, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, -1];\n\n        this._videoTrack = {type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0};\n        this._audioTrack = {type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0};\n\n        this._littleEndian = (function () {\n            let buf = new ArrayBuffer(2);\n            (new DataView(buf)).setInt16(0, 256, true);  // little-endian write\n            return (new Int16Array(buf))[0] === 256;  // platform-spec read, if equal then LE\n        })();\n    }\n\n    destroy() {\n        this._mediaInfo = null;\n        this._metadata = null;\n        this._audioMetadata = null;\n        this._videoMetadata = null;\n        this._videoTrack = null;\n        this._audioTrack = null;\n\n        this._onError = null;\n        this._onMediaInfo = null;\n        this._onMetaDataArrived = null;\n        this._onScriptDataArrived = null;\n        this._onTrackMetadata = null;\n        this._onDataAvailable = null;\n    }\n\n    static probe(buffer) {\n        let data = new Uint8Array(buffer);\n        let mismatch = {match: false};\n\n        if (data[0] !== 0x46 || data[1] !== 0x4C || data[2] !== 0x56 || data[3] !== 0x01) {\n            return mismatch;\n        }\n\n        let hasAudio = ((data[4] & 4) >>> 2) !== 0;\n        let hasVideo = (data[4] & 1) !== 0;\n\n        let offset = ReadBig32(data, 5);\n\n        if (offset < 9) {\n            return mismatch;\n        }\n\n        return {\n            match: true,\n            consumed: offset,\n            dataOffset: offset,\n            hasAudioTrack: hasAudio,\n            hasVideoTrack: hasVideo\n        };\n    }\n\n    bindDataSource(loader) {\n        loader.onDataArrival = this.parseChunks.bind(this);\n        return this;\n    }\n\n    // prototype: function(type: string, metadata: any): void\n    get onTrackMetadata() {\n        return this._onTrackMetadata;\n    }\n\n    set onTrackMetadata(callback) {\n        this._onTrackMetadata = callback;\n    }\n\n    // prototype: function(mediaInfo: MediaInfo): void\n    get onMediaInfo() {\n        return this._onMediaInfo;\n    }\n\n    set onMediaInfo(callback) {\n        this._onMediaInfo = callback;\n    }\n\n    get onMetaDataArrived() {\n        return this._onMetaDataArrived;\n    }\n\n    set onMetaDataArrived(callback) {\n        this._onMetaDataArrived = callback;\n    }\n\n    get onScriptDataArrived() {\n        return this._onScriptDataArrived;\n    }\n\n    set onScriptDataArrived(callback) {\n        this._onScriptDataArrived = callback;\n    }\n\n    // prototype: function(type: number, info: string): void\n    get onError() {\n        return this._onError;\n    }\n\n    set onError(callback) {\n        this._onError = callback;\n    }\n\n    // prototype: function(videoTrack: any, audioTrack: any): void\n    get onDataAvailable() {\n        return this._onDataAvailable;\n    }\n\n    set onDataAvailable(callback) {\n        this._onDataAvailable = callback;\n    }\n\n    // timestamp base for output samples, must be in milliseconds\n    get timestampBase() {\n        return this._timestampBase;\n    }\n\n    set timestampBase(base) {\n        this._timestampBase = base;\n    }\n\n    get overridedDuration() {\n        return this._duration;\n    }\n\n    // Force-override media duration. Must be in milliseconds, int32\n    set overridedDuration(duration) {\n        this._durationOverrided = true;\n        this._duration = duration;\n        this._mediaInfo.duration = duration;\n    }\n\n    // Force-override audio track present flag, boolean\n    set overridedHasAudio(hasAudio) {\n        this._hasAudioFlagOverrided = true;\n        this._hasAudio = hasAudio;\n        this._mediaInfo.hasAudio = hasAudio;\n    }\n\n    // Force-override video track present flag, boolean\n    set overridedHasVideo(hasVideo) {\n        this._hasVideoFlagOverrided = true;\n        this._hasVideo = hasVideo;\n        this._mediaInfo.hasVideo = hasVideo;\n    }\n\n    resetMediaInfo() {\n        this._mediaInfo = new MediaInfo();\n    }\n\n    _isInitialMetadataDispatched() {\n        if (this._hasAudio && this._hasVideo) {  // both audio & video\n            return this._audioInitialMetadataDispatched && this._videoInitialMetadataDispatched;\n        }\n        if (this._hasAudio && !this._hasVideo) {  // audio only\n            return this._audioInitialMetadataDispatched;\n        }\n        if (!this._hasAudio && this._hasVideo) {  // video only\n            return this._videoInitialMetadataDispatched;\n        }\n        return false;\n    }\n\n    // function parseChunks(chunk: ArrayBuffer, byteStart: number): number;\n    parseChunks(chunk, byteStart) {\n        if (!this._onError || !this._onMediaInfo || !this._onTrackMetadata || !this._onDataAvailable) {\n            throw new IllegalStateException('Flv: onError & onMediaInfo & onTrackMetadata & onDataAvailable callback must be specified');\n        }\n\n        let offset = 0;\n        let le = this._littleEndian;\n\n        if (byteStart === 0) {  // buffer with FLV header\n            if (chunk.byteLength > 13) {\n                let probeData = FLVDemuxer.probe(chunk);\n                offset = probeData.dataOffset;\n            } else {\n                return 0;\n            }\n        }\n\n        if (this._firstParse) {  // handle PreviousTagSize0 before Tag1\n            this._firstParse = false;\n            if (byteStart + offset !== this._dataOffset) {\n                Log.w(this.TAG, 'First time parsing but chunk byteStart invalid!');\n            }\n\n            let v = new DataView(chunk, offset);\n            let prevTagSize0 = v.getUint32(0, !le);\n            if (prevTagSize0 !== 0) {\n                Log.w(this.TAG, 'PrevTagSize0 !== 0 !!!');\n            }\n            offset += 4;\n        }\n\n        while (offset < chunk.byteLength) {\n            this._dispatch = true;\n\n            let v = new DataView(chunk, offset);\n\n            if (offset + 11 + 4 > chunk.byteLength) {\n                // data not enough for parsing an flv tag\n                break;\n            }\n\n            let tagType = v.getUint8(0);\n            let dataSize = v.getUint32(0, !le) & 0x00FFFFFF;\n\n            if (offset + 11 + dataSize + 4 > chunk.byteLength) {\n                // data not enough for parsing actual data body\n                break;\n            }\n\n            if (tagType !== 8 && tagType !== 9 && tagType !== 18) {\n                Log.w(this.TAG, `Unsupported tag type ${tagType}, skipped`);\n                // consume the whole tag (skip it)\n                offset += 11 + dataSize + 4;\n                continue;\n            }\n\n            let ts2 = v.getUint8(4);\n            let ts1 = v.getUint8(5);\n            let ts0 = v.getUint8(6);\n            let ts3 = v.getUint8(7);\n\n            let timestamp = ts0 | (ts1 << 8) | (ts2 << 16) | (ts3 << 24);\n\n            let streamId = v.getUint32(7, !le) & 0x00FFFFFF;\n            if (streamId !== 0) {\n                Log.w(this.TAG, 'Meet tag which has StreamID != 0!');\n            }\n\n            let dataOffset = offset + 11;\n\n            switch (tagType) {\n                case 8:  // Audio\n                    this._parseAudioData(chunk, dataOffset, dataSize, timestamp);\n                    break;\n                case 9:  // Video\n                    this._parseVideoData(chunk, dataOffset, dataSize, timestamp, byteStart + offset);\n                    break;\n                case 18:  // ScriptDataObject\n                    this._parseScriptData(chunk, dataOffset, dataSize);\n                    break;\n            }\n\n            let prevTagSize = v.getUint32(11 + dataSize, !le);\n            if (prevTagSize !== 11 + dataSize) {\n                Log.w(this.TAG, `Invalid PrevTagSize ${prevTagSize}`);\n            }\n\n            offset += 11 + dataSize + 4;  // tagBody + dataSize + prevTagSize\n        }\n\n        // dispatch parsed frames to consumer (typically, the remuxer)\n        if (this._isInitialMetadataDispatched()) {\n            if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {\n                this._onDataAvailable(this._audioTrack, this._videoTrack);\n            }\n        }\n\n        return offset;  // consumed bytes, just equals latest offset index\n    }\n\n    _parseScriptData(arrayBuffer, dataOffset, dataSize) {\n        let scriptData = AMF.parseScriptData(arrayBuffer, dataOffset, dataSize);\n\n        if (scriptData.hasOwnProperty('onMetaData')) {\n            if (scriptData.onMetaData == null || typeof scriptData.onMetaData !== 'object') {\n                Log.w(this.TAG, 'Invalid onMetaData structure!');\n                return;\n            }\n            if (this._metadata) {\n                Log.w(this.TAG, 'Found another onMetaData tag!');\n            }\n            this._metadata = scriptData;\n            let onMetaData = this._metadata.onMetaData;\n\n            if (this._onMetaDataArrived) {\n                this._onMetaDataArrived(Object.assign({}, onMetaData));\n            }\n\n            if (typeof onMetaData.hasAudio === 'boolean') {  // hasAudio\n                if (this._hasAudioFlagOverrided === false) {\n                    this._hasAudio = onMetaData.hasAudio;\n                    this._mediaInfo.hasAudio = this._hasAudio;\n                }\n            }\n            if (typeof onMetaData.hasVideo === 'boolean') {  // hasVideo\n                if (this._hasVideoFlagOverrided === false) {\n                    this._hasVideo = onMetaData.hasVideo;\n                    this._mediaInfo.hasVideo = this._hasVideo;\n                }\n            }\n            if (typeof onMetaData.audiodatarate === 'number') {  // audiodatarate\n                this._mediaInfo.audioDataRate = onMetaData.audiodatarate;\n            }\n            if (typeof onMetaData.videodatarate === 'number') {  // videodatarate\n                this._mediaInfo.videoDataRate = onMetaData.videodatarate;\n            }\n            if (typeof onMetaData.width === 'number') {  // width\n                this._mediaInfo.width = onMetaData.width;\n            }\n            if (typeof onMetaData.height === 'number') {  // height\n                this._mediaInfo.height = onMetaData.height;\n            }\n            if (typeof onMetaData.duration === 'number') {  // duration\n                if (!this._durationOverrided) {\n                    let duration = Math.floor(onMetaData.duration * this._timescale);\n                    this._duration = duration;\n                    this._mediaInfo.duration = duration;\n                }\n            } else {\n                this._mediaInfo.duration = 0;\n            }\n            if (typeof onMetaData.framerate === 'number') {  // framerate\n                let fps_num = Math.floor(onMetaData.framerate * 1000);\n                if (fps_num > 0) {\n                    let fps = fps_num / 1000;\n                    this._referenceFrameRate.fixed = true;\n                    this._referenceFrameRate.fps = fps;\n                    this._referenceFrameRate.fps_num = fps_num;\n                    this._referenceFrameRate.fps_den = 1000;\n                    this._mediaInfo.fps = fps;\n                }\n            }\n            if (typeof onMetaData.keyframes === 'object') {  // keyframes\n                this._mediaInfo.hasKeyframesIndex = true;\n                let keyframes = onMetaData.keyframes;\n                this._mediaInfo.keyframesIndex = this._parseKeyframesIndex(keyframes);\n                onMetaData.keyframes = null;  // keyframes has been extracted, remove it\n            } else {\n                this._mediaInfo.hasKeyframesIndex = false;\n            }\n            this._dispatch = false;\n            this._mediaInfo.metadata = onMetaData;\n            Log.v(this.TAG, 'Parsed onMetaData');\n            if (this._mediaInfo.isComplete()) {\n                this._onMediaInfo(this._mediaInfo);\n            }\n        }\n\n        if (Object.keys(scriptData).length > 0) {\n            if (this._onScriptDataArrived) {\n                this._onScriptDataArrived(Object.assign({}, scriptData));\n            }\n        }\n    }\n\n    _parseKeyframesIndex(keyframes) {\n        let times = [];\n        let filepositions = [];\n\n        // ignore first keyframe which is actually AVC Sequence Header (AVCDecoderConfigurationRecord)\n        for (let i = 1; i < keyframes.times.length; i++) {\n            let time = this._timestampBase + Math.floor(keyframes.times[i] * 1000);\n            times.push(time);\n            filepositions.push(keyframes.filepositions[i]);\n        }\n\n        return {\n            times: times,\n            filepositions: filepositions\n        };\n    }\n\n    _parseAudioData(arrayBuffer, dataOffset, dataSize, tagTimestamp) {\n        if (dataSize <= 1) {\n            Log.w(this.TAG, 'Flv: Invalid audio packet, missing SoundData payload!');\n            return;\n        }\n\n        if (this._hasAudioFlagOverrided === true && this._hasAudio === false) {\n            // If hasAudio: false indicated explicitly in MediaDataSource,\n            // Ignore all the audio packets\n            return;\n        }\n\n        let le = this._littleEndian;\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n\n        let soundSpec = v.getUint8(0);\n\n        let soundFormat = soundSpec >>> 4;\n        if (soundFormat !== 2 && soundFormat !== 10) {  // MP3 or AAC\n            this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat);\n            return;\n        }\n\n        let soundRate = 0;\n        let soundRateIndex = (soundSpec & 12) >>> 2;\n        if (soundRateIndex >= 0 && soundRateIndex <= 4) {\n            soundRate = this._flvSoundRateTable[soundRateIndex];\n        } else {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex);\n            return;\n        }\n\n        let soundSize = (soundSpec & 2) >>> 1;  // unused\n        let soundType = (soundSpec & 1);\n\n\n        let meta = this._audioMetadata;\n        let track = this._audioTrack;\n\n        if (!meta) {\n            if (this._hasAudio === false && this._hasAudioFlagOverrided === false) {\n                this._hasAudio = true;\n                this._mediaInfo.hasAudio = true;\n            }\n\n            // initial metadata\n            meta = this._audioMetadata = {};\n            meta.type = 'audio';\n            meta.id = track.id;\n            meta.timescale = this._timescale;\n            meta.duration = this._duration;\n            meta.audioSampleRate = soundRate;\n            meta.channelCount = (soundType === 0 ? 1 : 2);\n        }\n\n        if (soundFormat === 10) {  // AAC\n            let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1);\n            if (aacData == undefined) {\n                return;\n            }\n\n            if (aacData.packetType === 0) {  // AAC sequence header (AudioSpecificConfig)\n                if (meta.config) {\n                    Log.w(this.TAG, 'Found another AudioSpecificConfig!');\n                }\n                let misc = aacData.data;\n                meta.audioSampleRate = misc.samplingRate;\n                meta.channelCount = misc.channelCount;\n                meta.codec = misc.codec;\n                meta.originalCodec = misc.originalCodec;\n                meta.config = misc.config;\n                // The decode result of an aac sample is 1024 PCM samples\n                meta.refSampleDuration = 1024 / meta.audioSampleRate * meta.timescale;\n                Log.v(this.TAG, 'Parsed AudioSpecificConfig');\n\n                if (this._isInitialMetadataDispatched()) {\n                    // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer\n                    if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {\n                        this._onDataAvailable(this._audioTrack, this._videoTrack);\n                    }\n                } else {\n                    this._audioInitialMetadataDispatched = true;\n                }\n                // then notify new metadata\n                this._dispatch = false;\n                this._onTrackMetadata('audio', meta);\n\n                let mi = this._mediaInfo;\n                mi.audioCodec = meta.originalCodec;\n                mi.audioSampleRate = meta.audioSampleRate;\n                mi.audioChannelCount = meta.channelCount;\n                if (mi.hasVideo) {\n                    if (mi.videoCodec != null) {\n                        mi.mimeType = 'video/x-flv; codecs=\"' + mi.videoCodec + ',' + mi.audioCodec + '\"';\n                    }\n                } else {\n                    mi.mimeType = 'video/x-flv; codecs=\"' + mi.audioCodec + '\"';\n                }\n                if (mi.isComplete()) {\n                    this._onMediaInfo(mi);\n                }\n            } else if (aacData.packetType === 1) {  // AAC raw frame data\n                let dts = this._timestampBase + tagTimestamp;\n                let aacSample = {unit: aacData.data, length: aacData.data.byteLength, dts: dts, pts: dts};\n                track.samples.push(aacSample);\n                track.length += aacData.data.length;\n            } else {\n                Log.e(this.TAG, `Flv: Unsupported AAC data type ${aacData.packetType}`);\n            }\n        } else if (soundFormat === 2) {  // MP3\n            if (!meta.codec) {\n                // We need metadata for mp3 audio track, extract info from frame header\n                let misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true);\n                if (misc == undefined) {\n                    return;\n                }\n                meta.audioSampleRate = misc.samplingRate;\n                meta.channelCount = misc.channelCount;\n                meta.codec = misc.codec;\n                meta.originalCodec = misc.originalCodec;\n                // The decode result of an mp3 sample is 1152 PCM samples\n                meta.refSampleDuration = 1152 / meta.audioSampleRate * meta.timescale;\n                Log.v(this.TAG, 'Parsed MPEG Audio Frame Header');\n\n                this._audioInitialMetadataDispatched = true;\n                this._onTrackMetadata('audio', meta);\n\n                let mi = this._mediaInfo;\n                mi.audioCodec = meta.codec;\n                mi.audioSampleRate = meta.audioSampleRate;\n                mi.audioChannelCount = meta.channelCount;\n                mi.audioDataRate = misc.bitRate;\n                if (mi.hasVideo) {\n                    if (mi.videoCodec != null) {\n                        mi.mimeType = 'video/x-flv; codecs=\"' + mi.videoCodec + ',' + mi.audioCodec + '\"';\n                    }\n                } else {\n                    mi.mimeType = 'video/x-flv; codecs=\"' + mi.audioCodec + '\"';\n                }\n                if (mi.isComplete()) {\n                    this._onMediaInfo(mi);\n                }\n            }\n\n            // This packet is always a valid audio packet, extract it\n            let data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false);\n            if (data == undefined) {\n                return;\n            }\n            let dts = this._timestampBase + tagTimestamp;\n            let mp3Sample = {unit: data, length: data.byteLength, dts: dts, pts: dts};\n            track.samples.push(mp3Sample);\n            track.length += data.length;\n        }\n    }\n\n    _parseAACAudioData(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize <= 1) {\n            Log.w(this.TAG, 'Flv: Invalid AAC packet, missing AACPacketType or/and Data!');\n            return;\n        }\n\n        let result = {};\n        let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);\n\n        result.packetType = array[0];\n\n        if (array[0] === 0) {\n            result.data = this._parseAACAudioSpecificConfig(arrayBuffer, dataOffset + 1, dataSize - 1);\n        } else {\n            result.data = array.subarray(1);\n        }\n\n        return result;\n    }\n\n    _parseAACAudioSpecificConfig(arrayBuffer, dataOffset, dataSize) {\n        let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);\n        let config = null;\n\n        /* Audio Object Type:\n           0: Null\n           1: AAC Main\n           2: AAC LC\n           3: AAC SSR (Scalable Sample Rate)\n           4: AAC LTP (Long Term Prediction)\n           5: HE-AAC / SBR (Spectral Band Replication)\n           6: AAC Scalable\n        */\n\n        let audioObjectType = 0;\n        let originalAudioObjectType = 0;\n        let audioExtensionObjectType = null;\n        let samplingIndex = 0;\n        let extensionSamplingIndex = null;\n\n        // 5 bits\n        audioObjectType = originalAudioObjectType = array[0] >>> 3;\n        // 4 bits\n        samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7);\n        if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!');\n            return;\n        }\n\n        let samplingFrequence = this._mpegSamplingRates[samplingIndex];\n\n        // 4 bits\n        let channelConfig = (array[1] & 0x78) >>> 3;\n        if (channelConfig < 0 || channelConfig >= 8) {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid channel configuration');\n            return;\n        }\n\n        if (audioObjectType === 5) {  // HE-AAC?\n            // 4 bits\n            extensionSamplingIndex = ((array[1] & 0x07) << 1) | (array[2] >>> 7);\n            // 5 bits\n            audioExtensionObjectType = (array[2] & 0x7C) >>> 2;\n        }\n\n        // workarounds for various browsers\n        let userAgent = self.navigator.userAgent.toLowerCase();\n\n        if (userAgent.indexOf('firefox') !== -1) {\n            // firefox: use SBR (HE-AAC) if freq less than 24kHz\n            if (samplingIndex >= 6) {\n                audioObjectType = 5;\n                config = new Array(4);\n                extensionSamplingIndex = samplingIndex - 3;\n            } else {  // use LC-AAC\n                audioObjectType = 2;\n                config = new Array(2);\n                extensionSamplingIndex = samplingIndex;\n            }\n        } else if (userAgent.indexOf('android') !== -1) {\n            // android: always use LC-AAC\n            audioObjectType = 2;\n            config = new Array(2);\n            extensionSamplingIndex = samplingIndex;\n        } else {\n            // for other browsers, e.g. chrome...\n            // Always use HE-AAC to make it easier to switch aac codec profile\n            audioObjectType = 5;\n            extensionSamplingIndex = samplingIndex;\n            config = new Array(4);\n\n            if (samplingIndex >= 6) {\n                extensionSamplingIndex = samplingIndex - 3;\n            } else if (channelConfig === 1) {  // Mono channel\n                audioObjectType = 2;\n                config = new Array(2);\n                extensionSamplingIndex = samplingIndex;\n            }\n        }\n\n        config[0]  = audioObjectType << 3;\n        config[0] |= (samplingIndex & 0x0F) >>> 1;\n        config[1]  = (samplingIndex & 0x0F) << 7;\n        config[1] |= (channelConfig & 0x0F) << 3;\n        if (audioObjectType === 5) {\n            config[1] |= ((extensionSamplingIndex & 0x0F) >>> 1);\n            config[2]  = (extensionSamplingIndex & 0x01) << 7;\n            // extended audio object type: force to 2 (LC-AAC)\n            config[2] |= (2 << 2);\n            config[3]  = 0;\n        }\n\n        return {\n            config: config,\n            samplingRate: samplingFrequence,\n            channelCount: channelConfig,\n            codec: 'mp4a.40.' + audioObjectType,\n            originalCodec: 'mp4a.40.' + originalAudioObjectType\n        };\n    }\n\n    _parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) {\n        if (dataSize < 4) {\n            Log.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!');\n            return;\n        }\n\n        let le = this._littleEndian;\n        let array = new Uint8Array(arrayBuffer, dataOffset, dataSize);\n        let result = null;\n\n        if (requestHeader) {\n            if (array[0] !== 0xFF) {\n                return;\n            }\n            let ver = (array[1] >>> 3) & 0x03;\n            let layer = (array[1] & 0x06) >> 1;\n\n            let bitrate_index = (array[2] & 0xF0) >>> 4;\n            let sampling_freq_index = (array[2] & 0x0C) >>> 2;\n\n            let channel_mode = (array[3] >>> 6) & 0x03;\n            let channel_count = channel_mode !== 3 ? 2 : 1;\n\n            let sample_rate = 0;\n            let bit_rate = 0;\n            let object_type = 34;  // Layer-3, listed in MPEG-4 Audio Object Types\n\n            let codec = 'mp3';\n\n            switch (ver) {\n                case 0:  // MPEG 2.5\n                    sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index];\n                    break;\n                case 2:  // MPEG 2\n                    sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index];\n                    break;\n                case 3:  // MPEG 1\n                    sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index];\n                    break;\n            }\n\n            switch (layer) {\n                case 1:  // Layer 3\n                    object_type = 34;\n                    if (bitrate_index < this._mpegAudioL3BitRateTable.length) {\n                        bit_rate = this._mpegAudioL3BitRateTable[bitrate_index];\n                    }\n                    break;\n                case 2:  // Layer 2\n                    object_type = 33;\n                    if (bitrate_index < this._mpegAudioL2BitRateTable.length) {\n                        bit_rate = this._mpegAudioL2BitRateTable[bitrate_index];\n                    }\n                    break;\n                case 3:  // Layer 1\n                    object_type = 32;\n                    if (bitrate_index < this._mpegAudioL1BitRateTable.length) {\n                        bit_rate = this._mpegAudioL1BitRateTable[bitrate_index];\n                    }\n                    break;\n            }\n\n            result = {\n                bitRate: bit_rate,\n                samplingRate: sample_rate,\n                channelCount: channel_count,\n                codec: codec,\n                originalCodec: codec\n            };\n        } else {\n            result = array;\n        }\n\n        return result;\n    }\n\n    _parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) {\n        if (dataSize <= 1) {\n            Log.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!');\n            return;\n        }\n\n        if (this._hasVideoFlagOverrided === true && this._hasVideo === false) {\n            // If hasVideo: false indicated explicitly in MediaDataSource,\n            // Ignore all the video packets\n            return;\n        }\n\n        let spec = (new Uint8Array(arrayBuffer, dataOffset, dataSize))[0];\n\n        let frameType = (spec & 240) >>> 4;\n        let codecId = spec & 15;\n\n        if (codecId !== 7) {\n            this._onError(DemuxErrors.CODEC_UNSUPPORTED, `Flv: Unsupported codec in video frame: ${codecId}`);\n            return;\n        }\n\n        this._parseAVCVideoPacket(arrayBuffer, dataOffset + 1, dataSize - 1, tagTimestamp, tagPosition, frameType);\n    }\n\n    _parseAVCVideoPacket(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType) {\n        if (dataSize < 4) {\n            Log.w(this.TAG, 'Flv: Invalid AVC packet, missing AVCPacketType or/and CompositionTime');\n            return;\n        }\n\n        let le = this._littleEndian;\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n\n        let packetType = v.getUint8(0);\n        let cts_unsigned = v.getUint32(0, !le) & 0x00FFFFFF;\n        let cts = (cts_unsigned << 8) >> 8;  // convert to 24-bit signed int\n\n        if (packetType === 0) {  // AVCDecoderConfigurationRecord\n            this._parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset + 4, dataSize - 4);\n        } else if (packetType === 1) {  // One or more Nalus\n            this._parseAVCVideoData(arrayBuffer, dataOffset + 4, dataSize - 4, tagTimestamp, tagPosition, frameType, cts);\n        } else if (packetType === 2) {\n            // empty, AVC end of sequence\n        } else {\n            this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Invalid video packet type ${packetType}`);\n            return;\n        }\n    }\n\n    _parseAVCDecoderConfigurationRecord(arrayBuffer, dataOffset, dataSize) {\n        if (dataSize < 7) {\n            Log.w(this.TAG, 'Flv: Invalid AVCDecoderConfigurationRecord, lack of data!');\n            return;\n        }\n\n        let meta = this._videoMetadata;\n        let track = this._videoTrack;\n        let le = this._littleEndian;\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n\n        if (!meta) {\n            if (this._hasVideo === false && this._hasVideoFlagOverrided === false) {\n                this._hasVideo = true;\n                this._mediaInfo.hasVideo = true;\n            }\n\n            meta = this._videoMetadata = {};\n            meta.type = 'video';\n            meta.id = track.id;\n            meta.timescale = this._timescale;\n            meta.duration = this._duration;\n        } else {\n            if (typeof meta.avcc !== 'undefined') {\n                Log.w(this.TAG, 'Found another AVCDecoderConfigurationRecord!');\n            }\n        }\n\n        let version = v.getUint8(0);  // configurationVersion\n        let avcProfile = v.getUint8(1);  // avcProfileIndication\n        let profileCompatibility = v.getUint8(2);  // profile_compatibility\n        let avcLevel = v.getUint8(3);  // AVCLevelIndication\n\n        if (version !== 1 || avcProfile === 0) {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord');\n            return;\n        }\n\n        this._naluLengthSize = (v.getUint8(4) & 3) + 1;  // lengthSizeMinusOne\n        if (this._naluLengthSize !== 3 && this._naluLengthSize !== 4) {  // holy shit!!!\n            this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${this._naluLengthSize - 1}`);\n            return;\n        }\n\n        let spsCount = v.getUint8(5) & 31;  // numOfSequenceParameterSets\n        if (spsCount === 0) {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS');\n            return;\n        } else if (spsCount > 1) {\n            Log.w(this.TAG, `Flv: Strange AVCDecoderConfigurationRecord: SPS Count = ${spsCount}`);\n        }\n\n        let offset = 6;\n\n        for (let i = 0; i < spsCount; i++) {\n            let len = v.getUint16(offset, !le);  // sequenceParameterSetLength\n            offset += 2;\n\n            if (len === 0) {\n                continue;\n            }\n\n            // Notice: Nalu without startcode header (00 00 00 01)\n            let sps = new Uint8Array(arrayBuffer, dataOffset + offset, len);\n            offset += len;\n\n            let config = SPSParser.parseSPS(sps);\n            if (i !== 0) {\n                // ignore other sps's config\n                continue;\n            }\n\n            meta.codecWidth = config.codec_size.width;\n            meta.codecHeight = config.codec_size.height;\n            meta.presentWidth = config.present_size.width;\n            meta.presentHeight = config.present_size.height;\n\n            meta.profile = config.profile_string;\n            meta.level = config.level_string;\n            meta.bitDepth = config.bit_depth;\n            meta.chromaFormat = config.chroma_format;\n            meta.sarRatio = config.sar_ratio;\n            meta.frameRate = config.frame_rate;\n\n            if (config.frame_rate.fixed === false ||\n                config.frame_rate.fps_num === 0 ||\n                config.frame_rate.fps_den === 0) {\n                meta.frameRate = this._referenceFrameRate;\n            }\n\n            let fps_den = meta.frameRate.fps_den;\n            let fps_num = meta.frameRate.fps_num;\n            meta.refSampleDuration = meta.timescale * (fps_den / fps_num);\n\n            let codecArray = sps.subarray(1, 4);\n            let codecString = 'avc1.';\n            for (let j = 0; j < 3; j++) {\n                let h = codecArray[j].toString(16);\n                if (h.length < 2) {\n                    h = '0' + h;\n                }\n                codecString += h;\n            }\n            meta.codec = codecString;\n\n            let mi = this._mediaInfo;\n            mi.width = meta.codecWidth;\n            mi.height = meta.codecHeight;\n            mi.fps = meta.frameRate.fps;\n            mi.profile = meta.profile;\n            mi.level = meta.level;\n            mi.refFrames = config.ref_frames;\n            mi.chromaFormat = config.chroma_format_string;\n            mi.sarNum = meta.sarRatio.width;\n            mi.sarDen = meta.sarRatio.height;\n            mi.videoCodec = codecString;\n\n            if (mi.hasAudio) {\n                if (mi.audioCodec != null) {\n                    mi.mimeType = 'video/x-flv; codecs=\"' + mi.videoCodec + ',' + mi.audioCodec + '\"';\n                }\n            } else {\n                mi.mimeType = 'video/x-flv; codecs=\"' + mi.videoCodec + '\"';\n            }\n            if (mi.isComplete()) {\n                this._onMediaInfo(mi);\n            }\n        }\n\n        let ppsCount = v.getUint8(offset);  // numOfPictureParameterSets\n        if (ppsCount === 0) {\n            this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS');\n            return;\n        } else if (ppsCount > 1) {\n            Log.w(this.TAG, `Flv: Strange AVCDecoderConfigurationRecord: PPS Count = ${ppsCount}`);\n        }\n\n        offset++;\n\n        for (let i = 0; i < ppsCount; i++) {\n            let len = v.getUint16(offset, !le);  // pictureParameterSetLength\n            offset += 2;\n\n            if (len === 0) {\n                continue;\n            }\n\n            // pps is useless for extracting video information\n            offset += len;\n        }\n\n        meta.avcc = new Uint8Array(dataSize);\n        meta.avcc.set(new Uint8Array(arrayBuffer, dataOffset, dataSize), 0);\n        Log.v(this.TAG, 'Parsed AVCDecoderConfigurationRecord');\n\n        if (this._isInitialMetadataDispatched()) {\n            // flush parsed frames\n            if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) {\n                this._onDataAvailable(this._audioTrack, this._videoTrack);\n            }\n        } else {\n            this._videoInitialMetadataDispatched = true;\n        }\n        // notify new metadata\n        this._dispatch = false;\n        this._onTrackMetadata('video', meta);\n    }\n\n    _parseAVCVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition, frameType, cts) {\n        let le = this._littleEndian;\n        let v = new DataView(arrayBuffer, dataOffset, dataSize);\n\n        let units = [], length = 0;\n\n        let offset = 0;\n        const lengthSize = this._naluLengthSize;\n        let dts = this._timestampBase + tagTimestamp;\n        let keyframe = (frameType === 1);  // from FLV Frame Type constants\n\n        while (offset < dataSize) {\n            if (offset + 4 >= dataSize) {\n                Log.w(this.TAG, `Malformed Nalu near timestamp ${dts}, offset = ${offset}, dataSize = ${dataSize}`);\n                break;  // data not enough for next Nalu\n            }\n            // Nalu with length-header (AVC1)\n            let naluSize = v.getUint32(offset, !le);  // Big-Endian read\n            if (lengthSize === 3) {\n                naluSize >>>= 8;\n            }\n            if (naluSize > dataSize - lengthSize) {\n                Log.w(this.TAG, `Malformed Nalus near timestamp ${dts}, NaluSize > DataSize!`);\n                return;\n            }\n\n            let unitType = v.getUint8(offset + lengthSize) & 0x1F;\n\n            if (unitType === 5) {  // IDR\n                keyframe = true;\n            }\n\n            let data = new Uint8Array(arrayBuffer, dataOffset + offset, lengthSize + naluSize);\n            let unit = {type: unitType, data: data};\n            units.push(unit);\n            length += data.byteLength;\n\n            offset += lengthSize + naluSize;\n        }\n\n        if (units.length) {\n            let track = this._videoTrack;\n            let avcSample = {\n                units: units,\n                length: length,\n                isKeyframe: keyframe,\n                dts: dts,\n                cts: cts,\n                pts: (dts + cts)\n            };\n            if (keyframe) {\n                avcSample.fileposition = tagPosition;\n            }\n            track.samples.push(avcSample);\n            track.length += length;\n        }\n    }\n\n}\n\nexport default FLVDemuxer;"
  },
  {
    "path": "src/demux/sps-parser.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport ExpGolomb from './exp-golomb.js';\n\nclass SPSParser {\n\n    static _ebsp2rbsp(uint8array) {\n        let src = uint8array;\n        let src_length = src.byteLength;\n        let dst = new Uint8Array(src_length);\n        let dst_idx = 0;\n\n        for (let i = 0; i < src_length; i++) {\n            if (i >= 2) {\n                // Unescape: Skip 0x03 after 00 00\n                if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) {\n                    continue;\n                }\n            }\n            dst[dst_idx] = src[i];\n            dst_idx++;\n        }\n\n        return new Uint8Array(dst.buffer, 0, dst_idx);\n    }\n\n    static parseSPS(uint8array) {\n        let rbsp = SPSParser._ebsp2rbsp(uint8array);\n        let gb = new ExpGolomb(rbsp);\n\n        gb.readByte();\n        let profile_idc = gb.readByte();  // profile_idc\n        gb.readByte();  // constraint_set_flags[5] + reserved_zero[3]\n        let level_idc = gb.readByte();  // level_idc\n        gb.readUEG();  // seq_parameter_set_id\n\n        let profile_string = SPSParser.getProfileString(profile_idc);\n        let level_string = SPSParser.getLevelString(level_idc);\n        let chroma_format_idc = 1;\n        let chroma_format = 420;\n        let chroma_format_table = [0, 420, 422, 444];\n        let bit_depth = 8;\n\n        if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 ||\n            profile_idc === 244 || profile_idc === 44 || profile_idc === 83 ||\n            profile_idc === 86 || profile_idc === 118 || profile_idc === 128 ||\n            profile_idc === 138 || profile_idc === 144) {\n\n            chroma_format_idc = gb.readUEG();\n            if (chroma_format_idc === 3) {\n                gb.readBits(1);  // separate_colour_plane_flag\n            }\n            if (chroma_format_idc <= 3) {\n                chroma_format = chroma_format_table[chroma_format_idc];\n            }\n\n            bit_depth = gb.readUEG() + 8;  // bit_depth_luma_minus8\n            gb.readUEG();  // bit_depth_chroma_minus8\n            gb.readBits(1);  // qpprime_y_zero_transform_bypass_flag\n            if (gb.readBool()) {  // seq_scaling_matrix_present_flag\n                let scaling_list_count = (chroma_format_idc !== 3) ? 8 : 12;\n                for (let i = 0; i < scaling_list_count; i++) {\n                    if (gb.readBool()) {  // seq_scaling_list_present_flag\n                        if (i < 6) {\n                            SPSParser._skipScalingList(gb, 16);\n                        } else {\n                            SPSParser._skipScalingList(gb, 64);\n                        }\n                    }\n                }\n            }\n        }\n        gb.readUEG();  // log2_max_frame_num_minus4\n        let pic_order_cnt_type = gb.readUEG();\n        if (pic_order_cnt_type === 0) {\n            gb.readUEG();  // log2_max_pic_order_cnt_lsb_minus_4\n        } else if (pic_order_cnt_type === 1) {\n            gb.readBits(1);  // delta_pic_order_always_zero_flag\n            gb.readSEG();  // offset_for_non_ref_pic\n            gb.readSEG();  // offset_for_top_to_bottom_field\n            let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG();\n            for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {\n                gb.readSEG();  // offset_for_ref_frame\n            }\n        }\n        let ref_frames = gb.readUEG();  // max_num_ref_frames\n        gb.readBits(1);  // gaps_in_frame_num_value_allowed_flag\n\n        let pic_width_in_mbs_minus1 = gb.readUEG();\n        let pic_height_in_map_units_minus1 = gb.readUEG();\n\n        let frame_mbs_only_flag = gb.readBits(1);\n        if (frame_mbs_only_flag === 0) {\n            gb.readBits(1);  // mb_adaptive_frame_field_flag\n        }\n        gb.readBits(1);  // direct_8x8_inference_flag\n\n        let frame_crop_left_offset = 0;\n        let frame_crop_right_offset = 0;\n        let frame_crop_top_offset = 0;\n        let frame_crop_bottom_offset = 0;\n\n        let frame_cropping_flag = gb.readBool();\n        if (frame_cropping_flag) {\n            frame_crop_left_offset = gb.readUEG();\n            frame_crop_right_offset = gb.readUEG();\n            frame_crop_top_offset = gb.readUEG();\n            frame_crop_bottom_offset = gb.readUEG();\n        }\n\n        let sar_width = 1, sar_height = 1;\n        let fps = 0, fps_fixed = true, fps_num = 0, fps_den = 0;\n\n        let vui_parameters_present_flag = gb.readBool();\n        if (vui_parameters_present_flag) {\n            if (gb.readBool()) {  // aspect_ratio_info_present_flag\n                let aspect_ratio_idc = gb.readByte();\n                let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2];\n                let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33,  99, 3, 2, 1];\n\n                if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) {\n                    sar_width = sar_w_table[aspect_ratio_idc - 1];\n                    sar_height = sar_h_table[aspect_ratio_idc - 1];\n                } else if (aspect_ratio_idc === 255) {\n                    sar_width = gb.readByte() << 8 | gb.readByte();\n                    sar_height = gb.readByte() << 8 | gb.readByte();\n                }\n            }\n\n            if (gb.readBool()) {  // overscan_info_present_flag\n                gb.readBool();  // overscan_appropriate_flag\n            }\n            if (gb.readBool()) {  // video_signal_type_present_flag\n                gb.readBits(4);  // video_format & video_full_range_flag\n                if (gb.readBool()) {  // colour_description_present_flag\n                    gb.readBits(24);  // colour_primaries & transfer_characteristics & matrix_coefficients\n                }\n            }\n            if (gb.readBool()) {  // chroma_loc_info_present_flag\n                gb.readUEG();  // chroma_sample_loc_type_top_field\n                gb.readUEG();  // chroma_sample_loc_type_bottom_field\n            }\n            if (gb.readBool()) {  // timing_info_present_flag\n                let num_units_in_tick = gb.readBits(32);\n                let time_scale = gb.readBits(32);\n                fps_fixed = gb.readBool();  // fixed_frame_rate_flag\n\n                fps_num = time_scale;\n                fps_den = num_units_in_tick * 2;\n                fps = fps_num / fps_den;\n            }\n        }\n\n        let sarScale = 1;\n        if (sar_width !== 1 || sar_height !== 1) {\n            sarScale = sar_width / sar_height;\n        }\n\n        let crop_unit_x = 0, crop_unit_y = 0;\n        if (chroma_format_idc === 0) {\n            crop_unit_x = 1;\n            crop_unit_y = 2 - frame_mbs_only_flag;\n        } else {\n            let sub_wc = (chroma_format_idc === 3) ? 1 : 2;\n            let sub_hc = (chroma_format_idc === 1) ? 2 : 1;\n            crop_unit_x = sub_wc;\n            crop_unit_y = sub_hc * (2 - frame_mbs_only_flag);\n        }\n\n        let codec_width = (pic_width_in_mbs_minus1 + 1) * 16;\n        let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16);\n\n        codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x;\n        codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y;\n\n        let present_width = Math.ceil(codec_width * sarScale);\n\n        gb.destroy();\n        gb = null;\n\n        return {\n            profile_string: profile_string,  // baseline, high, high10, ...\n            level_string: level_string,  // 3, 3.1, 4, 4.1, 5, 5.1, ...\n            bit_depth: bit_depth,  // 8bit, 10bit, ...\n            ref_frames: ref_frames,\n            chroma_format: chroma_format,  // 4:2:0, 4:2:2, ...\n            chroma_format_string: SPSParser.getChromaFormatString(chroma_format),\n\n            frame_rate: {\n                fixed: fps_fixed,\n                fps: fps,\n                fps_den: fps_den,\n                fps_num: fps_num\n            },\n\n            sar_ratio: {\n                width: sar_width,\n                height: sar_height\n            },\n\n            codec_size: {\n                width: codec_width,\n                height: codec_height\n            },\n\n            present_size: {\n                width: present_width,\n                height: codec_height\n            }\n        };\n    }\n\n    static _skipScalingList(gb, count) {\n        let last_scale = 8, next_scale = 8;\n        let delta_scale = 0;\n        for (let i = 0; i < count; i++) {\n            if (next_scale !== 0) {\n                delta_scale = gb.readSEG();\n                next_scale = (last_scale + delta_scale + 256) % 256;\n            }\n            last_scale = (next_scale === 0) ? last_scale : next_scale;\n        }\n    }\n\n    static getProfileString(profile_idc) {\n        switch (profile_idc) {\n            case 66:\n                return 'Baseline';\n            case 77:\n                return 'Main';\n            case 88:\n                return 'Extended';\n            case 100:\n                return 'High';\n            case 110:\n                return 'High10';\n            case 122:\n                return 'High422';\n            case 244:\n                return 'High444';\n            default:\n                return 'Unknown';\n        }\n    }\n\n    static getLevelString(level_idc) {\n        return (level_idc / 10).toFixed(1);\n    }\n\n    static getChromaFormatString(chroma) {\n        switch (chroma) {\n            case 420:\n                return '4:2:0';\n            case 422:\n                return '4:2:2';\n            case 444:\n                return '4:4:4';\n            default:\n                return 'Unknown';\n        }\n    }\n\n}\n\nexport default SPSParser;"
  },
  {
    "path": "src/flv.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Polyfill from './utils/polyfill.js';\nimport Features from './core/features.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './io/loader.js';\nimport FlvPlayer from './player/flv-player.js';\nimport NativePlayer from './player/native-player.js';\nimport PlayerEvents from './player/player-events.js';\nimport {ErrorTypes, ErrorDetails} from './player/player-errors.js';\nimport LoggingControl from './utils/logging-control.js';\nimport {InvalidArgumentException} from './utils/exception.js';\n\n// here are all the interfaces\n\n// install polyfills\nPolyfill.install();\n\n\n// factory method\nfunction createPlayer(mediaDataSource, optionalConfig) {\n    let mds = mediaDataSource;\n    if (mds == null || typeof mds !== 'object') {\n        throw new InvalidArgumentException('MediaDataSource must be an javascript object!');\n    }\n\n    if (!mds.hasOwnProperty('type')) {\n        throw new InvalidArgumentException('MediaDataSource must has type field to indicate video file type!');\n    }\n\n    switch (mds.type) {\n        case 'flv':\n            return new FlvPlayer(mds, optionalConfig);\n        default:\n            return new NativePlayer(mds, optionalConfig);\n    }\n}\n\n\n// feature detection\nfunction isSupported() {\n    return Features.supportMSEH264Playback();\n}\n\nfunction getFeatureList() {\n    return Features.getFeatureList();\n}\n\n\n// interfaces\nlet flvjs = {};\n\nflvjs.createPlayer = createPlayer;\nflvjs.isSupported = isSupported;\nflvjs.getFeatureList = getFeatureList;\n\nflvjs.BaseLoader = BaseLoader;\nflvjs.LoaderStatus = LoaderStatus;\nflvjs.LoaderErrors = LoaderErrors;\n\nflvjs.Events = PlayerEvents;\nflvjs.ErrorTypes = ErrorTypes;\nflvjs.ErrorDetails = ErrorDetails;\n\nflvjs.FlvPlayer = FlvPlayer;\nflvjs.NativePlayer = NativePlayer;\nflvjs.LoggingControl = LoggingControl;\n\nObject.defineProperty(flvjs, 'version', {\n    enumerable: true,\n    get: function () {\n        // replace by webpack.DefinePlugin\n        return __VERSION__;\n    }\n});\n\nexport default flvjs;"
  },
  {
    "path": "src/index.js",
    "content": "// entry/index file\n\n// make it compatible with browserify's umd wrapper\nmodule.exports = require('./flv.js').default;\n"
  },
  {
    "path": "src/io/fetch-stream-loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport Browser from '../utils/browser.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';\nimport {RuntimeException} from '../utils/exception.js';\n\n/* fetch + stream IO loader. Currently working on chrome 43+.\n * fetch provides a better alternative http API to XMLHttpRequest\n *\n * fetch spec   https://fetch.spec.whatwg.org/\n * stream spec  https://streams.spec.whatwg.org/\n */\nclass FetchStreamLoader extends BaseLoader {\n\n    static isSupported() {\n        try {\n            // fetch + stream is broken on Microsoft Edge. Disable before build 15048.\n            // see https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8196907/\n            // Fixed in Jan 10, 2017. Build 15048+ removed from blacklist.\n            let isWorkWellEdge = Browser.msedge && Browser.version.minor >= 15048;\n            let browserNotBlacklisted = Browser.msedge ? isWorkWellEdge : true;\n            return (self.fetch && self.ReadableStream && browserNotBlacklisted);\n        } catch (e) {\n            return false;\n        }\n    }\n\n    constructor(seekHandler, config) {\n        super('fetch-stream-loader');\n        this.TAG = 'FetchStreamLoader';\n\n        this._seekHandler = seekHandler;\n        this._config = config;\n        this._needStash = true;\n\n        this._requestAbort = false;\n        this._contentLength = null;\n        this._receivedLength = 0;\n    }\n\n    destroy() {\n        if (this.isWorking()) {\n            this.abort();\n        }\n        super.destroy();\n    }\n\n    open(dataSource, range) {\n        this._dataSource = dataSource;\n        this._range = range;\n\n        let sourceURL = dataSource.url;\n        if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) {\n            sourceURL = dataSource.redirectedURL;\n        }\n\n        let seekConfig = this._seekHandler.getConfig(sourceURL, range);\n\n        let headers = new self.Headers();\n\n        if (typeof seekConfig.headers === 'object') {\n            let configHeaders = seekConfig.headers;\n            for (let key in configHeaders) {\n                if (configHeaders.hasOwnProperty(key)) {\n                    headers.append(key, configHeaders[key]);\n                }\n            }\n        }\n\n        let params = {\n            method: 'GET',\n            headers: headers,\n            mode: 'cors',\n            cache: 'default',\n            // The default policy of Fetch API in the whatwg standard\n            // Safari incorrectly indicates 'no-referrer' as default policy, fuck it\n            referrerPolicy: 'no-referrer-when-downgrade'\n        };\n\n        // add additional headers\n        if (typeof this._config.headers === 'object') {\n            for (let key in this._config.headers) {\n                headers.append(key, this._config.headers[key]);\n            }\n        }\n\n        // cors is enabled by default\n        if (dataSource.cors === false) {\n            // no-cors means 'disregard cors policy', which can only be used in ServiceWorker\n            params.mode = 'same-origin';\n        }\n\n        // withCredentials is disabled by default\n        if (dataSource.withCredentials) {\n            params.credentials = 'include';\n        }\n\n        // referrerPolicy from config\n        if (dataSource.referrerPolicy) {\n            params.referrerPolicy = dataSource.referrerPolicy;\n        }\n\n        // add abort controller, by wmlgl 2019-5-10 12:21:27\n        if (self.AbortController) {\n            this._abortController = new self.AbortController();\n            params.signal = this._abortController.signal;     \n        }\n\n        this._status = LoaderStatus.kConnecting;\n        self.fetch(seekConfig.url, params).then((res) => {\n            if (this._requestAbort) {\n                this._status = LoaderStatus.kIdle;\n                res.body.cancel();\n                return;\n            }\n            if (res.ok && (res.status >= 200 && res.status <= 299)) {\n                if (res.url !== seekConfig.url) {\n                    if (this._onURLRedirect) {\n                        let redirectedURL = this._seekHandler.removeURLParameters(res.url);\n                        this._onURLRedirect(redirectedURL);\n                    }\n                }\n\n                let lengthHeader = res.headers.get('Content-Length');\n                if (lengthHeader != null) {\n                    this._contentLength = parseInt(lengthHeader);\n                    if (this._contentLength !== 0) {\n                        if (this._onContentLengthKnown) {\n                            this._onContentLengthKnown(this._contentLength);\n                        }\n                    }\n                }\n\n                return this._pump.call(this, res.body.getReader());\n            } else {\n                this._status = LoaderStatus.kError;\n                if (this._onError) {\n                    this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: res.status, msg: res.statusText});\n                } else {\n                    throw new RuntimeException('FetchStreamLoader: Http code invalid, ' + res.status + ' ' + res.statusText);\n                }\n            }\n        }).catch((e) => {\n            if (this._abortController && this._abortController.signal.aborted) {\n                return;\n            }\n\n            this._status = LoaderStatus.kError;\n            if (this._onError) {\n                this._onError(LoaderErrors.EXCEPTION, {code: -1, msg: e.message});\n            } else {\n                throw e;\n            }\n        });\n    }\n\n    abort() {\n        this._requestAbort = true;\n\n        if (this._status !== LoaderStatus.kBuffering || !Browser.chrome) {\n            // Chrome may throw Exception-like things here, avoid using if is buffering\n            if (this._abortController) {\n                try {\n                    this._abortController.abort();\n                } catch (e) {}\n            }\n        }\n    }\n\n    _pump(reader) {  // ReadableStreamReader\n        return reader.read().then((result) => {\n            if (result.done) {\n                // First check received length\n                if (this._contentLength !== null && this._receivedLength < this._contentLength) {\n                    // Report Early-EOF\n                    this._status = LoaderStatus.kError;\n                    let type = LoaderErrors.EARLY_EOF;\n                    let info = {code: -1, msg: 'Fetch stream meet Early-EOF'};\n                    if (this._onError) {\n                        this._onError(type, info);\n                    } else {\n                        throw new RuntimeException(info.msg);\n                    }\n                } else {\n                    // OK. Download complete\n                    this._status = LoaderStatus.kComplete;\n                    if (this._onComplete) {\n                        this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);\n                    }\n                }\n            } else {\n                if (this._abortController && this._abortController.signal.aborted) {\n                    this._status = LoaderStatus.kComplete;\n                    return;\n                } else if (this._requestAbort === true) {\n                    this._status = LoaderStatus.kComplete;\n                    return reader.cancel();\n                }\n\n                this._status = LoaderStatus.kBuffering;\n\n                let chunk = result.value.buffer;\n                let byteStart = this._range.from + this._receivedLength;\n                this._receivedLength += chunk.byteLength;\n\n                if (this._onDataArrival) {\n                    this._onDataArrival(chunk, byteStart, this._receivedLength);\n                }\n\n                this._pump(reader);\n            }\n        }).catch((e) => {\n            if (this._abortController && this._abortController.signal.aborted) {\n                this._status = LoaderStatus.kComplete;\n                return;\n            }\n\n            if (e.code === 11 && Browser.msedge) {  // InvalidStateError on Microsoft Edge\n                // Workaround: Edge may throw InvalidStateError after ReadableStreamReader.cancel() call\n                // Ignore the unknown exception.\n                // Related issue: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/11265202/\n                return;\n            }\n\n            this._status = LoaderStatus.kError;\n            let type = 0;\n            let info = null;\n\n            if ((e.code === 19 || e.message === 'network error') && // NETWORK_ERR\n                (this._contentLength === null ||\n                (this._contentLength !== null && this._receivedLength < this._contentLength))) {\n                type = LoaderErrors.EARLY_EOF;\n                info = {code: e.code, msg: 'Fetch stream meet Early-EOF'};\n            } else {\n                type = LoaderErrors.EXCEPTION;\n                info = {code: e.code, msg: e.message};\n            }\n\n            if (this._onError) {\n                this._onError(type, info);\n            } else {\n                throw new RuntimeException(info.msg);\n            }\n        });\n    }\n\n}\n\nexport default FetchStreamLoader;\n"
  },
  {
    "path": "src/io/io-controller.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport SpeedSampler from './speed-sampler.js';\nimport {LoaderStatus, LoaderErrors} from './loader.js';\nimport FetchStreamLoader from './fetch-stream-loader.js';\nimport MozChunkedLoader from './xhr-moz-chunked-loader.js';\nimport MSStreamLoader from './xhr-msstream-loader.js';\nimport RangeLoader from './xhr-range-loader.js';\nimport WebSocketLoader from './websocket-loader.js';\nimport RangeSeekHandler from './range-seek-handler.js';\nimport ParamSeekHandler from './param-seek-handler.js';\nimport {RuntimeException, IllegalStateException, InvalidArgumentException} from '../utils/exception.js';\n\n/**\n * DataSource: {\n *     url: string,\n *     filesize: number,\n *     cors: boolean,\n *     withCredentials: boolean\n * }\n * \n */\n\n// Manage IO Loaders\nclass IOController {\n\n    constructor(dataSource, config, extraData) {\n        this.TAG = 'IOController';\n\n        this._config = config;\n        this._extraData = extraData;\n\n        this._stashInitialSize = 1024 * 384;  // default initial size: 384KB\n        if (config.stashInitialSize != undefined && config.stashInitialSize > 0) {\n            // apply from config\n            this._stashInitialSize = config.stashInitialSize;\n        }\n\n        this._stashUsed = 0;\n        this._stashSize = this._stashInitialSize;\n        this._bufferSize = 1024 * 1024 * 3;  // initial size: 3MB\n        this._stashBuffer = new ArrayBuffer(this._bufferSize);\n        this._stashByteStart = 0;\n        this._enableStash = true;\n        if (config.enableStashBuffer === false) {\n            this._enableStash = false;\n        }\n\n        this._loader = null;\n        this._loaderClass = null;\n        this._seekHandler = null;\n\n        this._dataSource = dataSource;\n        this._isWebSocketURL = /wss?:\\/\\/(.+?)/.test(dataSource.url);\n        this._refTotalLength = dataSource.filesize ? dataSource.filesize : null;\n        this._totalLength = this._refTotalLength;\n        this._fullRequestFlag = false;\n        this._currentRange = null;\n        this._redirectedURL = null;\n\n        this._speedNormalized = 0;\n        this._speedSampler = new SpeedSampler();\n        this._speedNormalizeList = [64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096];\n\n        this._isEarlyEofReconnecting = false;\n\n        this._paused = false;\n        this._resumeFrom = 0;\n\n        this._onDataArrival = null;\n        this._onSeeked = null;\n        this._onError = null;\n        this._onComplete = null;\n        this._onRedirect = null;\n        this._onRecoveredEarlyEof = null;\n\n        this._selectSeekHandler();\n        this._selectLoader();\n        this._createLoader();\n    }\n\n    destroy() {\n        if (this._loader.isWorking()) {\n            this._loader.abort();\n        }\n        this._loader.destroy();\n        this._loader = null;\n        this._loaderClass = null;\n        this._dataSource = null;\n        this._stashBuffer = null;\n        this._stashUsed = this._stashSize = this._bufferSize = this._stashByteStart = 0;\n        this._currentRange = null;\n        this._speedSampler = null;\n\n        this._isEarlyEofReconnecting = false;\n\n        this._onDataArrival = null;\n        this._onSeeked = null;\n        this._onError = null;\n        this._onComplete = null;\n        this._onRedirect = null;\n        this._onRecoveredEarlyEof = null;\n\n        this._extraData = null;\n    }\n\n    isWorking() {\n        return this._loader && this._loader.isWorking() && !this._paused;\n    }\n\n    isPaused() {\n        return this._paused;\n    }\n\n    get status() {\n        return this._loader.status;\n    }\n\n    get extraData() {\n        return this._extraData;\n    }\n\n    set extraData(data) {\n        this._extraData = data;\n    }\n\n    // prototype: function onDataArrival(chunks: ArrayBuffer, byteStart: number): number\n    get onDataArrival() {\n        return this._onDataArrival;\n    }\n\n    set onDataArrival(callback) {\n        this._onDataArrival = callback;\n    }\n\n    get onSeeked() {\n        return this._onSeeked;\n    }\n\n    set onSeeked(callback) {\n        this._onSeeked = callback;\n    }\n\n    // prototype: function onError(type: number, info: {code: number, msg: string}): void\n    get onError() {\n        return this._onError;\n    }\n\n    set onError(callback) {\n        this._onError = callback;\n    }\n\n    get onComplete() {\n        return this._onComplete;\n    }\n\n    set onComplete(callback) {\n        this._onComplete = callback;\n    }\n\n    get onRedirect() {\n        return this._onRedirect;\n    }\n\n    set onRedirect(callback) {\n        this._onRedirect = callback;\n    }\n\n    get onRecoveredEarlyEof() {\n        return this._onRecoveredEarlyEof;\n    }\n\n    set onRecoveredEarlyEof(callback) {\n        this._onRecoveredEarlyEof = callback;\n    }\n\n    get currentURL() {\n        return this._dataSource.url;\n    }\n\n    get hasRedirect() {\n        return (this._redirectedURL != null || this._dataSource.redirectedURL != undefined);\n    }\n\n    get currentRedirectedURL() {\n        return this._redirectedURL || this._dataSource.redirectedURL;\n    }\n\n    // in KB/s\n    get currentSpeed() {\n        if (this._loaderClass === RangeLoader) {\n            // SpeedSampler is inaccuracy if loader is RangeLoader\n            return this._loader.currentSpeed;\n        }\n        return this._speedSampler.lastSecondKBps;\n    }\n\n    get loaderType() {\n        return this._loader.type;\n    }\n\n    _selectSeekHandler() {\n        let config = this._config;\n\n        if (config.seekType === 'range') {\n            this._seekHandler = new RangeSeekHandler(this._config.rangeLoadZeroStart);\n        } else if (config.seekType === 'param') {\n            let paramStart = config.seekParamStart || 'bstart';\n            let paramEnd = config.seekParamEnd || 'bend';\n\n            this._seekHandler = new ParamSeekHandler(paramStart, paramEnd);\n        } else if (config.seekType === 'custom') {\n            if (typeof config.customSeekHandler !== 'function') {\n                throw new InvalidArgumentException('Custom seekType specified in config but invalid customSeekHandler!');\n            }\n            this._seekHandler = new config.customSeekHandler();\n        } else {\n            throw new InvalidArgumentException(`Invalid seekType in config: ${config.seekType}`);\n        }\n    }\n\n    _selectLoader() {\n        if (this._config.customLoader != null) {\n            this._loaderClass = this._config.customLoader;\n        } else if (this._isWebSocketURL) {\n            this._loaderClass = WebSocketLoader;\n        } else if (FetchStreamLoader.isSupported()) {\n            this._loaderClass = FetchStreamLoader;\n        } else if (MozChunkedLoader.isSupported()) {\n            this._loaderClass = MozChunkedLoader;\n        } else if (RangeLoader.isSupported()) {\n            this._loaderClass = RangeLoader;\n        } else {\n            throw new RuntimeException('Your browser doesn\\'t support xhr with arraybuffer responseType!');\n        }\n    }\n\n    _createLoader() {\n        this._loader = new this._loaderClass(this._seekHandler, this._config);\n        if (this._loader.needStashBuffer === false) {\n            this._enableStash = false;\n        }\n        this._loader.onContentLengthKnown = this._onContentLengthKnown.bind(this);\n        this._loader.onURLRedirect = this._onURLRedirect.bind(this);\n        this._loader.onDataArrival = this._onLoaderChunkArrival.bind(this);\n        this._loader.onComplete = this._onLoaderComplete.bind(this);\n        this._loader.onError = this._onLoaderError.bind(this);\n    }\n\n    open(optionalFrom) {\n        this._currentRange = {from: 0, to: -1};\n        if (optionalFrom) {\n            this._currentRange.from = optionalFrom;\n        }\n\n        this._speedSampler.reset();\n        if (!optionalFrom) {\n            this._fullRequestFlag = true;\n        }\n\n        this._loader.open(this._dataSource, Object.assign({}, this._currentRange));\n    }\n\n    abort() {\n        this._loader.abort();\n\n        if (this._paused) {\n            this._paused = false;\n            this._resumeFrom = 0;\n        }\n    }\n\n    pause() {\n        if (this.isWorking()) {\n            this._loader.abort();\n\n            if (this._stashUsed !== 0) {\n                this._resumeFrom = this._stashByteStart;\n                this._currentRange.to = this._stashByteStart - 1;\n            } else {\n                this._resumeFrom = this._currentRange.to + 1;\n            }\n            this._stashUsed = 0;\n            this._stashByteStart = 0;\n            this._paused = true;\n        }\n    }\n\n    resume() {\n        if (this._paused) {\n            this._paused = false;\n            let bytes = this._resumeFrom;\n            this._resumeFrom = 0;\n            this._internalSeek(bytes, true);\n        }\n    }\n\n    seek(bytes) {\n        this._paused = false;\n        this._stashUsed = 0;\n        this._stashByteStart = 0;\n        this._internalSeek(bytes, true);\n    }\n\n    /**\n     * When seeking request is from media seeking, unconsumed stash data should be dropped\n     * However, stash data shouldn't be dropped if seeking requested from http reconnection\n     *\n     * @dropUnconsumed: Ignore and discard all unconsumed data in stash buffer\n     */\n    _internalSeek(bytes, dropUnconsumed) {\n        if (this._loader.isWorking()) {\n            this._loader.abort();\n        }\n\n        // dispatch & flush stash buffer before seek\n        this._flushStashBuffer(dropUnconsumed);\n\n        this._loader.destroy();\n        this._loader = null;\n\n        let requestRange = {from: bytes, to: -1};\n        this._currentRange = {from: requestRange.from, to: -1};\n\n        this._speedSampler.reset();\n        this._stashSize = this._stashInitialSize;\n        this._createLoader();\n        this._loader.open(this._dataSource, requestRange);\n\n        if (this._onSeeked) {\n            this._onSeeked();\n        }\n    }\n\n    updateUrl(url) {\n        if (!url || typeof url !== 'string' || url.length === 0) {\n            throw new InvalidArgumentException('Url must be a non-empty string!');\n        }\n\n        this._dataSource.url = url;\n\n        // TODO: replace with new url\n    }\n\n    _expandBuffer(expectedBytes) {\n        let bufferNewSize = this._stashSize;\n        while (bufferNewSize + 1024 * 1024 * 1 < expectedBytes) {\n            bufferNewSize *= 2;\n        }\n\n        bufferNewSize += 1024 * 1024 * 1;  // bufferSize = stashSize + 1MB\n        if (bufferNewSize === this._bufferSize) {\n            return;\n        }\n\n        let newBuffer = new ArrayBuffer(bufferNewSize);\n\n        if (this._stashUsed > 0) {  // copy existing data into new buffer\n            let stashOldArray = new Uint8Array(this._stashBuffer, 0, this._stashUsed);\n            let stashNewArray = new Uint8Array(newBuffer, 0, bufferNewSize);\n            stashNewArray.set(stashOldArray, 0);\n        }\n\n        this._stashBuffer = newBuffer;\n        this._bufferSize = bufferNewSize;\n    }\n\n    _normalizeSpeed(input) {\n        let list = this._speedNormalizeList;\n        let last = list.length - 1;\n        let mid = 0;\n        let lbound = 0;\n        let ubound = last;\n\n        if (input < list[0]) {\n            return list[0];\n        }\n\n        // binary search\n        while (lbound <= ubound) {\n            mid = lbound + Math.floor((ubound - lbound) / 2);\n            if (mid === last || (input >= list[mid] && input < list[mid + 1])) {\n                return list[mid];\n            } else if (list[mid] < input) {\n                lbound = mid + 1;\n            } else {\n                ubound = mid - 1;\n            }\n        }\n    }\n\n    _adjustStashSize(normalized) {\n        let stashSizeKB = 0;\n\n        if (this._config.isLive) {\n            // live stream: always use single normalized speed for size of stashSizeKB\n            stashSizeKB = normalized;\n        } else {\n            if (normalized < 512) {\n                stashSizeKB = normalized;\n            } else if (normalized >= 512 && normalized <= 1024) {\n                stashSizeKB = Math.floor(normalized * 1.5);\n            } else {\n                stashSizeKB = normalized * 2;\n            }\n        }\n\n        if (stashSizeKB > 8192) {\n            stashSizeKB = 8192;\n        }\n\n        let bufferSize = stashSizeKB * 1024 + 1024 * 1024 * 1;  // stashSize + 1MB\n        if (this._bufferSize < bufferSize) {\n            this._expandBuffer(bufferSize);\n        }\n        this._stashSize = stashSizeKB * 1024;\n    }\n\n    _dispatchChunks(chunks, byteStart) {\n        this._currentRange.to = byteStart + chunks.byteLength - 1;\n        return this._onDataArrival(chunks, byteStart);\n    }\n\n    _onURLRedirect(redirectedURL) {\n        this._redirectedURL = redirectedURL;\n        if (this._onRedirect) {\n            this._onRedirect(redirectedURL);\n        }\n    }\n\n    _onContentLengthKnown(contentLength) {\n        if (contentLength && this._fullRequestFlag) {\n            this._totalLength = contentLength;\n            this._fullRequestFlag = false;\n        }\n    }\n\n    _onLoaderChunkArrival(chunk, byteStart, receivedLength) {\n        if (!this._onDataArrival) {\n            throw new IllegalStateException('IOController: No existing consumer (onDataArrival) callback!');\n        }\n        if (this._paused) {\n            return;\n        }\n        if (this._isEarlyEofReconnecting) {\n            // Auto-reconnect for EarlyEof succeed, notify to upper-layer by callback\n            this._isEarlyEofReconnecting = false;\n            if (this._onRecoveredEarlyEof) {\n                this._onRecoveredEarlyEof();\n            }\n        }\n\n        this._speedSampler.addBytes(chunk.byteLength);\n\n        // adjust stash buffer size according to network speed dynamically\n        let KBps = this._speedSampler.lastSecondKBps;\n        if (KBps !== 0) {\n            let normalized = this._normalizeSpeed(KBps);\n            if (this._speedNormalized !== normalized) {\n                this._speedNormalized = normalized;\n                this._adjustStashSize(normalized);\n            }\n        }\n\n        if (!this._enableStash) {  // disable stash\n            if (this._stashUsed === 0) {\n                // dispatch chunk directly to consumer;\n                // check ret value (consumed bytes) and stash unconsumed to stashBuffer\n                let consumed = this._dispatchChunks(chunk, byteStart);\n                if (consumed < chunk.byteLength) {  // unconsumed data remain.\n                    let remain = chunk.byteLength - consumed;\n                    if (remain > this._bufferSize) {\n                        this._expandBuffer(remain);\n                    }\n                    let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                    stashArray.set(new Uint8Array(chunk, consumed), 0);\n                    this._stashUsed += remain;\n                    this._stashByteStart = byteStart + consumed;\n                }\n            } else {\n                // else: Merge chunk into stashBuffer, and dispatch stashBuffer to consumer.\n                if (this._stashUsed + chunk.byteLength > this._bufferSize) {\n                    this._expandBuffer(this._stashUsed + chunk.byteLength);\n                }\n                let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                stashArray.set(new Uint8Array(chunk), this._stashUsed);\n                this._stashUsed += chunk.byteLength;\n                let consumed = this._dispatchChunks(this._stashBuffer.slice(0, this._stashUsed), this._stashByteStart);\n                if (consumed < this._stashUsed && consumed > 0) {  // unconsumed data remain\n                    let remainArray = new Uint8Array(this._stashBuffer, consumed);\n                    stashArray.set(remainArray, 0);\n                }\n                this._stashUsed -= consumed;\n                this._stashByteStart += consumed;\n            }\n        } else {  // enable stash\n            if (this._stashUsed === 0 && this._stashByteStart === 0) {  // seeked? or init chunk?\n                // This is the first chunk after seek action\n                this._stashByteStart = byteStart;\n            }\n            if (this._stashUsed + chunk.byteLength <= this._stashSize) {\n                // just stash\n                let stashArray = new Uint8Array(this._stashBuffer, 0, this._stashSize);\n                stashArray.set(new Uint8Array(chunk), this._stashUsed);\n                this._stashUsed += chunk.byteLength;\n            } else {  // stashUsed + chunkSize > stashSize, size limit exceeded\n                let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                if (this._stashUsed > 0) {  // There're stash datas in buffer\n                    // dispatch the whole stashBuffer, and stash remain data\n                    // then append chunk to stashBuffer (stash)\n                    let buffer = this._stashBuffer.slice(0, this._stashUsed);\n                    let consumed = this._dispatchChunks(buffer, this._stashByteStart);\n                    if (consumed < buffer.byteLength) {\n                        if (consumed > 0) {\n                            let remainArray = new Uint8Array(buffer, consumed);\n                            stashArray.set(remainArray, 0);\n                            this._stashUsed = remainArray.byteLength;\n                            this._stashByteStart += consumed;\n                        }\n                    } else {\n                        this._stashUsed = 0;\n                        this._stashByteStart += consumed;\n                    }\n                    if (this._stashUsed + chunk.byteLength > this._bufferSize) {\n                        this._expandBuffer(this._stashUsed + chunk.byteLength);\n                        stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                    }\n                    stashArray.set(new Uint8Array(chunk), this._stashUsed);\n                    this._stashUsed += chunk.byteLength;\n                } else {  // stash buffer empty, but chunkSize > stashSize (oh, holy shit)\n                    // dispatch chunk directly and stash remain data\n                    let consumed = this._dispatchChunks(chunk, byteStart);\n                    if (consumed < chunk.byteLength) {\n                        let remain = chunk.byteLength - consumed;\n                        if (remain > this._bufferSize) {\n                            this._expandBuffer(remain);\n                            stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                        }\n                        stashArray.set(new Uint8Array(chunk, consumed), 0);\n                        this._stashUsed += remain;\n                        this._stashByteStart = byteStart + consumed;\n                    }\n                }\n            }\n        }\n    }\n\n    _flushStashBuffer(dropUnconsumed) {\n        if (this._stashUsed > 0) {\n            let buffer = this._stashBuffer.slice(0, this._stashUsed);\n            let consumed = this._dispatchChunks(buffer, this._stashByteStart);\n            let remain = buffer.byteLength - consumed;\n\n            if (consumed < buffer.byteLength) {\n                if (dropUnconsumed) {\n                    Log.w(this.TAG, `${remain} bytes unconsumed data remain when flush buffer, dropped`);\n                } else {\n                    if (consumed > 0) {\n                        let stashArray = new Uint8Array(this._stashBuffer, 0, this._bufferSize);\n                        let remainArray = new Uint8Array(buffer, consumed);\n                        stashArray.set(remainArray, 0);\n                        this._stashUsed = remainArray.byteLength;\n                        this._stashByteStart += consumed;\n                    }\n                    return 0;\n                }\n            }\n            this._stashUsed = 0;\n            this._stashByteStart = 0;\n            return remain;\n        }\n        return 0;\n    }\n\n    _onLoaderComplete(from, to) {\n        // Force-flush stash buffer, and drop unconsumed data\n        this._flushStashBuffer(true);\n\n        if (this._onComplete) {\n            this._onComplete(this._extraData);\n        }\n    }\n\n    _onLoaderError(type, data) {\n        Log.e(this.TAG, `Loader error, code = ${data.code}, msg = ${data.msg}`);\n\n        this._flushStashBuffer(false);\n\n        if (this._isEarlyEofReconnecting) {\n            // Auto-reconnect for EarlyEof failed, throw UnrecoverableEarlyEof error to upper-layer\n            this._isEarlyEofReconnecting = false;\n            type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;\n        }\n\n        switch (type) {\n            case LoaderErrors.EARLY_EOF: {\n                if (!this._config.isLive) {\n                    // Do internal http reconnect if not live stream\n                    if (this._totalLength) {\n                        let nextFrom = this._currentRange.to + 1;\n                        if (nextFrom < this._totalLength) {\n                            Log.w(this.TAG, 'Connection lost, trying reconnect...');\n                            this._isEarlyEofReconnecting = true;\n                            this._internalSeek(nextFrom, false);\n                        }\n                        return;\n                    }\n                    // else: We don't know totalLength, throw UnrecoverableEarlyEof\n                }\n                // live stream: throw UnrecoverableEarlyEof error to upper-layer\n                type = LoaderErrors.UNRECOVERABLE_EARLY_EOF;\n                break;\n            }\n            case LoaderErrors.UNRECOVERABLE_EARLY_EOF:\n            case LoaderErrors.CONNECTING_TIMEOUT:\n            case LoaderErrors.HTTP_STATUS_CODE_INVALID:\n            case LoaderErrors.EXCEPTION:\n                break;\n        }\n\n        if (this._onError) {\n            this._onError(type, data);\n        } else {\n            throw new RuntimeException('IOException: ' + data.msg);\n        }\n    }\n\n}\n\nexport default IOController;"
  },
  {
    "path": "src/io/loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {NotImplementedException} from '../utils/exception.js';\n\nexport const LoaderStatus = {\n    kIdle: 0,\n    kConnecting: 1,\n    kBuffering: 2,\n    kError: 3,\n    kComplete: 4\n};\n\nexport const LoaderErrors = {\n    OK: 'OK',\n    EXCEPTION: 'Exception',\n    HTTP_STATUS_CODE_INVALID: 'HttpStatusCodeInvalid',\n    CONNECTING_TIMEOUT: 'ConnectingTimeout',\n    EARLY_EOF: 'EarlyEof',\n    UNRECOVERABLE_EARLY_EOF: 'UnrecoverableEarlyEof'\n};\n\n/* Loader has callbacks which have following prototypes:\n *     function onContentLengthKnown(contentLength: number): void\n *     function onURLRedirect(url: string): void\n *     function onDataArrival(chunk: ArrayBuffer, byteStart: number, receivedLength: number): void\n *     function onError(errorType: number, errorInfo: {code: number, msg: string}): void\n *     function onComplete(rangeFrom: number, rangeTo: number): void\n */\nexport class BaseLoader {\n\n    constructor(typeName) {\n        this._type = typeName || 'undefined';\n        this._status = LoaderStatus.kIdle;\n        this._needStash = false;\n        // callbacks\n        this._onContentLengthKnown = null;\n        this._onURLRedirect = null;\n        this._onDataArrival = null;\n        this._onError = null;\n        this._onComplete = null;\n    }\n\n    destroy() {\n        this._status = LoaderStatus.kIdle;\n        this._onContentLengthKnown = null;\n        this._onURLRedirect = null;\n        this._onDataArrival = null;\n        this._onError = null;\n        this._onComplete = null;\n    }\n\n    isWorking() {\n        return this._status === LoaderStatus.kConnecting || this._status === LoaderStatus.kBuffering;\n    }\n\n    get type() {\n        return this._type;\n    }\n\n    get status() {\n        return this._status;\n    }\n\n    get needStashBuffer() {\n        return this._needStash;\n    }\n\n    get onContentLengthKnown() {\n        return this._onContentLengthKnown;\n    }\n\n    set onContentLengthKnown(callback) {\n        this._onContentLengthKnown = callback;\n    }\n\n    get onURLRedirect() {\n        return this._onURLRedirect;\n    }\n\n    set onURLRedirect(callback) {\n        this._onURLRedirect = callback;\n    }\n\n    get onDataArrival() {\n        return this._onDataArrival;\n    }\n\n    set onDataArrival(callback) {\n        this._onDataArrival = callback;\n    }\n\n    get onError() {\n        return this._onError;\n    }\n\n    set onError(callback) {\n        this._onError = callback;\n    }\n\n    get onComplete() {\n        return this._onComplete;\n    }\n\n    set onComplete(callback) {\n        this._onComplete = callback;\n    }\n\n    // pure virtual\n    open(dataSource, range) {\n        throw new NotImplementedException('Unimplemented abstract function!');\n    }\n\n    abort() {\n        throw new NotImplementedException('Unimplemented abstract function!');\n    }\n\n\n}"
  },
  {
    "path": "src/io/param-seek-handler.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nclass ParamSeekHandler {\n\n    constructor(paramStart, paramEnd) {\n        this._startName = paramStart;\n        this._endName = paramEnd;\n    }\n\n    getConfig(baseUrl, range) {\n        let url = baseUrl;\n\n        if (range.from !== 0 || range.to !== -1) {\n            let needAnd = true;\n            if (url.indexOf('?') === -1) {\n                url += '?';\n                needAnd = false;\n            }\n\n            if (needAnd) {\n                url += '&';\n            }\n\n            url += `${this._startName}=${range.from.toString()}`;\n\n            if (range.to !== -1) {\n                url += `&${this._endName}=${range.to.toString()}`;\n            }\n        }\n\n        return {\n            url: url,\n            headers: {}\n        };\n    }\n\n    removeURLParameters(seekedURL) {\n        let baseURL = seekedURL.split('?')[0];\n        let params = undefined;\n\n        let queryIndex = seekedURL.indexOf('?');\n        if (queryIndex !== -1) {\n            params = seekedURL.substring(queryIndex + 1);\n        }\n\n        let resultParams = '';\n\n        if (params != undefined && params.length > 0) {\n            let pairs = params.split('&');\n\n            for (let i = 0; i < pairs.length; i++) {\n                let pair = pairs[i].split('=');\n                let requireAnd = (i > 0);\n\n                if (pair[0] !== this._startName && pair[0] !== this._endName) {\n                    if (requireAnd) {\n                        resultParams += '&';\n                    }\n                    resultParams += pairs[i];\n                }\n            }\n        }\n\n        return (resultParams.length === 0) ? baseURL : baseURL + '?' + resultParams;\n    }\n\n}\n\nexport default ParamSeekHandler;"
  },
  {
    "path": "src/io/range-seek-handler.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nclass RangeSeekHandler {\n\n    constructor(zeroStart) {\n        this._zeroStart = zeroStart || false;\n    }\n\n    getConfig(url, range) {\n        let headers = {};\n\n        if (range.from !== 0 || range.to !== -1) {\n            let param;\n            if (range.to !== -1) {\n                param = `bytes=${range.from.toString()}-${range.to.toString()}`;\n            } else {\n                param = `bytes=${range.from.toString()}-`;\n            }\n            headers['Range'] = param;\n        } else if (this._zeroStart) {\n            headers['Range'] = 'bytes=0-';\n        }\n\n        return {\n            url: url,\n            headers: headers\n        };\n    }\n\n    removeURLParameters(seekedURL) {\n        return seekedURL;\n    }\n\n}\n\nexport default RangeSeekHandler;"
  },
  {
    "path": "src/io/speed-sampler.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Utility class to calculate realtime network I/O speed\nclass SpeedSampler {\n\n    constructor() {\n        // milliseconds\n        this._firstCheckpoint = 0;\n        this._lastCheckpoint = 0;\n        this._intervalBytes = 0;\n        this._totalBytes = 0;\n        this._lastSecondBytes = 0;\n\n        // compatibility detection\n        if (self.performance && self.performance.now) {\n            this._now = self.performance.now.bind(self.performance);\n        } else {\n            this._now = Date.now;\n        }\n    }\n\n    reset() {\n        this._firstCheckpoint = this._lastCheckpoint = 0;\n        this._totalBytes = this._intervalBytes = 0;\n        this._lastSecondBytes = 0;\n    }\n\n    addBytes(bytes) {\n        if (this._firstCheckpoint === 0) {\n            this._firstCheckpoint = this._now();\n            this._lastCheckpoint = this._firstCheckpoint;\n            this._intervalBytes += bytes;\n            this._totalBytes += bytes;\n        } else if (this._now() - this._lastCheckpoint < 1000) {\n            this._intervalBytes += bytes;\n            this._totalBytes += bytes;\n        } else {  // duration >= 1000\n            this._lastSecondBytes = this._intervalBytes;\n            this._intervalBytes = bytes;\n            this._totalBytes += bytes;\n            this._lastCheckpoint = this._now();\n        }\n    }\n\n    get currentKBps() {\n        this.addBytes(0);\n\n        let durationSeconds = (this._now() - this._lastCheckpoint) / 1000;\n        if (durationSeconds == 0) durationSeconds = 1;\n        return (this._intervalBytes / durationSeconds) / 1024;\n    }\n\n    get lastSecondKBps() {\n        this.addBytes(0);\n\n        if (this._lastSecondBytes !== 0) {\n            return this._lastSecondBytes / 1024;\n        } else {  // lastSecondBytes === 0\n            if (this._now() - this._lastCheckpoint >= 500) {\n                // if time interval since last checkpoint has exceeded 500ms\n                // the speed is nearly accurate\n                return this.currentKBps;\n            } else {\n                // We don't know\n                return 0;\n            }\n        }\n    }\n\n    get averageKBps() {\n        let durationSeconds = (this._now() - this._firstCheckpoint) / 1000;\n        return (this._totalBytes / durationSeconds) / 1024;\n    }\n\n}\n\nexport default SpeedSampler;"
  },
  {
    "path": "src/io/websocket-loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';\nimport {RuntimeException} from '../utils/exception.js';\n\n// For FLV over WebSocket live stream\nclass WebSocketLoader extends BaseLoader {\n\n    static isSupported() {\n        try {\n            return (typeof self.WebSocket !== 'undefined');\n        } catch (e) {\n            return false;\n        }\n    }\n\n    constructor() {\n        super('websocket-loader');\n        this.TAG = 'WebSocketLoader';\n\n        this._needStash = true;\n\n        this._ws = null;\n        this._requestAbort = false;\n        this._receivedLength = 0;\n    }\n\n    destroy() {\n        if (this._ws) {\n            this.abort();\n        }\n        super.destroy();\n    }\n\n    open(dataSource) {\n        try {\n            let ws = this._ws = new self.WebSocket(dataSource.url);\n            ws.binaryType = 'arraybuffer';\n            ws.onopen = this._onWebSocketOpen.bind(this);\n            ws.onclose = this._onWebSocketClose.bind(this);\n            ws.onmessage = this._onWebSocketMessage.bind(this);\n            ws.onerror = this._onWebSocketError.bind(this);\n\n            this._status = LoaderStatus.kConnecting;\n        } catch (e) {\n            this._status = LoaderStatus.kError;\n\n            let info = {code: e.code, msg: e.message};\n\n            if (this._onError) {\n                this._onError(LoaderErrors.EXCEPTION, info);\n            } else {\n                throw new RuntimeException(info.msg);\n            }\n        }\n    }\n\n    abort() {\n        let ws = this._ws;\n        if (ws && (ws.readyState === 0 || ws.readyState === 1)) {  // CONNECTING || OPEN\n            this._requestAbort = true;\n            ws.close();\n        }\n\n        this._ws = null;\n        this._status = LoaderStatus.kComplete;\n    }\n\n    _onWebSocketOpen(e) {\n        this._status = LoaderStatus.kBuffering;\n    }\n\n    _onWebSocketClose(e) {\n        if (this._requestAbort === true) {\n            this._requestAbort = false;\n            return;\n        }\n\n        this._status = LoaderStatus.kComplete;\n\n        if (this._onComplete) {\n            this._onComplete(0, this._receivedLength - 1);\n        }\n    }\n\n    _onWebSocketMessage(e) {\n        if (e.data instanceof ArrayBuffer) {\n            this._dispatchArrayBuffer(e.data);\n        } else if (e.data instanceof Blob) {\n            let reader = new FileReader();\n            reader.onload = () => {\n                this._dispatchArrayBuffer(reader.result);\n            };\n            reader.readAsArrayBuffer(e.data);\n        } else {\n            this._status = LoaderStatus.kError;\n            let info = {code: -1, msg: 'Unsupported WebSocket message type: ' + e.data.constructor.name};\n\n            if (this._onError) {\n                this._onError(LoaderErrors.EXCEPTION, info);\n            } else {\n                throw new RuntimeException(info.msg);\n            }\n        }\n    }\n\n    _dispatchArrayBuffer(arraybuffer) {\n        let chunk = arraybuffer;\n        let byteStart = this._receivedLength;\n        this._receivedLength += chunk.byteLength;\n\n        if (this._onDataArrival) {\n            this._onDataArrival(chunk, byteStart, this._receivedLength);\n        }\n    }\n\n    _onWebSocketError(e) {\n        this._status = LoaderStatus.kError;\n\n        let info = {\n            code: e.code,\n            msg: e.message\n        };\n\n        if (this._onError) {\n            this._onError(LoaderErrors.EXCEPTION, info);\n        } else {\n            throw new RuntimeException(info.msg);\n        }\n    }\n\n}\n\nexport default WebSocketLoader;"
  },
  {
    "path": "src/io/xhr-moz-chunked-loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';\nimport {RuntimeException} from '../utils/exception.js';\n\n// For FireFox browser which supports `xhr.responseType = 'moz-chunked-arraybuffer'`\nclass MozChunkedLoader extends BaseLoader {\n\n    static isSupported() {\n        try {\n            let xhr = new XMLHttpRequest();\n            // Firefox 37- requires .open() to be called before setting responseType\n            xhr.open('GET', 'https://example.com', true);\n            xhr.responseType = 'moz-chunked-arraybuffer';\n            return (xhr.responseType === 'moz-chunked-arraybuffer');\n        } catch (e) {\n            Log.w('MozChunkedLoader', e.message);\n            return false;\n        }\n    }\n\n    constructor(seekHandler, config) {\n        super('xhr-moz-chunked-loader');\n        this.TAG = 'MozChunkedLoader';\n\n        this._seekHandler = seekHandler;\n        this._config = config;\n        this._needStash = true;\n\n        this._xhr = null;\n        this._requestAbort = false;\n        this._contentLength = null;\n        this._receivedLength = 0;\n    }\n\n    destroy() {\n        if (this.isWorking()) {\n            this.abort();\n        }\n        if (this._xhr) {\n            this._xhr.onreadystatechange = null;\n            this._xhr.onprogress = null;\n            this._xhr.onloadend = null;\n            this._xhr.onerror = null;\n            this._xhr = null;\n        }\n        super.destroy();\n    }\n\n    open(dataSource, range) {\n        this._dataSource = dataSource;\n        this._range = range;\n\n        let sourceURL = dataSource.url;\n        if (this._config.reuseRedirectedURL && dataSource.redirectedURL != undefined) {\n            sourceURL = dataSource.redirectedURL;\n        }\n\n        let seekConfig = this._seekHandler.getConfig(sourceURL, range);\n        this._requestURL = seekConfig.url;\n\n        let xhr = this._xhr = new XMLHttpRequest();\n        xhr.open('GET', seekConfig.url, true);\n        xhr.responseType = 'moz-chunked-arraybuffer';\n        xhr.onreadystatechange = this._onReadyStateChange.bind(this);\n        xhr.onprogress = this._onProgress.bind(this);\n        xhr.onloadend = this._onLoadEnd.bind(this);\n        xhr.onerror = this._onXhrError.bind(this);\n\n        // cors is auto detected and enabled by xhr\n\n        // withCredentials is disabled by default\n        if (dataSource.withCredentials) {\n            xhr.withCredentials = true;\n        }\n\n        if (typeof seekConfig.headers === 'object') {\n            let headers = seekConfig.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        // add additional headers\n        if (typeof this._config.headers === 'object') {\n            let headers = this._config.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        this._status = LoaderStatus.kConnecting;\n        xhr.send();\n    }\n\n    abort() {\n        this._requestAbort = true;\n        if (this._xhr) {\n            this._xhr.abort();\n        }\n        this._status = LoaderStatus.kComplete;\n    }\n\n    _onReadyStateChange(e) {\n        let xhr = e.target;\n\n        if (xhr.readyState === 2) {  // HEADERS_RECEIVED\n            if (xhr.responseURL != undefined && xhr.responseURL !== this._requestURL) {\n                if (this._onURLRedirect) {\n                    let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);\n                    this._onURLRedirect(redirectedURL);\n                }\n            }\n\n            if (xhr.status !== 0 && (xhr.status < 200 || xhr.status > 299)) {\n                this._status = LoaderStatus.kError;\n                if (this._onError) {\n                    this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});\n                } else {\n                    throw new RuntimeException('MozChunkedLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);\n                }\n            } else {\n                this._status = LoaderStatus.kBuffering;\n            }\n        }\n    }\n\n    _onProgress(e) {\n        if (this._status === LoaderStatus.kError) {\n            // Ignore error response\n            return;\n        }\n\n        if (this._contentLength === null) {\n            if (e.total !== null && e.total !== 0) {\n                this._contentLength = e.total;\n                if (this._onContentLengthKnown) {\n                    this._onContentLengthKnown(this._contentLength);\n                }\n            }\n        }\n\n        let chunk = e.target.response;\n        let byteStart = this._range.from + this._receivedLength;\n        this._receivedLength += chunk.byteLength;\n\n        if (this._onDataArrival) {\n            this._onDataArrival(chunk, byteStart, this._receivedLength);\n        }\n    }\n\n    _onLoadEnd(e) {\n        if (this._requestAbort === true) {\n            this._requestAbort = false;\n            return;\n        } else if (this._status === LoaderStatus.kError) {\n            return;\n        }\n\n        this._status = LoaderStatus.kComplete;\n        if (this._onComplete) {\n            this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);\n        }\n    }\n\n    _onXhrError(e) {\n        this._status = LoaderStatus.kError;\n        let type = 0;\n        let info = null;\n\n        if (this._contentLength && e.loaded < this._contentLength) {\n            type = LoaderErrors.EARLY_EOF;\n            info = {code: -1, msg: 'Moz-Chunked stream meet Early-Eof'};\n        } else {\n            type = LoaderErrors.EXCEPTION;\n            info = {code: -1, msg: e.constructor.name + ' ' + e.type};\n        }\n\n        if (this._onError) {\n            this._onError(type, info);\n        } else {\n            throw new RuntimeException(info.msg);\n        }\n    }\n\n}\n\nexport default MozChunkedLoader;"
  },
  {
    "path": "src/io/xhr-msstream-loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';\nimport {RuntimeException} from '../utils/exception.js';\n\n/* Notice: ms-stream may cause IE/Edge browser crash if seek too frequently!!!\n * The browser may crash in wininet.dll. Disable for now.\n *\n * For IE11/Edge browser by microsoft which supports `xhr.responseType = 'ms-stream'`\n * Notice that ms-stream API sucks. The buffer is always expanding along with downloading.\n *\n * We need to abort the xhr if buffer size exceeded limit size (e.g. 16 MiB), then do reconnect.\n * in order to release previous ArrayBuffer to avoid memory leak\n *\n * Otherwise, the ArrayBuffer will increase to a terrible size that equals final file size.\n */\nclass MSStreamLoader extends BaseLoader {\n\n    static isSupported() {\n        try {\n            if (typeof self.MSStream === 'undefined' || typeof self.MSStreamReader === 'undefined') {\n                return false;\n            }\n\n            let xhr = new XMLHttpRequest();\n            xhr.open('GET', 'https://example.com', true);\n            xhr.responseType = 'ms-stream';\n            return (xhr.responseType === 'ms-stream');\n        } catch (e) {\n            Log.w('MSStreamLoader', e.message);\n            return false;\n        }\n    }\n\n    constructor(seekHandler, config) {\n        super('xhr-msstream-loader');\n        this.TAG = 'MSStreamLoader';\n\n        this._seekHandler = seekHandler;\n        this._config = config;\n        this._needStash = true;\n\n        this._xhr = null;\n        this._reader = null;  // MSStreamReader\n\n        this._totalRange = null;\n        this._currentRange = null;\n\n        this._currentRequestURL = null;\n        this._currentRedirectedURL = null;\n\n        this._contentLength = null;\n        this._receivedLength = 0;\n\n        this._bufferLimit = 16 * 1024 * 1024;  // 16MB\n        this._lastTimeBufferSize = 0;\n        this._isReconnecting = false;\n    }\n\n    destroy() {\n        if (this.isWorking()) {\n            this.abort();\n        }\n        if (this._reader) {\n            this._reader.onprogress = null;\n            this._reader.onload = null;\n            this._reader.onerror = null;\n            this._reader = null;\n        }\n        if (this._xhr) {\n            this._xhr.onreadystatechange = null;\n            this._xhr = null;\n        }\n        super.destroy();\n    }\n\n    open(dataSource, range) {\n        this._internalOpen(dataSource, range, false);\n    }\n\n    _internalOpen(dataSource, range, isSubrange) {\n        this._dataSource = dataSource;\n\n        if (!isSubrange) {\n            this._totalRange = range;\n        } else {\n            this._currentRange = range;\n        }\n\n        let sourceURL = dataSource.url;\n        if (this._config.reuseRedirectedURL) {\n            if (this._currentRedirectedURL != undefined) {\n                sourceURL = this._currentRedirectedURL;\n            } else if (dataSource.redirectedURL != undefined) {\n                sourceURL = dataSource.redirectedURL;\n            }\n        }\n\n        let seekConfig = this._seekHandler.getConfig(sourceURL, range);\n        this._currentRequestURL = seekConfig.url;\n\n        let reader = this._reader = new self.MSStreamReader();\n        reader.onprogress = this._msrOnProgress.bind(this);\n        reader.onload = this._msrOnLoad.bind(this);\n        reader.onerror = this._msrOnError.bind(this);\n\n        let xhr = this._xhr = new XMLHttpRequest();\n        xhr.open('GET', seekConfig.url, true);\n        xhr.responseType = 'ms-stream';\n        xhr.onreadystatechange = this._xhrOnReadyStateChange.bind(this);\n        xhr.onerror = this._xhrOnError.bind(this);\n\n        if (dataSource.withCredentials) {\n            xhr.withCredentials = true;\n        }\n\n        if (typeof seekConfig.headers === 'object') {\n            let headers = seekConfig.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        // add additional headers\n        if (typeof this._config.headers === 'object') {\n            let headers = this._config.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        if (this._isReconnecting) {\n            this._isReconnecting = false;\n        } else {\n            this._status = LoaderStatus.kConnecting;\n        }\n        xhr.send();\n    }\n\n    abort() {\n        this._internalAbort();\n        this._status = LoaderStatus.kComplete;\n    }\n\n    _internalAbort() {\n        if (this._reader) {\n            if (this._reader.readyState === 1) {  // LOADING\n                this._reader.abort();\n            }\n            this._reader.onprogress = null;\n            this._reader.onload = null;\n            this._reader.onerror = null;\n            this._reader = null;\n        }\n        if (this._xhr) {\n            this._xhr.abort();\n            this._xhr.onreadystatechange = null;\n            this._xhr = null;\n        }\n    }\n\n    _xhrOnReadyStateChange(e) {\n        let xhr = e.target;\n\n        if (xhr.readyState === 2) {  // HEADERS_RECEIVED\n            if (xhr.status >= 200 && xhr.status <= 299) {\n                this._status = LoaderStatus.kBuffering;\n\n                if (xhr.responseURL != undefined) {\n                    let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);\n                    if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) {\n                        this._currentRedirectedURL = redirectedURL;\n                        if (this._onURLRedirect) {\n                            this._onURLRedirect(redirectedURL);\n                        }\n                    }\n                }\n\n                let lengthHeader = xhr.getResponseHeader('Content-Length');\n                if (lengthHeader != null && this._contentLength == null) {\n                    let length = parseInt(lengthHeader);\n                    if (length > 0) {\n                        this._contentLength = length;\n                        if (this._onContentLengthKnown) {\n                            this._onContentLengthKnown(this._contentLength);\n                        }\n                    }\n                }\n            } else {\n                this._status = LoaderStatus.kError;\n                if (this._onError) {\n                    this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});\n                } else {\n                    throw new RuntimeException('MSStreamLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);\n                }\n            }\n        } else if (xhr.readyState === 3) {  // LOADING\n            if (xhr.status >= 200 && xhr.status <= 299) {\n                this._status = LoaderStatus.kBuffering;\n\n                let msstream = xhr.response;\n                this._reader.readAsArrayBuffer(msstream);\n            }\n        }\n    }\n\n    _xhrOnError(e) {\n        this._status = LoaderStatus.kError;\n        let type = LoaderErrors.EXCEPTION;\n        let info = {code: -1, msg: e.constructor.name + ' ' + e.type};\n\n        if (this._onError) {\n            this._onError(type, info);\n        } else {\n            throw new RuntimeException(info.msg);\n        }\n    }\n\n    _msrOnProgress(e) {\n        let reader = e.target;\n        let bigbuffer = reader.result;\n        if (bigbuffer == null) {  // result may be null, workaround for buggy M$\n            this._doReconnectIfNeeded();\n            return;\n        }\n\n        let slice = bigbuffer.slice(this._lastTimeBufferSize);\n        this._lastTimeBufferSize = bigbuffer.byteLength;\n        let byteStart = this._totalRange.from + this._receivedLength;\n        this._receivedLength += slice.byteLength;\n\n        if (this._onDataArrival) {\n            this._onDataArrival(slice, byteStart, this._receivedLength);\n        }\n\n        if (bigbuffer.byteLength >= this._bufferLimit) {\n            Log.v(this.TAG, `MSStream buffer exceeded max size near ${byteStart + slice.byteLength}, reconnecting...`);\n            this._doReconnectIfNeeded();\n        }\n    }\n\n    _doReconnectIfNeeded() {\n        if (this._contentLength == null || this._receivedLength < this._contentLength) {\n            this._isReconnecting = true;\n            this._lastTimeBufferSize = 0;\n            this._internalAbort();\n\n            let range = {\n                from: this._totalRange.from + this._receivedLength,\n                to: -1\n            };\n            this._internalOpen(this._dataSource, range, true);\n        }\n    }\n\n    _msrOnLoad(e) {  // actually it is onComplete event\n        this._status = LoaderStatus.kComplete;\n        if (this._onComplete) {\n            this._onComplete(this._totalRange.from, this._totalRange.from + this._receivedLength - 1);\n        }\n    }\n\n    _msrOnError(e) {\n        this._status = LoaderStatus.kError;\n        let type = 0;\n        let info = null;\n\n        if (this._contentLength && this._receivedLength < this._contentLength) {\n            type = LoaderErrors.EARLY_EOF;\n            info = {code: -1, msg: 'MSStream meet Early-Eof'};\n        } else {\n            type = LoaderErrors.EARLY_EOF;\n            info = {code: -1, msg: e.constructor.name + ' ' + e.type};\n        }\n\n        if (this._onError) {\n            this._onError(type, info);\n        } else {\n            throw new RuntimeException(info.msg);\n        }\n    }\n}\n\nexport default MSStreamLoader;"
  },
  {
    "path": "src/io/xhr-range-loader.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport SpeedSampler from './speed-sampler.js';\nimport {BaseLoader, LoaderStatus, LoaderErrors} from './loader.js';\nimport {RuntimeException} from '../utils/exception.js';\n\n// Universal IO Loader, implemented by adding Range header in xhr's request header\nclass RangeLoader extends BaseLoader {\n\n    static isSupported() {\n        try {\n            let xhr = new XMLHttpRequest();\n            xhr.open('GET', 'https://example.com', true);\n            xhr.responseType = 'arraybuffer';\n            return (xhr.responseType === 'arraybuffer');\n        } catch (e) {\n            Log.w('RangeLoader', e.message);\n            return false;\n        }\n    }\n\n    constructor(seekHandler, config) {\n        super('xhr-range-loader');\n        this.TAG = 'RangeLoader';\n\n        this._seekHandler = seekHandler;\n        this._config = config;\n        this._needStash = false;\n\n        this._chunkSizeKBList = [\n            128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 5120, 6144, 7168, 8192\n        ];\n        this._currentChunkSizeKB = 384;\n        this._currentSpeedNormalized = 0;\n        this._zeroSpeedChunkCount = 0;\n\n        this._xhr = null;\n        this._speedSampler = new SpeedSampler();\n\n        this._requestAbort = false;\n        this._waitForTotalLength = false;\n        this._totalLengthReceived = false;\n\n        this._currentRequestURL = null;\n        this._currentRedirectedURL = null;\n        this._currentRequestRange = null;\n        this._totalLength = null;  // size of the entire file\n        this._contentLength = null;  // Content-Length of entire request range\n        this._receivedLength = 0;  // total received bytes\n        this._lastTimeLoaded = 0;  // received bytes of current request sub-range\n    }\n\n    destroy() {\n        if (this.isWorking()) {\n            this.abort();\n        }\n        if (this._xhr) {\n            this._xhr.onreadystatechange = null;\n            this._xhr.onprogress = null;\n            this._xhr.onload = null;\n            this._xhr.onerror = null;\n            this._xhr = null;\n        }\n        super.destroy();\n    }\n\n    get currentSpeed() {\n        return this._speedSampler.lastSecondKBps;\n    }\n\n    open(dataSource, range) {\n        this._dataSource = dataSource;\n        this._range = range;\n        this._status = LoaderStatus.kConnecting;\n\n        let useRefTotalLength = false;\n        if (this._dataSource.filesize != undefined && this._dataSource.filesize !== 0) {\n            useRefTotalLength = true;\n            this._totalLength = this._dataSource.filesize;\n        }\n\n        if (!this._totalLengthReceived && !useRefTotalLength) {\n            // We need total filesize\n            this._waitForTotalLength = true;\n            this._internalOpen(this._dataSource, {from: 0, to: -1});\n        } else {\n            // We have filesize, start loading\n            this._openSubRange();\n        }\n    }\n\n    _openSubRange() {\n        let chunkSize = this._currentChunkSizeKB * 1024;\n\n        let from = this._range.from + this._receivedLength;\n        let to = from + chunkSize;\n\n        if (this._contentLength != null) {\n            if (to - this._range.from >= this._contentLength) {\n                to = this._range.from + this._contentLength - 1;\n            }\n        }\n\n        this._currentRequestRange = {from, to};\n        this._internalOpen(this._dataSource, this._currentRequestRange);\n    }\n\n    _internalOpen(dataSource, range) {\n        this._lastTimeLoaded = 0;\n\n        let sourceURL = dataSource.url;\n        if (this._config.reuseRedirectedURL) {\n            if (this._currentRedirectedURL != undefined) {\n                sourceURL = this._currentRedirectedURL;\n            } else if (dataSource.redirectedURL != undefined) {\n                sourceURL = dataSource.redirectedURL;\n            }\n        }\n\n        let seekConfig = this._seekHandler.getConfig(sourceURL, range);\n        this._currentRequestURL = seekConfig.url;\n\n        let xhr = this._xhr = new XMLHttpRequest();\n        xhr.open('GET', seekConfig.url, true);\n        xhr.responseType = 'arraybuffer';\n        xhr.onreadystatechange = this._onReadyStateChange.bind(this);\n        xhr.onprogress = this._onProgress.bind(this);\n        xhr.onload = this._onLoad.bind(this);\n        xhr.onerror = this._onXhrError.bind(this);\n\n        if (dataSource.withCredentials) {\n            xhr.withCredentials = true;\n        }\n\n        if (typeof seekConfig.headers === 'object') {\n            let headers = seekConfig.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        // add additional headers\n        if (typeof this._config.headers === 'object') {\n            let headers = this._config.headers;\n\n            for (let key in headers) {\n                if (headers.hasOwnProperty(key)) {\n                    xhr.setRequestHeader(key, headers[key]);\n                }\n            }\n        }\n\n        xhr.send();\n    }\n\n    abort() {\n        this._requestAbort = true;\n        this._internalAbort();\n        this._status = LoaderStatus.kComplete;\n    }\n\n    _internalAbort() {\n        if (this._xhr) {\n            this._xhr.onreadystatechange = null;\n            this._xhr.onprogress = null;\n            this._xhr.onload = null;\n            this._xhr.onerror = null;\n            this._xhr.abort();\n            this._xhr = null;\n        }\n    }\n\n    _onReadyStateChange(e) {\n        let xhr = e.target;\n\n        if (xhr.readyState === 2) {  // HEADERS_RECEIVED\n            if (xhr.responseURL != undefined) {  // if the browser support this property\n                let redirectedURL = this._seekHandler.removeURLParameters(xhr.responseURL);\n                if (xhr.responseURL !== this._currentRequestURL && redirectedURL !== this._currentRedirectedURL) {\n                    this._currentRedirectedURL = redirectedURL;\n                    if (this._onURLRedirect) {\n                        this._onURLRedirect(redirectedURL);\n                    }\n                }\n            }\n\n            if ((xhr.status >= 200 && xhr.status <= 299)) {\n                if (this._waitForTotalLength) {\n                    return;\n                }\n                this._status = LoaderStatus.kBuffering;\n            } else {\n                this._status = LoaderStatus.kError;\n                if (this._onError) {\n                    this._onError(LoaderErrors.HTTP_STATUS_CODE_INVALID, {code: xhr.status, msg: xhr.statusText});\n                } else {\n                    throw new RuntimeException('RangeLoader: Http code invalid, ' + xhr.status + ' ' + xhr.statusText);\n                }\n            }\n        }\n    }\n\n    _onProgress(e) {\n        if (this._status === LoaderStatus.kError) {\n            // Ignore error response\n            return;\n        }\n\n        if (this._contentLength === null) {\n            let openNextRange = false;\n\n            if (this._waitForTotalLength) {\n                this._waitForTotalLength = false;\n                this._totalLengthReceived = true;\n                openNextRange = true;\n\n                let total = e.total;\n                this._internalAbort();\n                if (total != null & total !== 0) {\n                    this._totalLength = total;\n                }\n            }\n\n            // calculate currrent request range's contentLength\n            if (this._range.to === -1) {\n                this._contentLength = this._totalLength - this._range.from;\n            } else {  // to !== -1\n                this._contentLength = this._range.to - this._range.from + 1;\n            }\n\n            if (openNextRange) {\n                this._openSubRange();\n                return;\n            }\n            if (this._onContentLengthKnown) {\n                this._onContentLengthKnown(this._contentLength);\n            }\n        }\n\n        let delta = e.loaded - this._lastTimeLoaded;\n        this._lastTimeLoaded = e.loaded;\n        this._speedSampler.addBytes(delta);\n    }\n\n    _normalizeSpeed(input) {\n        let list = this._chunkSizeKBList;\n        let last = list.length - 1;\n        let mid = 0;\n        let lbound = 0;\n        let ubound = last;\n\n        if (input < list[0]) {\n            return list[0];\n        }\n\n        while (lbound <= ubound) {\n            mid = lbound + Math.floor((ubound - lbound) / 2);\n            if (mid === last || (input >= list[mid] && input < list[mid + 1])) {\n                return list[mid];\n            } else if (list[mid] < input) {\n                lbound = mid + 1;\n            } else {\n                ubound = mid - 1;\n            }\n        }\n    }\n\n    _onLoad(e) {\n        if (this._status === LoaderStatus.kError) {\n            // Ignore error response\n            return;\n        }\n\n        if (this._waitForTotalLength) {\n            this._waitForTotalLength = false;\n            return;\n        }\n\n        this._lastTimeLoaded = 0;\n        let KBps = this._speedSampler.lastSecondKBps;\n        if (KBps === 0) {\n            this._zeroSpeedChunkCount++;\n            if (this._zeroSpeedChunkCount >= 3) {\n                // Try get currentKBps after 3 chunks\n                KBps = this._speedSampler.currentKBps;\n            }\n        }\n\n        if (KBps !== 0) {\n            let normalized = this._normalizeSpeed(KBps);\n            if (this._currentSpeedNormalized !== normalized) {\n                this._currentSpeedNormalized = normalized;\n                this._currentChunkSizeKB = normalized;\n            }\n        }\n\n        let chunk = e.target.response;\n        let byteStart = this._range.from + this._receivedLength;\n        this._receivedLength += chunk.byteLength;\n\n        let reportComplete = false;\n\n        if (this._contentLength != null && this._receivedLength < this._contentLength) {\n            // continue load next chunk\n            this._openSubRange();\n        } else {\n            reportComplete = true;\n        }\n\n        // dispatch received chunk\n        if (this._onDataArrival) {\n            this._onDataArrival(chunk, byteStart, this._receivedLength);\n        }\n\n        if (reportComplete) {\n            this._status = LoaderStatus.kComplete;\n            if (this._onComplete) {\n                this._onComplete(this._range.from, this._range.from + this._receivedLength - 1);\n            }\n        }\n    }\n\n    _onXhrError(e) {\n        this._status = LoaderStatus.kError;\n        let type = 0;\n        let info = null;\n\n        if (this._contentLength && this._receivedLength > 0\n                                && this._receivedLength < this._contentLength) {\n            type = LoaderErrors.EARLY_EOF;\n            info = {code: -1, msg: 'RangeLoader meet Early-Eof'};\n        } else {\n            type = LoaderErrors.EXCEPTION;\n            info = {code: -1, msg: e.constructor.name + ' ' + e.type};\n        }\n\n        if (this._onError) {\n            this._onError(type, info);\n        } else {\n            throw new RuntimeException(info.msg);\n        }\n    }\n\n}\n\nexport default RangeLoader;"
  },
  {
    "path": "src/player/flv-player.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport Log from '../utils/logger.js';\nimport Browser from '../utils/browser.js';\nimport PlayerEvents from './player-events.js';\nimport Transmuxer from '../core/transmuxer.js';\nimport TransmuxingEvents from '../core/transmuxing-events.js';\nimport MSEController from '../core/mse-controller.js';\nimport MSEEvents from '../core/mse-events.js';\nimport {ErrorTypes, ErrorDetails} from './player-errors.js';\nimport {createDefaultConfig} from '../config.js';\nimport {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';\n\nclass FlvPlayer {\n\n    constructor(mediaDataSource, config) {\n        this.TAG = 'FlvPlayer';\n        this._type = 'FlvPlayer';\n        this._emitter = new EventEmitter();\n\n        this._config = createDefaultConfig();\n        if (typeof config === 'object') {\n            Object.assign(this._config, config);\n        }\n\n        if (mediaDataSource.type.toLowerCase() !== 'flv') {\n            throw new InvalidArgumentException('FlvPlayer requires an flv MediaDataSource input!');\n        }\n\n        if (mediaDataSource.isLive === true) {\n            this._config.isLive = true;\n        }\n\n        this.e = {\n            onvLoadedMetadata: this._onvLoadedMetadata.bind(this),\n            onvSeeking: this._onvSeeking.bind(this),\n            onvCanPlay: this._onvCanPlay.bind(this),\n            onvStalled: this._onvStalled.bind(this),\n            onvProgress: this._onvProgress.bind(this)\n        };\n\n        if (self.performance && self.performance.now) {\n            this._now = self.performance.now.bind(self.performance);\n        } else {\n            this._now = Date.now;\n        }\n\n        this._pendingSeekTime = null;  // in seconds\n        this._requestSetTime = false;\n        this._seekpointRecord = null;\n        this._progressChecker = null;\n\n        this._mediaDataSource = mediaDataSource;\n        this._mediaElement = null;\n        this._msectl = null;\n        this._transmuxer = null;\n\n        this._mseSourceOpened = false;\n        this._hasPendingLoad = false;\n        this._receivedCanPlay = false;\n\n        this._mediaInfo = null;\n        this._statisticsInfo = null;\n\n        let chromeNeedIDRFix = (Browser.chrome &&\n                               (Browser.version.major < 50 ||\n                               (Browser.version.major === 50 && Browser.version.build < 2661)));\n        this._alwaysSeekKeyframe = (chromeNeedIDRFix || Browser.msedge || Browser.msie) ? true : false;\n\n        if (this._alwaysSeekKeyframe) {\n            this._config.accurateSeek = false;\n        }\n    }\n\n    destroy() {\n        if (this._progressChecker != null) {\n            window.clearInterval(this._progressChecker);\n            this._progressChecker = null;\n        }\n        if (this._transmuxer) {\n            this.unload();\n        }\n        if (this._mediaElement) {\n            this.detachMediaElement();\n        }\n        this.e = null;\n        this._mediaDataSource = null;\n\n        this._emitter.removeAllListeners();\n        this._emitter = null;\n    }\n\n    on(event, listener) {\n        if (event === PlayerEvents.MEDIA_INFO) {\n            if (this._mediaInfo != null) {\n                Promise.resolve().then(() => {\n                    this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);\n                });\n            }\n        } else if (event === PlayerEvents.STATISTICS_INFO) {\n            if (this._statisticsInfo != null) {\n                Promise.resolve().then(() => {\n                    this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);\n                });\n            }\n        }\n        this._emitter.addListener(event, listener);\n    }\n\n    off(event, listener) {\n        this._emitter.removeListener(event, listener);\n    }\n\n    attachMediaElement(mediaElement) {\n        this._mediaElement = mediaElement;\n        mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);\n        mediaElement.addEventListener('seeking', this.e.onvSeeking);\n        mediaElement.addEventListener('canplay', this.e.onvCanPlay);\n        mediaElement.addEventListener('stalled', this.e.onvStalled);\n        mediaElement.addEventListener('progress', this.e.onvProgress);\n\n        this._msectl = new MSEController(this._config);\n\n        this._msectl.on(MSEEvents.UPDATE_END, this._onmseUpdateEnd.bind(this));\n        this._msectl.on(MSEEvents.BUFFER_FULL, this._onmseBufferFull.bind(this));\n        this._msectl.on(MSEEvents.SOURCE_OPEN, () => {\n            this._mseSourceOpened = true;\n            if (this._hasPendingLoad) {\n                this._hasPendingLoad = false;\n                this.load();\n            }\n        });\n        this._msectl.on(MSEEvents.ERROR, (info) => {\n            this._emitter.emit(PlayerEvents.ERROR,\n                               ErrorTypes.MEDIA_ERROR,\n                               ErrorDetails.MEDIA_MSE_ERROR,\n                               info\n            );\n        });\n\n        this._msectl.attachMediaElement(mediaElement);\n\n        if (this._pendingSeekTime != null) {\n            try {\n                mediaElement.currentTime = this._pendingSeekTime;\n                this._pendingSeekTime = null;\n            } catch (e) {\n                // IE11 may throw InvalidStateError if readyState === 0\n                // We can defer set currentTime operation after loadedmetadata\n            }\n        }\n    }\n\n    detachMediaElement() {\n        if (this._mediaElement) {\n            this._msectl.detachMediaElement();\n            this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);\n            this._mediaElement.removeEventListener('seeking', this.e.onvSeeking);\n            this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);\n            this._mediaElement.removeEventListener('stalled', this.e.onvStalled);\n            this._mediaElement.removeEventListener('progress', this.e.onvProgress);\n            this._mediaElement = null;\n        }\n        if (this._msectl) {\n            this._msectl.destroy();\n            this._msectl = null;\n        }\n    }\n\n    load() {\n        if (!this._mediaElement) {\n            throw new IllegalStateException('HTMLMediaElement must be attached before load()!');\n        }\n        if (this._transmuxer) {\n            throw new IllegalStateException('FlvPlayer.load() has been called, please call unload() first!');\n        }\n        if (this._hasPendingLoad) {\n            return;\n        }\n\n        if (this._config.deferLoadAfterSourceOpen && this._mseSourceOpened === false) {\n            this._hasPendingLoad = true;\n            return;\n        }\n\n        if (this._mediaElement.readyState > 0) {\n            this._requestSetTime = true;\n            // IE11 may throw InvalidStateError if readyState === 0\n            this._mediaElement.currentTime = 0;\n        }\n\n        this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);\n\n        this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {\n            this._msectl.appendInitSegment(is);\n        });\n        this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {\n            this._msectl.appendMediaSegment(ms);\n\n            // lazyLoad check\n            if (this._config.lazyLoad && !this._config.isLive) {\n                let currentTime = this._mediaElement.currentTime;\n                if (ms.info.endDts >= (currentTime + this._config.lazyLoadMaxDuration) * 1000) {\n                    if (this._progressChecker == null) {\n                        Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');\n                        this._suspendTransmuxer();\n                    }\n                }\n            }\n        });\n        this._transmuxer.on(TransmuxingEvents.LOADING_COMPLETE, () => {\n            this._msectl.endOfStream();\n            this._emitter.emit(PlayerEvents.LOADING_COMPLETE);\n        });\n        this._transmuxer.on(TransmuxingEvents.RECOVERED_EARLY_EOF, () => {\n            this._emitter.emit(PlayerEvents.RECOVERED_EARLY_EOF);\n        });\n        this._transmuxer.on(TransmuxingEvents.IO_ERROR, (detail, info) => {\n            this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, detail, info);\n        });\n        this._transmuxer.on(TransmuxingEvents.DEMUX_ERROR, (detail, info) => {\n            this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, detail, {code: -1, msg: info});\n        });\n        this._transmuxer.on(TransmuxingEvents.MEDIA_INFO, (mediaInfo) => {\n            this._mediaInfo = mediaInfo;\n            this._emitter.emit(PlayerEvents.MEDIA_INFO, Object.assign({}, mediaInfo));\n        });\n        this._transmuxer.on(TransmuxingEvents.METADATA_ARRIVED, (metadata) => {\n            this._emitter.emit(PlayerEvents.METADATA_ARRIVED, metadata);\n        });\n        this._transmuxer.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, (data) => {\n            this._emitter.emit(PlayerEvents.SCRIPTDATA_ARRIVED, data);\n        });\n        this._transmuxer.on(TransmuxingEvents.STATISTICS_INFO, (statInfo) => {\n            this._statisticsInfo = this._fillStatisticsInfo(statInfo);\n            this._emitter.emit(PlayerEvents.STATISTICS_INFO, Object.assign({}, this._statisticsInfo));\n        });\n        this._transmuxer.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, (milliseconds) => {\n            if (this._mediaElement && !this._config.accurateSeek) {\n                this._requestSetTime = true;\n                this._mediaElement.currentTime = milliseconds / 1000;\n            }\n        });\n\n        this._transmuxer.open();\n    }\n\n    unload() {\n        if (this._mediaElement) {\n            this._mediaElement.pause();\n        }\n        if (this._msectl) {\n            this._msectl.seek(0);\n        }\n        if (this._transmuxer) {\n            this._transmuxer.close();\n            this._transmuxer.destroy();\n            this._transmuxer = null;\n        }\n    }\n\n    play() {\n        return this._mediaElement.play();\n    }\n\n    pause() {\n        this._mediaElement.pause();\n    }\n\n    get type() {\n        return this._type;\n    }\n\n    get buffered() {\n        return this._mediaElement.buffered;\n    }\n\n    get duration() {\n        return this._mediaElement.duration;\n    }\n\n    get volume() {\n        return this._mediaElement.volume;\n    }\n\n    set volume(value) {\n        this._mediaElement.volume = value;\n    }\n\n    get muted() {\n        return this._mediaElement.muted;\n    }\n\n    set muted(muted) {\n        this._mediaElement.muted = muted;\n    }\n\n    get currentTime() {\n        if (this._mediaElement) {\n            return this._mediaElement.currentTime;\n        }\n        return 0;\n    }\n\n    set currentTime(seconds) {\n        if (this._mediaElement) {\n            this._internalSeek(seconds);\n        } else {\n            this._pendingSeekTime = seconds;\n        }\n    }\n\n    get mediaInfo() {\n        return Object.assign({}, this._mediaInfo);\n    }\n\n    get statisticsInfo() {\n        if (this._statisticsInfo == null) {\n            this._statisticsInfo = {};\n        }\n        this._statisticsInfo = this._fillStatisticsInfo(this._statisticsInfo);\n        return Object.assign({}, this._statisticsInfo);\n    }\n\n    _fillStatisticsInfo(statInfo) {\n        statInfo.playerType = this._type;\n\n        if (!(this._mediaElement instanceof HTMLVideoElement)) {\n            return statInfo;\n        }\n\n        let hasQualityInfo = true;\n        let decoded = 0;\n        let dropped = 0;\n\n        if (this._mediaElement.getVideoPlaybackQuality) {\n            let quality = this._mediaElement.getVideoPlaybackQuality();\n            decoded = quality.totalVideoFrames;\n            dropped = quality.droppedVideoFrames;\n        } else if (this._mediaElement.webkitDecodedFrameCount != undefined) {\n            decoded = this._mediaElement.webkitDecodedFrameCount;\n            dropped = this._mediaElement.webkitDroppedFrameCount;\n        } else {\n            hasQualityInfo = false;\n        }\n\n        if (hasQualityInfo) {\n            statInfo.decodedFrames = decoded;\n            statInfo.droppedFrames = dropped;\n        }\n\n        return statInfo;\n    }\n\n    _onmseUpdateEnd() {\n        if (!this._config.lazyLoad || this._config.isLive) {\n            return;\n        }\n\n        let buffered = this._mediaElement.buffered;\n        let currentTime = this._mediaElement.currentTime;\n        let currentRangeStart = 0;\n        let currentRangeEnd = 0;\n\n        for (let i = 0; i < buffered.length; i++) {\n            let start = buffered.start(i);\n            let end = buffered.end(i);\n            if (start <= currentTime && currentTime < end) {\n                currentRangeStart = start;\n                currentRangeEnd = end;\n                break;\n            }\n        }\n\n        if (currentRangeEnd >= currentTime + this._config.lazyLoadMaxDuration && this._progressChecker == null) {\n            Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');\n            this._suspendTransmuxer();\n        }\n    }\n\n    _onmseBufferFull() {\n        Log.v(this.TAG, 'MSE SourceBuffer is full, suspend transmuxing task');\n        if (this._progressChecker == null) {\n            this._suspendTransmuxer();\n        }\n    }\n\n    _suspendTransmuxer() {\n        if (this._transmuxer) {\n            this._transmuxer.pause();\n\n            if (this._progressChecker == null) {\n                this._progressChecker = window.setInterval(this._checkProgressAndResume.bind(this), 1000);\n            }\n        }\n    }\n\n    _checkProgressAndResume() {\n        let currentTime = this._mediaElement.currentTime;\n        let buffered = this._mediaElement.buffered;\n\n        let needResume = false;\n\n        for (let i = 0; i < buffered.length; i++) {\n            let from = buffered.start(i);\n            let to = buffered.end(i);\n            if (currentTime >= from && currentTime < to) {\n                if (currentTime >= to - this._config.lazyLoadRecoverDuration) {\n                    needResume = true;\n                }\n                break;\n            }\n        }\n\n        if (needResume) {\n            window.clearInterval(this._progressChecker);\n            this._progressChecker = null;\n            if (needResume) {\n                Log.v(this.TAG, 'Continue loading from paused position');\n                this._transmuxer.resume();\n            }\n        }\n    }\n\n    _isTimepointBuffered(seconds) {\n        let buffered = this._mediaElement.buffered;\n\n        for (let i = 0; i < buffered.length; i++) {\n            let from = buffered.start(i);\n            let to = buffered.end(i);\n            if (seconds >= from && seconds < to) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    _internalSeek(seconds) {\n        let directSeek = this._isTimepointBuffered(seconds);\n\n        let directSeekBegin = false;\n        let directSeekBeginTime = 0;\n\n        if (seconds < 1.0 && this._mediaElement.buffered.length > 0) {\n            let videoBeginTime = this._mediaElement.buffered.start(0);\n            if ((videoBeginTime < 1.0 && seconds < videoBeginTime) || Browser.safari) {\n                directSeekBegin = true;\n                // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid\n                directSeekBeginTime = Browser.safari ? 0.1 : videoBeginTime;\n            }\n        }\n\n        if (directSeekBegin) {  // seek to video begin, set currentTime directly if beginPTS buffered\n            this._requestSetTime = true;\n            this._mediaElement.currentTime = directSeekBeginTime;\n        }  else if (directSeek) {  // buffered position\n            if (!this._alwaysSeekKeyframe) {\n                this._requestSetTime = true;\n                this._mediaElement.currentTime = seconds;\n            } else {\n                let idr = this._msectl.getNearestKeyframe(Math.floor(seconds * 1000));\n                this._requestSetTime = true;\n                if (idr != null) {\n                    this._mediaElement.currentTime = idr.dts / 1000;\n                } else {\n                    this._mediaElement.currentTime = seconds;\n                }\n            }\n            if (this._progressChecker != null) {\n                this._checkProgressAndResume();\n            }\n        } else {\n            if (this._progressChecker != null) {\n                window.clearInterval(this._progressChecker);\n                this._progressChecker = null;\n            }\n            this._msectl.seek(seconds);\n            this._transmuxer.seek(Math.floor(seconds * 1000));  // in milliseconds\n            // no need to set mediaElement.currentTime if non-accurateSeek,\n            // just wait for the recommend_seekpoint callback\n            if (this._config.accurateSeek) {\n                this._requestSetTime = true;\n                this._mediaElement.currentTime = seconds;\n            }\n        }\n    }\n\n    _checkAndApplyUnbufferedSeekpoint() {\n        if (this._seekpointRecord) {\n            if (this._seekpointRecord.recordTime <= this._now() - 100) {\n                let target = this._mediaElement.currentTime;\n                this._seekpointRecord = null;\n                if (!this._isTimepointBuffered(target)) {\n                    if (this._progressChecker != null) {\n                        window.clearTimeout(this._progressChecker);\n                        this._progressChecker = null;\n                    }\n                    // .currentTime is consists with .buffered timestamp\n                    // Chrome/Edge use DTS, while FireFox/Safari use PTS\n                    this._msectl.seek(target);\n                    this._transmuxer.seek(Math.floor(target * 1000));\n                    // set currentTime if accurateSeek, or wait for recommend_seekpoint callback\n                    if (this._config.accurateSeek) {\n                        this._requestSetTime = true;\n                        this._mediaElement.currentTime = target;\n                    }\n                }\n            } else {\n                window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);\n            }\n        }\n    }\n\n    _checkAndResumeStuckPlayback(stalled) {\n        let media = this._mediaElement;\n        if (stalled || !this._receivedCanPlay || media.readyState < 2) {  // HAVE_CURRENT_DATA\n            let buffered = media.buffered;\n            if (buffered.length > 0 && media.currentTime < buffered.start(0)) {\n                Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`);\n                this._requestSetTime = true;\n                this._mediaElement.currentTime = buffered.start(0);\n                this._mediaElement.removeEventListener('progress', this.e.onvProgress);\n            }\n        } else {\n            // Playback didn't stuck, remove progress event listener\n            this._mediaElement.removeEventListener('progress', this.e.onvProgress);\n        }\n    }\n\n    _onvLoadedMetadata(e) {\n        if (this._pendingSeekTime != null) {\n            this._mediaElement.currentTime = this._pendingSeekTime;\n            this._pendingSeekTime = null;\n        }\n    }\n\n    _onvSeeking(e) {  // handle seeking request from browser's progress bar\n        let target = this._mediaElement.currentTime;\n        let buffered = this._mediaElement.buffered;\n\n        if (this._requestSetTime) {\n            this._requestSetTime = false;\n            return;\n        }\n\n        if (target < 1.0 && buffered.length > 0) {\n            // seek to video begin, set currentTime directly if beginPTS buffered\n            let videoBeginTime = buffered.start(0);\n            if ((videoBeginTime < 1.0 && target < videoBeginTime) || Browser.safari) {\n                this._requestSetTime = true;\n                // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid\n                this._mediaElement.currentTime = Browser.safari ? 0.1 : videoBeginTime;\n                return;\n            }\n        }\n\n        if (this._isTimepointBuffered(target)) {\n            if (this._alwaysSeekKeyframe) {\n                let idr = this._msectl.getNearestKeyframe(Math.floor(target * 1000));\n                if (idr != null) {\n                    this._requestSetTime = true;\n                    this._mediaElement.currentTime = idr.dts / 1000;\n                }\n            }\n            if (this._progressChecker != null) {\n                this._checkProgressAndResume();\n            }\n            return;\n        }\n\n        this._seekpointRecord = {\n            seekPoint: target,\n            recordTime: this._now()\n        };\n        window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);\n    }\n\n    _onvCanPlay(e) {\n        this._receivedCanPlay = true;\n        this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);\n    }\n\n    _onvStalled(e) {\n        this._checkAndResumeStuckPlayback(true);\n    }\n\n    _onvProgress(e) {\n        this._checkAndResumeStuckPlayback();\n    }\n\n}\n\nexport default FlvPlayer;"
  },
  {
    "path": "src/player/native-player.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport PlayerEvents from './player-events.js';\nimport {createDefaultConfig} from '../config.js';\nimport {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';\n\n// Player wrapper for browser's native player (HTMLVideoElement) without MediaSource src. \nclass NativePlayer {\n\n    constructor(mediaDataSource, config) {\n        this.TAG = 'NativePlayer';\n        this._type = 'NativePlayer';\n        this._emitter = new EventEmitter();\n\n        this._config = createDefaultConfig();\n        if (typeof config === 'object') {\n            Object.assign(this._config, config);\n        }\n\n        if (mediaDataSource.type.toLowerCase() === 'flv') {\n            throw new InvalidArgumentException('NativePlayer does\\'t support flv MediaDataSource input!');\n        }\n        if (mediaDataSource.hasOwnProperty('segments')) {\n            throw new InvalidArgumentException(`NativePlayer(${mediaDataSource.type}) doesn't support multipart playback!`);\n        }\n\n        this.e = {\n            onvLoadedMetadata: this._onvLoadedMetadata.bind(this)\n        };\n\n        this._pendingSeekTime = null;\n        this._statisticsReporter = null;\n\n        this._mediaDataSource = mediaDataSource;\n        this._mediaElement = null;\n    }\n\n    destroy() {\n        if (this._mediaElement) {\n            this.unload();\n            this.detachMediaElement();\n        }\n        this.e = null;\n        this._mediaDataSource = null;\n        this._emitter.removeAllListeners();\n        this._emitter = null;\n    }\n\n    on(event, listener) {\n        if (event === PlayerEvents.MEDIA_INFO) {\n            if (this._mediaElement != null && this._mediaElement.readyState !== 0) {  // HAVE_NOTHING\n                Promise.resolve().then(() => {\n                    this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);\n                });\n            }\n        } else if (event === PlayerEvents.STATISTICS_INFO) {\n            if (this._mediaElement != null && this._mediaElement.readyState !== 0) {\n                Promise.resolve().then(() => {\n                    this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);\n                });\n            }\n        }\n        this._emitter.addListener(event, listener);\n    }\n\n    off(event, listener) {\n        this._emitter.removeListener(event, listener);\n    }\n\n    attachMediaElement(mediaElement) {\n        this._mediaElement = mediaElement;\n        mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);\n\n        if (this._pendingSeekTime != null) {\n            try {\n                mediaElement.currentTime = this._pendingSeekTime;\n                this._pendingSeekTime = null;\n            } catch (e) {\n                // IE11 may throw InvalidStateError if readyState === 0\n                // Defer set currentTime operation after loadedmetadata\n            }\n        }\n    }\n\n    detachMediaElement() {\n        if (this._mediaElement) {\n            this._mediaElement.src = '';\n            this._mediaElement.removeAttribute('src');\n            this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);\n            this._mediaElement = null;\n        }\n        if (this._statisticsReporter != null) {\n            window.clearInterval(this._statisticsReporter);\n            this._statisticsReporter = null;\n        }\n    }\n\n    load() {\n        if (!this._mediaElement) {\n            throw new IllegalStateException('HTMLMediaElement must be attached before load()!');\n        }\n        this._mediaElement.src = this._mediaDataSource.url;\n\n        if (this._mediaElement.readyState > 0) {\n            this._mediaElement.currentTime = 0;\n        }\n\n        this._mediaElement.preload = 'auto';\n        this._mediaElement.load();\n        this._statisticsReporter = window.setInterval(\n            this._reportStatisticsInfo.bind(this),\n        this._config.statisticsInfoReportInterval);\n    }\n\n    unload() {\n        if (this._mediaElement) {\n            this._mediaElement.src = '';\n            this._mediaElement.removeAttribute('src');\n        }\n        if (this._statisticsReporter != null) {\n            window.clearInterval(this._statisticsReporter);\n            this._statisticsReporter = null;\n        }\n    }\n\n    play() {\n        return this._mediaElement.play();\n    }\n\n    pause() {\n        this._mediaElement.pause();\n    }\n\n    get type() {\n        return this._type;\n    }\n\n    get buffered() {\n        return this._mediaElement.buffered;\n    }\n\n    get duration() {\n        return this._mediaElement.duration;\n    }\n\n    get volume() {\n        return this._mediaElement.volume;\n    }\n\n    set volume(value) {\n        this._mediaElement.volume = value;\n    }\n\n    get muted() {\n        return this._mediaElement.muted;\n    }\n\n    set muted(muted) {\n        this._mediaElement.muted = muted;\n    }\n\n    get currentTime() {\n        if (this._mediaElement) {\n            return this._mediaElement.currentTime;\n        }\n        return 0;\n    }\n\n    set currentTime(seconds) {\n        if (this._mediaElement) {\n            this._mediaElement.currentTime = seconds;\n        } else {\n            this._pendingSeekTime = seconds;\n        }\n    }\n\n    get mediaInfo() {\n        let mediaPrefix = (this._mediaElement instanceof HTMLAudioElement) ? 'audio/' : 'video/';\n        let info = {\n            mimeType: mediaPrefix + this._mediaDataSource.type\n        };\n        if (this._mediaElement) {\n            info.duration = Math.floor(this._mediaElement.duration * 1000);\n            if (this._mediaElement instanceof HTMLVideoElement) {\n                info.width = this._mediaElement.videoWidth;\n                info.height = this._mediaElement.videoHeight;\n            }\n        }\n        return info;\n    }\n\n    get statisticsInfo() {\n        let info = {\n            playerType: this._type,\n            url: this._mediaDataSource.url\n        };\n\n        if (!(this._mediaElement instanceof HTMLVideoElement)) {\n            return info;\n        }\n\n        let hasQualityInfo = true;\n        let decoded = 0;\n        let dropped = 0;\n\n        if (this._mediaElement.getVideoPlaybackQuality) {\n            let quality = this._mediaElement.getVideoPlaybackQuality();\n            decoded = quality.totalVideoFrames;\n            dropped = quality.droppedVideoFrames;\n        } else if (this._mediaElement.webkitDecodedFrameCount != undefined) {\n            decoded = this._mediaElement.webkitDecodedFrameCount;\n            dropped = this._mediaElement.webkitDroppedFrameCount;\n        } else {\n            hasQualityInfo = false;\n        }\n\n        if (hasQualityInfo) {\n            info.decodedFrames = decoded;\n            info.droppedFrames = dropped;\n        }\n        \n        return info;\n    }\n\n    _onvLoadedMetadata(e) {\n        if (this._pendingSeekTime != null) {\n            this._mediaElement.currentTime = this._pendingSeekTime;\n            this._pendingSeekTime = null;\n        }\n        this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);\n    }\n\n    _reportStatisticsInfo() {\n        this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);\n    }\n\n}\n\nexport default NativePlayer;"
  },
  {
    "path": "src/player/player-errors.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {LoaderErrors} from '../io/loader.js';\nimport DemuxErrors from '../demux/demux-errors.js';\n\nexport const ErrorTypes = {\n    NETWORK_ERROR: 'NetworkError',\n    MEDIA_ERROR: 'MediaError',\n    OTHER_ERROR: 'OtherError'\n};\n\nexport const ErrorDetails = {\n    NETWORK_EXCEPTION: LoaderErrors.EXCEPTION,\n    NETWORK_STATUS_CODE_INVALID: LoaderErrors.HTTP_STATUS_CODE_INVALID,\n    NETWORK_TIMEOUT: LoaderErrors.CONNECTING_TIMEOUT,\n    NETWORK_UNRECOVERABLE_EARLY_EOF: LoaderErrors.UNRECOVERABLE_EARLY_EOF,\n\n    MEDIA_MSE_ERROR: 'MediaMSEError',\n\n    MEDIA_FORMAT_ERROR: DemuxErrors.FORMAT_ERROR,\n    MEDIA_FORMAT_UNSUPPORTED: DemuxErrors.FORMAT_UNSUPPORTED,\n    MEDIA_CODEC_UNSUPPORTED: DemuxErrors.CODEC_UNSUPPORTED\n};"
  },
  {
    "path": "src/player/player-events.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst PlayerEvents = {\n    ERROR: 'error',\n    LOADING_COMPLETE: 'loading_complete',\n    RECOVERED_EARLY_EOF: 'recovered_early_eof',\n    MEDIA_INFO: 'media_info',\n    METADATA_ARRIVED: 'metadata_arrived',\n    SCRIPTDATA_ARRIVED: 'scriptdata_arrived',\n    STATISTICS_INFO: 'statistics_info'\n};\n\nexport default PlayerEvents;"
  },
  {
    "path": "src/remux/aac-silent.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is modified from dailymotion's hls.js library (hls.js/src/helper/aac.js)\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nclass AAC {\n\n    static getSilentFrame(codec, channelCount) {\n        if (codec === 'mp4a.40.2') {\n            // handle LC-AAC\n            if (channelCount === 1) {\n                return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x23, 0x80]);\n            } else if (channelCount === 2) {\n                return new Uint8Array([0x21, 0x00, 0x49, 0x90, 0x02, 0x19, 0x00, 0x23, 0x80]);\n            } else if (channelCount === 3) {\n                return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x8e]);\n            } else if (channelCount === 4) {\n                return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x80, 0x2c, 0x80, 0x08, 0x02, 0x38]);\n            } else if (channelCount === 5) {\n                return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x38]);\n            } else if (channelCount === 6) {\n                return new Uint8Array([0x00, 0xc8, 0x00, 0x80, 0x20, 0x84, 0x01, 0x26, 0x40, 0x08, 0x64, 0x00, 0x82, 0x30, 0x04, 0x99, 0x00, 0x21, 0x90, 0x02, 0x00, 0xb2, 0x00, 0x20, 0x08, 0xe0]);\n            }\n        } else {\n            // handle HE-AAC (mp4a.40.5 / mp4a.40.29)\n            if (channelCount === 1) {\n                // ffmpeg -y -f lavfi -i \"aevalsrc=0:d=0.05\" -c:a libfdk_aac -profile:a aac_he -b:a 4k output.aac && hexdump -v -e '16/1 \"0x%x,\" \"\\n\"' -v output.aac\n                return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x4e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x1c, 0x6, 0xf1, 0xc1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);\n            } else if (channelCount === 2) {\n                // ffmpeg -y -f lavfi -i \"aevalsrc=0|0:d=0.05\" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 \"0x%x,\" \"\\n\"' -v output.aac\n                return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);\n            } else if (channelCount === 3) {\n                // ffmpeg -y -f lavfi -i \"aevalsrc=0|0|0:d=0.05\" -c:a libfdk_aac -profile:a aac_he_v2 -b:a 4k output.aac && hexdump -v -e '16/1 \"0x%x,\" \"\\n\"' -v output.aac\n                return new Uint8Array([0x1, 0x40, 0x22, 0x80, 0xa3, 0x5e, 0xe6, 0x80, 0xba, 0x8, 0x0, 0x0, 0x0, 0x0, 0x95, 0x0, 0x6, 0xf1, 0xa1, 0xa, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5a, 0x5e]);\n            }\n        }\n        return null;\n    }\n\n}\n\nexport default AAC;"
  },
  {
    "path": "src/remux/mp4-generator.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is derived from dailymotion's hls.js library (hls.js/src/remux/mp4-generator.js)\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n//  MP4 boxes generator for ISO BMFF (ISO Base Media File Format, defined in ISO/IEC 14496-12)\nclass MP4 {\n\n    static init() {\n        MP4.types = {\n            avc1: [], avcC: [], btrt: [], dinf: [],\n            dref: [], esds: [], ftyp: [], hdlr: [],\n            mdat: [], mdhd: [], mdia: [], mfhd: [],\n            minf: [], moof: [], moov: [], mp4a: [],\n            mvex: [], mvhd: [], sdtp: [], stbl: [],\n            stco: [], stsc: [], stsd: [], stsz: [],\n            stts: [], tfdt: [], tfhd: [], traf: [],\n            trak: [], trun: [], trex: [], tkhd: [],\n            vmhd: [], smhd: [], '.mp3': []\n        };\n\n        for (let name in MP4.types) {\n            if (MP4.types.hasOwnProperty(name)) {\n                MP4.types[name] = [\n                    name.charCodeAt(0),\n                    name.charCodeAt(1),\n                    name.charCodeAt(2),\n                    name.charCodeAt(3)\n                ];\n            }\n        }\n\n        let constants = MP4.constants = {};\n\n        constants.FTYP = new Uint8Array([\n            0x69, 0x73, 0x6F, 0x6D,  // major_brand: isom\n            0x0,  0x0,  0x0,  0x1,   // minor_version: 0x01\n            0x69, 0x73, 0x6F, 0x6D,  // isom\n            0x61, 0x76, 0x63, 0x31   // avc1\n        ]);\n\n        constants.STSD_PREFIX = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x01   // entry_count\n        ]);\n\n        constants.STTS = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00   // entry_count\n        ]);\n\n        constants.STSC = constants.STCO = constants.STTS;\n\n        constants.STSZ = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // sample_size\n            0x00, 0x00, 0x00, 0x00   // sample_count\n        ]);\n\n        constants.HDLR_VIDEO = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // pre_defined\n            0x76, 0x69, 0x64, 0x65,  // handler_type: 'vide'\n            0x00, 0x00, 0x00, 0x00,  // reserved: 3 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x56, 0x69, 0x64, 0x65,\n            0x6F, 0x48, 0x61, 0x6E,\n            0x64, 0x6C, 0x65, 0x72, 0x00  // name: VideoHandler\n        ]);\n\n        constants.HDLR_AUDIO = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // pre_defined\n            0x73, 0x6F, 0x75, 0x6E,  // handler_type: 'soun'\n            0x00, 0x00, 0x00, 0x00,  // reserved: 3 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x53, 0x6F, 0x75, 0x6E,\n            0x64, 0x48, 0x61, 0x6E,\n            0x64, 0x6C, 0x65, 0x72, 0x00  // name: SoundHandler\n        ]);\n\n        constants.DREF = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x01,  // entry_count\n            0x00, 0x00, 0x00, 0x0C,  // entry_size\n            0x75, 0x72, 0x6C, 0x20,  // type 'url '\n            0x00, 0x00, 0x00, 0x01   // version(0) + flags\n        ]);\n\n        // Sound media header\n        constants.SMHD = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00   // balance(2) + reserved(2)\n        ]);\n\n        // video media header\n        constants.VMHD = new Uint8Array([\n            0x00, 0x00, 0x00, 0x01,  // version(0) + flags\n            0x00, 0x00,              // graphicsmode: 2 bytes\n            0x00, 0x00, 0x00, 0x00,  // opcolor: 3 * 2 bytes\n            0x00, 0x00\n        ]);\n    }\n\n    // Generate a box\n    static box(type) {\n        let size = 8;\n        let result = null;\n        let datas = Array.prototype.slice.call(arguments, 1);\n        let arrayCount = datas.length;\n\n        for (let i = 0; i < arrayCount; i++) {\n            size += datas[i].byteLength;\n        }\n\n        result = new Uint8Array(size);\n        result[0] = (size >>> 24) & 0xFF;  // size\n        result[1] = (size >>> 16) & 0xFF;\n        result[2] = (size >>>  8) & 0xFF;\n        result[3] = (size) & 0xFF;\n\n        result.set(type, 4);  // type\n\n        let offset = 8;\n        for (let i = 0; i < arrayCount; i++) {  // data body\n            result.set(datas[i], offset);\n            offset += datas[i].byteLength;\n        }\n\n        return result;\n    }\n\n    // emit ftyp & moov\n    static generateInitSegment(meta) {\n        let ftyp = MP4.box(MP4.types.ftyp, MP4.constants.FTYP);\n        let moov = MP4.moov(meta);\n\n        let result = new Uint8Array(ftyp.byteLength + moov.byteLength);\n        result.set(ftyp, 0);\n        result.set(moov, ftyp.byteLength);\n        return result;\n    }\n\n    // Movie metadata box\n    static moov(meta) {\n        let mvhd = MP4.mvhd(meta.timescale, meta.duration);\n        let trak = MP4.trak(meta);\n        let mvex = MP4.mvex(meta);\n        return MP4.box(MP4.types.moov, mvhd, trak, mvex);\n    }\n\n    // Movie header box\n    static mvhd(timescale, duration) {\n        return MP4.box(MP4.types.mvhd, new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // creation_time\n            0x00, 0x00, 0x00, 0x00,  // modification_time\n            (timescale >>> 24) & 0xFF,  // timescale: 4 bytes\n            (timescale >>> 16) & 0xFF,\n            (timescale >>>  8) & 0xFF,\n            (timescale) & 0xFF,\n            (duration >>> 24) & 0xFF,   // duration: 4 bytes\n            (duration >>> 16) & 0xFF,\n            (duration >>>  8) & 0xFF,\n            (duration) & 0xFF,\n            0x00, 0x01, 0x00, 0x00,  // Preferred rate: 1.0\n            0x01, 0x00, 0x00, 0x00,  // PreferredVolume(1.0, 2bytes) + reserved(2bytes)\n            0x00, 0x00, 0x00, 0x00,  // reserved: 4 + 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x01, 0x00, 0x00,  // ----begin composition matrix----\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x01, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x40, 0x00, 0x00, 0x00,  // ----end composition matrix----\n            0x00, 0x00, 0x00, 0x00,  // ----begin pre_defined 6 * 4 bytes----\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,  // ----end pre_defined 6 * 4 bytes----\n            0xFF, 0xFF, 0xFF, 0xFF   // next_track_ID\n        ]));\n    }\n\n    // Track box\n    static trak(meta) {\n        return MP4.box(MP4.types.trak, MP4.tkhd(meta), MP4.mdia(meta));\n    }\n\n    // Track header box\n    static tkhd(meta) {\n        let trackId = meta.id, duration = meta.duration;\n        let width = meta.presentWidth, height = meta.presentHeight;\n\n        return MP4.box(MP4.types.tkhd, new Uint8Array([\n            0x00, 0x00, 0x00, 0x07,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // creation_time\n            0x00, 0x00, 0x00, 0x00,  // modification_time\n            (trackId >>> 24) & 0xFF,  // track_ID: 4 bytes\n            (trackId >>> 16) & 0xFF,\n            (trackId >>>  8) & 0xFF,\n            (trackId) & 0xFF,\n            0x00, 0x00, 0x00, 0x00,  // reserved: 4 bytes\n            (duration >>> 24) & 0xFF, // duration: 4 bytes\n            (duration >>> 16) & 0xFF,\n            (duration >>>  8) & 0xFF,\n            (duration) & 0xFF,\n            0x00, 0x00, 0x00, 0x00,  // reserved: 2 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,  // layer(2bytes) + alternate_group(2bytes)\n            0x00, 0x00, 0x00, 0x00,  // volume(2bytes) + reserved(2bytes)\n            0x00, 0x01, 0x00, 0x00,  // ----begin composition matrix----\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x01, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x40, 0x00, 0x00, 0x00,  // ----end composition matrix----\n            (width >>> 8) & 0xFF,    // width and height\n            (width) & 0xFF,\n            0x00, 0x00,\n            (height >>> 8) & 0xFF,\n            (height) & 0xFF,\n            0x00, 0x00\n        ]));\n    }\n\n    // Media Box\n    static mdia(meta) {\n        return MP4.box(MP4.types.mdia, MP4.mdhd(meta), MP4.hdlr(meta), MP4.minf(meta));\n    }\n\n    // Media header box\n    static mdhd(meta) {\n        let timescale = meta.timescale;\n        let duration = meta.duration;\n        return MP4.box(MP4.types.mdhd, new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            0x00, 0x00, 0x00, 0x00,  // creation_time\n            0x00, 0x00, 0x00, 0x00,  // modification_time\n            (timescale >>> 24) & 0xFF,  // timescale: 4 bytes\n            (timescale >>> 16) & 0xFF,\n            (timescale >>>  8) & 0xFF,\n            (timescale) & 0xFF,\n            (duration >>> 24) & 0xFF,   // duration: 4 bytes\n            (duration >>> 16) & 0xFF,\n            (duration >>>  8) & 0xFF,\n            (duration) & 0xFF,\n            0x55, 0xC4,             // language: und (undetermined)\n            0x00, 0x00              // pre_defined = 0\n        ]));\n    }\n\n    // Media handler reference box\n    static hdlr(meta) {\n        let data = null;\n        if (meta.type === 'audio') {\n            data = MP4.constants.HDLR_AUDIO;\n        } else {\n            data = MP4.constants.HDLR_VIDEO;\n        }\n        return MP4.box(MP4.types.hdlr, data);\n    }\n\n    // Media infomation box\n    static minf(meta) {\n        let xmhd = null;\n        if (meta.type === 'audio') {\n            xmhd = MP4.box(MP4.types.smhd, MP4.constants.SMHD);\n        } else {\n            xmhd = MP4.box(MP4.types.vmhd, MP4.constants.VMHD);\n        }\n        return MP4.box(MP4.types.minf, xmhd, MP4.dinf(), MP4.stbl(meta));\n    }\n\n    // Data infomation box\n    static dinf() {\n        let result = MP4.box(MP4.types.dinf,\n            MP4.box(MP4.types.dref, MP4.constants.DREF)\n        );\n        return result;\n    }\n\n    // Sample table box\n    static stbl(meta) {\n        let result = MP4.box(MP4.types.stbl,  // type: stbl\n            MP4.stsd(meta),  // Sample Description Table\n            MP4.box(MP4.types.stts, MP4.constants.STTS),  // Time-To-Sample\n            MP4.box(MP4.types.stsc, MP4.constants.STSC),  // Sample-To-Chunk\n            MP4.box(MP4.types.stsz, MP4.constants.STSZ),  // Sample size\n            MP4.box(MP4.types.stco, MP4.constants.STCO)   // Chunk offset\n        ); \n        return result; \n    }\n\n    // Sample description box\n    static stsd(meta) {\n        if (meta.type === 'audio') {\n            if (meta.codec === 'mp3') {\n                return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp3(meta));\n            }\n            // else: aac -> mp4a\n            return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp4a(meta));\n        } else {\n            return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.avc1(meta));\n        }\n    }\n\n    static mp3(meta) {\n        let channelCount = meta.channelCount;\n        let sampleRate = meta.audioSampleRate;\n\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // reserved(4)\n            0x00, 0x00, 0x00, 0x01,  // reserved(2) + data_reference_index(2)\n            0x00, 0x00, 0x00, 0x00,  // reserved: 2 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, channelCount,      // channelCount(2)\n            0x00, 0x10,              // sampleSize(2)\n            0x00, 0x00, 0x00, 0x00,  // reserved(4)\n            (sampleRate >>> 8) & 0xFF,  // Audio sample rate\n            (sampleRate) & 0xFF,\n            0x00, 0x00\n        ]);\n\n        return MP4.box(MP4.types['.mp3'], data);\n    }\n\n    static mp4a(meta) {\n        let channelCount = meta.channelCount;\n        let sampleRate = meta.audioSampleRate;\n\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // reserved(4)\n            0x00, 0x00, 0x00, 0x01,  // reserved(2) + data_reference_index(2)\n            0x00, 0x00, 0x00, 0x00,  // reserved: 2 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, channelCount,      // channelCount(2)\n            0x00, 0x10,              // sampleSize(2)\n            0x00, 0x00, 0x00, 0x00,  // reserved(4)\n            (sampleRate >>> 8) & 0xFF,  // Audio sample rate\n            (sampleRate) & 0xFF,\n            0x00, 0x00\n        ]);\n\n        return MP4.box(MP4.types.mp4a, data, MP4.esds(meta));\n    }\n\n    static esds(meta) {\n        let config = meta.config || [];\n        let configSize = config.length;\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version 0 + flags\n\n            0x03,                    // descriptor_type\n            0x17 + configSize,       // length3\n            0x00, 0x01,              // es_id\n            0x00,                    // stream_priority\n\n            0x04,                    // descriptor_type\n            0x0F + configSize,       // length\n            0x40,                    // codec: mpeg4_audio\n            0x15,                    // stream_type: Audio\n            0x00, 0x00, 0x00,        // buffer_size\n            0x00, 0x00, 0x00, 0x00,  // maxBitrate\n            0x00, 0x00, 0x00, 0x00,  // avgBitrate\n\n            0x05                     // descriptor_type\n        ].concat([\n            configSize\n        ]).concat(\n            config\n        ).concat([\n            0x06, 0x01, 0x02         // GASpecificConfig\n        ]));\n        return MP4.box(MP4.types.esds, data);\n    }\n\n    static avc1(meta) {\n        let avcc = meta.avcc;\n        let width = meta.codecWidth, height = meta.codecHeight;\n\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // reserved(4)\n            0x00, 0x00, 0x00, 0x01,  // reserved(2) + data_reference_index(2)\n            0x00, 0x00, 0x00, 0x00,  // pre_defined(2) + reserved(2)\n            0x00, 0x00, 0x00, 0x00,  // pre_defined: 3 * 4 bytes\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            (width >>> 8) & 0xFF,    // width: 2 bytes\n            (width) & 0xFF,\n            (height >>> 8) & 0xFF,   // height: 2 bytes\n            (height) & 0xFF,\n            0x00, 0x48, 0x00, 0x00,  // horizresolution: 4 bytes\n            0x00, 0x48, 0x00, 0x00,  // vertresolution: 4 bytes\n            0x00, 0x00, 0x00, 0x00,  // reserved: 4 bytes\n            0x00, 0x01,              // frame_count\n            0x0A,                    // strlen\n            0x78, 0x71, 0x71, 0x2F,  // compressorname: 32 bytes\n            0x66, 0x6C, 0x76, 0x2E,\n            0x6A, 0x73, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00, 0x00,\n            0x00, 0x00, 0x00,\n            0x00, 0x18,              // depth\n            0xFF, 0xFF               // pre_defined = -1\n        ]);\n        return MP4.box(MP4.types.avc1, data, MP4.box(MP4.types.avcC, avcc));\n    }\n\n    // Movie Extends box\n    static mvex(meta) {\n        return MP4.box(MP4.types.mvex, MP4.trex(meta));\n    }\n\n    // Track Extends box\n    static trex(meta) {\n        let trackId = meta.id;\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) + flags\n            (trackId >>> 24) & 0xFF, // track_ID\n            (trackId >>> 16) & 0xFF,\n            (trackId >>>  8) & 0xFF,\n            (trackId) & 0xFF,\n            0x00, 0x00, 0x00, 0x01,  // default_sample_description_index\n            0x00, 0x00, 0x00, 0x00,  // default_sample_duration\n            0x00, 0x00, 0x00, 0x00,  // default_sample_size\n            0x00, 0x01, 0x00, 0x01   // default_sample_flags\n        ]);\n        return MP4.box(MP4.types.trex, data);\n    }\n\n    // Movie fragment box\n    static moof(track, baseMediaDecodeTime) {\n        return MP4.box(MP4.types.moof, MP4.mfhd(track.sequenceNumber), MP4.traf(track, baseMediaDecodeTime));\n    }\n\n    static mfhd(sequenceNumber) {\n        let data = new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,\n            (sequenceNumber >>> 24) & 0xFF,  // sequence_number: int32\n            (sequenceNumber >>> 16) & 0xFF,\n            (sequenceNumber >>>  8) & 0xFF,\n            (sequenceNumber) & 0xFF\n        ]);\n        return MP4.box(MP4.types.mfhd, data);\n    }\n\n    // Track fragment box\n    static traf(track, baseMediaDecodeTime) {\n        let trackId = track.id;\n\n        // Track fragment header box\n        let tfhd = MP4.box(MP4.types.tfhd, new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) & flags\n            (trackId >>> 24) & 0xFF, // track_ID\n            (trackId >>> 16) & 0xFF,\n            (trackId >>>  8) & 0xFF,\n            (trackId) & 0xFF\n        ]));\n        // Track Fragment Decode Time\n        let tfdt = MP4.box(MP4.types.tfdt, new Uint8Array([\n            0x00, 0x00, 0x00, 0x00,  // version(0) & flags\n            (baseMediaDecodeTime >>> 24) & 0xFF,  // baseMediaDecodeTime: int32\n            (baseMediaDecodeTime >>> 16) & 0xFF,\n            (baseMediaDecodeTime >>>  8) & 0xFF,\n            (baseMediaDecodeTime) & 0xFF\n        ]));\n        let sdtp = MP4.sdtp(track);\n        let trun = MP4.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8);\n\n        return MP4.box(MP4.types.traf, tfhd, tfdt, trun, sdtp);\n    }\n\n    // Sample Dependency Type box\n    static sdtp(track) {\n        let samples = track.samples || [];\n        let sampleCount = samples.length;\n        let data = new Uint8Array(4 + sampleCount);\n        // 0~4 bytes: version(0) & flags\n        for (let i = 0; i < sampleCount; i++) {\n            let flags = samples[i].flags;\n            data[i + 4] = (flags.isLeading << 6)    // is_leading: 2 (bit)\n                        | (flags.dependsOn << 4)    // sample_depends_on\n                        | (flags.isDependedOn << 2) // sample_is_depended_on\n                        | (flags.hasRedundancy);    // sample_has_redundancy\n        }\n        return MP4.box(MP4.types.sdtp, data);\n    }\n\n    // Track fragment run box\n    static trun(track, offset) {\n        let samples = track.samples || [];\n        let sampleCount = samples.length;\n        let dataSize = 12 + 16 * sampleCount;\n        let data = new Uint8Array(dataSize);\n        offset += 8 + dataSize;\n\n        data.set([\n            0x00, 0x00, 0x0F, 0x01,      // version(0) & flags\n            (sampleCount >>> 24) & 0xFF, // sample_count\n            (sampleCount >>> 16) & 0xFF,\n            (sampleCount >>>  8) & 0xFF,\n            (sampleCount) & 0xFF,\n            (offset >>> 24) & 0xFF,      // data_offset\n            (offset >>> 16) & 0xFF,\n            (offset >>>  8) & 0xFF,\n            (offset) & 0xFF\n        ], 0);\n\n        for (let i = 0; i < sampleCount; i++) {\n            let duration = samples[i].duration;\n            let size = samples[i].size;\n            let flags = samples[i].flags;\n            let cts = samples[i].cts;\n            data.set([\n                (duration >>> 24) & 0xFF,  // sample_duration\n                (duration >>> 16) & 0xFF,\n                (duration >>>  8) & 0xFF,\n                (duration) & 0xFF,\n                (size >>> 24) & 0xFF,      // sample_size\n                (size >>> 16) & 0xFF,\n                (size >>>  8) & 0xFF,\n                (size) & 0xFF,\n                (flags.isLeading << 2) | flags.dependsOn,  // sample_flags\n                (flags.isDependedOn << 6) | (flags.hasRedundancy << 4) | flags.isNonSync,\n                0x00, 0x00,                // sample_degradation_priority\n                (cts >>> 24) & 0xFF,       // sample_composition_time_offset\n                (cts >>> 16) & 0xFF,\n                (cts >>>  8) & 0xFF,\n                (cts) & 0xFF\n            ], 12 + 16 * i);\n        }\n        return MP4.box(MP4.types.trun, data);\n    }\n\n    static mdat(data) {\n        return MP4.box(MP4.types.mdat, data);\n    }\n\n}\n\nMP4.init();\n\nexport default MP4;"
  },
  {
    "path": "src/remux/mp4-remuxer.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport Log from '../utils/logger.js';\nimport MP4 from './mp4-generator.js';\nimport AAC from './aac-silent.js';\nimport Browser from '../utils/browser.js';\nimport { SampleInfo, MediaSegmentInfo, MediaSegmentInfoList } from '../core/media-segment-info.js';\nimport { IllegalStateException } from '../utils/exception.js';\n\n\n// Fragmented mp4 remuxer\nclass MP4Remuxer {\n\n    constructor(config) {\n        this.TAG = 'MP4Remuxer';\n\n        this._config = config;\n        this._isLive = (config.isLive === true) ? true : false;\n\n        this._dtsBase = -1;\n        this._dtsBaseInited = false;\n        this._audioDtsBase = Infinity;\n        this._videoDtsBase = Infinity;\n        this._audioNextDts = undefined;\n        this._videoNextDts = undefined;\n        this._audioStashedLastSample = null;\n        this._videoStashedLastSample = null;\n\n        this._audioMeta = null;\n        this._videoMeta = null;\n\n        this._audioSegmentInfoList = new MediaSegmentInfoList('audio');\n        this._videoSegmentInfoList = new MediaSegmentInfoList('video');\n\n        this._onInitSegment = null;\n        this._onMediaSegment = null;\n\n        // Workaround for chrome < 50: Always force first sample as a Random Access Point in media segment\n        // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412\n        this._forceFirstIDR = (Browser.chrome &&\n            (Browser.version.major < 50 ||\n                (Browser.version.major === 50 && Browser.version.build < 2661))) ? true : false;\n\n        // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking\n        // Make audio beginDts equals with video beginDts, in order to fix seek freeze\n        this._fillSilentAfterSeek = (Browser.msedge || Browser.msie);\n\n        // While only FireFox supports 'audio/mp4, codecs=\"mp3\"', use 'audio/mpeg' for chrome, safari, ...\n        this._mp3UseMpegAudio = !Browser.firefox;\n\n        this._fillAudioTimestampGap = this._config.fixAudioTimestampGap;\n    }\n\n    destroy() {\n        this._dtsBase = -1;\n        this._dtsBaseInited = false;\n        this._audioMeta = null;\n        this._videoMeta = null;\n        this._audioSegmentInfoList.clear();\n        this._audioSegmentInfoList = null;\n        this._videoSegmentInfoList.clear();\n        this._videoSegmentInfoList = null;\n        this._onInitSegment = null;\n        this._onMediaSegment = null;\n    }\n\n    bindDataSource(producer) {\n        producer.onDataAvailable = this.remux.bind(this);\n        producer.onTrackMetadata = this._onTrackMetadataReceived.bind(this);\n        return this;\n    }\n\n    /* prototype: function onInitSegment(type: string, initSegment: ArrayBuffer): void\n       InitSegment: {\n           type: string,\n           data: ArrayBuffer,\n           codec: string,\n           container: string\n       }\n    */\n    get onInitSegment() {\n        return this._onInitSegment;\n    }\n\n    set onInitSegment(callback) {\n        this._onInitSegment = callback;\n    }\n\n    /* prototype: function onMediaSegment(type: string, mediaSegment: MediaSegment): void\n       MediaSegment: {\n           type: string,\n           data: ArrayBuffer,\n           sampleCount: int32\n           info: MediaSegmentInfo\n       }\n    */\n    get onMediaSegment() {\n        return this._onMediaSegment;\n    }\n\n    set onMediaSegment(callback) {\n        this._onMediaSegment = callback;\n    }\n\n    insertDiscontinuity() {\n        this._audioNextDts = this._videoNextDts = undefined;\n    }\n\n    seek(originalDts) {\n        this._audioStashedLastSample = null;\n        this._videoStashedLastSample = null;\n        this._videoSegmentInfoList.clear();\n        this._audioSegmentInfoList.clear();\n    }\n\n    remux(audioTrack, videoTrack) {\n        if (!this._onMediaSegment) {\n            throw new IllegalStateException('MP4Remuxer: onMediaSegment callback must be specificed!');\n        }\n        if (!this._dtsBaseInited) {\n            this._calculateDtsBase(audioTrack, videoTrack);\n        }\n        this._remuxVideo(videoTrack);\n        this._remuxAudio(audioTrack);\n    }\n\n    _onTrackMetadataReceived(type, metadata) {\n        let metabox = null;\n\n        let container = 'mp4';\n        let codec = metadata.codec;\n\n        if (type === 'audio') {\n            this._audioMeta = metadata;\n            if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) {\n                // 'audio/mpeg' for MP3 audio track\n                container = 'mpeg';\n                codec = '';\n                metabox = new Uint8Array();\n            } else {\n                // 'audio/mp4, codecs=\"codec\"'\n                metabox = MP4.generateInitSegment(metadata);\n            }\n        } else if (type === 'video') {\n            this._videoMeta = metadata;\n            metabox = MP4.generateInitSegment(metadata);\n        } else {\n            return;\n        }\n\n        // dispatch metabox (Initialization Segment)\n        if (!this._onInitSegment) {\n            throw new IllegalStateException('MP4Remuxer: onInitSegment callback must be specified!');\n        }\n        this._onInitSegment(type, {\n            type: type,\n            data: metabox.buffer,\n            codec: codec,\n            container: `${type}/${container}`,\n            mediaDuration: metadata.duration  // in timescale 1000 (milliseconds)\n        });\n    }\n\n    _calculateDtsBase(audioTrack, videoTrack) {\n        if (this._dtsBaseInited) {\n            return;\n        }\n\n        if (audioTrack.samples && audioTrack.samples.length) {\n            this._audioDtsBase = audioTrack.samples[0].dts;\n        }\n        if (videoTrack.samples && videoTrack.samples.length) {\n            this._videoDtsBase = videoTrack.samples[0].dts;\n        }\n\n        this._dtsBase = Math.min(this._audioDtsBase, this._videoDtsBase);\n        this._dtsBaseInited = true;\n    }\n\n    flushStashedSamples() {\n        let videoSample = this._videoStashedLastSample;\n        let audioSample = this._audioStashedLastSample;\n\n        let videoTrack = {\n            type: 'video',\n            id: 1,\n            sequenceNumber: 0,\n            samples: [],\n            length: 0\n        };\n\n        if (videoSample != null) {\n            videoTrack.samples.push(videoSample);\n            videoTrack.length = videoSample.length;\n        }\n\n        let audioTrack = {\n            type: 'audio',\n            id: 2,\n            sequenceNumber: 0,\n            samples: [],\n            length: 0\n        };\n\n        if (audioSample != null) {\n            audioTrack.samples.push(audioSample);\n            audioTrack.length = audioSample.length;\n        }\n\n        this._videoStashedLastSample = null;\n        this._audioStashedLastSample = null;\n\n        this._remuxVideo(videoTrack, true);\n        this._remuxAudio(audioTrack, true);\n    }\n\n    _remuxAudio(audioTrack, force) {\n        if (this._audioMeta == null) {\n            return;\n        }\n\n        let track = audioTrack;\n        let samples = track.samples;\n        let dtsCorrection = undefined;\n        let firstDts = -1, lastDts = -1, lastPts = -1;\n        let refSampleDuration = this._audioMeta.refSampleDuration;\n\n        let mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio;\n        let firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined;\n\n        let insertPrefixSilentFrame = false;\n\n        if (!samples || samples.length === 0) {\n            return;\n        }\n        if (samples.length === 1 && !force) {\n            // If [sample count in current batch] === 1 && (force != true)\n            // Ignore and keep in demuxer's queue\n            return;\n        }  // else if (force === true) do remux\n\n        let offset = 0;\n        let mdatbox = null;\n        let mdatBytes = 0;\n\n        // calculate initial mdat size\n        if (mpegRawTrack) {\n            // for raw mpeg buffer\n            offset = 0;\n            mdatBytes = track.length;\n        } else {\n            // for fmp4 mdat box\n            offset = 8;  // size + type\n            mdatBytes = 8 + track.length;\n        }\n\n\n        let lastSample = null;\n\n        // Pop the lastSample and waiting for stash\n        if (samples.length > 1) {\n            lastSample = samples.pop();\n            mdatBytes -= lastSample.length;\n        }\n\n        // Insert [stashed lastSample in the previous batch] to the front\n        if (this._audioStashedLastSample != null) {\n            let sample = this._audioStashedLastSample;\n            this._audioStashedLastSample = null;\n            samples.unshift(sample);\n            mdatBytes += sample.length;\n        }\n\n        // Stash the lastSample of current batch, waiting for next batch\n        if (lastSample != null) {\n            this._audioStashedLastSample = lastSample;\n        }\n\n\n        let firstSampleOriginalDts = samples[0].dts - this._dtsBase;\n\n        // calculate dtsCorrection\n        if (this._audioNextDts) {\n            dtsCorrection = firstSampleOriginalDts - this._audioNextDts;\n        } else {  // this._audioNextDts == undefined\n            if (this._audioSegmentInfoList.isEmpty()) {\n                dtsCorrection = 0;\n                if (this._fillSilentAfterSeek && !this._videoSegmentInfoList.isEmpty()) {\n                    if (this._audioMeta.originalCodec !== 'mp3') {\n                        insertPrefixSilentFrame = true;\n                    }\n                }\n            } else {\n                let lastSample = this._audioSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts);\n                if (lastSample != null) {\n                    let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration));\n                    if (distance <= 3) {\n                        distance = 0;\n                    }\n                    let expectedDts = lastSample.dts + lastSample.duration + distance;\n                    dtsCorrection = firstSampleOriginalDts - expectedDts;\n                } else { // lastSample == null, cannot found\n                    dtsCorrection = 0;\n                }\n            }\n        }\n\n        if (insertPrefixSilentFrame) {\n            // align audio segment beginDts to match with current video segment's beginDts\n            let firstSampleDts = firstSampleOriginalDts - dtsCorrection;\n            let videoSegment = this._videoSegmentInfoList.getLastSegmentBefore(firstSampleOriginalDts);\n            if (videoSegment != null && videoSegment.beginDts < firstSampleDts) {\n                let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount);\n                if (silentUnit) {\n                    let dts = videoSegment.beginDts;\n                    let silentFrameDuration = firstSampleDts - videoSegment.beginDts;\n                    Log.v(this.TAG, `InsertPrefixSilentAudio: dts: ${dts}, duration: ${silentFrameDuration}`);\n                    samples.unshift({ unit: silentUnit, dts: dts, pts: dts });\n                    mdatBytes += silentUnit.byteLength;\n                }  // silentUnit == null: Cannot generate, skip\n            } else {\n                insertPrefixSilentFrame = false;\n            }\n        }\n\n        let mp4Samples = [];\n\n        // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples\n        for (let i = 0; i < samples.length; i++) {\n            let sample = samples[i];\n            let unit = sample.unit;\n            let originalDts = sample.dts - this._dtsBase;\n            let dts = originalDts;\n            let needFillSilentFrames = false;\n            let silentFrames = null;\n            let sampleDuration = 0;\n\n            if (originalDts < -0.001) {\n                continue; //pass the first sample with the invalid dts\n            }\n\n            if (this._audioMeta.codec !== 'mp3') {\n                // for AAC codec, we need to keep dts increase based on refSampleDuration\n                let curRefDts = originalDts;\n                const maxAudioFramesDrift = 3;\n                if (this._audioNextDts) {\n                    curRefDts = this._audioNextDts;\n                }\n\n                dtsCorrection = originalDts - curRefDts;\n                if (dtsCorrection <= -maxAudioFramesDrift * refSampleDuration) {\n                    // If we're overlapping by more than maxAudioFramesDrift number of frame, drop this sample\n                    Log.w(this.TAG, `Dropping 1 audio frame (originalDts: ${originalDts} ms ,curRefDts: ${curRefDts} ms)  due to dtsCorrection: ${dtsCorrection} ms overlap.`);\n                    continue;\n                }\n                else if (dtsCorrection >= maxAudioFramesDrift * refSampleDuration && this._fillAudioTimestampGap && !Browser.safari) {\n                    // Silent frame generation, if large timestamp gap detected && config.fixAudioTimestampGap\n                    needFillSilentFrames = true;\n                    // We need to insert silent frames to fill timestamp gap\n                    let frameCount = Math.floor(dtsCorrection / refSampleDuration);\n                    Log.w(this.TAG, 'Large audio timestamp gap detected, may cause AV sync to drift. ' +\n                        'Silent frames will be generated to avoid unsync.\\n' +\n                        `originalDts: ${originalDts} ms, curRefDts: ${curRefDts} ms, ` +\n                        `dtsCorrection: ${Math.round(dtsCorrection)} ms, generate: ${frameCount} frames`);\n\n\n                    dts = Math.floor(curRefDts);\n                    sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts;\n\n                    let silentUnit = AAC.getSilentFrame(this._audioMeta.originalCodec, this._audioMeta.channelCount);\n                    if (silentUnit == null) {\n                        Log.w(this.TAG, 'Unable to generate silent frame for ' +\n                            `${this._audioMeta.originalCodec} with ${this._audioMeta.channelCount} channels, repeat last frame`);\n                        // Repeat last frame\n                        silentUnit = unit;\n                    }\n                    silentFrames = [];\n\n                    for (let j = 0; j < frameCount; j++) {\n                        curRefDts = curRefDts + refSampleDuration;\n                        let intDts = Math.floor(curRefDts);  // change to integer\n                        let intDuration = Math.floor(curRefDts + refSampleDuration) - intDts;\n                        let frame = {\n                            dts: intDts,\n                            pts: intDts,\n                            cts: 0,\n                            unit: silentUnit,\n                            size: silentUnit.byteLength,\n                            duration: intDuration,  // wait for next sample\n                            originalDts: originalDts,\n                            flags: {\n                                isLeading: 0,\n                                dependsOn: 1,\n                                isDependedOn: 0,\n                                hasRedundancy: 0\n                            }\n                        };\n                        silentFrames.push(frame);\n                        mdatBytes += frame.size;;\n\n                    }\n\n                    this._audioNextDts = curRefDts + refSampleDuration;\n\n                } else {\n\n                    dts = Math.floor(curRefDts);\n                    sampleDuration = Math.floor(curRefDts + refSampleDuration) - dts;\n                    this._audioNextDts = curRefDts + refSampleDuration;\n\n                }\n            } else {\n                // keep the original dts calculate algorithm for mp3\n                dts = originalDts - dtsCorrection;\n\n\n                if (i !== samples.length - 1) {\n                    let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection;\n                    sampleDuration = nextDts - dts;\n                } else {  // the last sample\n                    if (lastSample != null) {  // use stashed sample's dts to calculate sample duration\n                        let nextDts = lastSample.dts - this._dtsBase - dtsCorrection;\n                        sampleDuration = nextDts - dts;\n                    } else if (mp4Samples.length >= 1) {  // use second last sample duration\n                        sampleDuration = mp4Samples[mp4Samples.length - 1].duration;\n                    } else {  // the only one sample, use reference sample duration\n                        sampleDuration = Math.floor(refSampleDuration);\n                    }\n                }\n                this._audioNextDts = dts + sampleDuration;\n            }\n\n            if (firstDts === -1) {\n                firstDts = dts;\n            }\n            mp4Samples.push({\n                dts: dts,\n                pts: dts,\n                cts: 0,\n                unit: sample.unit,\n                size: sample.unit.byteLength,\n                duration: sampleDuration,\n                originalDts: originalDts,\n                flags: {\n                    isLeading: 0,\n                    dependsOn: 1,\n                    isDependedOn: 0,\n                    hasRedundancy: 0\n                }\n            });\n\n            if (needFillSilentFrames) {\n                // Silent frames should be inserted after wrong-duration frame\n                mp4Samples.push.apply(mp4Samples, silentFrames);\n            }\n        }\n\n        if (mp4Samples.length === 0) {\n            //no samples need to remux\n            track.samples = [];\n            track.length = 0;\n            return;\n        }\n\n        // allocate mdatbox\n        if (mpegRawTrack) {\n            // allocate for raw mpeg buffer\n            mdatbox = new Uint8Array(mdatBytes);\n        } else {\n            // allocate for fmp4 mdat box\n            mdatbox = new Uint8Array(mdatBytes);\n            // size field\n            mdatbox[0] = (mdatBytes >>> 24) & 0xFF;\n            mdatbox[1] = (mdatBytes >>> 16) & 0xFF;\n            mdatbox[2] = (mdatBytes >>> 8) & 0xFF;\n            mdatbox[3] = (mdatBytes) & 0xFF;\n            // type field (fourCC)\n            mdatbox.set(MP4.types.mdat, 4);\n        }\n\n        // Write samples into mdatbox\n        for (let i = 0; i < mp4Samples.length; i++) {\n            let unit = mp4Samples[i].unit;\n            mdatbox.set(unit, offset);\n            offset += unit.byteLength;\n        }\n\n        let latest = mp4Samples[mp4Samples.length - 1];\n        lastDts = latest.dts + latest.duration;\n        //this._audioNextDts = lastDts;\n\n        // fill media segment info & add to info list\n        let info = new MediaSegmentInfo();\n        info.beginDts = firstDts;\n        info.endDts = lastDts;\n        info.beginPts = firstDts;\n        info.endPts = lastDts;\n        info.originalBeginDts = mp4Samples[0].originalDts;\n        info.originalEndDts = latest.originalDts + latest.duration;\n        info.firstSample = new SampleInfo(mp4Samples[0].dts,\n            mp4Samples[0].pts,\n            mp4Samples[0].duration,\n            mp4Samples[0].originalDts,\n            false);\n        info.lastSample = new SampleInfo(latest.dts,\n            latest.pts,\n            latest.duration,\n            latest.originalDts,\n            false);\n        if (!this._isLive) {\n            this._audioSegmentInfoList.append(info);\n        }\n\n        track.samples = mp4Samples;\n        track.sequenceNumber++;\n\n        let moofbox = null;\n\n        if (mpegRawTrack) {\n            // Generate empty buffer, because useless for raw mpeg\n            moofbox = new Uint8Array();\n        } else {\n            // Generate moof for fmp4 segment\n            moofbox = MP4.moof(track, firstDts);\n        }\n\n        track.samples = [];\n        track.length = 0;\n\n        let segment = {\n            type: 'audio',\n            data: this._mergeBoxes(moofbox, mdatbox).buffer,\n            sampleCount: mp4Samples.length,\n            info: info\n        };\n\n        if (mpegRawTrack && firstSegmentAfterSeek) {\n            // For MPEG audio stream in MSE, if seeking occurred, before appending new buffer\n            // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer.\n            segment.timestampOffset = firstDts;\n        }\n\n        this._onMediaSegment('audio', segment);\n    }\n\n    _remuxVideo(videoTrack, force) {\n        if (this._videoMeta == null) {\n            return;\n        }\n\n        let track = videoTrack;\n        let samples = track.samples;\n        let dtsCorrection = undefined;\n        let firstDts = -1, lastDts = -1;\n        let firstPts = -1, lastPts = -1;\n\n        if (!samples || samples.length === 0) {\n            return;\n        }\n        if (samples.length === 1 && !force) {\n            // If [sample count in current batch] === 1 && (force != true)\n            // Ignore and keep in demuxer's queue\n            return;\n        }  // else if (force === true) do remux\n\n        let offset = 8;\n        let mdatbox = null;\n        let mdatBytes = 8 + videoTrack.length;\n\n\n        let lastSample = null;\n\n        // Pop the lastSample and waiting for stash\n        if (samples.length > 1) {\n            lastSample = samples.pop();\n            mdatBytes -= lastSample.length;\n        }\n\n        // Insert [stashed lastSample in the previous batch] to the front\n        if (this._videoStashedLastSample != null) {\n            let sample = this._videoStashedLastSample;\n            this._videoStashedLastSample = null;\n            samples.unshift(sample);\n            mdatBytes += sample.length;\n        }\n\n        // Stash the lastSample of current batch, waiting for next batch\n        if (lastSample != null) {\n            this._videoStashedLastSample = lastSample;\n        }\n\n\n        let firstSampleOriginalDts = samples[0].dts - this._dtsBase;\n\n        // calculate dtsCorrection\n        if (this._videoNextDts) {\n            dtsCorrection = firstSampleOriginalDts - this._videoNextDts;\n        } else {  // this._videoNextDts == undefined\n            if (this._videoSegmentInfoList.isEmpty()) {\n                dtsCorrection = 0;\n            } else {\n                let lastSample = this._videoSegmentInfoList.getLastSampleBefore(firstSampleOriginalDts);\n                if (lastSample != null) {\n                    let distance = (firstSampleOriginalDts - (lastSample.originalDts + lastSample.duration));\n                    if (distance <= 3) {\n                        distance = 0;\n                    }\n                    let expectedDts = lastSample.dts + lastSample.duration + distance;\n                    dtsCorrection = firstSampleOriginalDts - expectedDts;\n                } else { // lastSample == null, cannot found\n                    dtsCorrection = 0;\n                }\n            }\n        }\n\n        let info = new MediaSegmentInfo();\n        let mp4Samples = [];\n\n        // Correct dts for each sample, and calculate sample duration. Then output to mp4Samples\n        for (let i = 0; i < samples.length; i++) {\n            let sample = samples[i];\n            let originalDts = sample.dts - this._dtsBase;\n            let isKeyframe = sample.isKeyframe;\n            let dts = originalDts - dtsCorrection;\n            let cts = sample.cts;\n            let pts = dts + cts;\n\n            if (firstDts === -1) {\n                firstDts = dts;\n                firstPts = pts;\n            }\n\n            let sampleDuration = 0;\n\n            if (i !== samples.length - 1) {\n                let nextDts = samples[i + 1].dts - this._dtsBase - dtsCorrection;\n                sampleDuration = nextDts - dts;\n            } else {  // the last sample\n                if (lastSample != null) {  // use stashed sample's dts to calculate sample duration\n                    let nextDts = lastSample.dts - this._dtsBase - dtsCorrection;\n                    sampleDuration = nextDts - dts;\n                } else if (mp4Samples.length >= 1) {  // use second last sample duration\n                    sampleDuration = mp4Samples[mp4Samples.length - 1].duration;\n                } else {  // the only one sample, use reference sample duration\n                    sampleDuration = Math.floor(this._videoMeta.refSampleDuration);\n                }\n            }\n\n            if (isKeyframe) {\n                let syncPoint = new SampleInfo(dts, pts, sampleDuration, sample.dts, true);\n                syncPoint.fileposition = sample.fileposition;\n                info.appendSyncPoint(syncPoint);\n            }\n\n            mp4Samples.push({\n                dts: dts,\n                pts: pts,\n                cts: cts,\n                units: sample.units,\n                size: sample.length,\n                isKeyframe: isKeyframe,\n                duration: sampleDuration,\n                originalDts: originalDts,\n                flags: {\n                    isLeading: 0,\n                    dependsOn: isKeyframe ? 2 : 1,\n                    isDependedOn: isKeyframe ? 1 : 0,\n                    hasRedundancy: 0,\n                    isNonSync: isKeyframe ? 0 : 1\n                }\n            });\n        }\n\n        // allocate mdatbox\n        mdatbox = new Uint8Array(mdatBytes);\n        mdatbox[0] = (mdatBytes >>> 24) & 0xFF;\n        mdatbox[1] = (mdatBytes >>> 16) & 0xFF;\n        mdatbox[2] = (mdatBytes >>> 8) & 0xFF;\n        mdatbox[3] = (mdatBytes) & 0xFF;\n        mdatbox.set(MP4.types.mdat, 4);\n\n        // Write samples into mdatbox\n        for (let i = 0; i < mp4Samples.length; i++) {\n            let units = mp4Samples[i].units;\n            while (units.length) {\n                let unit = units.shift();\n                let data = unit.data;\n                mdatbox.set(data, offset);\n                offset += data.byteLength;\n            }\n        }\n\n        let latest = mp4Samples[mp4Samples.length - 1];\n        lastDts = latest.dts + latest.duration;\n        lastPts = latest.pts + latest.duration;\n        this._videoNextDts = lastDts;\n\n        // fill media segment info & add to info list\n        info.beginDts = firstDts;\n        info.endDts = lastDts;\n        info.beginPts = firstPts;\n        info.endPts = lastPts;\n        info.originalBeginDts = mp4Samples[0].originalDts;\n        info.originalEndDts = latest.originalDts + latest.duration;\n        info.firstSample = new SampleInfo(mp4Samples[0].dts,\n            mp4Samples[0].pts,\n            mp4Samples[0].duration,\n            mp4Samples[0].originalDts,\n            mp4Samples[0].isKeyframe);\n        info.lastSample = new SampleInfo(latest.dts,\n            latest.pts,\n            latest.duration,\n            latest.originalDts,\n            latest.isKeyframe);\n        if (!this._isLive) {\n            this._videoSegmentInfoList.append(info);\n        }\n\n        track.samples = mp4Samples;\n        track.sequenceNumber++;\n\n        // workaround for chrome < 50: force first sample as a random access point\n        // see https://bugs.chromium.org/p/chromium/issues/detail?id=229412\n        if (this._forceFirstIDR) {\n            let flags = mp4Samples[0].flags;\n            flags.dependsOn = 2;\n            flags.isNonSync = 0;\n        }\n\n        let moofbox = MP4.moof(track, firstDts);\n        track.samples = [];\n        track.length = 0;\n\n        this._onMediaSegment('video', {\n            type: 'video',\n            data: this._mergeBoxes(moofbox, mdatbox).buffer,\n            sampleCount: mp4Samples.length,\n            info: info\n        });\n    }\n\n    _mergeBoxes(moof, mdat) {\n        let result = new Uint8Array(moof.byteLength + mdat.byteLength);\n        result.set(moof, 0);\n        result.set(mdat, moof.byteLength);\n        return result;\n    }\n\n}\n\nexport default MP4Remuxer;\n"
  },
  {
    "path": "src/utils/browser.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nlet Browser = {};\n\nfunction detect() {\n    // modified from jquery-browser-plugin\n\n    let ua = self.navigator.userAgent.toLowerCase();\n\n    let match = /(edge)\\/([\\w.]+)/.exec(ua) ||\n        /(opr)[\\/]([\\w.]+)/.exec(ua) ||\n        /(chrome)[ \\/]([\\w.]+)/.exec(ua) ||\n        /(iemobile)[\\/]([\\w.]+)/.exec(ua) ||\n        /(version)(applewebkit)[ \\/]([\\w.]+).*(safari)[ \\/]([\\w.]+)/.exec(ua) ||\n        /(webkit)[ \\/]([\\w.]+).*(version)[ \\/]([\\w.]+).*(safari)[ \\/]([\\w.]+)/.exec(ua) ||\n        /(webkit)[ \\/]([\\w.]+)/.exec(ua) ||\n        /(opera)(?:.*version|)[ \\/]([\\w.]+)/.exec(ua) ||\n        /(msie) ([\\w.]+)/.exec(ua) ||\n        ua.indexOf('trident') >= 0 && /(rv)(?::| )([\\w.]+)/.exec(ua) ||\n        ua.indexOf('compatible') < 0 && /(firefox)[ \\/]([\\w.]+)/.exec(ua) ||\n        [];\n\n    let platform_match = /(ipad)/.exec(ua) ||\n        /(ipod)/.exec(ua) ||\n        /(windows phone)/.exec(ua) ||\n        /(iphone)/.exec(ua) ||\n        /(kindle)/.exec(ua) ||\n        /(android)/.exec(ua) ||\n        /(windows)/.exec(ua) ||\n        /(mac)/.exec(ua) ||\n        /(linux)/.exec(ua) ||\n        /(cros)/.exec(ua) ||\n        [];\n\n    let matched = {\n        browser: match[5] || match[3] || match[1] || '',\n        version: match[2] || match[4] || '0',\n        majorVersion: match[4] || match[2] || '0',\n        platform: platform_match[0] || ''\n    };\n\n    let browser = {};\n    if (matched.browser) {\n        browser[matched.browser] = true;\n\n        let versionArray = matched.majorVersion.split('.');\n        browser.version = {\n            major: parseInt(matched.majorVersion, 10),\n            string: matched.version\n        };\n        if (versionArray.length > 1) {\n            browser.version.minor = parseInt(versionArray[1], 10);\n        }\n        if (versionArray.length > 2) {\n            browser.version.build = parseInt(versionArray[2], 10);\n        }\n    }\n\n    if (matched.platform) {\n        browser[matched.platform] = true;\n    }\n\n    if (browser.chrome || browser.opr || browser.safari) {\n        browser.webkit = true;\n    }\n\n    // MSIE. IE11 has 'rv' identifer\n    if (browser.rv || browser.iemobile) {\n        if (browser.rv) {\n            delete browser.rv;\n        }\n        let msie = 'msie';\n        matched.browser = msie;\n        browser[msie] = true;\n    }\n\n    // Microsoft Edge\n    if (browser.edge) {\n        delete browser.edge;\n        let msedge = 'msedge';\n        matched.browser = msedge;\n        browser[msedge] = true;\n    }\n\n    // Opera 15+\n    if (browser.opr) {\n        let opera = 'opera';\n        matched.browser = opera;\n        browser[opera] = true;\n    }\n\n    // Stock android browsers are marked as Safari\n    if (browser.safari && browser.android) {\n        let android = 'android';\n        matched.browser = android;\n        browser[android] = true;\n    }\n\n    browser.name = matched.browser;\n    browser.platform = matched.platform;\n\n    for (let key in Browser) {\n        if (Browser.hasOwnProperty(key)) {\n            delete Browser[key];\n        }\n    }\n    Object.assign(Browser, browser);\n}\n\ndetect();\n\nexport default Browser;"
  },
  {
    "path": "src/utils/exception.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport class RuntimeException {\n\n    constructor(message) {\n        this._message = message;\n    }\n\n    get name() {\n        return 'RuntimeException';\n    }\n\n    get message() {\n        return this._message;\n    }\n\n    toString() {\n        return this.name + ': ' + this.message;\n    }\n\n}\n\nexport class IllegalStateException extends RuntimeException {\n\n    constructor(message) {\n        super(message);\n    }\n\n    get name() {\n        return 'IllegalStateException';\n    }\n\n}\n\nexport class InvalidArgumentException extends RuntimeException {\n\n    constructor(message) {\n        super(message);\n    }\n\n    get name() {\n        return 'InvalidArgumentException';\n    }\n\n}\n\nexport class NotImplementedException extends RuntimeException {\n\n    constructor(message) {\n        super(message);\n    }\n\n    get name() {\n        return 'NotImplementedException';\n    }\n\n}\n"
  },
  {
    "path": "src/utils/logger.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\n\nclass Log {\n\n    static e(tag, msg) {\n        if (!tag || Log.FORCE_GLOBAL_TAG)\n            tag = Log.GLOBAL_TAG;\n\n        let str = `[${tag}] > ${msg}`;\n\n        if (Log.ENABLE_CALLBACK) {\n            Log.emitter.emit('log', 'error', str);\n        }\n\n        if (!Log.ENABLE_ERROR) {\n            return;\n        }\n\n        if (console.error) {\n            console.error(str);\n        } else if (console.warn) {\n            console.warn(str);\n        } else {\n            console.log(str);\n        }\n    }\n\n    static i(tag, msg) {\n        if (!tag || Log.FORCE_GLOBAL_TAG)\n            tag = Log.GLOBAL_TAG;\n\n        let str = `[${tag}] > ${msg}`;\n\n        if (Log.ENABLE_CALLBACK) {\n            Log.emitter.emit('log', 'info', str);\n        }\n\n        if (!Log.ENABLE_INFO) {\n            return;\n        }\n\n        if (console.info) {\n            console.info(str);\n        } else {\n            console.log(str);\n        }\n    }\n\n    static w(tag, msg) {\n        if (!tag || Log.FORCE_GLOBAL_TAG)\n            tag = Log.GLOBAL_TAG;\n\n        let str = `[${tag}] > ${msg}`;\n\n        if (Log.ENABLE_CALLBACK) {\n            Log.emitter.emit('log', 'warn', str);\n        }\n\n        if (!Log.ENABLE_WARN) {\n            return;\n        }\n\n        if (console.warn) {\n            console.warn(str);\n        } else {\n            console.log(str);\n        }\n    }\n\n    static d(tag, msg) {\n        if (!tag || Log.FORCE_GLOBAL_TAG)\n            tag = Log.GLOBAL_TAG;\n\n        let str = `[${tag}] > ${msg}`;\n\n        if (Log.ENABLE_CALLBACK) {\n            Log.emitter.emit('log', 'debug', str);\n        }\n\n        if (!Log.ENABLE_DEBUG) {\n            return;\n        }\n\n        if (console.debug) {\n            console.debug(str);\n        } else {\n            console.log(str);\n        }\n    }\n\n    static v(tag, msg) {\n        if (!tag || Log.FORCE_GLOBAL_TAG)\n            tag = Log.GLOBAL_TAG;\n\n        let str = `[${tag}] > ${msg}`;\n\n        if (Log.ENABLE_CALLBACK) {\n            Log.emitter.emit('log', 'verbose', str);\n        }\n\n        if (!Log.ENABLE_VERBOSE) {\n            return;\n        }\n\n        console.log(str);\n    }\n\n}\n\nLog.GLOBAL_TAG = 'flv.js';\nLog.FORCE_GLOBAL_TAG = false;\nLog.ENABLE_ERROR = true;\nLog.ENABLE_INFO = true;\nLog.ENABLE_WARN = true;\nLog.ENABLE_DEBUG = true;\nLog.ENABLE_VERBOSE = true;\n\nLog.ENABLE_CALLBACK = false;\n\nLog.emitter = new EventEmitter();\n\nexport default Log;"
  },
  {
    "path": "src/utils/logging-control.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport EventEmitter from 'events';\nimport Log from './logger.js';\n\nclass LoggingControl {\n\n    static get forceGlobalTag() {\n        return Log.FORCE_GLOBAL_TAG;\n    }\n\n    static set forceGlobalTag(enable) {\n        Log.FORCE_GLOBAL_TAG = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get globalTag() {\n        return Log.GLOBAL_TAG;\n    }\n\n    static set globalTag(tag) {\n        Log.GLOBAL_TAG = tag;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableAll() {\n        return Log.ENABLE_VERBOSE\n            && Log.ENABLE_DEBUG\n            && Log.ENABLE_INFO\n            && Log.ENABLE_WARN\n            && Log.ENABLE_ERROR;\n    }\n\n    static set enableAll(enable) {\n        Log.ENABLE_VERBOSE = enable;\n        Log.ENABLE_DEBUG = enable;\n        Log.ENABLE_INFO = enable;\n        Log.ENABLE_WARN = enable;\n        Log.ENABLE_ERROR = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableDebug() {\n        return Log.ENABLE_DEBUG;\n    }\n\n    static set enableDebug(enable) {\n        Log.ENABLE_DEBUG = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableVerbose() {\n        return Log.ENABLE_VERBOSE;\n    }\n\n    static set enableVerbose(enable) {\n        Log.ENABLE_VERBOSE = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableInfo() {\n        return Log.ENABLE_INFO;\n    }\n\n    static set enableInfo(enable) {\n        Log.ENABLE_INFO = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableWarn() {\n        return Log.ENABLE_WARN;\n    }\n\n    static set enableWarn(enable) {\n        Log.ENABLE_WARN = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static get enableError() {\n        return Log.ENABLE_ERROR;\n    }\n\n    static set enableError(enable) {\n        Log.ENABLE_ERROR = enable;\n        LoggingControl._notifyChange();\n    }\n\n    static getConfig() {\n        return {\n            globalTag: Log.GLOBAL_TAG,\n            forceGlobalTag: Log.FORCE_GLOBAL_TAG,\n            enableVerbose: Log.ENABLE_VERBOSE,\n            enableDebug: Log.ENABLE_DEBUG,\n            enableInfo: Log.ENABLE_INFO,\n            enableWarn: Log.ENABLE_WARN,\n            enableError: Log.ENABLE_ERROR,\n            enableCallback: Log.ENABLE_CALLBACK\n        };\n    }\n\n    static applyConfig(config) {\n        Log.GLOBAL_TAG = config.globalTag;\n        Log.FORCE_GLOBAL_TAG = config.forceGlobalTag;\n        Log.ENABLE_VERBOSE = config.enableVerbose;\n        Log.ENABLE_DEBUG = config.enableDebug;\n        Log.ENABLE_INFO = config.enableInfo;\n        Log.ENABLE_WARN = config.enableWarn;\n        Log.ENABLE_ERROR = config.enableError;\n        Log.ENABLE_CALLBACK = config.enableCallback;\n    }\n\n    static _notifyChange() {\n        let emitter = LoggingControl.emitter;\n\n        if (emitter.listenerCount('change') > 0) {\n            let config = LoggingControl.getConfig();\n            emitter.emit('change', config);\n        }\n    }\n\n    static registerListener(listener) {\n        LoggingControl.emitter.addListener('change', listener);\n    }\n\n    static removeListener(listener) {\n        LoggingControl.emitter.removeListener('change', listener);\n    }\n\n    static addLogListener(listener) {\n        Log.emitter.addListener('log', listener);\n        if (Log.emitter.listenerCount('log') > 0) {\n            Log.ENABLE_CALLBACK = true;\n            LoggingControl._notifyChange();\n        }\n    }\n\n    static removeLogListener(listener) {\n        Log.emitter.removeListener('log', listener);\n        if (Log.emitter.listenerCount('log') === 0) {\n            Log.ENABLE_CALLBACK = false;\n            LoggingControl._notifyChange();\n        }\n    }\n\n}\n\nLoggingControl.emitter = new EventEmitter();\n\nexport default LoggingControl;"
  },
  {
    "path": "src/utils/polyfill.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nclass Polyfill {\n\n    static install() {\n        // ES6 Object.setPrototypeOf\n        Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {\n            obj.__proto__ = proto;\n            return obj;\n        };\n\n        // ES6 Object.assign\n        Object.assign = Object.assign || function (target) {\n            if (target === undefined || target === null) {\n                throw new TypeError('Cannot convert undefined or null to object');\n            }\n\n            let output = Object(target);\n            for (let i = 1; i < arguments.length; i++) {\n                let source = arguments[i];\n                if (source !== undefined && source !== null) {\n                    for (let key in source) {\n                        if (source.hasOwnProperty(key)) {\n                            output[key] = source[key];\n                        }\n                    }\n                }\n            }\n            return output;\n        };\n\n        // ES6 Promise (missing support in IE11)\n        if (typeof self.Promise !== 'function') {\n            require('es6-promise').polyfill();\n        }\n    }\n\n}\n\nPolyfill.install();\n\nexport default Polyfill;"
  },
  {
    "path": "src/utils/utf8-conv.js",
    "content": "/*\n * Copyright (C) 2016 Bilibili. All Rights Reserved.\n *\n * This file is derived from C++ project libWinTF8 (https://github.com/m13253/libWinTF8)\n * @author zheng qian <xqq@xqq.im>\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nfunction checkContinuation(uint8array, start, checkLength) {\n    let array = uint8array;\n    if (start + checkLength < array.length) {\n        while (checkLength--) {\n            if ((array[++start] & 0xC0) !== 0x80)\n                return false;\n        }\n        return true;\n    } else {\n        return false;\n    }\n}\n\nfunction decodeUTF8(uint8array) {\n    let out = [];\n    let input = uint8array;\n    let i = 0;\n    let length = uint8array.length;\n\n    while (i < length) {\n        if (input[i] < 0x80) {\n            out.push(String.fromCharCode(input[i]));\n            ++i;\n            continue;\n        } else if (input[i] < 0xC0) {\n            // fallthrough\n        } else if (input[i] < 0xE0) {\n            if (checkContinuation(input, i, 1)) {\n                let ucs4 = (input[i] & 0x1F) << 6 | (input[i + 1] & 0x3F);\n                if (ucs4 >= 0x80) {\n                    out.push(String.fromCharCode(ucs4 & 0xFFFF));\n                    i += 2;\n                    continue;\n                }\n            }\n        } else if (input[i] < 0xF0) {\n            if (checkContinuation(input, i, 2)) {\n                let ucs4 = (input[i] & 0xF) << 12 | (input[i + 1] & 0x3F) << 6 | input[i + 2] & 0x3F;\n                if (ucs4 >= 0x800 && (ucs4 & 0xF800) !== 0xD800) {\n                    out.push(String.fromCharCode(ucs4 & 0xFFFF));\n                    i += 3;\n                    continue;\n                }\n            }\n        } else if (input[i] < 0xF8) {\n            if (checkContinuation(input, i, 3)) {\n                let ucs4 = (input[i] & 0x7) << 18 | (input[i + 1] & 0x3F) << 12\n                         | (input[i + 2] & 0x3F) << 6 | (input[i + 3] & 0x3F);\n                if (ucs4 > 0x10000 && ucs4 < 0x110000) {\n                    ucs4 -= 0x10000;\n                    out.push(String.fromCharCode((ucs4 >>> 10) | 0xD800));\n                    out.push(String.fromCharCode((ucs4 & 0x3FF) | 0xDC00));\n                    i += 4;\n                    continue;\n                }\n            }\n        }\n        out.push(String.fromCharCode(0xFFFD));\n        ++i;\n    }\n\n    return out.join('');\n}\n\nexport default decodeUTF8;"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"outDir\": \"./dist/\",\n        \"allowJs\": true,\n        \"sourceMap\": true,\n        \"module\": \"es6\",\n        \"target\": \"es5\",\n        \"experimentalDecorators\": true,\n        \"declaration\": true,\n        \"declarationDir\": \"./d.ts/\",\n        \"lib\": [\"dom\", \"es5\", \"es2015.promise\", \"es2015.collection\"]\n    },\n    \"include\": [\n        \"./src/**/*\",\n    ]\n}"
  },
  {
    "path": "tslint.json",
    "content": "{ \"extends\": \"dtslint/dt.json\" }\n"
  },
  {
    "path": "types/index.d.ts",
    "content": "// TypeScript Version: 2.3\n\nimport '../d.ts/flv.d.ts';\n"
  },
  {
    "path": "types/test-flv.ts",
    "content": "import flvjs from '../';\n\ntype LoaderStatusAlias = flvjs.LoaderStatus;\ntype LoaderErrorsAlias = flvjs.LoaderErrors;\n\ninterface MediaDataSourceExt extends flvjs.MediaDataSource {\n    example: string;\n}\n"
  },
  {
    "path": "types/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"module\": \"commonjs\",\n        \"lib\": [\n            \"es6\",\n            \"dom\"\n        ],\n        \"noImplicitAny\": true,\n        \"noImplicitThis\": true,\n        \"strictNullChecks\": true,\n        \"strictFunctionTypes\": true,\n        \"baseUrl\": \"./\",\n        \"typeRoots\": [\n            \"./\"\n        ],\n        \"types\": [],\n        \"noEmit\": true,\n        \"forceConsistentCasingInFileNames\": true\n    },\n    \"files\": [\n        \"index.d.ts\",\n        \"../d.ts/flv.d.ts\"\n    ]\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require('webpack');\nconst pkg = require('./package.json');\nconst path = require('path');\nconst TerserPlugin = require('terser-webpack-plugin');\n\nlet config = {\n    entry: './src/index.js',\n    output: {\n        filename: 'flv.js',\n        path: path.resolve(__dirname, 'dist'),\n        library: 'flvjs',\n        libraryTarget: 'umd',\n        environment: {\n            arrowFunction: false,\n            bigIntLiteral: false,\n            const: false,\n            destructuring: false,\n            dynamicImport: false,\n            forOf: false,\n            module: false\n        }\n    },\n\n    devtool: 'source-map',\n\n    resolve: {\n        extensions: ['.ts', '.tsx', '.js', '.json'],\n        fallback: {\n            fs: false,\n            path: false\n        }\n    },\n\n    plugins: [\n        new webpack.DefinePlugin({\n            __VERSION__: JSON.stringify(pkg.version)\n        })\n    ],\n\n    optimization: {\n        minimizer: [\n            new TerserPlugin({\n                extractComments: false,\n            })\n        ]\n    },\n\n    module: {\n        rules: [\n            {\n                test: /\\.(ts|js)$/,\n                use: 'ts-loader',\n                exclude: /node-modules/\n            },\n            {\n                enforce: 'pre',\n                test: /\\.js$/,\n                use: 'source-map-loader'\n            }\n        ]\n    }\n};\n\nmodule.exports = (env, argv) => {\n    if (argv.mode === 'production') {\n        config.output.filename = 'flv.min.js';\n    }\n\n    return config;\n};\n"
  }
]